diff options
Diffstat (limited to 'src')
37 files changed, 7937 insertions, 1267 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..313cd38 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -1,41 +1,162 @@ #include "../include/Config.hpp" #include "../include/Utils.hpp" #include "../include/GsrInfo.hpp" +#include "../include/GlobalHotkeys/GlobalHotkeys.hpp" #include <variant> #include <limits.h> #include <inttypes.h> #include <libgen.h> +#include <string.h> +#include <assert.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) { - const std::string default_save_directory = get_videos_dir(); + static const std::string_view add_audio_track_tag = "[add_audio_track]"; + + 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 void string_remove_all(std::string &str, const std::string &substr) { + size_t index = 0; + while(true) { + index = str.find(substr, index); + if(index == std::string::npos) + break; + str.erase(index, substr.size()); + } + } + + ReplayStartupMode replay_startup_string_to_type(const char *startup_mode_str) { + if(strcmp(startup_mode_str, "dont_turn_on_automatically") == 0) + return ReplayStartupMode::DONT_TURN_ON_AUTOMATICALLY; + else if(strcmp(startup_mode_str, "turn_on_at_system_startup") == 0) + return ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP; + else if(strcmp(startup_mode_str, "turn_on_at_fullscreen") == 0) + return ReplayStartupMode::TURN_ON_AT_FULLSCREEN; + else if(strcmp(startup_mode_str, "turn_on_at_power_supply_connected") == 0) + return ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED; + else + return ReplayStartupMode::DONT_TURN_ON_AUTOMATICALLY; + } + + bool ConfigHotkey::operator==(const ConfigHotkey &other) const { + return key == other.key && modifiers == other.modifiers; + } + + bool ConfigHotkey::operator!=(const ConfigHotkey &other) const { + return !operator==(other); + } + + std::string ConfigHotkey::to_string(bool spaces, bool modifier_side) const { + std::string result; + + const std::vector<mgl::Keyboard::Key> modifier_keys = hotkey_modifiers_to_mgl_keys(modifiers); + std::string modifier_str; + for(const mgl::Keyboard::Key modifier_key : modifier_keys) { + if(!result.empty()) { + if(spaces) + result += " + "; + else + result += "+"; + } + + modifier_str = mgl::Keyboard::key_to_string(modifier_key); + if(!modifier_side) { + string_remove_all(modifier_str, "Left "); + string_remove_all(modifier_str, "Right "); + } + result += modifier_str; + } + + if(key != 0) { + if(!result.empty()) { + if(spaces) + result += " + "; + else + result += "+"; + } + result += mgl::Keyboard::key_to_string((mgl::Keyboard::Key)key); + } + + return result; + } + + bool AudioTrack::operator==(const AudioTrack &other) const { + return audio_inputs == other.audio_inputs && application_audio_invert == other.application_audio_invert; + } + + bool AudioTrack::operator!=(const AudioTrack &other) const { + return !operator==(other); + } + + Config::Config(const SupportedCaptureOptions &capture_options) { + const std::string default_videos_save_directory = get_videos_dir(); + const std::string default_pictures_save_directory = get_pictures_dir(); + + set_hotkeys_to_default(); streaming_config.record_options.video_quality = "custom"; - streaming_config.record_options.audio_tracks.push_back("default_output"); - streaming_config.record_options.video_bitrate = 15000; + streaming_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false}); + streaming_config.record_options.video_bitrate = 8000; - record_config.save_directory = default_save_directory; - record_config.record_options.audio_tracks.push_back("default_output"); - record_config.record_options.video_bitrate = 45000; + record_config.save_directory = default_videos_save_directory; + record_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false}); + record_config.record_options.video_bitrate = 40000; 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; + replay_config.save_directory = default_videos_save_directory; + replay_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false}); + replay_config.record_options.video_bitrate = 40000; + + screenshot_config.save_directory = default_pictures_save_directory; + + if(!capture_options.monitors.empty()) { + streaming_config.record_options.record_area_option = "focused_monitor"; + record_config.record_options.record_area_option = "focused_monitor"; + replay_config.record_options.record_area_option = "focused_monitor"; + screenshot_config.record_area_option = "focused_monitor"; } } + void Config::set_hotkeys_to_default() { + streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT}; + + record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT}; + record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT}; + + 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.save_1_min_hotkey = {mgl::Keyboard::F11, HOTKEY_MOD_LALT}; + replay_config.save_10_min_hotkey = {mgl::Keyboard::F12, HOTKEY_MOD_LALT}; + + screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Printscreen, 0}; + screenshot_config.take_screenshot_region_hotkey = {mgl::Keyboard::Printscreen, HOTKEY_MOD_LCTRL}; + + main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT}; + } + 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) @@ -43,12 +164,16 @@ namespace gsr { return KeyValue{line.substr(0, space_index), line.substr(space_index + 1)}; } - using ConfigValue = std::variant<bool*, std::string*, int32_t*, ConfigHotkey*, std::vector<std::string>*>; + using ConfigValue = std::variant<bool*, std::string*, int32_t*, ConfigHotkey*, std::vector<std::string>*, std::vector<AudioTrack>*>; static std::map<std::string_view, ConfigValue> get_config_options(Config &config) { 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}, @@ -61,6 +186,7 @@ namespace gsr { {"streaming.record_options.application_audio_invert", &config.streaming_config.record_options.application_audio_invert}, {"streaming.record_options.change_video_resolution", &config.streaming_config.record_options.change_video_resolution}, {"streaming.record_options.audio_track", &config.streaming_config.record_options.audio_tracks}, + {"streaming.record_options.audio_track_item", &config.streaming_config.record_options.audio_tracks_list}, {"streaming.record_options.color_range", &config.streaming_config.record_options.color_range}, {"streaming.record_options.video_quality", &config.streaming_config.record_options.video_quality}, {"streaming.record_options.codec", &config.streaming_config.record_options.video_codec}, @@ -75,9 +201,10 @@ namespace gsr { {"streaming.service", &config.streaming_config.streaming_service}, {"streaming.youtube.key", &config.streaming_config.youtube.stream_key}, {"streaming.twitch.key", &config.streaming_config.twitch.stream_key}, + {"streaming.rumble.key", &config.streaming_config.rumble.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}, @@ -90,6 +217,7 @@ namespace gsr { {"record.record_options.application_audio_invert", &config.record_config.record_options.application_audio_invert}, {"record.record_options.change_video_resolution", &config.record_config.record_options.change_video_resolution}, {"record.record_options.audio_track", &config.record_config.record_options.audio_tracks}, + {"record.record_options.audio_track_item", &config.record_config.record_options.audio_tracks_list}, {"record.record_options.color_range", &config.record_config.record_options.color_range}, {"record.record_options.video_quality", &config.record_config.record_options.video_quality}, {"record.record_options.codec", &config.record_config.record_options.video_codec}, @@ -102,10 +230,11 @@ namespace gsr { {"record.save_video_in_game_folder", &config.record_config.save_video_in_game_folder}, {"record.show_recording_started_notifications", &config.record_config.show_recording_started_notifications}, {"record.show_video_saved_notifications", &config.record_config.show_video_saved_notifications}, + {"record.show_video_paused_notifications", &config.record_config.show_video_paused_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}, @@ -118,6 +247,7 @@ namespace gsr { {"replay.record_options.application_audio_invert", &config.replay_config.record_options.application_audio_invert}, {"replay.record_options.change_video_resolution", &config.replay_config.record_options.change_video_resolution}, {"replay.record_options.audio_track", &config.replay_config.record_options.audio_tracks}, + {"replay.record_options.audio_track_item", &config.replay_config.record_options.audio_tracks_list}, {"replay.record_options.color_range", &config.replay_config.record_options.color_range}, {"replay.record_options.video_quality", &config.replay_config.record_options.video_quality}, {"replay.record_options.codec", &config.replay_config.record_options.video_codec}, @@ -129,18 +259,84 @@ 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.replay_storage", &config.replay_config.replay_storage}, + {"replay.start_stop_hotkey", &config.replay_config.start_stop_hotkey}, + {"replay.save_hotkey", &config.replay_config.save_hotkey}, + {"replay.save_1_min_hotkey", &config.replay_config.save_1_min_hotkey}, + {"replay.save_10_min_hotkey", &config.replay_config.save_10_min_hotkey}, + + {"screenshot.record_area_option", &config.screenshot_config.record_area_option}, + {"screenshot.image_width", &config.screenshot_config.image_width}, + {"screenshot.image_height", &config.screenshot_config.image_height}, + {"screenshot.change_image_resolution", &config.screenshot_config.change_image_resolution}, + {"screenshot.image_quality", &config.screenshot_config.image_quality}, + {"screenshot.image_format", &config.screenshot_config.image_format}, + {"screenshot.record_cursor", &config.screenshot_config.record_cursor}, + {"screenshot.restore_portal_session", &config.screenshot_config.restore_portal_session}, + {"screenshot.save_screenshot_in_game_folder", &config.screenshot_config.save_screenshot_in_game_folder}, + {"screenshot.show_screenshot_saved_notifications", &config.screenshot_config.show_screenshot_saved_notifications}, + {"screenshot.save_directory", &config.screenshot_config.save_directory}, + {"screenshot.take_screenshot_hotkey", &config.screenshot_config.take_screenshot_hotkey}, + {"screenshot.take_screenshot_region_hotkey", &config.screenshot_config.take_screenshot_region_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; + } else if(std::holds_alternative<std::vector<AudioTrack>*>(it.second)) { + if(*std::get<std::vector<AudioTrack>*>(it.second) != *std::get<std::vector<AudioTrack>*>(it_other->second)) + return false; + } else { + assert(false); + } + } + return true; + } + + bool Config::operator!=(const Config &other) { + return !operator==(other); + } + + static void populate_new_audio_track_from_old(RecordOptions &record_options) { + if(record_options.merge_audio_tracks) { + record_options.audio_tracks_list.push_back({std::move(record_options.audio_tracks), record_options.application_audio_invert}); + } else { + for(const std::string &audio_input : record_options.audio_tracks) { + record_options.audio_tracks_list.push_back({std::vector<std::string>{audio_input}, record_options.application_audio_invert}); + } + } + record_options.audio_tracks.clear(); + } + + std::optional<Config> read_config(const SupportedCaptureOptions &capture_options) { std::optional<Config> config; const std::string config_path = get_config_dir() + "/config_ui"; @@ -150,11 +346,16 @@ 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(); + config->streaming_config.record_options.audio_tracks_list.clear(); + config->record_config.record_options.audio_tracks_list.clear(); + config->replay_config.record_options.audio_tracks_list.clear(); + auto config_options = get_config_options(config.value()); string_split_char(file_content, '\n', [&](std::string_view line) { @@ -185,29 +386,53 @@ 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)) { std::string array_value(key_value->value); std::get<std::vector<std::string>*>(it->second)->push_back(std::move(array_value)); + } else if(std::holds_alternative<std::vector<AudioTrack>*>(it->second)) { + const size_t space_index = key_value->value.find(' '); + if(space_index == std::string_view::npos) { + fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key_value->key.size(), key_value->key.data()); + return true; + } + + const bool application_audio_invert = key_value->value.substr(0, space_index) == "true"; + const std::string_view audio_input = key_value->value.substr(space_index + 1); + std::vector<AudioTrack> &audio_tracks = *std::get<std::vector<AudioTrack>*>(it->second); + + if(audio_input == add_audio_track_tag) { + audio_tracks.push_back({std::vector<std::string>{}, application_audio_invert}); + } else if(!audio_tracks.empty()) { + audio_tracks.back().application_audio_invert = application_audio_invert; + audio_tracks.back().audio_inputs.emplace_back(audio_input); + } + } else { + assert(false); } return true; }); - if(config->main_config.config_file_version != CONFIG_FILE_VERSION) { - fprintf(stderr, "Info: the config file is outdated, resetting it\n"); - config = std::nullopt; + if(config->main_config.config_file_version == 1) { + populate_new_audio_track_from_old(config->streaming_config.record_options); + populate_new_audio_track_from_old(config->record_config.record_options); + populate_new_audio_track_from_old(config->replay_config.record_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(); + return config; } 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,12 +461,22 @@ 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) { - fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), value.c_str()); + std::vector<std::string> *audio_inputs = std::get<std::vector<std::string>*>(it.second); + for(const std::string &audio_input : *audio_inputs) { + fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), audio_input.c_str()); + } + } else if(std::holds_alternative<std::vector<AudioTrack>*>(it.second)) { + std::vector<AudioTrack> *audio_tracks = std::get<std::vector<AudioTrack>*>(it.second); + for(const AudioTrack &audio_track : *audio_tracks) { + fprintf(file, "%.*s %s %.*s\n", (int)it.first.size(), it.first.data(), audio_track.application_audio_invert ? "true" : "false", (int)add_audio_track_tag.size(), add_audio_track_tag.data()); + for(const std::string &audio_input : audio_track.audio_inputs) { + fprintf(file, "%.*s %s %s\n", (int)it.first.size(), it.first.data(), audio_track.application_audio_invert ? "true" : "false", audio_input.c_str()); + } } + } else { + assert(false); } } diff --git a/src/CursorTracker/CursorTrackerWayland.cpp b/src/CursorTracker/CursorTrackerWayland.cpp new file mode 100644 index 0000000..7af86b4 --- /dev/null +++ b/src/CursorTracker/CursorTrackerWayland.cpp @@ -0,0 +1,538 @@ +#include "../../include/CursorTracker/CursorTrackerWayland.hpp" +#include <string.h> +#include <unistd.h> +#include <fcntl.h> +#include <xf86drm.h> +#include <xf86drmMode.h> +#include <wayland-client.h> +#include "xdg-output-unstable-v1-client-protocol.h" + +namespace gsr { + static const int MAX_CONNECTORS = 32; + static const uint32_t plane_property_all = 0xF; + + typedef enum { + PLANE_PROPERTY_CRTC_X = 1 << 0, + PLANE_PROPERTY_CRTC_Y = 1 << 1, + PLANE_PROPERTY_CRTC_ID = 1 << 2, + PLANE_PROPERTY_TYPE_CURSOR = 1 << 3, + } plane_property_mask; + + typedef struct { + uint64_t crtc_id; + mgl::vec2i size; + bool vrr_enabled; + } drm_connector; + + typedef struct { + drm_connector connectors[MAX_CONNECTORS]; + int num_connectors; + bool has_any_crtc_with_vrr_enabled; + } drm_connectors; + + /* Returns plane_property_mask */ + static uint32_t plane_get_properties(int drm_fd, uint32_t plane_id, int *crtc_x, int *crtc_y, int *crtc_id) { + *crtc_x = 0; + *crtc_y = 0; + *crtc_id = 0; + + uint32_t property_mask = 0; + + drmModeObjectPropertiesPtr props = drmModeObjectGetProperties(drm_fd, plane_id, DRM_MODE_OBJECT_PLANE); + if(!props) + return property_mask; + + for(uint32_t i = 0; i < props->count_props; ++i) { + drmModePropertyPtr prop = drmModeGetProperty(drm_fd, props->props[i]); + if(!prop) + continue; + + // SRC_* values are fixed 16.16 points + const uint32_t type = prop->flags & (DRM_MODE_PROP_LEGACY_TYPE | DRM_MODE_PROP_EXTENDED_TYPE); + if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_X") == 0) { + *crtc_x = (int)props->prop_values[i]; + property_mask |= PLANE_PROPERTY_CRTC_X; + } else if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_Y") == 0) { + *crtc_y = (int)props->prop_values[i]; + property_mask |= PLANE_PROPERTY_CRTC_Y; + } else if((type & DRM_MODE_PROP_OBJECT) && strcmp(prop->name, "CRTC_ID") == 0) { + *crtc_id = (int)props->prop_values[i]; + property_mask |= PLANE_PROPERTY_CRTC_ID; + } else if((type & DRM_MODE_PROP_ENUM) && strcmp(prop->name, "type") == 0) { + const uint64_t current_enum_value = props->prop_values[i]; + for(int j = 0; j < prop->count_enums; ++j) { + if(prop->enums[j].value == current_enum_value && strcmp(prop->enums[j].name, "Cursor") == 0) { + property_mask |= PLANE_PROPERTY_TYPE_CURSOR; + break; + } + } + } + + drmModeFreeProperty(prop); + } + + drmModeFreeObjectProperties(props); + return property_mask; + } + + static bool get_drm_property_by_name(int drm_fd, drmModeObjectPropertiesPtr props, const char *name, uint64_t *result) { + for(uint32_t i = 0; i < props->count_props; ++i) { + drmModePropertyPtr prop = drmModeGetProperty(drm_fd, props->props[i]); + if(!prop) + continue; + + if(strcmp(name, prop->name) == 0) { + *result = props->prop_values[i]; + drmModeFreeProperty(prop); + return true; + } + drmModeFreeProperty(prop); + } + return false; + } + + static bool connector_get_property_by_name(int drm_fd, drmModeConnectorPtr props, const char *name, uint64_t *result) { + drmModeObjectProperties properties; + properties.count_props = (uint32_t)props->count_props; + properties.props = props->props; + properties.prop_values = props->prop_values; + return get_drm_property_by_name(drm_fd, &properties, name, result); + } + + // Note: this monitor name logic is kept in sync with gpu screen recorder + static std::string get_monitor_name_from_crtc_id(int drm_fd, uint32_t crtc_id) { + std::string result; + drmModeResPtr resources = drmModeGetResources(drm_fd); + if(!resources) + return result; + + for(int i = 0; i < resources->count_connectors; ++i) { + uint64_t connector_crtc_id = 0; + drmModeConnectorPtr connector = drmModeGetConnectorCurrent(drm_fd, resources->connectors[i]); + if(!connector) + continue; + + const char *connection_name = drmModeGetConnectorTypeName(connector->connector_type); + if(!connection_name) + goto next; + + if(connector->connection != DRM_MODE_CONNECTED) + goto next; + + if(connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &connector_crtc_id) && connector_crtc_id == crtc_id) { + result = connection_name; + result += "-"; + result += std::to_string(connector->connector_type_id); + drmModeFreeConnector(connector); + break; + } + + next: + drmModeFreeConnector(connector); + } + + drmModeFreeResources(resources); + return result; + } + + // Name is the crtc name. TODO: verify if this works on all wayland compositors + static const WaylandOutput* get_wayland_monitor_by_name(const std::vector<WaylandOutput> &monitors, const std::string &name) { + for(const WaylandOutput &monitor : monitors) { + if(monitor.name == name) + return &monitor; + } + return nullptr; + } + + static WaylandOutput* get_wayland_monitor_by_output(CursorTrackerWayland &cursor_tracker_wayland, struct wl_output *output) { + for(WaylandOutput &monitor : cursor_tracker_wayland.monitors) { + if(monitor.output == output) + return &monitor; + } + return nullptr; + } + + static void output_handle_geometry(void *data, struct wl_output *wl_output, + int32_t x, int32_t y, int32_t phys_width, int32_t phys_height, + int32_t subpixel, const char *make, const char *model, + int32_t transform) { + (void)wl_output; + (void)phys_width; + (void)phys_height; + (void)subpixel; + (void)make; + (void)model; + CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data; + WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output); + if(!monitor) + return; + + monitor->pos.x = x; + monitor->pos.y = y; + monitor->transform = transform; + } + + static void output_handle_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { + (void)wl_output; + (void)flags; + (void)refresh; + CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data; + WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output); + if(!monitor) + return; + + monitor->size.x = width; + monitor->size.y = height; + } + + static void output_handle_done(void *data, struct wl_output *wl_output) { + (void)data; + (void)wl_output; + } + + static void output_handle_scale(void* data, struct wl_output *wl_output, int32_t factor) { + (void)data; + (void)wl_output; + (void)factor; + } + + static void output_handle_name(void *data, struct wl_output *wl_output, const char *name) { + (void)wl_output; + CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data; + WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_wayland, wl_output); + if(!monitor) + return; + + monitor->name = name; + } + + static void output_handle_description(void *data, struct wl_output *wl_output, const char *description) { + (void)data; + (void)wl_output; + (void)description; + } + + static const struct wl_output_listener output_listener = { + output_handle_geometry, + output_handle_mode, + output_handle_done, + output_handle_scale, + output_handle_name, + output_handle_description, + }; + + static void registry_add_object(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { + (void)version; + CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data; + if(strcmp(interface, wl_output_interface.name) == 0) { + if(version < 4) { + fprintf(stderr, "Warning: wl output interface version is < 4, expected >= 4\n"); + return; + } + + struct wl_output *output = (struct wl_output*)wl_registry_bind(registry, name, &wl_output_interface, 4); + cursor_tracker_wayland->monitors.push_back( + WaylandOutput{ + name, + output, + nullptr, + mgl::vec2i{0, 0}, + mgl::vec2i{0, 0}, + 0, + "" + }); + wl_output_add_listener(output, &output_listener, cursor_tracker_wayland); + } else if(strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { + if(version < 1) { + fprintf(stderr, "Warning: xdg output interface version is < 1, expected >= 1\n"); + return; + } + + if(cursor_tracker_wayland->xdg_output_manager) { + zxdg_output_manager_v1_destroy(cursor_tracker_wayland->xdg_output_manager); + cursor_tracker_wayland->xdg_output_manager = NULL; + } + cursor_tracker_wayland->xdg_output_manager = (struct zxdg_output_manager_v1*)wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, 1); + } + } + + static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name) { + (void)data; + (void)registry; + (void)name; + // TODO: Remove output + } + + static struct wl_registry_listener registry_listener = { + registry_add_object, + registry_remove_object, + }; + + static void xdg_output_logical_position(void *data, struct zxdg_output_v1 *zxdg_output_v1, int32_t x, int32_t y) { + (void)zxdg_output_v1; + WaylandOutput *monitor = (WaylandOutput*)data; + monitor->pos.x = x; + monitor->pos.y = y; + } + + static void xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) { + (void)data; + (void)xdg_output; + (void)width; + (void)height; + } + + static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) { + (void)data; + (void)xdg_output; + } + + static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) { + (void)data; + (void)xdg_output; + (void)name; + } + + static void xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) { + (void)data; + (void)xdg_output; + (void)description; + } + + static const struct zxdg_output_v1_listener xdg_output_listener = { + xdg_output_logical_position, + xdg_output_handle_logical_size, + xdg_output_handle_done, + xdg_output_handle_name, + xdg_output_handle_description, + }; + + /* Returns nullptr if not found */ + static drm_connector* get_drm_connector_by_crtc_id(drm_connectors *connectors, uint32_t crtc_id) { + for(int i = 0; i < connectors->num_connectors; ++i) { + if(connectors->connectors[i].crtc_id == crtc_id) + return &connectors->connectors[i]; + } + return nullptr; + } + + static void get_drm_connectors(int drm_fd, drm_connectors *drm_connectors) { + drm_connectors->num_connectors = 0; + drm_connectors->has_any_crtc_with_vrr_enabled = false; + + drmModeResPtr resources = drmModeGetResources(drm_fd); + if(!resources) + return; + + for(int i = 0; i < resources->count_connectors && drm_connectors->num_connectors < MAX_CONNECTORS; ++i) { + drmModeConnectorPtr connector = nullptr; + drmModeCrtcPtr crtc = nullptr; + + connector = drmModeGetConnectorCurrent(drm_fd, resources->connectors[i]); + if(!connector) + continue; + + uint64_t crtc_id = 0; + connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &crtc_id); + if(crtc_id == 0) + goto next_connector; + + crtc = drmModeGetCrtc(drm_fd, crtc_id); + if(!crtc) + goto next_connector; + + drm_connectors->connectors[drm_connectors->num_connectors].crtc_id = crtc_id; + drm_connectors->connectors[drm_connectors->num_connectors].size = mgl::vec2i{(int)crtc->width, (int)crtc->height}; + drm_connectors->connectors[drm_connectors->num_connectors].vrr_enabled = false; + ++drm_connectors->num_connectors; + + next_connector: + if(crtc) + drmModeFreeCrtc(crtc); + + if(connector) + drmModeFreeConnector(connector); + } + + for(int i = 0; i < resources->count_crtcs; ++i) { + drmModeCrtcPtr crtc = nullptr; + drmModeObjectPropertiesPtr properties = nullptr; + uint64_t vrr_enabled = 0; + drm_connector *connector = nullptr; + + crtc = drmModeGetCrtc(drm_fd, resources->crtcs[i]); + if(!crtc) + continue; + + properties = drmModeObjectGetProperties(drm_fd, crtc->crtc_id, DRM_MODE_OBJECT_CRTC); + if(!properties) + goto next_crtc; + + if(!get_drm_property_by_name(drm_fd, properties, "VRR_ENABLED", &vrr_enabled)) + goto next_crtc; + + connector = get_drm_connector_by_crtc_id(drm_connectors, crtc->crtc_id); + if(!connector) + goto next_crtc; + + if(vrr_enabled) { + connector->vrr_enabled = true; + drm_connectors->has_any_crtc_with_vrr_enabled = true; + } + + next_crtc: + if(properties) + drmModeFreeObjectProperties(properties); + + if(crtc) + drmModeFreeCrtc(crtc); + } + + drmModeFreeResources(resources); + } + + CursorTrackerWayland::CursorTrackerWayland(const char *card_path) { + drm_fd = open(card_path, O_RDONLY); + if(drm_fd <= 0) { + fprintf(stderr, "Error: CursorTrackerWayland: failed to open %s\n", card_path); + return; + } + + drmSetClientCap(drm_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1); + drmSetClientCap(drm_fd, DRM_CLIENT_CAP_ATOMIC, 1); + } + + CursorTrackerWayland::~CursorTrackerWayland() { + if(drm_fd > 0) + close(drm_fd); + } + + void CursorTrackerWayland::update() { + if(drm_fd <= 0) + return; + + drm_connectors connectors; + connectors.num_connectors = 0; + connectors.has_any_crtc_with_vrr_enabled = false; + get_drm_connectors(drm_fd, &connectors); + + drmModePlaneResPtr planes = drmModeGetPlaneResources(drm_fd); + if(!planes) + return; + + bool found_cursor = false; + for(uint32_t i = 0; i < planes->count_planes; ++i) { + drmModePlanePtr plane = nullptr; + const drm_connector *connector = nullptr; + int crtc_x = 0; + int crtc_y = 0; + int crtc_id = 0; + uint32_t property_mask = 0; + + plane = drmModeGetPlane(drm_fd, planes->planes[i]); + if(!plane) + goto next; + + if(!plane->fb_id) + goto next; + + property_mask = plane_get_properties(drm_fd, planes->planes[i], &crtc_x, &crtc_y, &crtc_id); + if(property_mask != plane_property_all || crtc_id <= 0) + goto next; + + connector = get_drm_connector_by_crtc_id(&connectors, crtc_id); + if(!connector) + goto next; + + if(crtc_x >= 0 && crtc_x <= connector->size.x && crtc_y >= 0 && crtc_y <= connector->size.y) { + latest_cursor_position.x = crtc_x; + latest_cursor_position.y = crtc_y; + latest_crtc_id = crtc_id; + found_cursor = true; + drmModeFreePlane(plane); + break; + } + + next: + drmModeFreePlane(plane); + } + + // On kde plasma wayland (and possibly other wayland compositors) it uses a software cursor only for the monitors with vrr enabled. + // In that case we cant know the cursor location and we instead want to fallback to getting focused monitor by using the hack of creating a window and getting the position. + if(!found_cursor && latest_crtc_id > 0 && connectors.has_any_crtc_with_vrr_enabled) + latest_crtc_id = -1; + + drmModeFreePlaneResources(planes); + } + + void CursorTrackerWayland::set_monitor_outputs_from_xdg_output(struct wl_display *dpy) { + if(!xdg_output_manager) { + fprintf(stderr, "Warning: CursorTrackerWayland::set_monitor_outputs_from_xdg_output: zxdg_output_manager not found. Registered monitor positions might be incorrect\n"); + return; + } + + for(WaylandOutput &monitor : monitors) { + monitor.xdg_output = zxdg_output_manager_v1_get_xdg_output(xdg_output_manager, monitor.output); + zxdg_output_v1_add_listener(monitor.xdg_output, &xdg_output_listener, &monitor); + } + + // Fetch xdg_output + wl_display_roundtrip(dpy); + } + + std::optional<CursorInfo> CursorTrackerWayland::get_latest_cursor_info() { + if(drm_fd <= 0 || latest_crtc_id == -1) + return std::nullopt; + + std::string monitor_name = get_monitor_name_from_crtc_id(drm_fd, latest_crtc_id); + if(monitor_name.empty()) + return std::nullopt; + + struct wl_display *dpy = wl_display_connect(nullptr); + if(!dpy) { + fprintf(stderr, "Error: CursorTrackerWayland::get_latest_cursor_info: failed to connect to the wayland server\n"); + return std::nullopt; + } + + monitors.clear(); + struct wl_registry *registry = wl_display_get_registry(dpy); + wl_registry_add_listener(registry, ®istry_listener, this); + + // Fetch globals + wl_display_roundtrip(dpy); + + // Fetch wl_output + wl_display_roundtrip(dpy); + + set_monitor_outputs_from_xdg_output(dpy); + + mgl::vec2i cursor_position = latest_cursor_position; + const WaylandOutput *wayland_monitor = get_wayland_monitor_by_name(monitors, monitor_name); + if(!wayland_monitor) + return std::nullopt; + + cursor_position = wayland_monitor->pos + latest_cursor_position; + for(WaylandOutput &monitor : monitors) { + if(monitor.output) { + wl_output_destroy(monitor.output); + monitor.output = nullptr; + } + + if(monitor.xdg_output) { + zxdg_output_v1_destroy(monitor.xdg_output); + monitor.xdg_output = nullptr; + } + } + monitors.clear(); + + if(xdg_output_manager) { + zxdg_output_manager_v1_destroy(xdg_output_manager); + xdg_output_manager = nullptr; + } + + wl_registry_destroy(registry); + wl_display_disconnect(dpy); + + return CursorInfo{ cursor_position, std::move(monitor_name) }; + } +}
\ No newline at end of file diff --git a/src/CursorTracker/CursorTrackerX11.cpp b/src/CursorTracker/CursorTrackerX11.cpp new file mode 100644 index 0000000..7c98f4d --- /dev/null +++ b/src/CursorTracker/CursorTrackerX11.cpp @@ -0,0 +1,29 @@ +#include "../../include/CursorTracker/CursorTrackerX11.hpp" +#include "../../include/WindowUtils.hpp" + +namespace gsr { + CursorTrackerX11::CursorTrackerX11(Display *dpy) : dpy(dpy) { + + } + + std::optional<CursorInfo> CursorTrackerX11::get_latest_cursor_info() { + Window window = None; + const auto cursor_pos = get_cursor_position(dpy, &window); + const auto monitors = get_monitors(dpy); + std::string monitor_name; + + for(const auto &monitor : monitors) { + if(cursor_pos.x >= monitor.position.x && cursor_pos.x <= monitor.position.x + monitor.size.x + && cursor_pos.y >= monitor.position.y && cursor_pos.y <= monitor.position.y + monitor.size.y) + { + monitor_name = monitor.name; + break; + } + } + + if(monitor_name.empty()) + return std::nullopt; + + return CursorInfo{ cursor_pos, std::move(monitor_name) }; + } +}
\ No newline at end of file diff --git a/src/GlobalHotkeys/GlobalHotkeysJoystick.cpp b/src/GlobalHotkeys/GlobalHotkeysJoystick.cpp new file mode 100644 index 0000000..5969438 --- /dev/null +++ b/src/GlobalHotkeys/GlobalHotkeysJoystick.cpp @@ -0,0 +1,392 @@ +#include "../../include/GlobalHotkeys/GlobalHotkeysJoystick.hpp" +#include <string.h> +#include <errno.h> +#include <fcntl.h> +#include <unistd.h> +#include <sys/eventfd.h> + +namespace gsr { + static constexpr int button_pressed = 1; + static constexpr int cross_button = 0; + static constexpr int triangle_button = 2; + static constexpr int options_button = 9; + static constexpr int playstation_button = 10; + static constexpr int l3_button = 11; + static constexpr int r3_button = 12; + static constexpr int axis_up_down = 7; + static constexpr int axis_left_right = 6; + + struct DeviceId { + uint16_t vendor; + uint16_t product; + }; + + static bool read_file_hex_number(const char *path, unsigned int *value) { + *value = 0; + FILE *f = fopen(path, "rb"); + if(!f) + return false; + + fscanf(f, "%x", value); + fclose(f); + return true; + } + + static DeviceId joystick_get_device_id(const char *path) { + DeviceId device_id; + device_id.vendor = 0; + device_id.product = 0; + + const char *js_path_id = nullptr; + const int len = strlen(path); + for(int i = len - 1; i >= 0; --i) { + if(path[i] == '/') { + js_path_id = path + i + 1; + break; + } + } + + if(!js_path_id) + return device_id; + + unsigned int vendor = 0; + unsigned int product = 0; + char path_buf[1024]; + + snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/vendor", js_path_id); + if(!read_file_hex_number(path_buf, &vendor)) + return device_id; + + snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/product", js_path_id); + if(!read_file_hex_number(path_buf, &product)) + return device_id; + + device_id.vendor = vendor; + device_id.product = product; + return device_id; + } + + static bool is_ps4_controller(DeviceId device_id) { + return device_id.vendor == 0x054C && (device_id.product == 0x09CC || device_id.product == 0x0BA0 || device_id.product == 0x05C4); + } + + static bool is_ps5_controller(DeviceId device_id) { + return device_id.vendor == 0x054C && (device_id.product == 0x0DF2 || device_id.product == 0x0CE6); + } + + static bool is_stadia_controller(DeviceId device_id) { + return device_id.vendor == 0x18D1 && (device_id.product == 0x9400); + } + + // Returns -1 on error + 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"); + } + + if(save_1_min_replay) { + save_1_min_replay = false; + auto it = bound_actions_by_id.find("save_1_min_replay"); + if(it != bound_actions_by_id.end()) + it->second("save_1_min_replay"); + } + + if(save_10_min_replay) { + save_10_min_replay = false; + auto it = bound_actions_by_id.find("save_10_min_replay"); + if(it != bound_actions_by_id.end()) + it->second("save_10_min_replay"); + } + + if(take_screenshot) { + take_screenshot = false; + auto it = bound_actions_by_id.find("take_screenshot"); + if(it != bound_actions_by_id.end()) + it->second("take_screenshot"); + } + + if(toggle_record) { + toggle_record = false; + auto it = bound_actions_by_id.find("toggle_record"); + if(it != bound_actions_by_id.end()) + it->second("toggle_record"); + } + + if(toggle_replay) { + toggle_replay = false; + auto it = bound_actions_by_id.find("toggle_replay"); + if(it != bound_actions_by_id.end()) + it->second("toggle_replay"); + } + + if(toggle_show) { + toggle_show = false; + auto it = bound_actions_by_id.find("toggle_show"); + if(it != bound_actions_by_id.end()) + it->second("toggle_show"); + } + } + + 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) == JS_EVENT_BUTTON) { + switch(event.number) { + case playstation_button: { + // Workaround weird steam input (in-game) behavior where steam triggers playstation button + options when pressing both l3 and r3 at the same time + playstation_button_pressed = (event.value == button_pressed) && !l3_button_pressed && !r3_button_pressed; + break; + } + case options_button: { + if(playstation_button_pressed && event.value == button_pressed) + toggle_show = true; + break; + } + case cross_button: { + if(playstation_button_pressed && event.value == button_pressed) + save_1_min_replay = true; + break; + } + case triangle_button: { + if(playstation_button_pressed && event.value == button_pressed) + save_10_min_replay = true; + break; + } + case l3_button: { + l3_button_pressed = event.value == button_pressed; + break; + } + case r3_button: { + r3_button_pressed = event.value == button_pressed; + break; + } + } + } else if((event.type & JS_EVENT_AXIS) == JS_EVENT_AXIS && playstation_button_pressed) { + const int trigger_threshold = 16383; + const bool prev_up_pressed = up_pressed; + const bool prev_down_pressed = down_pressed; + const bool prev_left_pressed = left_pressed; + const bool prev_right_pressed = right_pressed; + + if(event.number == axis_up_down) { + up_pressed = event.value <= -trigger_threshold; + down_pressed = event.value >= trigger_threshold; + } else if(event.number == axis_left_right) { + left_pressed = event.value <= -trigger_threshold; + right_pressed = event.value >= trigger_threshold; + } + + if(up_pressed && !prev_up_pressed) + take_screenshot = true; + else if(down_pressed && !prev_down_pressed) + save_replay = true; + else if(left_pressed && !prev_left_pressed) + toggle_record = true; + else if(right_pressed && !prev_right_pressed) + toggle_replay = true; + } + } + + 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 + }; + + //const DeviceId device_id = joystick_get_device_id(dev_input_filepath); + + ++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/GlobalHotkeys/GlobalHotkeysLinux.cpp b/src/GlobalHotkeys/GlobalHotkeysLinux.cpp new file mode 100644 index 0000000..a56bbc6 --- /dev/null +++ b/src/GlobalHotkeys/GlobalHotkeysLinux.cpp @@ -0,0 +1,275 @@ +#include "../../include/GlobalHotkeys/GlobalHotkeysLinux.hpp" +#include <sys/wait.h> +#include <fcntl.h> +#include <limits.h> +#include <string.h> + +extern "C" { +#include <mgl/mgl.h> +} +#include <X11/Xlib.h> +#include <X11/keysym.h> +#include <linux/input-event-codes.h> + +#define PIPE_READ 0 +#define PIPE_WRITE 1 + +namespace gsr { + 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; + } + + static bool x11_key_is_alpha_numerical(KeySym keysym) { + return (keysym >= XK_A && keysym <= XK_Z) || (keysym >= XK_a && keysym <= XK_z) || (keysym >= XK_0 && keysym <= XK_9); + } + + GlobalHotkeysLinux::GlobalHotkeysLinux(GrabType grab_type) : grab_type(grab_type) { + for(int i = 0; i < 2; ++i) { + read_pipes[i] = -1; + write_pipes[i] = -1; + } + } + + GlobalHotkeysLinux::~GlobalHotkeysLinux() { + if(write_pipes[PIPE_WRITE] > 0) { + char command[32]; + const int command_size = snprintf(command, sizeof(command), "exit\n"); + if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) { + fprintf(stderr, "Error: GlobalHotkeysLinux::~GlobalHotkeysLinux: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno)); + close_fds(); + } + } else { + close_fds(); + } + + if(process_id > 0) { + int status; + waitpid(process_id, &status, 0); + } + + close_fds(); + } + + void GlobalHotkeysLinux::close_fds() { + for(int i = 0; i < 2; ++i) { + if(read_pipes[i] > 0) { + close(read_pipes[i]); + read_pipes[i] = -1; + } + + if(write_pipes[i] > 0) { + close(write_pipes[i]); + write_pipes[i] = -1; + } + } + + if(read_file) { + fclose(read_file); + read_file = nullptr; + } + } + + 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(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(read_pipes[i]); + close(write_pipes[i]); + read_pipes[i] = -1; + write_pipes[i] = -1; + } + return false; + } else if(pid == 0) { /* child */ + dup2(read_pipes[PIPE_WRITE], STDOUT_FILENO); + for(int i = 0; i < 2; ++i) { + close(read_pipes[i]); + } + + dup2(write_pipes[PIPE_READ], STDIN_FILENO); + for(int i = 0; i < 2; ++i) { + close(write_pipes[i]); + } + + if(inside_flatpak) { + const char *args[] = { "flatpak-spawn", "--host", "/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(read_pipes[PIPE_WRITE]); + read_pipes[PIPE_WRITE] = -1; + + close(write_pipes[PIPE_READ]); + write_pipes[PIPE_READ] = -1; + + 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) + read_pipes[PIPE_READ] = -1; + else + fprintf(stderr, "fdopen failed for read, error: %s\n", strerror(errno)); + } + + return true; + } + + 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 && x11_key_is_alpha_numerical(hotkey.key)) { + //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]; + int command_size = 0; + if(modifiers_command.empty()) + command_size = snprintf(command, sizeof(command), "bind %s %d\n", id.c_str(), (int)keycode); + else + 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"); + return; + } + + if(!read_file) { + //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; + + int line_len = strlen(line); + if(line_len == 0) + continue; + + if(line[line_len - 1] == '\n') { + line[line_len - 1] = '\0'; + --line_len; + } + + 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/GlobalHotkeys/GlobalHotkeysX11.cpp index 6b01bfd..bc79ce8 100644 --- a/src/GlobalHotkeysX11.cpp +++ b/src/GlobalHotkeys/GlobalHotkeysX11.cpp @@ -1,6 +1,7 @@ -#include "../include/GlobalHotkeysX11.hpp" -#define XK_MISCELLANY -#include <X11/keysymdef.h> +#include "../../include/GlobalHotkeys/GlobalHotkeysX11.hpp" +#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/GlobalHotkeysLinux.cpp b/src/GlobalHotkeysLinux.cpp deleted file mode 100644 index b0e8e52..0000000 --- a/src/GlobalHotkeysLinux.cpp +++ /dev/null @@ -1,110 +0,0 @@ -#include "../include/GlobalHotkeysLinux.hpp" -#include <signal.h> -#include <sys/wait.h> -#include <fcntl.h> -#include <string.h> - -#define PIPE_READ 0 -#define PIPE_WRITE 1 - -namespace gsr { - GlobalHotkeysLinux::GlobalHotkeysLinux() { - for(int i = 0; i < 2; ++i) { - pipes[i] = -1; - } - } - - GlobalHotkeysLinux::~GlobalHotkeysLinux() { - for(int i = 0; i < 2; ++i) { - if(pipes[i] > 0) - close(pipes[i]); - } - - if(read_file) - fclose(read_file); - - if(process_id > 0) { - kill(process_id, SIGKILL); - int status; - waitpid(process_id, &status, 0); - } - } - - bool GlobalHotkeysLinux::start() { - if(process_id > 0) - return false; - - if(pipe(pipes) == -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; - } - return false; - } else if(pid == 0) { /* child */ - dup2(pipes[PIPE_WRITE], STDOUT_FILENO); - for(int i = 0; i < 2; ++i) { - close(pipes[i]); - } - - const char *args[] = { "gsr-global-hotkeys", NULL }; - execvp(args[0], (char* const*)args); - perror("execvp"); - _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); - - read_file = fdopen(pipes[PIPE_READ], "r"); - if(read_file) - pipes[PIPE_READ] = -1; - else - fprintf(stderr, "fdopen failed, 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; - } - - 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"); - return; - } - - if(!read_file) { - fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, read file hasn't opened\n"); - return; - } - - char buffer[256]; - while(true) { - char *line = fgets(buffer, sizeof(buffer), read_file); - if(!line) - break; - - const int line_len = strlen(line); - if(line_len == 0) - continue; - - if(line[line_len - 1] == '\n') - line[line_len - 1] = '\0'; - - const std::string action = line; - auto it = bound_actions_by_id.find(action); - if(it != bound_actions_by_id.end()) - it->second(action); - } - } -} diff --git a/src/GsrInfo.cpp b/src/GsrInfo.cpp index 276870b..d7212d7 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,10 @@ 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->value == "broadcom") + gsr_info->gpu_info.vendor = GpuVendor::BROADCOM; + } else if(key_value->key == "card_path") { + gsr_info->gpu_info.card_path = key_value->value; } } @@ -64,36 +159,11 @@ 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())); - } + static void parse_image_formats_line(GsrInfo *gsr_info, std::string_view line) { + if(line == "jpeg") + gsr_info->supported_image_formats.jpeg = true; + else if(line == "png") + gsr_info->supported_image_formats.png = true; } enum class GsrInfoSection { @@ -101,34 +171,26 @@ namespace gsr { SYSTEM_INFO, GPU_INFO, VIDEO_CODECS, + IMAGE_FORMATS, CAPTURE_OPTIONS }; - static bool starts_with(std::string_view str, const char *substr) { - size_t len = strlen(substr); - return str.size() >= len && memcmp(str.data(), substr, len) == 0; - } - 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") @@ -137,6 +199,8 @@ namespace gsr { section = GsrInfoSection::GPU_INFO; else if(section_name == "video_codecs") section = GsrInfoSection::VIDEO_CODECS; + else if(section_name == "image_formats") + section = GsrInfoSection::IMAGE_FORMATS; else if(section_name == "capture_options") section = GsrInfoSection::CAPTURE_OPTIONS; else @@ -160,8 +224,12 @@ namespace gsr { parse_video_codecs_line(gsr_info, line); break; } + case GsrInfoSection::IMAGE_FORMATS: { + parse_image_formats_line(gsr_info, line); + 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 +237,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 +253,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, false) != 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 +273,79 @@ 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 == "region") + capture_options.region = 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"; + case GpuVendor::BROADCOM: return "broadcom"; + } + 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..0f5155c --- /dev/null +++ b/src/Hotplug.cpp @@ -0,0 +1,82 @@ +#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) - 1); + if(bytes_read <= 0) + return; + event_data[bytes_read] = '\0'; + + /* 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..794ef92 100644 --- a/src/Overlay.cpp +++ b/src/Overlay.cpp @@ -7,22 +7,38 @@ #include "../include/gui/DropdownButton.hpp" #include "../include/gui/CustomRendererWidget.hpp" #include "../include/gui/SettingsPage.hpp" +#include "../include/gui/ScreenshotSettingsPage.hpp" +#include "../include/gui/GlobalSettingsPage.hpp" #include "../include/gui/Utils.hpp" #include "../include/gui/PageStack.hpp" +#include "../include/WindowUtils.hpp" +#include "../include/GlobalHotkeys/GlobalHotkeys.hpp" +#include "../include/GlobalHotkeys/GlobalHotkeysLinux.hpp" +#include "../include/CursorTracker/CursorTrackerX11.hpp" +#include "../include/CursorTracker/CursorTrackerWayland.hpp" #include <string.h> #include <assert.h> #include <sys/wait.h> #include <limits.h> #include <fcntl.h> +#include <poll.h> +#include <malloc.h> #include <stdexcept> +#include <algorithm> +#include <inttypes.h> #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/shapeconst.h> +#include <X11/Xcursor/Xcursor.h> #include <mglpp/system/Rect.hpp> #include <mglpp/window/Event.hpp> +#include <mglpp/system/Utf8.hpp> extern "C" { #include <mgl/mgl.h> @@ -31,66 +47,11 @@ extern "C" { 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_status_update_check_timeout_seconds = 1.5; + static const double replay_saving_notification_timeout_seconds = 0.5; + static const double notification_timeout_seconds = 2.5; + static const double notification_error_timeout_seconds = 5.0; + static const double cursor_tracker_update_timeout_sec = 0.1; static mgl::Texture texture_from_ximage(XImage *img) { uint8_t *texture_data = (uint8_t*)malloc(img->width * img->height * 3); @@ -117,6 +78,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 +175,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 +184,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) { @@ -201,95 +209,20 @@ namespace gsr { return false; }*/ - static bool window_is_fullscreen(Display *display, Window window) { - const Atom wm_state_atom = XInternAtom(display, "_NET_WM_STATE", False); - const Atom wm_state_fullscreen_atom = XInternAtom(display, "_NET_WM_STATE_FULLSCREEN", False); - - Atom type = None; - int format = 0; - unsigned long num_items = 0; - unsigned long bytes_after = 0; - unsigned char *properties = nullptr; - if(XGetWindowProperty(display, window, wm_state_atom, 0, 1024, False, XA_ATOM, &type, &format, &num_items, &bytes_after, &properties) < Success) { - fprintf(stderr, "Failed to get window wm state property\n"); - return false; - } - - if(!properties) - return false; - - bool is_fullscreen = false; - Atom *atoms = (Atom*)properties; - for(unsigned long i = 0; i < num_items; ++i) { - if(atoms[i] == wm_state_fullscreen_atom) { - is_fullscreen = true; - break; - } + static const Monitor* find_monitor_at_position(const std::vector<Monitor> &monitors, mgl::vec2i pos) { + for(const Monitor &monitor : monitors) { + if(mgl::IntRect(monitor.position, monitor.size).contains(pos)) + return &monitor; } - - XFree(properties); - 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 - - static Bool set_window_wm_state(Display *dpy, Window window, Atom atom) { - const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False); - - XClientMessageEvent xclient; - memset(&xclient, 0, sizeof(xclient)); - - xclient.type = ClientMessage; - xclient.window = window; - xclient.message_type = net_wm_state_atom; - xclient.format = 32; - xclient.data.l[0] = _NET_WM_STATE_ADD; - xclient.data.l[1] = atom; - xclient.data.l[2] = 0; - xclient.data.l[3] = 0; - xclient.data.l[4] = 0; - - XSendEvent(dpy, DefaultRootWindow(dpy), False, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent*)&xclient); - XFlush(dpy); - return True; - } - - static Bool make_window_sticky(Display *dpy, Window window) { - return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_STICKY", False)); - } - - static Bool hide_window_from_taskbar(Display *dpy, Window window) { - return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_SKIP_TASKBAR", False)); + return nullptr; } - // 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_by_name(const std::vector<Monitor> &monitors, const std::string &name) { + for(const Monitor &monitor : monitors) { + if(monitor.name == name) + 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 nullptr; } static std::string get_power_supply_online_filepath() { @@ -320,17 +253,200 @@ 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 bool is_hyprland_waybar_running_as_dock() { + const char *args[] = { "hyprctl", "layers", nullptr }; + std::string stdout_str; + if(exec_program_on_host_get_stdout(args, stdout_str) != 0) + return false; + + int waybar_layer_level = -1; + int current_layer_level = 0; + string_split_char(stdout_str, '\n', [&](const std::string_view line) { + if(line.find("Layer level 0") != std::string_view::npos) + current_layer_level = 0; + else if(line.find("Layer level 1") != std::string_view::npos) + current_layer_level = 1; + else if(line.find("Layer level 2") != std::string_view::npos) + current_layer_level = 2; + else if(line.find("Layer level 3") != std::string_view::npos) + current_layer_level = 3; + else if(line.find("namespace: waybar") != std::string_view::npos) { + waybar_layer_level = current_layer_level; + return false; + } + return true; + }); + + return waybar_layer_level >= 0 && waybar_layer_level <= 1; + } + + 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), + "toggle_show", [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(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().replay_config.save_1_min_hotkey), + "replay_save_1_min", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay_1_min(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().replay_config.save_10_min_hotkey), + "replay_save_10_min", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay_10_min(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().screenshot_config.take_screenshot_hotkey), + "take_screenshot", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->take_screenshot(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(overlay->get_config().screenshot_config.take_screenshot_region_hotkey), + "take_screenshot_region", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->take_screenshot_region(); + }); + + global_hotkeys->bind_key_press( + config_hotkey_to_hotkey(ConfigHotkey{ mgl::Keyboard::Key::Escape, HOTKEY_MOD_LCTRL | HOTKEY_MOD_LSHIFT | HOTKEY_MOD_LALT }), + "exit", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->go_back_to_old_ui(); + }); + } + + 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("toggle_show", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_show(); + }); + + global_hotkeys_js->bind_action("save_replay", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay(); + }); + + global_hotkeys_js->bind_action("save_1_min_replay", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay_1_min(); + }); + + global_hotkeys_js->bind_action("save_10_min_replay", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->save_replay_10_min(); + }); + + global_hotkeys_js->bind_action("take_screenshot", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->take_screenshot(); + }); + + global_hotkeys_js->bind_action("toggle_record", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_record(); + }); + + global_hotkeys_js->bind_action("toggle_replay", [overlay](const std::string &id) { + fprintf(stderr, "pressed %s\n", id.c_str()); + overlay->toggle_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 +456,35 @@ 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(); + replay_startup_mode = replay_startup_string_to_type(config.replay_config.turn_on_replay_automatically_mode.c_str()); + + 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.replay_config.turn_on_replay_automatically_mode == "turn_on_at_system_startup") - on_press_start_replay(true); + 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"); + + if(this->gsr_info.system_info.display_server == DisplayServer::X11) + cursor_tracker = std::make_unique<CursorTrackerX11>((Display*)mgl_get_context()->connection); + else if(this->gsr_info.system_info.display_server == DisplayServer::WAYLAND && !this->gsr_info.gpu_info.card_path.empty()) + cursor_tracker = std::make_unique<CursorTrackerWayland>(this->gsr_info.gpu_info.card_path.c_str()); } Overlay::~Overlay() { @@ -377,6 +509,144 @@ namespace gsr { } gpu_screen_recorder_process = -1; } + + if(gpu_screen_recorder_screenshot_process > 0) { + kill(gpu_screen_recorder_screenshot_process, SIGINT); + int status; + if(waitpid(gpu_screen_recorder_screenshot_process, &status, 0) == -1) { + perror("waitpid failed"); + /* Ignore... */ + } + gpu_screen_recorder_screenshot_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)window->get_system_handle(); + xi_output_xev->xmotion.subwindow = (Window)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)window->get_system_handle(); + xi_output_xev->xbutton.subwindow = (Window)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)window->get_system_handle(); + xi_output_xev->xkey.subwindow = (Window)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 +667,71 @@ 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(); + + if(cursor_tracker_update_clock.get_elapsed_time_seconds() >= cursor_tracker_update_timeout_sec) { + cursor_tracker_update_clock.restart(); + if(cursor_tracker) + cursor_tracker->update(); + } + + handle_keyboard_mapping_event(); + region_selector.poll_events(); + if(region_selector.take_canceled()) { + on_region_selected = nullptr; + } else if(region_selector.take_selection() && on_region_selected) { + on_region_selected(); + on_region_selected = nullptr; + } + + window_selector.poll_events(); + if(window_selector.take_canceled()) { + on_window_selected = nullptr; + } else if(window_selector.take_selection() && on_window_selected) { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + const Window selected_window = window_selector.get_selection(); + if(selected_window && selected_window != DefaultRootWindow(display)) { + on_window_selected(); + } else { + show_notification("No window selected", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + } + on_window_selected = nullptr; + } + 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 +740,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; @@ -418,10 +750,37 @@ namespace gsr { } bool Overlay::draw() { + remove_widgets_to_be_removed(); + update_notification_process_status(); + process_gsr_output(); update_gsr_process_status(); + update_gsr_screenshot_process_status(); replay_status_update_status(); + if(start_region_capture) { + start_region_capture = false; + hide(); + if(!region_selector.start(get_color_theme().tint_color)) { + show_notification("Failed to start region capture", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + on_region_selected = nullptr; + } + } + + if(start_window_capture) { + start_window_capture = false; + hide(); + if(!window_selector.start(get_color_theme().tint_color)) { + show_notification("Failed to start window capture", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + on_window_selected = nullptr; + } + } + + if(region_selector.is_started() || window_selector.is_started()) { + usleep(5 * 1000); // 5 ms + return true; + } + if(!visible) return false; @@ -433,87 +792,313 @@ 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)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)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); + } + void Overlay::show() { + if(visible) + return; + + if(region_selector.is_started() || window_selector.is_started()) + 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; + const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos; + const bool hyprland_waybar_is_dock = is_hyprland && is_hyprland_waybar_running_as_dock(); + + std::optional<CursorInfo> cursor_info; + if(cursor_tracker) { + cursor_tracker->update(); + cursor_info = cursor_tracker->get_latest_cursor_info(); + } + + // 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; + mgl::vec2i cursor_position = get_cursor_position(display, &x11_cursor_window); + const Monitor *focused_monitor = nullptr; + if(cursor_info) { + focused_monitor = find_monitor_by_name(monitors, cursor_info->monitor_name); + if(!focused_monitor) + focused_monitor = &monitors.front(); + cursor_position = cursor_info->position; + } else { + 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); + focused_monitor = find_monitor_at_position(monitors, monitor_position_query_value); + if(!focused_monitor) + focused_monitor = &monitors.front(); + } + + // Wayland doesn't allow XGrabPointer/XGrabKeyboard when a wayland application is focused. + // If the focused window is a wayland application then don't use override redirect and instead create + // a fullscreen window for the ui. + // TODO: (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor)) + const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots || is_hyprland; + + 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; - - if(!window->create("gsr ui", window_create_params)) + 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.graphics_api = gsr_info.system_info.display_server == DisplayServer::WAYLAND ? MGL_GRAPHICS_API_GLX : MGL_GRAPHICS_API_EGL; + + if(!window->create("gsr ui", window_create_params)) { fprintf(stderr, "error: failed to create window\n"); + window.reset(); + return; + } - 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); + XChangeProperty(display, (Window)window->get_system_handle(), XInternAtom(display, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1); data = 1; - XChangeProperty(display, window->get_system_handle(), XInternAtom(display, "GAMESCOPE_EXTERNAL_OVERLAY", False), XA_CARDINAL, 32, PropModeReplace, &data, 1); + XChangeProperty(display, (Window)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); + window.reset(); + return; + } + get_theme().set_window_size(window_size); + + if(prevent_game_minimizing) { + window->set_size(window_size); + window->set_size_limits(window_size, window_size); } + window->set_position(focused_monitor->position + focused_monitor->size / 2 - original_window_size / 2); mgl_window *win = window->internal_window(); - if(win->num_monitors == 0) { - fprintf(stderr, "gsr warning: no monitors found, not showing overlay\n"); - return; + win->cursor_position.x = cursor_position.x - window_pos.x; + win->cursor_position.y = cursor_position.y - window_pos.y; + + update_compositor_texture(*focused_monitor); + + create_frontpage_ui_components(); + + // 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)window->get_system_handle()); + + window->set_visible(true); + + make_window_sticky(display, (Window)window->get_system_handle()); + hide_window_from_taskbar(display, (Window)window->get_system_handle()); + + if(default_cursor) { + XFreeCursor(display, default_cursor); + default_cursor = 0; } + default_cursor = XCreateFontCursor(display, XC_left_ptr); + XFlush(display); - 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); + grab_mouse_and_keyboard(); - window->set_size(window_size); - window->set_size_limits(window_size, window_size); - window->set_position(window_pos); + // The real cursor doesn't move when all devices are grabbed, so we create our own cursor and diplay that while grabbed + cursor_hotspot = {0, 0}; + xi_setup_fake_cursor(); + if(cursor_info && gsr_info.system_info.display_server == DisplayServer::WAYLAND) { + win->cursor_position.x += cursor_hotspot.x; + win->cursor_position.y += cursor_hotspot.y; + } - update_compositor_texture(focused_monitor); + // 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(xi_display); - top_bar_text = mgl::Text("GPU Screen Recorder", get_theme().top_bar_font); - logo_sprite = mgl::Sprite(&get_theme().logo_texture); + if(!is_wlroots && !hyprland_waybar_is_dock) + window->set_fullscreen(true); + + visible = true; + if(gpu_screen_recorder_process > 0) { + switch(recording_status) { + case RecordingStatus::NONE: + break; + case RecordingStatus::REPLAY: + update_ui_replay_started(); + break; + case RecordingStatus::RECORD: + update_ui_recording_started(); + break; + case RecordingStatus::STREAM: + update_ui_streaming_started(); + break; + } + } + + if(paused) + update_ui_recording_paused(); + + if(replay_recording) + update_ui_recording_started(); + + // 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::create_frontpage_ui_components() { 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()); top_bar_text = mgl::Text("GPU Screen Recorder", get_theme().top_bar_font); @@ -545,62 +1130,93 @@ 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, mgl::vec2f(button_width, button_height)); replay_dropdown_button_ptr = button.get(); - button->add_item("Turn on", "start", "Alt+Shift+F10"); - button->add_item("Save", "save", "Alt+F10"); + button->add_item("Turn on", "start", config.replay_config.start_stop_hotkey.to_string(false, false)); + button->add_item("Save", "save", config.replay_config.save_hotkey.to_string(false, false)); + if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) { + button->add_item("Save 1 min", "save_1_min", config.replay_config.save_1_min_hotkey.to_string(false, false)); + button->add_item("Save 10 min", "save_10_min", config.replay_config.save_10_min_hotkey.to_string(false, false)); + } 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("save_1_min", &get_theme().save_texture); + button->set_item_icon("save_10_min", &get_theme().save_texture); + button->set_item_icon("settings", &get_theme().settings_extra_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]() { + replay_startup_mode = replay_startup_string_to_type(config.replay_config.turn_on_replay_automatically_mode.c_str()); + if(recording_status == RecordingStatus::REPLAY) + show_notification("Replay settings have been modified.\nYou may need to restart replay to apply the changes.", notification_timeout_seconds, 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(); + } else if(id == "save_1_min") { + on_press_save_replay_1_min_replay(); + } else if(id == "save_10_min") { + on_press_save_replay_10_min_replay(); } else if(id == "start") { - on_press_start_replay(false); + on_press_start_replay(false, false); } }; + button->set_item_enabled("save", false); + button->set_item_enabled("save_1_min", false); + button->set_item_enabled("save_10_min", false); main_buttons_list->add_widget(std::move(button)); } { auto button = std::make_unique<DropdownButton>(&get_theme().title_font, &get_theme().body_font, "Record", "Not recording", &get_theme().record_button_texture, mgl::vec2f(button_width, button_height)); record_dropdown_button_ptr = button.get(); - button->add_item("Start", "start", "Alt+F9"); - button->add_item("Pause", "pause", "Alt+F7"); + button->add_item("Start", "start", config.record_config.start_stop_hotkey.to_string(false, false)); + button->add_item("Pause", "pause", config.record_config.pause_unpause_hotkey.to_string(false, false)); 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_extra_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.", notification_timeout_seconds, 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(); } else if(id == "start") { - on_press_start_record(); + on_press_start_record(false); } }; + button->set_item_enabled("pause", false); main_buttons_list->add_widget(std::move(button)); } { auto button = std::make_unique<DropdownButton>(&get_theme().title_font, &get_theme().body_font, "Livestream", "Not streaming", &get_theme().stream_button_texture, mgl::vec2f(button_width, button_height)); stream_dropdown_button_ptr = button.get(); - button->add_item("Start", "start", "Alt+F8"); + button->add_item("Start", "start", config.streaming_config.start_stop_hotkey.to_string(false, false)); button->add_item("Settings", "settings"); button->set_item_icon("start", &get_theme().play_texture); + button->set_item_icon("settings", &get_theme().settings_extra_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.", notification_timeout_seconds, 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(); + on_press_start_stream(false); } }; main_buttons_list->add_widget(std::move(button)); @@ -610,6 +1226,86 @@ 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.33f; + 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.", 7.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", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::NONE); + else + show_notification("Failed to remove GPU Screen Recorder from system startup", notification_timeout_seconds, 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(); + }; + + settings_page->on_page_closed = [this]() { + replay_dropdown_button_ptr->set_item_description("start", config.replay_config.start_stop_hotkey.to_string(false, false)); + replay_dropdown_button_ptr->set_item_description("save", config.replay_config.save_hotkey.to_string(false, false)); + replay_dropdown_button_ptr->set_item_description("save_1_min", config.replay_config.save_1_min_hotkey.to_string(false, false)); + replay_dropdown_button_ptr->set_item_description("save_10_min", config.replay_config.save_10_min_hotkey.to_string(false, false)); + + record_dropdown_button_ptr->set_item_description("start", config.record_config.start_stop_hotkey.to_string(false, false)); + record_dropdown_button_ptr->set_item_description("pause", config.record_config.pause_unpause_hotkey.to_string(false, false)); + + stream_dropdown_button_ptr->set_item_description("start", config.streaming_config.start_stop_hotkey.to_string(false, false)); + }; + + page_stack.push(std::move(settings_page)); + }; + front_page_ptr->add_widget(std::move(button)); + } + + { + const mgl::vec2f main_buttons_size = main_buttons_list_ptr->get_size(); + const int settings_button_size = main_buttons_size.y * 0.33f; + 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*2) + 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().screenshot_texture); + button->set_icon_padding_scale(1.2f); + button->on_click = [&]() { + auto screenshot_settings_page = std::make_unique<ScreenshotSettingsPage>(&gsr_info, config, &page_stack); + page_stack.push(std::move(screenshot_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); @@ -636,70 +1332,19 @@ namespace gsr { } return true; }; - - window->set_fullscreen(true); - window->set_visible(true); - make_window_sticky(display, window->get_system_handle()); - hide_window_from_taskbar(display, window->get_system_handle()); - - if(default_cursor) { - XFreeCursor(display, default_cursor); - default_cursor = 0; - } - default_cursor = XCreateFontCursor(display, XC_arrow); - - // 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 - - // 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); - - set_focused_window(display, window->get_system_handle()); - XFlush(display); - - //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: - break; - case RecordingStatus::REPLAY: - update_ui_replay_started(); - break; - case RecordingStatus::RECORD: - update_ui_recording_started(); - break; - case RecordingStatus::STREAM: - update_ui_streaming_started(); - break; - } - } - - if(paused) - update_ui_recording_paused(); } void Overlay::hide() { + if(!visible) + return; + mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; while(!page_stack.empty()) { page_stack.pop(); } + remove_widgets_to_be_removed(); if(default_cursor) { XFreeCursor(display, default_cursor); @@ -710,29 +1355,83 @@ 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; + start_region_capture = false; + start_window_capture = 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) { + 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); + xi_warp_all_mouse_devices(xi_display, new_cursor_position); + XFlush(display); + + XFixesShowCursor(display, DefaultRootWindow(display)); + XFlush(display); + } + + XCloseDisplay(xi_display); + xi_display = nullptr; + } + 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(); } deinit_theme(); + malloc_trim(0); } 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() { - on_press_start_record(); + on_press_start_record(false); } void Overlay::toggle_pause() { @@ -741,10 +1440,12 @@ namespace gsr { if(paused) { update_ui_recording_unpaused(); - show_notification("Recording has been unpaused", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); + if(config.record_config.show_video_paused_notifications) + show_notification("Recording has been unpaused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); } else { update_ui_recording_paused(); - show_notification("Recording has been paused", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); + if(config.record_config.show_video_paused_notifications) + show_notification("Recording has been paused", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); } kill(gpu_screen_recorder_process, SIGUSR2); @@ -752,60 +1453,268 @@ namespace gsr { } void Overlay::toggle_stream() { - on_press_start_stream(); + on_press_start_stream(false); } void Overlay::toggle_replay() { - on_press_start_replay(false); + on_press_start_replay(false, false); } void Overlay::save_replay() { on_press_save_replay(); } + void Overlay::save_replay_1_min() { + on_press_save_replay_1_min_replay(); + } + + void Overlay::save_replay_10_min() { + on_press_save_replay_10_min_replay(); + } + + void Overlay::take_screenshot() { + on_press_take_screenshot(false, false); + } + + void Overlay::take_screenshot_region() { + on_press_take_screenshot(false, true); + } + static const char* notification_type_to_string(NotificationType notification_type) { switch(notification_type) { - case NotificationType::NONE: return nullptr; - case NotificationType::RECORD: return "record"; - case NotificationType::REPLAY: return "replay"; - case NotificationType::STREAM: return "stream"; + case NotificationType::NONE: return nullptr; + case NotificationType::RECORD: return "record"; + case NotificationType::REPLAY: return "replay"; + case NotificationType::STREAM: return "stream"; + case NotificationType::SCREENSHOT: return "screenshot"; } return nullptr; } - void Overlay::show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type) { + static void truncate_string(std::string &str, int max_length) { + int index = 0; + size_t byte_index = 0; + + while(index < max_length && byte_index < str.size()) { + uint32_t codepoint = 0; + size_t codepoint_length = 0; + mgl::utf8_decode((const unsigned char*)str.c_str() + byte_index, str.size() - byte_index, &codepoint, &codepoint_length); + if(codepoint_length == 0) + codepoint_length = 1; + + index += 1; + byte_index += codepoint_length; + } + + if(byte_index < str.size()) { + str.erase(byte_index); + str += "..."; + } + } + + static bool is_hex_num(char c) { + return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'); + } + + static bool contains_non_hex_number(const char *str) { + bool hex_start = false; + size_t len = strlen(str); + if(len >= 2 && memcmp(str, "0x", 2) == 0) { + str += 2; + len -= 2; + hex_start = true; + } + + bool is_hex = false; + for(size_t i = 0; i < len; ++i) { + char c = str[i]; + if(c == '\0') + return false; + if(!is_hex_num(c)) + return true; + if((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) + is_hex = true; + } + + return is_hex && !hex_start; + } + + static bool is_number(const char *str) { + const char *p = str; + while(*p) { + char c = *p; + if(c < '0' || c > '9') + return false; + ++p; + } + return true; + } + + static bool is_capture_target_monitor(const char *capture_target) { + return strcmp(capture_target, "window") != 0 && strcmp(capture_target, "focused") != 0 && strcmp(capture_target, "region") != 0 && strcmp(capture_target, "portal") != 0 && contains_non_hex_number(capture_target); + } + + static std::string capture_target_get_notification_name(const char *capture_target) { + std::string result; + if(is_capture_target_monitor(capture_target)) { + result = "this monitor"; + } else if(is_number(capture_target)) { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + int64_t window_id = None; + sscanf(capture_target, "%" PRIi64, &window_id); + + const std::optional<std::string> window_title = get_window_title(display, window_id); + if(window_title) { + result = strip(window_title.value()); + truncate_string(result, 20); + result = "window \"" + result + "\""; + } else { + result = std::string("window ") + capture_target; + } + } else { + result = capture_target; + } + return result; + } + + static std::string get_valid_monitor_x11(const std::string &target_monitor_name, const std::vector<Monitor> &monitors) { + std::string target_monitor_name_clean = target_monitor_name; + if(starts_with(target_monitor_name_clean, "HDMI-A")) + target_monitor_name_clean.replace(0, 6, "HDMI"); + + for(const Monitor &monitor : monitors) { + std::string monitor_name_clean = monitor.name; + if(starts_with(monitor_name_clean, "HDMI-A")) + monitor_name_clean.replace(0, 6, "HDMI"); + + if(target_monitor_name_clean == monitor_name_clean) + return monitor.name; + } + + return ""; + } + + static std::string get_focused_monitor_by_cursor(CursorTracker *cursor_tracker, const GsrInfo &gsr_info, const std::vector<Monitor> &x11_monitors) { + std::optional<CursorInfo> cursor_info; + if(cursor_tracker) { + cursor_tracker->update(); + cursor_info = cursor_tracker->get_latest_cursor_info(); + } + + std::string focused_monitor_name; + if(cursor_info) { + focused_monitor_name = std::move(cursor_info->monitor_name); + } else { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + Window x11_cursor_window = None; + 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(x11_monitors, monitor_position_query_value); + if(focused_monitor) + focused_monitor_name = focused_monitor->name; + } + + return focused_monitor_name; + } + + void Overlay::show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target) { char timeout_seconds_str[32]; snprintf(timeout_seconds_str, sizeof(timeout_seconds_str), "%f", timeout_seconds); const std::string icon_color_str = color_to_hex_str(icon_color); const std::string bg_color_str = color_to_hex_str(bg_color); - const char *notification_args[12] = { + const char *notification_args[14] = { "gsr-notify", "--text", str, "--timeout", timeout_seconds_str, "--icon-color", icon_color_str.c_str(), "--bg-color", bg_color_str.c_str(), }; + int arg_index = 9; 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[11] = nullptr; - } else { - notification_args[9] = nullptr; + notification_args[arg_index++] = "--icon"; + notification_args[arg_index++] = notification_type_str; + } + + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + + std::string monitor_name; + const auto monitors = get_monitors(display); + + if(capture_target && is_capture_target_monitor(capture_target)) + monitor_name = capture_target; + else + monitor_name = get_focused_monitor_by_cursor(cursor_tracker.get(), gsr_info, monitors); + + monitor_name = get_valid_monitor_x11(monitor_name, monitors); + if(!monitor_name.empty()) { + notification_args[arg_index++] = "--monitor"; + notification_args[arg_index++] = monitor_name.c_str(); + } else if(!monitors.empty()) { + notification_args[arg_index++] = "--monitor"; + notification_args[arg_index++] = monitors.front().name.c_str(); } + notification_args[arg_index++] = nullptr; + if(notification_process > 0) { kill(notification_process, SIGKILL); int status = 0; 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; + } + + void Overlay::go_back_to_old_ui() { + const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; + if(inside_flatpak) + exit_reason = "back-to-old-ui"; + else + exit_reason = "exit"; + + const char *args[] = { "systemctl", "disable", "--user", "gpu-screen-recorder-ui", nullptr }; + std::string stdout_str; + exec_program_on_host_get_stdout(args, stdout_str); + exit(); + } + + 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,63 +1728,237 @@ 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; + } + + 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)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); + const char *capture_target = nullptr; + char msg[512]; + + switch(notification_type) { + case NotificationType::RECORD: { + if(!config.record_config.show_video_saved_notifications) + return; + + snprintf(msg, sizeof(msg), "Saved a recording of %s to \"%s\"", capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str()); + capture_target = recording_capture_target.c_str(); + break; + } + case NotificationType::REPLAY: { + if(!config.replay_config.show_replay_saved_notifications) + return; + + char duration[32]; + if(replay_save_duration_min > 0) + snprintf(duration, sizeof(duration), " %d minute ", replay_save_duration_min); + else + snprintf(duration, sizeof(duration), " "); + + snprintf(msg, sizeof(msg), "Saved a%sreplay of %s to \"%s\"", duration, capture_target_get_notification_name(recording_capture_target.c_str()).c_str(), focused_window_name.c_str()); + capture_target = recording_capture_target.c_str(); + break; + } + case NotificationType::SCREENSHOT: { + if(!config.screenshot_config.show_screenshot_saved_notifications) + return; + + snprintf(msg, sizeof(msg), "Saved a screenshot of %s to \"%s\"", capture_target_get_notification_name(screenshot_capture_target.c_str()).c_str(), focused_window_name.c_str()); + capture_target = screenshot_capture_target.c_str(); + break; + } + case NotificationType::NONE: + case NotificationType::STREAM: + break; + } + show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, notification_type, capture_target); + } + + static NotificationType recording_status_to_notification_type(RecordingStatus recording_status) { + switch(recording_status) { + case RecordingStatus::NONE: return NotificationType::NONE; + case RecordingStatus::REPLAY: return NotificationType::REPLAY; + case RecordingStatus::RECORD: return NotificationType::RECORD; + case RecordingStatus::STREAM: return NotificationType::STREAM; + } + return NotificationType::NONE; + } + + 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 if(config.replay_config.show_replay_saved_notifications) { + char duration[32]; + if(replay_save_duration_min > 0) + snprintf(duration, sizeof(duration), " %d minute ", replay_save_duration_min); + else + snprintf(duration, sizeof(duration), " "); + + char msg[512]; + snprintf(msg, sizeof(msg), "Saved a%sreplay of %s", duration, capture_target_get_notification_name(recording_capture_target.c_str()).c_str()); + show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str()); + } + } + + void Overlay::process_gsr_output() { + 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", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + } + + if(gpu_screen_recorder_process_output_file) { + char buffer[1024]; + char *line = fgets(buffer, sizeof(buffer), gpu_screen_recorder_process_output_file); + if(!line || line[0] == '\0') + return; + + const int line_len = strlen(line); + if(line[line_len - 1] == '\n') + line[line_len - 1] = '\0'; + + if(starts_with({line, (size_t)line_len}, "Error: ")) { + show_notification(line + 7, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), recording_status_to_notification_type(recording_status)); + return; + } + + const std::string video_filepath = filepath_get_filename(line); + if(starts_with(video_filepath, "Video_")) { + on_stop_recording(0, line); + return; + } + + switch(recording_status) { + case RecordingStatus::NONE: + break; + case RecordingStatus::REPLAY: + on_replay_saved(line); + break; + case RecordingStatus::RECORD: + break; + case RecordingStatus::STREAM: + break; + } + } else if(gpu_screen_recorder_process_output_fd > 0) { + char buffer[1024]; + read(gpu_screen_recorder_process_output_fd, buffer, sizeof(buffer)); + } + } + + void Overlay::on_gsr_process_error(int exit_code, NotificationType notification_type) { + fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code); + if(exit_code == 50) { + show_notification("Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type); + } else if(exit_code == 60) { + show_notification("Stopped capture because the user canceled the desktop portal", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type); + } else { + const char *prefix = ""; + switch(notification_type) { + case NotificationType::NONE: + case NotificationType::SCREENSHOT: + break; + case NotificationType::RECORD: + prefix = "Failed to start/save recording"; + break; + case NotificationType::REPLAY: + prefix = "Replay stopped because of an error"; + break; + case NotificationType::STREAM: + prefix = "Streaming stopped because of an error"; + break; + } + + char msg[256]; + snprintf(msg, sizeof(msg), "%s. Verify if settings are correct", prefix); + show_notification(msg, notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type); + } + } + 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: break; case RecordingStatus::REPLAY: { + replay_save_duration_min = 0; update_ui_replay_stopped(); if(exit_code == 0) { if(config.replay_config.show_replay_stopped_notifications) - show_notification("Replay stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + show_notification("Replay stopped", notification_timeout_seconds, 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); + on_gsr_process_error(exit_code, 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, record_filepath); break; } case RecordingStatus::STREAM: { update_ui_streaming_stopped(); if(exit_code == 0) { if(config.streaming_config.show_streaming_stopped_notifications) - show_notification("Streaming has stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); + show_notification("Streaming has stopped", notification_timeout_seconds, 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); + on_gsr_process_error(exit_code, NotificationType::STREAM); } break; } @@ -885,6 +1968,60 @@ namespace gsr { recording_status = RecordingStatus::NONE; } + void Overlay::update_gsr_screenshot_process_status() { + if(gpu_screen_recorder_screenshot_process <= 0) + return; + + int status; + if(waitpid(gpu_screen_recorder_screenshot_process, &status, WNOHANG) == 0) { + // Still running + return; + } + + int exit_code = -1; + if(WIFEXITED(status)) + exit_code = WEXITSTATUS(status); + + if(exit_code == 0) { + if(config.screenshot_config.save_screenshot_in_game_folder) { + save_video_in_current_game_directory(screenshot_filepath.c_str(), NotificationType::SCREENSHOT); + } else if(config.screenshot_config.show_screenshot_saved_notifications) { + char msg[512]; + snprintf(msg, sizeof(msg), "Saved a screenshot of %s", capture_target_get_notification_name(screenshot_capture_target.c_str()).c_str()); + show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT, screenshot_capture_target.c_str()); + } + } else { + fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_screenshot_process, exit_code); + show_notification("Failed to take a screenshot. Verify if settings are correct", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT); + } + + gpu_screen_recorder_screenshot_process = -1; + } + + static bool are_all_audio_tracks_available_to_capture(const std::vector<AudioTrack> &audio_tracks) { + const auto audio_devices = get_audio_devices(); + for(const AudioTrack &audio_track : audio_tracks) { + for(const std::string &audio_input : audio_track.audio_inputs) { + std::string_view audio_track_name(audio_input.c_str()); + const bool is_app_audio = starts_with(audio_track_name, "app:"); + if(is_app_audio) + continue; + + if(starts_with(audio_track_name, "device:")) + audio_track_name.remove_prefix(7); + + auto it = std::find_if(audio_devices.begin(), audio_devices.end(), [&](const auto &audio_device) { + return audio_device.name == audio_track_name; + }); + if(it == audio_devices.end()) { + //fprintf(stderr, "Audio not ready\n"); + return false; + } + } + } + return true; + } + void Overlay::replay_status_update_status() { if(replay_status_update_clock.get_elapsed_time_seconds() < replay_status_update_check_timeout_seconds) return; @@ -892,41 +2029,71 @@ namespace gsr { replay_status_update_clock.restart(); update_focused_fullscreen_status(); update_power_supply_status(); + update_system_startup_status(); } void Overlay::update_focused_fullscreen_status() { - if(config.replay_config.turn_on_replay_automatically_mode != "turn_on_at_fullscreen") + if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_FULLSCREEN) return; mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; - const Window focused_window = get_focused_window(display); - if(window && focused_window == window->get_system_handle()) + const Window focused_window = get_focused_window(display, WindowCaptureType::FOCUSED); + if(window && focused_window == (Window)window->get_system_handle()) return; const bool prev_focused_window_is_fullscreen = focused_window_is_fullscreen; focused_window_is_fullscreen = focused_window != 0 && window_is_fullscreen(display, focused_window); if(focused_window_is_fullscreen != prev_focused_window_is_fullscreen) { - if(recording_status == RecordingStatus::NONE && focused_window_is_fullscreen) - on_press_start_replay(false); - else if(recording_status == RecordingStatus::REPLAY && !focused_window_is_fullscreen) - on_press_start_replay(true); + if(recording_status == RecordingStatus::NONE && focused_window_is_fullscreen) { + if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list)) + on_press_start_replay(false, false); + } else if(recording_status == RecordingStatus::REPLAY && !focused_window_is_fullscreen) { + on_press_start_replay(true, false); + } } } + // 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") + if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED) return; const bool prev_power_supply_status = power_supply_connected; power_supply_connected = power_supply_online_filepath.empty() || power_supply_is_connected(power_supply_online_filepath.c_str()); if(power_supply_connected != prev_power_supply_status) { - if(recording_status == RecordingStatus::NONE && power_supply_connected) - on_press_start_replay(false); - else if(recording_status == RecordingStatus::REPLAY && !power_supply_connected) - on_press_start_replay(false); + if(recording_status == RecordingStatus::NONE && power_supply_connected) { + if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list)) + on_press_start_replay(false, false); + } else if(recording_status == RecordingStatus::REPLAY && !power_supply_connected) { + on_press_start_replay(false, false); + } + } + } + + void Overlay::update_system_startup_status() { + if(replay_startup_mode != ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP || recording_status != RecordingStatus::NONE || !try_replay_startup) + return; + + if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks_list)) + on_press_start_replay(true, false); + } + + void Overlay::on_stop_recording(int exit_code, const std::string &video_filepath) { + if(exit_code == 0) { + if(config.record_config.save_video_in_game_folder) { + save_video_in_current_game_directory(video_filepath.c_str(), NotificationType::RECORD); + } else if(config.record_config.show_video_saved_notifications) { + char msg[512]; + snprintf(msg, sizeof(msg), "Saved a recording of %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str()); + show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str()); + } + } else { + on_gsr_process_error(exit_code, NotificationType::RECORD); } + update_ui_recording_stopped(); + replay_recording = false; } void Overlay::update_ui_recording_paused() { @@ -955,6 +2122,7 @@ namespace gsr { record_dropdown_button_ptr->set_activated(true); record_dropdown_button_ptr->set_description("Recording"); record_dropdown_button_ptr->set_item_icon("start", &get_theme().stop_texture); + record_dropdown_button_ptr->set_item_enabled("pause", recording_status == RecordingStatus::RECORD); } void Overlay::update_ui_recording_stopped() { @@ -968,7 +2136,9 @@ namespace gsr { record_dropdown_button_ptr->set_item_label("pause", "Pause"); record_dropdown_button_ptr->set_item_icon("pause", &get_theme().pause_texture); + record_dropdown_button_ptr->set_item_enabled("pause", false); paused = false; + replay_recording = false; } void Overlay::update_ui_streaming_started() { @@ -989,6 +2159,7 @@ namespace gsr { stream_dropdown_button_ptr->set_activated(false); stream_dropdown_button_ptr->set_description("Not streaming"); stream_dropdown_button_ptr->set_item_icon("start", &get_theme().play_texture); + update_ui_recording_stopped(); } void Overlay::update_ui_replay_started() { @@ -999,6 +2170,9 @@ namespace gsr { replay_dropdown_button_ptr->set_activated(true); replay_dropdown_button_ptr->set_description("On"); replay_dropdown_button_ptr->set_item_icon("start", &get_theme().stop_texture); + replay_dropdown_button_ptr->set_item_enabled("save", true); + replay_dropdown_button_ptr->set_item_enabled("save_1_min", true); + replay_dropdown_button_ptr->set_item_enabled("save_10_min", true); } void Overlay::update_ui_replay_stopped() { @@ -1009,6 +2183,10 @@ namespace gsr { replay_dropdown_button_ptr->set_activated(false); replay_dropdown_button_ptr->set_description("Off"); replay_dropdown_button_ptr->set_item_icon("start", &get_theme().play_texture); + replay_dropdown_button_ptr->set_item_enabled("save", false); + replay_dropdown_button_ptr->set_item_enabled("save_1_min", false); + replay_dropdown_button_ptr->set_item_enabled("save_10_min", false); + update_ui_recording_stopped(); } static std::string get_date_str() { @@ -1030,38 +2208,58 @@ namespace gsr { return container; } - static bool starts_with(std::string_view str, const char *substr) { - size_t len = strlen(substr); - return str.size() >= len && memcmp(str.data(), substr, len) == 0; - } - - static std::vector<std::string> create_audio_tracks_real_names(const std::vector<std::string> &audio_tracks, bool application_audio_invert, const GsrInfo &gsr_info) { + static std::vector<std::string> create_audio_tracks_cli_args(const std::vector<AudioTrack> &audio_tracks, const GsrInfo &gsr_info) { std::vector<std::string> result; - for(const std::string &audio_track : audio_tracks) { - std::string audio_track_name = audio_track; - const bool is_app_audio = starts_with(audio_track_name, "app:"); - if(is_app_audio && !gsr_info.system_info.supports_app_audio) - continue; + result.reserve(audio_tracks.size()); + + for(const AudioTrack &audio_track : audio_tracks) { + std::string audio_track_merged; + int num_app_audio = 0; - if(is_app_audio && application_audio_invert) - audio_track_name.replace(0, 4, "app-inverse:"); + for(const std::string &audio_input_name : audio_track.audio_inputs) { + std::string new_audio_input_name = audio_input_name; + const bool is_app_audio = starts_with(new_audio_input_name, "app:"); + if(is_app_audio && !gsr_info.system_info.supports_app_audio) + continue; - result.push_back(std::move(audio_track_name)); + if(is_app_audio && audio_track.application_audio_invert) + new_audio_input_name.replace(0, 4, "app-inverse:"); + + if(is_app_audio) + ++num_app_audio; + + if(!audio_track_merged.empty()) + audio_track_merged += "|"; + + audio_track_merged += new_audio_input_name; + } + + if(num_app_audio == 0 && audio_track.application_audio_invert) { + if(!audio_track_merged.empty()) + audio_track_merged += "|"; + + audio_track_merged += "app-inverse:"; + } + + if(!audio_track_merged.empty()) + result.push_back(std::move(audio_track_merged)); } + return result; } - static std::string merge_audio_tracks(const std::vector<std::string> &audio_tracks) { - std::string result; - for(size_t i = 0; i < audio_tracks.size(); ++i) { - if(i > 0) - result += "|"; - result += audio_tracks[i]; + static void add_region_command(std::vector<const char*> &args, char *region_str, int region_str_size, const RegionSelector ®ion_selector) { + Region region = region_selector.get_selection(); + if(region.size.x <= 32 && region.size.y <= 32) { + region.size.x = 0; + region.size.y = 0; } - return result; + snprintf(region_str, region_str_size, "%dx%d+%d+%d", region.size.x, region.size.y, region.pos.x, region.pos.y); + args.push_back("-region"); + args.push_back(region_str); } - static void add_common_gpu_screen_recorder_args(std::vector<const char*> &args, const RecordOptions &record_options, const std::vector<std::string> &audio_tracks, const std::string &video_bitrate, const char *region, const std::string &audio_devices_merged) { + static void add_common_gpu_screen_recorder_args(std::vector<const char*> &args, const RecordOptions &record_options, const std::vector<std::string> &audio_tracks, const std::string &video_bitrate, const char *region, char *region_str, int region_str_size, const RegionSelector ®ion_selector) { if(record_options.video_quality == "custom") { args.push_back("-bm"); args.push_back("cbr"); @@ -1077,43 +2275,192 @@ namespace gsr { args.push_back(region); } - if(record_options.merge_audio_tracks) { - if(!audio_devices_merged.empty()) { - args.push_back("-a"); - args.push_back(audio_devices_merged.c_str()); - } + for(const std::string &audio_track : audio_tracks) { + args.push_back("-a"); + args.push_back(audio_track.c_str()); + } + + if(record_options.restore_portal_session) { + args.push_back("-restore-portal-session"); + args.push_back("yes"); + } + + if(record_options.record_area_option == "region") + add_region_command(args, region_str, region_str_size, region_selector); + } + + static bool validate_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) { + if(capture_target == "window") { + return capture_options.window; + } else if(capture_target == "focused") { + return capture_options.focused; + } else if(capture_target == "region") { + return capture_options.region; + } else if(capture_target == "portal") { + return capture_options.portal; + } else if(capture_target == "focused_monitor") { + return !capture_options.monitors.empty(); } else { - for(const std::string &audio_track : audio_tracks) { - args.push_back("-a"); - args.push_back(audio_track.c_str()); + for(const GsrMonitor &monitor : capture_options.monitors) { + if(capture_target == monitor.name) + return true; } + return false; } } + static std::string get_valid_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) { + std::string capture_target_clean = capture_target; + if(starts_with(capture_target_clean, "HDMI-A")) + capture_target_clean.replace(0, 6, "HDMI"); + + for(const GsrMonitor &monitor : capture_options.monitors) { + std::string monitor_name_clean = monitor.name; + if(starts_with(monitor_name_clean, "HDMI-A")) + monitor_name_clean.replace(0, 6, "HDMI"); + + if(capture_target_clean == monitor_name_clean) + return monitor.name; + } + + return ""; + } + + std::string Overlay::get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) { + if(capture_target == "window") { + return std::to_string(window_selector.get_selection()); + } else if(capture_target == "focused_monitor") { + std::optional<CursorInfo> cursor_info; + if(cursor_tracker) { + cursor_tracker->update(); + cursor_info = cursor_tracker->get_latest_cursor_info(); + } + + std::string focused_monitor_name; + if(cursor_info) { + focused_monitor_name = std::move(cursor_info->monitor_name); + } else { + mgl_context *context = mgl_get_context(); + Display *display = (Display*)context->connection; + focused_monitor_name = get_focused_monitor_by_cursor(cursor_tracker.get(), gsr_info, get_monitors(display)); + } + + focused_monitor_name = get_valid_capture_target(focused_monitor_name, capture_options); + if(!focused_monitor_name.empty()) + return focused_monitor_name; + else if(!capture_options.monitors.empty()) + return capture_options.monitors.front().name; + else + return ""; + } else { + return capture_target; + } + } + + void Overlay::prepare_gsr_output_for_reading() { + if(gpu_screen_recorder_process_output_fd <= 0) + return; + + 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; + } + void Overlay::on_press_save_replay() { if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0) return; + replay_save_duration_min = 0; + replay_save_show_notification = true; + replay_save_clock.restart(); kill(gpu_screen_recorder_process, SIGUSR1); } - void Overlay::on_press_start_replay(bool disable_notification) { + void Overlay::on_press_save_replay_1_min_replay() { + if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0) + return; + + replay_save_duration_min = 1; + replay_save_show_notification = true; + replay_save_clock.restart(); + kill(gpu_screen_recorder_process, SIGRTMIN+3); + } + + void Overlay::on_press_save_replay_10_min_replay() { + if(recording_status != RecordingStatus::REPLAY || gpu_screen_recorder_process <= 0) + return; + + replay_save_duration_min = 10; + replay_save_show_notification = true; + replay_save_clock.restart(); + kill(gpu_screen_recorder_process, SIGRTMIN+5); + } + + static const char* switch_video_codec_to_usable_hardware_encoder(const GsrInfo &gsr_info) { + if(gsr_info.supported_video_codecs.h264) + return "h264"; + else if(gsr_info.supported_video_codecs.hevc) + return "hevc"; + else if(gsr_info.supported_video_codecs.av1) + return "av1"; + else if(gsr_info.supported_video_codecs.vp8) + return "vp8"; + else if(gsr_info.supported_video_codecs.vp9) + return "vp9"; + return nullptr; + } + + static const char* change_container_if_codec_not_supported(const char *video_codec, const char *container) { + if(strcmp(video_codec, "vp8") == 0 || strcmp(video_codec, "vp9") == 0) { + if(strcmp(container, "webm") != 0 && strcmp(container, "matroska") != 0) { + fprintf(stderr, "Warning: container '%s' is not compatible with video codec '%s', using webm container instead\n", container, video_codec); + return "webm"; + } + } else if(strcmp(container, "webm") == 0) { + fprintf(stderr, "Warning: container webm is not compatible with video codec '%s', using mp4 container instead\n", video_codec); + return "mp4"; + } + return container; + } + + static void choose_video_codec_and_container_with_fallback(const GsrInfo &gsr_info, const char **video_codec, const char **container, const char **encoder) { + *encoder = "gpu"; + if(strcmp(*video_codec, "h264_software") == 0) { + *video_codec = "h264"; + *encoder = "cpu"; + } else if(strcmp(*video_codec, "auto") == 0) { + *video_codec = switch_video_codec_to_usable_hardware_encoder(gsr_info); + if(!*video_codec) { + *video_codec = "h264"; + *encoder = "cpu"; + } + } + *container = change_container_if_codec_not_supported(*video_codec, *container); + } + + bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) { + if(region_selector.is_started() || window_selector.is_started()) + return false; + switch(recording_status) { case RecordingStatus::NONE: case RecordingStatus::REPLAY: break; case RecordingStatus::RECORD: - show_notification("Unable to start replay when recording.\nStop recording before starting replay.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); - return; + show_notification("Unable to start replay when recording.\nStop recording before starting replay.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); + return false; case RecordingStatus::STREAM: - show_notification("Unable to start replay when streaming.\nStop streaming before starting replay.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); - return; + show_notification("Unable to start replay when streaming.\nStop streaming before starting replay.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM); + return false; } paused = false; + replay_save_show_notification = false; + try_replay_startup = 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); @@ -1125,38 +2472,64 @@ namespace gsr { gpu_screen_recorder_process = -1; recording_status = RecordingStatus::NONE; + replay_save_duration_min = 0; update_ui_replay_stopped(); // TODO: Show this with a slight delay to make sure it doesn't show up in the video if(!disable_notification && config.replay_config.show_replay_stopped_notifications) - show_notification("Replay stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); - return; + show_notification("Replay stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + + return true; + } + + const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info); + recording_capture_target = get_capture_target(config.replay_config.record_options.record_area_option, capture_options); + if(!validate_capture_target(recording_capture_target, capture_options)) { + char err_msg[256]; + snprintf(err_msg, sizeof(err_msg), "Failed to start replay, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str()); + show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY); + return false; + } + + if(config.replay_config.record_options.record_area_option == "region" && !finished_selection) { + start_region_capture = true; + on_region_selected = [disable_notification, this]() { + on_press_start_replay(disable_notification, true); + }; + return false; + } + + if(config.replay_config.record_options.record_area_option == "window" && !finished_selection) { + start_window_capture = true; + on_window_selected = [disable_notification, this]() { + on_press_start_replay(disable_notification, true); + }; + return false; } // 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); const std::string output_directory = config.replay_config.save_directory; - const std::vector<std::string> audio_tracks = create_audio_tracks_real_names(config.replay_config.record_options.audio_tracks, config.replay_config.record_options.application_audio_invert, gsr_info); - const std::string audio_tracks_merged = merge_audio_tracks(audio_tracks); + const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.replay_config.record_options.audio_tracks_list, gsr_info); const std::string framerate_mode = config.replay_config.record_options.framerate_mode == "auto" ? "vfr" : config.replay_config.record_options.framerate_mode; const std::string replay_time = std::to_string(config.replay_config.replay_time); + const char *container = config.replay_config.container.c_str(); const char *video_codec = config.replay_config.record_options.video_codec.c_str(); const char *encoder = "gpu"; - if(strcmp(video_codec, "h264_software") == 0) { - video_codec = "h264"; - encoder = "cpu"; - } + choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder); - 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); + char size[64]; + size[0] = '\0'; + if(config.replay_config.record_options.record_area_option == "focused") + snprintf(size, sizeof(size), "%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); + snprintf(size, sizeof(size), "%dx%d", (int)config.replay_config.record_options.video_width, (int)config.replay_config.record_options.video_height); std::vector<const char*> args = { - "gpu-screen-recorder", "-w", config.replay_config.record_options.record_area_option.c_str(), - "-c", config.replay_config.container.c_str(), + "gpu-screen-recorder", "-w", recording_capture_target.c_str(), + "-c", container, "-ac", config.replay_config.record_options.audio_codec.c_str(), "-cursor", config.replay_config.record_options.record_cursor ? "yes" : "no", "-cr", config.replay_config.record_options.color_range.c_str(), @@ -1169,23 +2542,37 @@ 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"); + } + + if(gsr_info.system_info.gsr_version >= GsrVersion{5, 5, 0}) { + args.push_back("-replay-storage"); + args.push_back(config.replay_config.replay_storage.c_str()); + } - 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()); + char region_str[128]; + add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, size, region_str, sizeof(region_str), region_selector); + + if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) { + args.push_back("-ro"); + args.push_back(config.record_config.save_directory.c_str()); + } 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 + show_notification("Failed to launch gpu-screen-recorder to start replay", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY); + return false; } else { recording_status = RecordingStatus::REPLAY; update_ui_replay_started(); } + prepare_gsr_output_for_reading(); + // 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: @@ -1195,27 +2582,62 @@ namespace gsr { // TODO: Do not run this is a daemon. Instead get the pid and when launching another notification close the current notification // program and start another one. This can also be used to check when the notification has finished by checking with waitpid NOWAIT // to see when the program has exit. - if(!disable_notification && config.replay_config.show_replay_started_notifications) - show_notification("Replay has started", 3.0, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY); + if(!disable_notification && config.replay_config.show_replay_started_notifications) { + char msg[256]; + snprintf(msg, sizeof(msg), "Started replaying %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str()); + show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str()); + } + + return true; } - void Overlay::on_press_start_record() { + void Overlay::on_press_start_record(bool finished_selection) { + if(region_selector.is_started() || window_selector.is_started()) + return; + switch(recording_status) { case RecordingStatus::NONE: case RecordingStatus::RECORD: break; - case RecordingStatus::REPLAY: - show_notification("Unable to start recording when replay is turned on.\nTurn off replay before starting recording.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + case RecordingStatus::REPLAY: { + if(gpu_screen_recorder_process <= 0) + return; + + if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) { + if(!replay_recording) { + if(config.record_config.show_recording_started_notifications) + show_notification("Started recording in the replay session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD); + update_ui_recording_started(); + } + replay_recording = true; + kill(gpu_screen_recorder_process, SIGRTMIN); + } else { + show_notification("Unable to start recording when replay is turned on.\nTurn off replay before starting recording.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), get_color_theme().tint_color, NotificationType::REPLAY); + } return; - case RecordingStatus::STREAM: - show_notification("Unable to start recording when streaming.\nStop streaming before starting recording.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); + } + case RecordingStatus::STREAM: { + if(gpu_screen_recorder_process <= 0) + return; + + if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) { + if(!replay_recording) { + if(config.record_config.show_recording_started_notifications) + show_notification("Started recording in the streaming session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD); + update_ui_recording_started(); + } + replay_recording = true; + kill(gpu_screen_recorder_process, SIGRTMIN); + } else { + show_notification("Unable to start recording when streaming.\nStop streaming before starting recording.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), get_color_theme().tint_color, NotificationType::STREAM); + } return; + } } paused = 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); @@ -1223,40 +2645,69 @@ 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, record_filepath); } - // 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; + } + + const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info); + recording_capture_target = get_capture_target(config.record_config.record_options.record_area_option, capture_options); + if(!validate_capture_target(config.record_config.record_options.record_area_option, capture_options)) { + char err_msg[256]; + snprintf(err_msg, sizeof(err_msg), "Failed to start recording, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str()); + show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); return; } + if(config.record_config.record_options.record_area_option == "region" && !finished_selection) { + start_region_capture = true; + on_region_selected = [this]() { + on_press_start_record(true); + }; + return; + } + + if(config.record_config.record_options.record_area_option == "window" && !finished_selection) { + start_window_capture = true; + on_window_selected = [this]() { + on_press_start_record(true); + }; + 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); const std::string output_file = config.record_config.save_directory + "/Video_" + get_date_str() + "." + container_to_file_extension(config.record_config.container.c_str()); - const std::vector<std::string> audio_tracks = create_audio_tracks_real_names(config.record_config.record_options.audio_tracks, config.record_config.record_options.application_audio_invert, gsr_info); - const std::string audio_tracks_merged = merge_audio_tracks(audio_tracks); + const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.record_config.record_options.audio_tracks_list, gsr_info); const std::string framerate_mode = config.record_config.record_options.framerate_mode == "auto" ? "vfr" : config.record_config.record_options.framerate_mode; + const char *container = config.record_config.container.c_str(); const char *video_codec = config.record_config.record_options.video_codec.c_str(); const char *encoder = "gpu"; - if(strcmp(video_codec, "h264_software") == 0) { - video_codec = "h264"; - encoder = "cpu"; - } + choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder); - 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); + char size[64]; + size[0] = '\0'; + if(config.record_config.record_options.record_area_option == "focused") + snprintf(size, sizeof(size), "%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); + snprintf(size, sizeof(size), "%dx%d", (int)config.record_config.record_options.video_width, (int)config.record_config.record_options.video_height); std::vector<const char*> args = { - "gpu-screen-recorder", "-w", config.record_config.record_options.record_area_option.c_str(), - "-c", config.record_config.container.c_str(), + "gpu-screen-recorder", "-w", recording_capture_target.c_str(), + "-c", container, "-ac", config.record_config.record_options.audio_codec.c_str(), "-cursor", config.record_config.record_options.record_cursor ? "yes" : "no", "-cr", config.record_config.record_options.color_range.c_str(), @@ -1268,31 +2719,34 @@ namespace gsr { "-o", output_file.c_str() }; - 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()); + char region_str[128]; + add_common_gpu_screen_recorder_args(args, config.record_config.record_options, audio_tracks, video_bitrate, size, region_str, sizeof(region_str), region_selector); args.push_back(nullptr); - gpu_screen_recorder_process = exec_program(args.data()); + record_filepath = output_file; + 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 + show_notification("Failed to launch gpu-screen-recorder to start recording", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); + return; } else { recording_status = RecordingStatus::RECORD; update_ui_recording_started(); } + prepare_gsr_output_for_reading(); + // 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: // Starting recording in 3... // 2... // 1... - if(config.record_config.show_recording_started_notifications) - show_notification("Recording has started", 3.0, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD); + if(config.record_config.show_recording_started_notifications) { + char msg[256]; + snprintf(msg, sizeof(msg), "Started recording %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str()); + show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str()); + } } static std::string streaming_get_url(const Config &config) { @@ -1303,6 +2757,9 @@ namespace gsr { } else if(config.streaming_config.streaming_service == "youtube") { url += "rtmp://a.rtmp.youtube.com/live2/"; url += config.streaming_config.youtube.stream_key; + } else if(config.streaming_config.streaming_service == "rumble") { + url += "rtmp://rtmp.rumble.com/live/"; + url += config.streaming_config.rumble.stream_key; } else if(config.streaming_config.streaming_service == "custom") { url = config.streaming_config.custom.url; if(url.size() >= 7 && strncmp(url.c_str(), "rtmp://", 7) == 0) @@ -1327,23 +2784,25 @@ namespace gsr { return url; } - void Overlay::on_press_start_stream() { + void Overlay::on_press_start_stream(bool finished_selection) { + if(region_selector.is_started() || window_selector.is_started()) + return; + switch(recording_status) { case RecordingStatus::NONE: case RecordingStatus::STREAM: break; case RecordingStatus::REPLAY: - show_notification("Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY); + show_notification("Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY); return; case RecordingStatus::RECORD: - show_notification("Unable to start streaming when recording.\nStop recording before starting streaming.", 5.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::RECORD); + show_notification("Unable to start streaming when recording.\nStop recording before starting streaming.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD); return; } paused = 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); @@ -1359,61 +2818,95 @@ namespace gsr { // TODO: Show this with a slight delay to make sure it doesn't show up in the video if(config.streaming_config.show_streaming_stopped_notifications) - show_notification("Streaming has stopped", 3.0, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); + show_notification("Streaming has stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM); + return; + } + + const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info); + recording_capture_target = get_capture_target(config.streaming_config.record_options.record_area_option, capture_options); + if(!validate_capture_target(config.streaming_config.record_options.record_area_option, capture_options)) { + char err_msg[256]; + snprintf(err_msg, sizeof(err_msg), "Failed to start streaming, capture target \"%s\" is invalid.\nPlease change capture target in settings", recording_capture_target.c_str()); + show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM); + return; + } + + if(config.streaming_config.record_options.record_area_option == "region" && !finished_selection) { + start_region_capture = true; + on_region_selected = [this]() { + on_press_start_stream(true); + }; + return; + } + + if(config.streaming_config.record_options.record_area_option == "window" && !finished_selection) { + start_window_capture = true; + on_window_selected = [this]() { + on_press_start_stream(true); + }; 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); - const std::vector<std::string> audio_tracks = create_audio_tracks_real_names(config.streaming_config.record_options.audio_tracks, config.streaming_config.record_options.application_audio_invert, gsr_info); - const std::string audio_tracks_merged = merge_audio_tracks(audio_tracks); + std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.streaming_config.record_options.audio_tracks_list, gsr_info); + // This isn't possible unless the user modified the config file manually, + // But we check it anyways as streaming on some sites can fail if there is more than one audio track + if(audio_tracks.size() > 1) + audio_tracks.resize(1); const std::string framerate_mode = config.streaming_config.record_options.framerate_mode == "auto" ? "vfr" : config.streaming_config.record_options.framerate_mode; + const char *container = "flv"; + if(config.streaming_config.streaming_service == "custom") + container = config.streaming_config.custom.container.c_str(); const char *video_codec = config.streaming_config.record_options.video_codec.c_str(); const char *encoder = "gpu"; - if(strcmp(video_codec, "h264_software") == 0) { - video_codec = "h264"; - encoder = "cpu"; - } - - std::string container = "flv"; - if(config.streaming_config.streaming_service == "custom") - container = config.streaming_config.custom.container; + choose_video_codec_and_container_with_fallback(gsr_info, &video_codec, &container, &encoder); 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); + char size[64]; + size[0] = '\0'; + if(config.record_config.record_options.record_area_option == "focused") + snprintf(size, sizeof(size), "%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); + snprintf(size, sizeof(size), "%dx%d", (int)config.streaming_config.record_options.video_width, (int)config.streaming_config.record_options.video_height); std::vector<const char*> args = { - "gpu-screen-recorder", "-w", config.streaming_config.record_options.record_area_option.c_str(), - "-c", container.c_str(), + "gpu-screen-recorder", "-w", recording_capture_target.c_str(), + "-c", container, "-ac", config.streaming_config.record_options.audio_codec.c_str(), "-cursor", config.streaming_config.record_options.record_cursor ? "yes" : "no", "-cr", config.streaming_config.record_options.color_range.c_str(), "-fm", framerate_mode.c_str(), "-encoder", encoder, "-f", fps.c_str(), - "-f", fps.c_str(), "-v", "no", "-o", url.c_str() }; - add_common_gpu_screen_recorder_args(args, config.streaming_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); + char region_str[128]; + add_common_gpu_screen_recorder_args(args, config.streaming_config.record_options, audio_tracks, video_bitrate, size, region_str, sizeof(region_str), region_selector); + + if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) { + args.push_back("-ro"); + args.push_back(config.record_config.save_directory.c_str()); + } 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 + show_notification("Failed to launch gpu-screen-recorder to start streaming", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM); + return; } else { recording_status = RecordingStatus::STREAM; update_ui_streaming_started(); } + prepare_gsr_output_for_reading(); + // 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: @@ -1423,11 +2916,88 @@ namespace gsr { // TODO: Do not run this is a daemon. Instead get the pid and when launching another notification close the current notification // program and start another one. This can also be used to check when the notification has finished by checking with waitpid NOWAIT // to see when the program has exit. - if(config.streaming_config.show_streaming_started_notifications) - show_notification("Streaming has started", 3.0, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM); + if(config.streaming_config.show_streaming_started_notifications) { + char msg[256]; + snprintf(msg, sizeof(msg), "Started streaming %s", capture_target_get_notification_name(recording_capture_target.c_str()).c_str()); + show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM, recording_capture_target.c_str()); + } + } + + void Overlay::on_press_take_screenshot(bool finished_selection, bool force_region_capture) { + if(region_selector.is_started() || window_selector.is_started()) + return; + + if(gpu_screen_recorder_screenshot_process > 0) { + fprintf(stderr, "Error: failed to take screenshot, another screenshot is currently being saved\n"); + return; + } + + const bool region_capture = config.screenshot_config.record_area_option == "region" || force_region_capture; + const char *record_area_option = region_capture ? "region" : config.screenshot_config.record_area_option.c_str(); + const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info); + screenshot_capture_target = get_capture_target(record_area_option, capture_options); + if(!validate_capture_target(record_area_option, capture_options)) { + char err_msg[256]; + snprintf(err_msg, sizeof(err_msg), "Failed to take a screenshot, capture target \"%s\" is invalid.\nPlease change capture target in settings", screenshot_capture_target.c_str()); + show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT); + return; + } + + if(region_capture && !finished_selection) { + start_region_capture = true; + on_region_selected = [this, force_region_capture]() { + usleep(200 * 1000); // Hack: wait 0.2 seconds before taking a screenshot to allow user to move cursor away. TODO: Remove this + on_press_take_screenshot(true, force_region_capture); + }; + return; + } + + if(config.screenshot_config.record_area_option == "window" && !finished_selection) { + start_window_capture = true; + on_window_selected = [this, force_region_capture]() { + on_press_take_screenshot(true, force_region_capture); + }; + return; + } + + // TODO: Validate input, fallback to valid values + const std::string output_file = config.screenshot_config.save_directory + "/Screenshot_" + get_date_str() + "." + config.screenshot_config.image_format; // TODO: Validate image format + + std::vector<const char*> args = { + "gpu-screen-recorder", "-w", screenshot_capture_target.c_str(), + "-cursor", config.screenshot_config.record_cursor ? "yes" : "no", + "-v", "no", + "-q", config.screenshot_config.image_quality.c_str(), + "-o", output_file.c_str() + }; + + char size[64]; + size[0] = '\0'; + if(config.screenshot_config.change_image_resolution) { + snprintf(size, sizeof(size), "%dx%d", (int)config.screenshot_config.image_width, (int)config.screenshot_config.image_height); + args.push_back("-s"); + args.push_back(size); + } + + if(config.screenshot_config.restore_portal_session) { + args.push_back("-restore-portal-session"); + args.push_back("yes"); + } + + char region_str[128]; + if(region_capture) + add_region_command(args, region_str, sizeof(region_str), region_selector); + + args.push_back(nullptr); + + screenshot_filepath = output_file; + gpu_screen_recorder_screenshot_process = exec_program(args.data(), nullptr); + if(gpu_screen_recorder_screenshot_process == -1) { + show_notification("Failed to launch gpu-screen-recorder to take a screenshot", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT); + } } - 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 +3010,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"); @@ -1470,8 +3042,8 @@ namespace gsr { mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; - XRaiseWindow(display, window->get_system_handle()); + XRaiseWindow(display, (Window)window->get_system_handle()); XFlush(display); } } -}
\ No newline at end of file +} diff --git a/src/Process.cpp b/src/Process.cpp index 07d9dc6..c02753a 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,14 +22,33 @@ namespace gsr { fprintf(stderr, "\n"); } - bool exec_program_daemonized(const char **args) { + 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, bool debug) { /* 1 argument */ if(args[0] == nullptr) return false; - debug_print_args(args); + if(debug) + debug_print_args(args); - pid_t pid = vfork(); + const pid_t pid = vfork(); if(pid == -1) { perror("Failed to vfork"); return false; @@ -35,10 +57,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,30 +73,124 @@ namespace gsr { return true; } - pid_t exec_program(const char **args) { + pid_t exec_program(const char **args, int *read_fd, bool debug) { + if(read_fd) + *read_fd = -1; + /* 1 argument */ if(args[0] == nullptr) return -1; - debug_print_args(args); + int fds[2] = {-1, -1}; + if(pipe(fds) == -1) + return -1; + + if(debug) + 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, bool debug) { + result.clear(); + int read_fd = -1; + const pid_t process_id = exec_program(args, &read_fd, debug); + 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; + } + 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, bool debug) { + 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, debug); + } else { + return exec_program_get_stdout(args, result, debug); + } + } + + static const char *get_basename(const char *path, int size) { + for(int i = size - 1; i >= 0; --i) { + if(path[i] == '/') + return path + i + 1; + } + return path; + } + + // |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_start = NULL; const char *arg0_end = NULL; + int arg0_size = 0; int fd = open(filepath, O_RDONLY); if(fd == -1) return false; @@ -84,17 +200,50 @@ namespace gsr { if(bytes_read == -1) goto err; - arg0_end = (const char*)memchr(buffer, '\0', bytes_read); + arg0_start = buffer; + arg0_end = (const char*)memchr(arg0_start, '\0', bytes_read); if(!arg0_end) goto err; - memcpy(output_buffer, buffer, arg0_end - buffer); - output_buffer[arg0_end - buffer] = '\0'; - close(fd); - return true; + arg0_start = get_basename(arg0_start, arg0_end - arg0_start); + arg0_size = arg0_end - arg0_start; + if(arg0_size + 1 <= output_buffer_size) { + memcpy(output_buffer, arg0_start, arg0_size); + output_buffer[arg0_size] = '\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/RegionSelector.cpp b/src/RegionSelector.cpp new file mode 100644 index 0000000..89a0209 --- /dev/null +++ b/src/RegionSelector.cpp @@ -0,0 +1,450 @@ +#include "../include/RegionSelector.hpp" + +#include <stdio.h> +#include <string.h> + +#include <X11/extensions/XInput2.h> +#include <X11/extensions/Xrandr.h> +#include <X11/extensions/shape.h> + +namespace gsr { + static const int cursor_window_size = 32; + static const int cursor_thickness = 5; + static const int region_border_size = 2; + + 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, "error: RegionSelector: 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, "error: RegionSelector: XInput 2.1 is not supported\n"); + return false; + } + + return true; + } + + static int max_int(int a, int b) { + return a >= b ? a : b; + } + + static void set_region_rectangle(Display *dpy, Window window, int x, int y, int width, int height, int border_size) { + if(width < 0) { + x += width; + width = abs(width); + } + + if(height < 0) { + y += height; + height = abs(height); + } + + XRectangle rectangles[] = { + { + (short)max_int(0, x), (short)max_int(0, y), + (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height) + }, // Left + { + (short)max_int(0, x + width - border_size), (short)max_int(0, y), + (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height) + }, // Right + { + (short)max_int(0, x + border_size), (short)max_int(0, y), + (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size) + }, // Top + { + (short)max_int(0, x + border_size), (short)max_int(0, y + height - border_size), + (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size) + }, // Bottom + }; + XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted); + XFlush(dpy); + } + + static void set_window_shape_cross(Display *dpy, Window window, int window_width, int window_height, int thickness) { + XRectangle rectangles[] = { + { + (short)(window_width / 2 - thickness / 2), (short)0, + (unsigned short)thickness, (unsigned short)window_height + }, // Vertical + { + (short)(0), (short)(window_height / 2 - thickness / 2), + (unsigned short)window_width, (unsigned short)thickness + }, // Horizontal + }; + XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 2, ShapeSet, Unsorted); + XFlush(dpy); + } + + static void draw_rectangle(Display *dpy, Window window, GC gc, int x, int y, int width, int height) { + if(width < 0) { + x += width; + width = abs(width); + } + + if(height < 0) { + y += height; + height = abs(height); + } + + XDrawRectangle(dpy, window, gc, x, y, width, height); + } + + static Window create_cursor_window(Display *dpy, int width, int height, XVisualInfo *vinfo, unsigned long background_pixel) { + XSetWindowAttributes window_attr; + window_attr.background_pixel = background_pixel; + window_attr.border_pixel = 0; + window_attr.override_redirect = true; + window_attr.event_mask = StructureNotifyMask | PointerMotionMask; + window_attr.colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo->visual, AllocNone); + const Window window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, width, height, 0, vinfo->depth, InputOutput, vinfo->visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr); + if(window) { + set_window_size_not_resizable(dpy, window, width, height); + set_window_shape_cross(dpy, window, width, height, cursor_thickness); + make_window_click_through(dpy, window); + } + return window; + } + + static void draw_rectangle_around_selected_monitor(Display *dpy, Window window, GC region_gc, int region_border_size, bool is_wayland, const std::vector<Monitor> &monitors, mgl::vec2i cursor_pos) { + const Monitor *focused_monitor = nullptr; + for(const Monitor &monitor : monitors) { + if(cursor_pos.x >= monitor.position.x && cursor_pos.x <= monitor.position.x + monitor.size.x + && cursor_pos.y >= monitor.position.y && cursor_pos.y <= monitor.position.y + monitor.size.y) + { + focused_monitor = &monitor; + break; + } + } + + int x = 0; + int y = 0; + int width = 0; + int height = 0; + if(focused_monitor) { + x = focused_monitor->position.x; + y = focused_monitor->position.y; + width = focused_monitor->size.x; + height = focused_monitor->size.y; + } + + if(is_wayland) + draw_rectangle(dpy, window, region_gc, x, y, width, height); + else + set_region_rectangle(dpy, window, x, y, width, height, region_border_size); + } + + static void update_cursor_window(Display *dpy, Window window, Window cursor_window, bool is_wayland, int cursor_x, int cursor_y, int cursor_window_size, int thickness, GC cursor_gc) { + if(is_wayland) { + const int x = cursor_x - cursor_window_size / 2; + const int y = cursor_y - cursor_window_size / 2; + XFillRectangle(dpy, window, cursor_gc, x + cursor_window_size / 2 - thickness / 2 , y, thickness, cursor_window_size); + XFillRectangle(dpy, window, cursor_gc, x, y + cursor_window_size / 2 - thickness / 2, cursor_window_size, thickness); + } else if(cursor_window) { + XMoveWindow(dpy, cursor_window, cursor_x - cursor_window_size / 2, cursor_y - cursor_window_size / 2); + } + XFlush(dpy); + } + + static bool is_xwayland(Display *dpy) { + int opcode, event, error; + return XQueryExtension(dpy, "XWAYLAND", &opcode, &event, &error); + } + + static unsigned long mgl_color_to_x11_color(mgl::Color color) { + if(color.a == 0) + return 0; + return ((uint32_t)color.a << 24) | (((uint32_t)color.r * color.a / 0xFF) << 16) | (((uint32_t)color.g * color.a / 0xFF) << 8) | ((uint32_t)color.b * color.a / 0xFF); + } + + RegionSelector::RegionSelector() { + + } + + RegionSelector::~RegionSelector() { + stop(); + } + + bool RegionSelector::start(mgl::Color border_color) { + if(dpy) + return false; + + const unsigned long border_color_x11 = mgl_color_to_x11_color(border_color); + dpy = XOpenDisplay(nullptr); + if(!dpy) { + fprintf(stderr, "Error: RegionSelector::start: failed to connect to the X11 server\n"); + return false; + } + + xi_opcode = 0; + if(!xinput_is_supported(dpy, &xi_opcode)) { + fprintf(stderr, "Error: RegionSelector::start: xinput not supported on your system\n"); + stop(); + return false; + } + + is_wayland = is_xwayland(dpy); + monitors = get_monitors(dpy); + + Window x11_cursor_window = None; + cursor_pos = get_cursor_position(dpy, &x11_cursor_window); + region.pos = {0, 0}; + region.size = {0, 0}; + + XVisualInfo vinfo; + memset(&vinfo, 0, sizeof(vinfo)); + XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo); + region_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone); + + XSetWindowAttributes window_attr; + window_attr.background_pixel = is_wayland ? 0 : border_color_x11; + window_attr.border_pixel = 0; + window_attr.override_redirect = true; + window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask; + window_attr.colormap = region_window_colormap; + + Screen *screen = XDefaultScreenOfDisplay(dpy); + region_window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, XWidthOfScreen(screen), XHeightOfScreen(screen), 0, + vinfo.depth, InputOutput, vinfo.visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr); + if(!region_window) { + fprintf(stderr, "Error: RegionSelector::start: failed to create region window\n"); + stop(); + return false; + } + set_window_size_not_resizable(dpy, region_window, XWidthOfScreen(screen), XHeightOfScreen(screen)); + + if(!is_wayland) { + cursor_window = create_cursor_window(dpy, cursor_window_size, cursor_window_size, &vinfo, border_color_x11); + if(!cursor_window) + fprintf(stderr, "Warning: RegionSelector::start: failed to create cursor window\n"); + set_region_rectangle(dpy, region_window, 0, 0, 0, 0, 0); + } + + XGCValues region_gc_values; + memset(®ion_gc_values, 0, sizeof(region_gc_values)); + region_gc_values.foreground = border_color_x11; + region_gc_values.line_width = region_border_size; + region_gc_values.line_style = LineSolid; + region_gc = XCreateGC(dpy, region_window, GCForeground | GCLineWidth | GCLineStyle, ®ion_gc_values); + + XGCValues cursor_gc_values; + memset(&cursor_gc_values, 0, sizeof(cursor_gc_values)); + cursor_gc_values.foreground = border_color_x11; + cursor_gc_values.line_width = cursor_thickness; + cursor_gc_values.line_style = LineSolid; + cursor_gc = XCreateGC(dpy, region_window, GCForeground | GCLineWidth | GCLineStyle, &cursor_gc_values); + + if(!region_gc || !cursor_gc) { + fprintf(stderr, "Error: RegionSelector::start: failed to create gc\n"); + stop(); + return false; + } + + XMapWindow(dpy, region_window); + make_window_sticky(dpy, region_window); + hide_window_from_taskbar(dpy, region_window); + XFixesHideCursor(dpy, region_window); + XGrabPointer(dpy, DefaultRootWindow(dpy), True, ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime); + XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync, CurrentTime); + xi_grab_all_mouse_devices(dpy); + XFlush(dpy); + + window_set_fullscreen(dpy, region_window, true); + + if(!is_wayland || x11_cursor_window) + update_cursor_window(dpy, region_window, cursor_window, is_wayland, cursor_pos.x, cursor_pos.y, cursor_window_size, cursor_thickness, cursor_gc); + + if(cursor_window) { + XMapWindow(dpy, cursor_window); + make_window_sticky(dpy, cursor_window); + hide_window_from_taskbar(dpy, cursor_window); + } + + draw_rectangle_around_selected_monitor(dpy, region_window, region_gc, region_border_size, is_wayland, monitors, cursor_pos); + + XFlush(dpy); + selected = false; + canceled = false; + return true; + } + + void RegionSelector::stop() { + if(!dpy) + return; + + XWarpPointer(dpy, DefaultRootWindow(dpy), DefaultRootWindow(dpy), 0, 0, 0, 0, cursor_pos.x, cursor_pos.y); + xi_warp_all_mouse_devices(dpy, cursor_pos); + XFixesShowCursor(dpy, region_window); + + XUngrabPointer(dpy, CurrentTime); + XUngrabKeyboard(dpy, CurrentTime); + xi_ungrab_all_mouse_devices(dpy); + XFlush(dpy); + + if(region_gc) { + XFreeGC(dpy, region_gc); + region_gc = nullptr; + } + + if(cursor_gc) { + XFreeGC(dpy, cursor_gc); + cursor_gc = nullptr; + } + + if(region_window_colormap) { + XFreeColormap(dpy, region_window_colormap); + region_window_colormap = 0; + } + + if(region_window) { + XDestroyWindow(dpy, region_window); + region_window = 0; + } + + XCloseDisplay(dpy); + dpy = nullptr; + selecting_region = false; + } + + bool RegionSelector::is_started() const { + return dpy != nullptr; + } + + bool RegionSelector::failed() const { + return !dpy; + } + + bool RegionSelector::poll_events() { + if(!dpy || selected) + return false; + + XEvent xev; + while(XPending(dpy)) { + XNextEvent(dpy, &xev); + + if(xev.type == KeyRelease && XKeycodeToKeysym(dpy, xev.xkey.keycode, 0) == XK_Escape) { + canceled = true; + selected = false; + stop(); + break; + } + + XGenericEventCookie *cookie = &xev.xcookie; + if(cookie->type != GenericEvent || cookie->extension != xi_opcode || !XGetEventData(dpy, cookie)) + continue; + + const XIDeviceEvent *de = (XIDeviceEvent*)cookie->data; + switch(cookie->evtype) { + case XI_ButtonPress: { + on_button_press(de); + break; + } + case XI_ButtonRelease: { + on_button_release(de); + break; + } + case XI_Motion: { + on_mouse_motion(de); + break; + } + } + XFreeEventData(dpy, cookie); + + if(selected) { + stop(); + break; + } + } + return true; + } + + bool RegionSelector::take_selection() { + const bool result = selected; + selected = false; + return result; + } + + bool RegionSelector::take_canceled() { + const bool result = canceled; + canceled = false; + return result; + } + + Region RegionSelector::get_selection() const { + return region; + } + + void RegionSelector::on_button_press(const void *de) { + const XIDeviceEvent *device_event = (XIDeviceEvent*)de; + if(device_event->detail != Button1) + return; + + region.pos = { (int)device_event->root_x, (int)device_event->root_y }; + selecting_region = true; + } + + void RegionSelector::on_button_release(const void *de) { + const XIDeviceEvent *device_event = (XIDeviceEvent*)de; + if(device_event->detail != Button1) + return; + + if(!selecting_region) + return; + + if(is_wayland) { + XClearWindow(dpy, region_window); + XFlush(dpy); + } else { + set_region_rectangle(dpy, region_window, 0, 0, 0, 0, 0); + } + selecting_region = false; + + cursor_pos = region.pos + region.size; + + if(region.size.x < 0) { + region.pos.x += region.size.x; + region.size.x = abs(region.size.x); + } + + if(region.size.y < 0) { + region.pos.y += region.size.y; + region.size.y = abs(region.size.y); + } + + if(region.size.x > 0) + region.size.x += 1; + + if(region.size.y > 0) + region.size.y += 1; + + selected = true; + } + + void RegionSelector::on_mouse_motion(const void *de) { + const XIDeviceEvent *device_event = (XIDeviceEvent*)de; + XClearWindow(dpy, region_window); + if(selecting_region) { + region.size.x = device_event->root_x - region.pos.x; + region.size.y = device_event->root_y - region.pos.y; + cursor_pos = region.pos + region.size; + + if(is_wayland) + draw_rectangle(dpy, region_window, region_gc, region.pos.x, region.pos.y, region.size.x, region.size.y); + else + set_region_rectangle(dpy, region_window, region.pos.x, region.pos.y, region.size.x, region.size.y, region_border_size); + } else { + cursor_pos = { (int)device_event->root_x, (int)device_event->root_y }; + draw_rectangle_around_selected_monitor(dpy, region_window, region_gc, region_border_size, is_wayland, monitors, cursor_pos); + } + update_cursor_window(dpy, region_window, cursor_window, is_wayland, cursor_pos.x, cursor_pos.y, cursor_window_size, cursor_thickness, cursor_gc); + XFlush(dpy); + } +}
\ 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..2bef3c8 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,30 @@ 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); + case GpuVendor::BROADCOM: return mgl::Color(221, 0, 49); + } + 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; + else if(color_name == "broadcom") + vendor = GpuVendor::BROADCOM; + 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; @@ -38,49 +63,85 @@ namespace gsr { if(!theme->title_font_file.load((resources_path + "fonts/NotoSans-Bold.ttf").c_str(), mgl::MemoryMappedFile::LoadOptions{true, false})) goto error; - if(!theme->combobox_arrow_texture.load_from_file((resources_path + "images/combobox_arrow.png").c_str())) + if(!theme->combobox_arrow_texture.load_from_file((resources_path + "images/combobox_arrow.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; if(!theme->settings_texture.load_from_file((resources_path + "images/settings.png").c_str())) goto error; - if(!theme->folder_texture.load_from_file((resources_path + "images/folder.png").c_str())) + if(!theme->settings_small_texture.load_from_file((resources_path + "images/settings_small.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->settings_extra_small_texture.load_from_file((resources_path + "images/settings_extra_small.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->up_arrow_texture.load_from_file((resources_path + "images/up_arrow.png").c_str())) + if(!theme->folder_texture.load_from_file((resources_path + "images/folder.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->replay_button_texture.load_from_file((resources_path + "images/replay.png").c_str())) + if(!theme->up_arrow_texture.load_from_file((resources_path + "images/up_arrow.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->record_button_texture.load_from_file((resources_path + "images/record.png").c_str())) + if(!theme->replay_button_texture.load_from_file((resources_path + "images/replay.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->stream_button_texture.load_from_file((resources_path + "images/stream.png").c_str())) + if(!theme->record_button_texture.load_from_file((resources_path + "images/record.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->close_texture.load_from_file((resources_path + "images/cross.png").c_str())) + if(!theme->stream_button_texture.load_from_file((resources_path + "images/stream.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->close_texture.load_from_file((resources_path + "images/cross.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; if(!theme->logo_texture.load_from_file((resources_path + "images/gpu_screen_recorder_logo.png").c_str())) goto error; - if(!theme->checkbox_circle_texture.load_from_file((resources_path + "images/checkbox_circle.png").c_str())) + if(!theme->checkbox_circle_texture.load_from_file((resources_path + "images/checkbox_circle.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->checkbox_background_texture.load_from_file((resources_path + "images/checkbox_background.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->play_texture.load_from_file((resources_path + "images/play.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->stop_texture.load_from_file((resources_path + "images/stop.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->checkbox_background_texture.load_from_file((resources_path + "images/checkbox_background.png").c_str())) + if(!theme->pause_texture.load_from_file((resources_path + "images/pause.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->play_texture.load_from_file((resources_path + "images/play.png").c_str())) + if(!theme->save_texture.load_from_file((resources_path + "images/save.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->stop_texture.load_from_file((resources_path + "images/stop.png").c_str())) + if(!theme->screenshot_texture.load_from_file((resources_path + "images/screenshot.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->pause_texture.load_from_file((resources_path + "images/pause.png").c_str())) + if(!theme->trash_texture.load_from_file((resources_path + "images/trash.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; - if(!theme->save_texture.load_from_file((resources_path + "images/save.png").c_str())) + if(!theme->ps4_home_texture.load_from_file((resources_path + "images/ps4_home.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->ps4_options_texture.load_from_file((resources_path + "images/ps4_options.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->ps4_dpad_up_texture.load_from_file((resources_path + "images/ps4_dpad_up.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->ps4_dpad_down_texture.load_from_file((resources_path + "images/ps4_dpad_down.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->ps4_dpad_left_texture.load_from_file((resources_path + "images/ps4_dpad_left.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->ps4_dpad_right_texture.load_from_file((resources_path + "images/ps4_dpad_right.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->ps4_cross_texture.load_from_file((resources_path + "images/ps4_cross.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) + goto error; + + if(!theme->ps4_triangle_texture.load_from_file((resources_path + "images/ps4_triangle.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP})) goto error; return true; @@ -102,29 +163,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/Utils.cpp b/src/Utils.cpp index 6d45196..c36a64a 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -22,6 +22,38 @@ namespace gsr { } } + bool starts_with(std::string_view str, const char *substr) { + size_t len = strlen(substr); + return str.size() >= len && memcmp(str.data(), substr, len) == 0; + } + + bool ends_with(std::string_view str, const char *substr) { + size_t len = strlen(substr); + return str.size() >= len && memcmp(str.data() + str.size() - len, substr, len) == 0; + } + + 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_home_dir() { const char *home_dir = getenv("HOME"); if(!home_dir) { @@ -114,6 +146,14 @@ namespace gsr { return xdg_videos_dir; } + std::string get_pictures_dir() { + auto xdg_vars = get_xdg_variables(); + std::string xdg_videos_dir = xdg_vars["XDG_PICTURES_DIR"]; + if(xdg_videos_dir.empty()) + xdg_videos_dir = get_home_dir() + "/Pictures"; + return xdg_videos_dir; + } + int create_directory_recursive(char *path) { int path_len = strlen(path); char *p = path; diff --git a/src/WindowSelector.cpp b/src/WindowSelector.cpp new file mode 100644 index 0000000..f04d600 --- /dev/null +++ b/src/WindowSelector.cpp @@ -0,0 +1,229 @@ +#include "../include/WindowSelector.hpp" +#include "../include/WindowUtils.hpp" + +#include <stdio.h> +#include <string.h> + +#include <X11/extensions/shape.h> +#include <X11/cursorfont.h> +#include <X11/keysym.h> + +namespace gsr { + static const int rectangle_border_size = 2; + + static int max_int(int a, int b) { + return a >= b ? a : b; + } + + static void set_region_rectangle(Display *dpy, Window window, int x, int y, int width, int height, int border_size) { + if(width < 0) { + x += width; + width = abs(width); + } + + if(height < 0) { + y += height; + height = abs(height); + } + + XRectangle rectangles[] = { + { + (short)max_int(0, x), (short)max_int(0, y), + (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height) + }, // Left + { + (short)max_int(0, x + width - border_size), (short)max_int(0, y), + (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height) + }, // Right + { + (short)max_int(0, x + border_size), (short)max_int(0, y), + (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size) + }, // Top + { + (short)max_int(0, x + border_size), (short)max_int(0, y + height - border_size), + (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size) + }, // Bottom + }; + XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted); + XFlush(dpy); + } + + static unsigned long mgl_color_to_x11_color(mgl::Color color) { + if(color.a == 0) + return 0; + return ((uint32_t)color.a << 24) | (((uint32_t)color.r * color.a / 0xFF) << 16) | (((uint32_t)color.g * color.a / 0xFF) << 8) | ((uint32_t)color.b * color.a / 0xFF); + } + + static Window get_cursor_window(Display *dpy) { + Window root_window = None; + Window 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); + return window; + } + + static void get_window_geometry(Display *dpy, Window window, mgl::vec2i &pos, mgl::vec2i &size) { + Window root_window; + int x = 0; + int y = 0; + unsigned int w = 0; + unsigned int h = 0; + unsigned int dummy_border, dummy_depth; + XGetGeometry(dpy, window, &root_window, &x, &y, &w, &h, &dummy_border, &dummy_depth); + pos.x = x; + pos.y = y; + size.x = w; + size.y = h; + } + + WindowSelector::WindowSelector() { + + } + + WindowSelector::~WindowSelector() { + stop(); + } + + bool WindowSelector::start(mgl::Color border_color) { + if(dpy) + return false; + + const unsigned long border_color_x11 = mgl_color_to_x11_color(border_color); + dpy = XOpenDisplay(nullptr); + if(!dpy) { + fprintf(stderr, "Error: WindowSelector::start: failed to connect to the X11 server\n"); + return false; + } + + const Window cursor_window = get_cursor_window(dpy); + mgl::vec2i cursor_window_pos, cursor_window_size; + get_window_geometry(dpy, cursor_window, cursor_window_pos, cursor_window_size); + + XVisualInfo vinfo; + memset(&vinfo, 0, sizeof(vinfo)); + XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo); + border_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone); + + XSetWindowAttributes window_attr; + window_attr.background_pixel = border_color_x11; + window_attr.border_pixel = 0; + window_attr.override_redirect = true; + window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask; + window_attr.colormap = border_window_colormap; + + Screen *screen = XDefaultScreenOfDisplay(dpy); + border_window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, XWidthOfScreen(screen), XHeightOfScreen(screen), 0, + vinfo.depth, InputOutput, vinfo.visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr); + if(!border_window) { + fprintf(stderr, "Error: WindowSelector::start: failed to create region window\n"); + stop(); + return false; + } + set_window_size_not_resizable(dpy, border_window, XWidthOfScreen(screen), XHeightOfScreen(screen)); + if(cursor_window && cursor_window != DefaultRootWindow(dpy)) + set_region_rectangle(dpy, border_window, cursor_window_pos.x, cursor_window_pos.y, cursor_window_size.x, cursor_window_size.y, rectangle_border_size); + else + set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0); + make_window_click_through(dpy, border_window); + XMapWindow(dpy, border_window); + + crosshair_cursor = XCreateFontCursor(dpy, XC_crosshair); + XGrabPointer(dpy, DefaultRootWindow(dpy), True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, crosshair_cursor, CurrentTime); + XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync, CurrentTime); + XFlush(dpy); + + selected = false; + canceled = false; + selected_window = None; + return true; + } + + void WindowSelector::stop() { + if(!dpy) + return; + + XUngrabPointer(dpy, CurrentTime); + XUngrabKeyboard(dpy, CurrentTime); + + if(border_window_colormap) { + XFreeColormap(dpy, border_window_colormap); + border_window_colormap = 0; + } + + if(border_window) { + XDestroyWindow(dpy, border_window); + border_window = 0; + } + + if(crosshair_cursor) { + XFreeCursor(dpy, crosshair_cursor); + crosshair_cursor = None; + } + + XFlush(dpy); + XCloseDisplay(dpy); + dpy = nullptr; + } + + bool WindowSelector::is_started() const { + return dpy != nullptr; + } + + bool WindowSelector::failed() const { + return !dpy; + } + + bool WindowSelector::poll_events() { + if(!dpy || selected) + return false; + + XEvent xev; + while(XPending(dpy)) { + XNextEvent(dpy, &xev); + + if(xev.type == MotionNotify) { + const Window motion_window = xev.xmotion.subwindow; + mgl::vec2i motion_window_pos, motion_window_size; + get_window_geometry(dpy, motion_window, motion_window_pos, motion_window_size); + if(motion_window && motion_window != DefaultRootWindow(dpy)) + set_region_rectangle(dpy, border_window, motion_window_pos.x, motion_window_pos.y, motion_window_size.x, motion_window_size.y, rectangle_border_size); + else + set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0); + XFlush(dpy); + } else if(xev.type == ButtonRelease && xev.xbutton.button == Button1) { + selected_window = xev.xbutton.subwindow; + const Window clicked_window_real = window_get_target_window_child(dpy, selected_window); + if(clicked_window_real) + selected_window = clicked_window_real; + selected = true; + + stop(); + break; + } else if(xev.type == KeyRelease && XKeycodeToKeysym(dpy, xev.xkey.keycode, 0) == XK_Escape) { + canceled = true; + selected = false; + stop(); + break; + } + } + return true; + } + + bool WindowSelector::take_selection() { + const bool result = selected; + selected = false; + return result; + } + + bool WindowSelector::take_canceled() { + const bool result = canceled; + canceled = false; + return result; + } + + Window WindowSelector::get_selection() const { + return selected_window; + } +}
\ No newline at end of file diff --git a/src/WindowUtils.cpp b/src/WindowUtils.cpp new file mode 100644 index 0000000..c6b278b --- /dev/null +++ b/src/WindowUtils.cpp @@ -0,0 +1,686 @@ +#include "../include/WindowUtils.hpp" +#include "../include/Utils.hpp" + +#include <X11/Xatom.h> +#include <X11/Xutil.h> +#include <X11/extensions/XInput2.h> +#include <X11/extensions/Xfixes.h> +#include <X11/extensions/shapeconst.h> +#include <X11/extensions/Xrandr.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); + } + + 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; + } + + 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); + + if(class_hint.res_name) + XFree(class_hint.res_name); + + if(class_hint.res_class) + XFree(class_hint.res_class); + + 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); + } + + void set_window_size_not_resizable(Display *dpy, Window window, int width, int height) { + XSizeHints *size_hints = XAllocSizeHints(); + if(size_hints) { + size_hints->width = width; + size_hints->height = height; + size_hints->min_width = width; + size_hints->min_height = height; + size_hints->max_width = width; + size_hints->max_height = height; + size_hints->flags = PSize | PMinSize | PMaxSize; + XSetWMNormalHints(dpy, window, size_hints); + XFree(size_hints); + } + } + + 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); + set_window_size_not_resizable(display, window, size, size); + + 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); + set_window_size_not_resizable(display, window, size, size); + + 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; + } + + std::vector<Monitor> get_monitors(Display *dpy) { + std::vector<Monitor> monitors; + int nmonitors = 0; + XRRMonitorInfo *monitor_info = XRRGetMonitors(dpy, DefaultRootWindow(dpy), True, &nmonitors); + if(monitor_info) { + for(int i = 0; i < nmonitors; ++i) { + char *monitor_name = XGetAtomName(dpy, monitor_info[i].name); + if(!monitor_name) + continue; + + monitors.push_back({mgl::vec2i(monitor_info[i].x, monitor_info[i].y), mgl::vec2i(monitor_info[i].width, monitor_info[i].height), std::string(monitor_name)}); + XFree(monitor_name); + } + XRRFreeMonitors(monitor_info); + } + return monitors; + } + + 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; + } + + static void xi_grab_all_mouse_devices(Display *dpy, bool grab) { + if(!dpy) + return; + + int num_devices = 0; + XIDeviceInfo *info = XIQueryDevice(dpy, 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; + if(grab) + XIGrabDevice(dpy, dev->deviceid, DefaultRootWindow(dpy), CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, XIOwnerEvents, &xi_masks); + else + XIUngrabDevice(dpy, dev->deviceid, CurrentTime); + } + + XFlush(dpy); + XIFreeDeviceInfo(info); + } + + void xi_grab_all_mouse_devices(Display *dpy) { + xi_grab_all_mouse_devices(dpy, true); + } + + void xi_ungrab_all_mouse_devices(Display *dpy) { + xi_grab_all_mouse_devices(dpy, false); + } + + void xi_warp_all_mouse_devices(Display *dpy, mgl::vec2i position) { + if(!dpy) + return; + + int num_devices = 0; + XIDeviceInfo *info = XIQueryDevice(dpy, XIAllDevices, &num_devices); + if(!info) + return; + + for (int i = 0; i < num_devices; ++i) { + const XIDeviceInfo *dev = &info[i]; + if(!device_is_mouse(dev)) + continue; + + XIWarpPointer(dpy, dev->deviceid, DefaultRootWindow(dpy), DefaultRootWindow(dpy), 0, 0, 0, 0, position.x, position.y); + } + + XFlush(dpy); + XIFreeDeviceInfo(info); + } + + void window_set_fullscreen(Display *dpy, Window window, bool fullscreen) { + const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False); + const Atom net_wm_state_fullscreen_atom = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False); + + XEvent xev; + xev.type = ClientMessage; + xev.xclient.window = window; + xev.xclient.message_type = net_wm_state_atom; + xev.xclient.format = 32; + xev.xclient.data.l[0] = fullscreen ? 1 : 0; + xev.xclient.data.l[1] = net_wm_state_fullscreen_atom; + xev.xclient.data.l[2] = 0; + xev.xclient.data.l[3] = 1; + xev.xclient.data.l[4] = 0; + + if(!XSendEvent(dpy, DefaultRootWindow(dpy), 0, SubstructureRedirectMask | SubstructureNotifyMask, &xev)) { + fprintf(stderr, "mgl warning: failed to change window fullscreen state\n"); + return; + } + + XFlush(dpy); + } + + bool window_is_fullscreen(Display *display, Window window) { + const Atom wm_state_atom = XInternAtom(display, "_NET_WM_STATE", False); + const Atom wm_state_fullscreen_atom = XInternAtom(display, "_NET_WM_STATE_FULLSCREEN", False); + + Atom type = None; + int format = 0; + unsigned long num_items = 0; + unsigned long bytes_after = 0; + unsigned char *properties = nullptr; + if(XGetWindowProperty(display, window, wm_state_atom, 0, 1024, False, XA_ATOM, &type, &format, &num_items, &bytes_after, &properties) < Success) { + fprintf(stderr, "Failed to get window wm state property\n"); + return false; + } + + if(!properties) + return false; + + bool is_fullscreen = false; + Atom *atoms = (Atom*)properties; + for(unsigned long i = 0; i < num_items; ++i) { + if(atoms[i] == wm_state_fullscreen_atom) { + is_fullscreen = true; + break; + } + } + + XFree(properties); + return is_fullscreen; + } + + #define _NET_WM_STATE_REMOVE 0 + #define _NET_WM_STATE_ADD 1 + #define _NET_WM_STATE_TOGGLE 2 + + bool set_window_wm_state(Display *dpy, Window window, Atom atom) { + const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False); + + XClientMessageEvent xclient; + memset(&xclient, 0, sizeof(xclient)); + + xclient.type = ClientMessage; + xclient.window = window; + xclient.message_type = net_wm_state_atom; + xclient.format = 32; + xclient.data.l[0] = _NET_WM_STATE_ADD; + xclient.data.l[1] = atom; + xclient.data.l[2] = 0; + xclient.data.l[3] = 0; + xclient.data.l[4] = 0; + + XSendEvent(dpy, DefaultRootWindow(dpy), False, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent*)&xclient); + XFlush(dpy); + return true; + } + + 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); + } + + bool make_window_sticky(Display *dpy, Window window) { + return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_STICKY", False)); + } + + bool hide_window_from_taskbar(Display *dpy, Window window) { + return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_SKIP_TASKBAR", False)); + } +}
\ No newline at end of file diff --git a/src/gui/Button.cpp b/src/gui/Button.cpp index fbf5cdd..6e343c4 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.15f; + + 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,31 @@ 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); + if(sprite.get_texture() && sprite.get_texture()->is_valid()) { + scale_sprite_to_button_size(); + const int padding_left = padding_left_scale * get_theme().window_height; + if(text.get_string().empty()) // Center + sprite.set_position((background.get_position() + background.get_size() * 0.5f - sprite.get_size() * 0.5f).floor()); + else // Left + sprite.set_position((draw_pos + mgl::vec2f(padding_left, background.get_size().y * 0.5f - sprite.get_size().y * 0.5f)).floor()); + window.draw(sprite); + + const int padding_icon_right = padding_right_icon_scale * get_button_height(); + text.set_position((sprite.get_position() + mgl::vec2f(sprite.get_size().x + padding_icon_right, sprite.get_size().y * 0.5f - text.get_bounds().size.y * 0.52f)).floor()); + window.draw(text); + } else { + 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(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)); @@ -58,24 +80,43 @@ namespace gsr { if(!visible) return {0.0f, 0.0f}; - 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; const int padding_right = padding_right_scale * get_theme().window_height; const mgl::vec2f text_bounds = text.get_bounds().size; - mgl::vec2f s = size; - if(s.x < 0.0001f) - s.x = padding_left + text_bounds.x + padding_right; - if(s.y < 0.0001f) - s.y = padding_top + text_bounds.y + padding_bottom; - return s; + mgl::vec2f widget_size = size; + + if(widget_size.y < 0.0001f) + widget_size.y = get_button_height(); + + if(widget_size.x < 0.0001f) { + widget_size.x = padding_left + text_bounds.x + padding_right; + if(sprite.get_texture() && sprite.get_texture()->is_valid()) { + scale_sprite_to_button_size(); + const int padding_icon_right = text_bounds.x > 0.001f ? padding_right_icon_scale * widget_size.y : 0.0f; + widget_size.x += sprite.get_size().x + padding_icon_right; + } + } + + return widget_size; } void Button::set_border_scale(float scale) { border_scale = scale; } + void Button::set_icon_padding_scale(float scale) { + icon_padding_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 +124,28 @@ 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 float widget_height = get_button_height(); + + const int padding_icon_top = padding_top_icon_scale * icon_padding_scale * widget_height; + const int padding_icon_bottom = padding_bottom_icon_scale * icon_padding_scale * widget_height; + + const float desired_height = widget_height - (padding_icon_top + padding_icon_bottom); + sprite.set_height((int)desired_height); + } + + float Button::get_button_height() { + const int padding_top = padding_top_scale * get_theme().window_height; + const int padding_bottom = padding_bottom_scale * get_theme().window_height; + + float widget_height = size.y; + if(widget_height < 0.0001f) + widget_height = padding_top + text.get_bounds().size.y + padding_bottom; + + return widget_height; + } }
\ No newline at end of file diff --git a/src/gui/ComboBox.cpp b/src/gui/ComboBox.cpp index 62b2086..4287a53 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() * 20); // 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 : font->get_character_size()) }; 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 : font->get_character_size()) }; } 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..5d1cc38 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)); } @@ -110,6 +110,14 @@ namespace gsr { window.draw(rect); } + if(activated) { + description.set_color(get_color_theme().tint_color); + icon_sprite.set_color(get_color_theme().tint_color); + } else { + description.set_color(mgl::Color(150, 150, 150)); + icon_sprite.set_color(mgl::Color(255, 255, 255)); + } + const int text_margin = size.y * 0.085; const auto title_bounds = title.get_bounds(); @@ -148,7 +156,7 @@ namespace gsr { window.draw(separator); } - if(mouse_inside_item == -1) { + if(mouse_inside_item == -1 && item.enabled) { const bool inside = mgl::FloatRect(item_position, item_size).contains({ (float)mouse_pos.x, (float)mouse_pos.y }); if(inside) { draw_rectangle_outline(window, item_position, item_size, get_color_theme().tint_color, border_size); @@ -161,16 +169,18 @@ namespace gsr { mgl::Sprite icon(item.icon_texture); icon.set_height((int)(item_size.y * 0.4f)); icon.set_position((item_position + mgl::vec2f(padding_left, item_size.y * 0.5f - icon.get_size().y * 0.5f)).floor()); + icon.set_color(item.enabled ? mgl::Color(255, 255, 255, 255) : mgl::Color(255, 255, 255, 80)); window.draw(icon); icon_offset = icon.get_size().x + icon_spacing; } item.text.set_position((item_position + mgl::vec2f(padding_left + icon_offset, item_size.y * 0.5f - text_bounds.size.y * 0.5f)).floor()); + item.text.set_color(item.enabled ? mgl::Color(255, 255, 255, 255) : mgl::Color(255, 255, 255, 80)); window.draw(item.text); const auto description_bounds = item.description_text.get_bounds(); item.description_text.set_position((item_position + mgl::vec2f(item_size.x - description_bounds.size.x - padding_right, item_size.y * 0.5f - description_bounds.size.y * 0.5f)).floor()); - item.description_text.set_color(mgl::Color(255, 255, 255, 120)); + item.description_text.set_color(item.enabled ? mgl::Color(255, 255, 255, 120) : mgl::Color(255, 255, 255, 40)); window.draw(item.description_text); item_position.y += item_size.y; @@ -179,6 +189,10 @@ namespace gsr { } void DropdownButton::add_item(const std::string &text, const std::string &id, const std::string &description) { + for(auto &item : items) { + if(item.id == id) + return; + } items.push_back({mgl::Text(text, *title_font), mgl::Text(description, *description_font), nullptr, id}); dirty = true; } @@ -201,6 +215,24 @@ namespace gsr { } } + void DropdownButton::set_item_description(const std::string &id, const std::string &new_description) { + for(auto &item : items) { + if(item.id == id) { + item.description_text.set_string(new_description); + return; + } + } + } + + void DropdownButton::set_item_enabled(const std::string &id, bool enabled) { + for(auto &item : items) { + if(item.id == id) { + item.enabled = enabled; + return; + } + } + } + void DropdownButton::set_description(std::string description_text) { description.set_string(std::move(description_text)); } @@ -210,14 +242,6 @@ namespace gsr { return; this->activated = activated; - - if(activated) { - description.set_color(get_color_theme().tint_color); - icon_sprite.set_color(get_color_theme().tint_color); - } else { - description.set_color(mgl::Color(150, 150, 150)); - icon_sprite.set_color(mgl::Color(255, 255, 255)); - } } void DropdownButton::update_if_dirty() { @@ -242,4 +266,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..6650c69 --- /dev/null +++ b/src/gui/GlobalSettingsPage.cpp @@ -0,0 +1,753 @@ +#include "../../include/gui/GlobalSettingsPage.hpp" + +#include "../../include/Overlay.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/Image.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"; + case GpuVendor::BROADCOM: return "broadcom"; + } + 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"; + case GpuVendor::BROADCOM: return "Broadcom"; + } + 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 bool key_is_alpha_numerical(mgl::Keyboard::Key key) { + return key >= mgl::Keyboard::A && key <= mgl::Keyboard::Num9; + } + + 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>("Global", "Settings"); + 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("Alpha-numerical keys can't be used alone in hotkeys, they have to be used one or more of these keys: Alt, Ctrl, Shift and Super.\nPress Esc to cancel or Backspace to remove the hotkey.", 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.13f).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()); + 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(title_text); + + const float title_text_bottom = title_text.get_position().y + title_text.get_bounds().size.y; + hotkey_text.set_position( + mgl::vec2f( + bg_rect.get_position().x + bg_rect.get_size().x*0.5f - hotkey_text.get_bounds().size.x*0.5f, + title_text_bottom + (description_text.get_position().y - title_text_bottom) * 0.5f - hotkey_text.get_bounds().size.y*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); + + 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_replay_partial_save_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, "Save 1 minute replay:", get_color_theme().text_color)); + auto save_replay_1_min_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + save_replay_1_min_button_ptr = save_replay_1_min_button.get(); + list->add_widget(std::move(save_replay_1_min_button)); + + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Save 10 minute replay:", get_color_theme().text_color)); + auto save_replay_10_min_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + save_replay_10_min_button_ptr = save_replay_10_min_button.get(); + list->add_widget(std::move(save_replay_10_min_button)); + + save_replay_1_min_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::REPLAY_SAVE_1_MIN); + }; + + save_replay_10_min_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::REPLAY_SAVE_10_MIN); + }; + + 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_screenshot_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, "Take a screenshot:", get_color_theme().text_color)); + auto take_screenshot_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + take_screenshot_button_ptr = take_screenshot_button.get(); + list->add_widget(std::move(take_screenshot_button)); + + take_screenshot_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::TAKE_SCREENSHOT); + }; + + return list; + } + + std::unique_ptr<List> GlobalSettingsPage::create_screenshot_region_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, "Take a screenshot of a region:", get_color_theme().text_color)); + auto take_screenshot_region_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + take_screenshot_region_button_ptr = take_screenshot_region_button.get(); + list->add_widget(std::move(take_screenshot_region_button)); + + take_screenshot_region_button_ptr->on_click = [this] { + configure_hotkey_start(ConfigureHotkeyType::TAKE_SCREENSHOT_REGION); + }; + + 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.replay_config.save_1_min_hotkey = {mgl::Keyboard::Unknown, 0}; + config.replay_config.save_10_min_hotkey = {mgl::Keyboard::Unknown, 0}; + config.screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Unknown, 0}; + config.screenshot_config.take_screenshot_region_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.set_hotkeys_to_default(); + load_hotkeys(); + overlay->rebind_all_keyboard_hotkeys(); + }; + list->add_widget(std::move(reset_hotkeys_button)); + + return list; + } + + static std::unique_ptr<List> create_joystick_hotkey_text(mgl::Texture *image1, mgl::Texture *image2, float max_height, const char *suffix) { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Press", get_color_theme().text_color)); + list->add_widget(std::make_unique<Image>(image1, mgl::vec2f{max_height, 1000.0f}, Image::ScaleBehavior::SCALE)); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "and", get_color_theme().text_color)); + list->add_widget(std::make_unique<Image>(image2, mgl::vec2f{max_height, 1000.0f}, Image::ScaleBehavior::SCALE)); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, suffix, get_color_theme().text_color)); + return list; + } + + std::unique_ptr<Subsection> GlobalSettingsPage::create_keyboard_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>("Keyboard 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<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_replay_partial_save_hotkey_options()); + list_ptr->add_widget(create_record_hotkey_options()); + list_ptr->add_widget(create_stream_hotkey_options()); + list_ptr->add_widget(create_screenshot_hotkey_options()); + list_ptr->add_widget(create_screenshot_region_hotkey_options()); + list_ptr->add_widget(create_hotkey_control_buttons()); + return subsection; + } + + std::unique_ptr<Subsection> GlobalSettingsPage::create_controller_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>("Controller 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 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_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_options_texture, get_theme().body_font.get_character_size(), "to show/hide the UI")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_up_texture, get_theme().body_font.get_character_size(), "to take a screenshot")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_down_texture, get_theme().body_font.get_character_size(), "to save a replay")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_left_texture, get_theme().body_font.get_character_size(), "to start/stop recording")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_right_texture, get_theme().body_font.get_character_size(), "to turn replay on/off")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_cross_texture, get_theme().body_font.get_character_size(), "to save a 1 minute replay")); + list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_triangle_texture, get_theme().body_font.get_character_size(), "to save a 10 minute replay")); + 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_keyboard_hotkey_subsection(scrollable_page.get())); + settings_list->add_widget(create_controller_hotkey_subsection(scrollable_page.get())); + settings_list->add_widget(create_application_options_subsection(scrollable_page.get())); + settings_list->add_widget(create_application_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(); + if(on_page_closed) + on_page_closed(); + } + + 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.replay_config.start_stop_hotkey.to_string()); + save_replay_button_ptr->set_text(config.replay_config.save_hotkey.to_string()); + save_replay_1_min_button_ptr->set_text(config.replay_config.save_1_min_hotkey.to_string()); + save_replay_10_min_button_ptr->set_text(config.replay_config.save_10_min_hotkey.to_string()); + + start_stop_recording_button_ptr->set_text(config.record_config.start_stop_hotkey.to_string()); + pause_unpause_recording_button_ptr->set_text(config.record_config.pause_unpause_hotkey.to_string()); + + start_stop_streaming_button_ptr->set_text(config.streaming_config.start_stop_hotkey.to_string()); + + take_screenshot_button_ptr->set_text(config.screenshot_config.take_screenshot_hotkey.to_string()); + take_screenshot_region_button_ptr->set_text(config.screenshot_config.take_screenshot_region_hotkey.to_string()); + + show_hide_button_ptr->set_text(config.main_config.show_hide_hotkey.to_string()); + } + + 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(event.key.code == mgl::Keyboard::Backspace) { + configure_config_hotkey = {mgl::Keyboard::Unknown, 0}; + configure_hotkey_button->set_text(""); + configure_hotkey_stop_and_save(); + 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(configure_config_hotkey.to_string()); + } else if(event.key.code != mgl::Keyboard::Unknown && (configure_config_hotkey.modifiers != 0 || !key_is_alpha_numerical(event.key.code))) { + configure_config_hotkey.key = event.key.code; + configure_hotkey_button->set_text(configure_config_hotkey.to_string()); + 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(configure_config_hotkey.to_string()); + } + + 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::REPLAY_SAVE_1_MIN: + return save_replay_1_min_button_ptr; + case ConfigureHotkeyType::REPLAY_SAVE_10_MIN: + return save_replay_10_min_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::TAKE_SCREENSHOT: + return take_screenshot_button_ptr; + case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION: + return take_screenshot_region_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::REPLAY_SAVE_1_MIN: + return &config.replay_config.save_1_min_hotkey; + case ConfigureHotkeyType::REPLAY_SAVE_10_MIN: + return &config.replay_config.save_10_min_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::TAKE_SCREENSHOT: + return &config.screenshot_config.take_screenshot_hotkey; + case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION: + return &config.screenshot_config.take_screenshot_region_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.screenshot_config.take_screenshot_hotkey, + &config.screenshot_config.take_screenshot_region_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::REPLAY_SAVE_1_MIN: + hotkey_configure_action_name = "Save 1 minute replay"; + break; + case ConfigureHotkeyType::REPLAY_SAVE_10_MIN: + hotkey_configure_action_name = "Save 10 minute 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::TAKE_SCREENSHOT: + hotkey_configure_action_name = "Take a screenshot"; + break; + case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION: + hotkey_configure_action_name = "Take a screenshot of a region"; + 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()); + + 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; + if(configure_config_hotkey.key != mgl::Keyboard::Unknown) { + 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 \"" + configure_config_hotkey.to_string() + " 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()); + 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..b4005f5 100644 --- a/src/gui/GsrPage.cpp +++ b/src/gui/GsrPage.cpp @@ -8,8 +8,9 @@ namespace gsr { static const float button_spacing_scale = 0.015f; - GsrPage::GsrPage() : - label_text("Settings", get_theme().title_font) + GsrPage::GsrPage(const char *top_text, const char *bottom_text) : + top_text(top_text, get_theme().title_font), + bottom_text(bottom_text, get_theme().title_font) { const float margin = 0.02f; set_margins(margin, margin, margin, margin); @@ -38,8 +39,9 @@ namespace gsr { // Process widgets by visibility (backwards) return widgets.for_each_reverse([selected_widget, &window, &event, content_page_position](std::unique_ptr<Widget> &widget) { - if(widget.get() != selected_widget) { - if(!widget->on_event(event, window, content_page_position)) + Widget *p = widget.get(); + if(p != selected_widget) { + if(!p->on_event(event, window, content_page_position)) return false; } return true; @@ -80,13 +82,17 @@ namespace gsr { window.draw(background); const int text_margin = background.get_size().y * 0.085; - label_text.set_position((background.get_position() + mgl::vec2f(background.get_size().x * 0.5f - label_text.get_bounds().size.x * 0.5f, text_margin)).floor()); - window.draw(label_text); + + top_text.set_position((background.get_position() + mgl::vec2f(background.get_size().x * 0.5f - top_text.get_bounds().size.x * 0.5f, text_margin)).floor()); + window.draw(top_text); mgl::Sprite icon(&get_theme().settings_texture); icon.set_height((int)(background.get_size().y * 0.5f)); icon.set_position((background.get_position() + background.get_size() * 0.5f - icon.get_size() * 0.5f).floor()); window.draw(icon); + + bottom_text.set_position((background.get_position() + mgl::vec2f(background.get_size().x * 0.5f - bottom_text.get_bounds().size.x * 0.5f, background.get_size().y - bottom_text.get_bounds().size.y - text_margin)).floor()); + window.draw(bottom_text); } void GsrPage::draw_buttons(mgl::Window &window, mgl::vec2f body_pos, mgl::vec2f body_size) { @@ -102,15 +108,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 +120,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/Image.cpp b/src/gui/Image.cpp new file mode 100644 index 0000000..b6cec9a --- /dev/null +++ b/src/gui/Image.cpp @@ -0,0 +1,39 @@ +#include "../../include/gui/Image.hpp" +#include "../../include/gui/Utils.hpp" + +#include <mglpp/window/Window.hpp> +#include <mglpp/graphics/Texture.hpp> + +namespace gsr { + Image::Image(mgl::Texture *texture, mgl::vec2f size, ScaleBehavior scale_behavior) : + sprite(texture), size(size), scale_behavior(scale_behavior) + { + + } + + bool Image::on_event(mgl::Event&, mgl::Window&, mgl::vec2f) { + return true; + } + + void Image::draw(mgl::Window &window, mgl::vec2f offset) { + if(!visible) + return; + + sprite.set_size(get_size()); + sprite.set_position((position + offset).floor()); + window.draw(sprite); + } + + mgl::vec2f Image::get_size() { + if(!visible || !sprite.get_texture()) + return {0.0f, 0.0f}; + + const mgl::vec2f sprite_size = sprite.get_texture()->get_size().to_vec2f(); + if(size.x < 0.001f && size.y < 0.001f) + return sprite_size; + else if(scale_behavior == ScaleBehavior::SCALE) + return scale_keep_aspect_ratio(sprite_size, size); + else + return clamp_keep_aspect_ratio(sprite_size, size); + } +}
\ No newline at end of file diff --git a/src/gui/List.cpp b/src/gui/List.cpp index 5294e36..57a6045 100644 --- a/src/gui/List.cpp +++ b/src/gui/List.cpp @@ -24,14 +24,23 @@ namespace gsr { // Process widgets by visibility (backwards) return widgets.for_each_reverse([selected_widget, &event, &window](std::unique_ptr<Widget> &widget) { // Ignore offset because widgets are positioned with offset in ::draw, this solution is simpler - if(widget.get() != selected_widget) { - if(!widget->on_event(event, window, mgl::vec2f(0.0f, 0.0f))) + Widget *p = widget.get(); + if(p != selected_widget) { + if(!p->on_event(event, window, mgl::vec2f(0.0f, 0.0f))) return false; } return true; }); } + List::~List() { + widgets.for_each([this](std::unique_ptr<Widget> &widget) { + if(widget->parent_widget == this) + widget->parent_widget = nullptr; + return true; + }, true); + } + void List::draw(mgl::Window &window, mgl::vec2f offset) { if(!visible) return; @@ -104,15 +113,6 @@ namespace gsr { selected_widget->draw(window, mgl::vec2f(0.0f, 0.0f)); } - // void List::remove_child_widget(Widget *widget) { - // for(auto it = widgets.begin(), end = widgets.end(); it != end; ++it) { - // if(it->get() == widget) { - // widgets.erase(it); - // return; - // } - // } - // } - void List::add_widget(std::unique_ptr<Widget> widget) { widget->parent_widget = this; widgets.push_back(std::move(widget)); @@ -122,6 +122,10 @@ namespace gsr { widgets.remove(widget); } + void List::replace_widget(Widget *widget, std::unique_ptr<Widget> new_widget) { + widgets.replace_item(widget, std::move(new_widget)); + } + void List::clear() { widgets.clear(); } @@ -137,6 +141,10 @@ namespace gsr { return nullptr; } + size_t List::get_num_children() const { + return widgets.size(); + } + void List::set_spacing(float spacing) { spacing_scale = spacing; } diff --git a/src/gui/Page.cpp b/src/gui/Page.cpp index ae13d82..5f21b71 100644 --- a/src/gui/Page.cpp +++ b/src/gui/Page.cpp @@ -1,14 +1,13 @@ #include "../../include/gui/Page.hpp" namespace gsr { - // void Page::remove_child_widget(Widget *widget) { - // for(auto it = widgets.begin(), end = widgets.end(); it != end; ++it) { - // if(it->get() == widget) { - // widgets.erase(it); - // return; - // } - // } - // } + Page::~Page() { + widgets.for_each([this](std::unique_ptr<Widget> &widget) { + if(widget->parent_widget == this) + widget->parent_widget = nullptr; + return true; + }, true); + } void Page::add_widget(std::unique_ptr<Widget> widget) { widget->parent_widget = this; diff --git a/src/gui/RadioButton.cpp b/src/gui/RadioButton.cpp index 061d811..bbb958a 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,18 +158,18 @@ 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; } } } - const std::string RadioButton::get_selected_id() const { + const std::string& RadioButton::get_selected_id() const { if(items.empty()) { static std::string dummy; return dummy; @@ -177,4 +177,13 @@ namespace gsr { return items[selected_item].id; } } + + const std::string& RadioButton::get_selected_text() const { + if(items.empty()) { + static std::string dummy; + return dummy; + } else { + return items[selected_item].text.get_string(); + } + } }
\ No newline at end of file diff --git a/src/gui/ScreenshotSettingsPage.cpp b/src/gui/ScreenshotSettingsPage.cpp new file mode 100644 index 0000000..27a94b0 --- /dev/null +++ b/src/gui/ScreenshotSettingsPage.cpp @@ -0,0 +1,332 @@ +#include "../../include/gui/ScreenshotSettingsPage.hpp" +#include "../../include/gui/GsrPage.hpp" +#include "../../include/gui/PageStack.hpp" +#include "../../include/Theme.hpp" +#include "../../include/GsrInfo.hpp" +#include "../../include/Utils.hpp" +#include "../../include/gui/List.hpp" +#include "../../include/gui/ScrollablePage.hpp" +#include "../../include/gui/Label.hpp" +#include "../../include/gui/Subsection.hpp" +#include "../../include/gui/FileChooser.hpp" + +namespace gsr { + ScreenshotSettingsPage::ScreenshotSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack) : + StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()), + config(config), + gsr_info(gsr_info), + page_stack(page_stack) + { + capture_options = get_supported_capture_options(*gsr_info); + + auto content_page = std::make_unique<GsrPage>("Screenshot", "Settings"); + 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(); + } + + std::unique_ptr<ComboBox> ScreenshotSettingsPage::create_record_area_box() { + auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font); + // TODO: Show options not supported but disable them + if(capture_options.window) + record_area_box->add_item("Window", "window"); + if(capture_options.region) + record_area_box->add_item("Region", "region"); + if(!capture_options.monitors.empty()) + record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor"); + 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(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> ScreenshotSettingsPage::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()); + return record_area_list; + } + + std::unique_ptr<Entry> ScreenshotSettingsPage::create_image_width_entry() { + auto image_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3); + image_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15); + image_width_entry_ptr = image_width_entry.get(); + return image_width_entry; + } + + std::unique_ptr<Entry> ScreenshotSettingsPage::create_image_height_entry() { + auto image_height_entry = std::make_unique<Entry>(&get_theme().body_font, "1080", get_theme().body_font.get_character_size() * 3); + image_height_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15); + image_height_entry_ptr = image_height_entry.get(); + return image_height_entry; + } + + std::unique_ptr<List> ScreenshotSettingsPage::create_image_resolution() { + auto area_size_params_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + area_size_params_list->add_widget(create_image_width_entry()); + area_size_params_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "x", get_color_theme().text_color)); + area_size_params_list->add_widget(create_image_height_entry()); + return area_size_params_list; + } + + std::unique_ptr<List> ScreenshotSettingsPage::create_image_resolution_section() { + auto image_resolution_list = std::make_unique<List>(List::Orientation::VERTICAL); + image_resolution_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image resolution limit:", get_color_theme().text_color)); + image_resolution_list->add_widget(create_image_resolution()); + image_resolution_list_ptr = image_resolution_list.get(); + return image_resolution_list; + } + + std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_restore_portal_session_checkbox() { + auto restore_portal_session_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Restore portal session"); + restore_portal_session_checkbox->set_checked(true); + restore_portal_session_checkbox_ptr = restore_portal_session_checkbox.get(); + return restore_portal_session_checkbox; + } + + std::unique_ptr<List> ScreenshotSettingsPage::create_restore_portal_session_section() { + auto restore_portal_session_list = std::make_unique<List>(List::Orientation::VERTICAL); + restore_portal_session_list->add_widget(std::make_unique<Label>(&get_theme().body_font, " ", get_color_theme().text_color)); + restore_portal_session_list->add_widget(create_restore_portal_session_checkbox()); + restore_portal_session_list_ptr = restore_portal_session_list.get(); + return restore_portal_session_list; + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_change_image_resolution_section() { + auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Change image resolution"); + change_image_resolution_checkbox_ptr = checkbox.get(); + return checkbox; + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_capture_target_section() { + 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()); + capture_target_list->add_widget(create_image_resolution_section()); + capture_target_list->add_widget(create_restore_portal_session_section()); + + ll->add_widget(std::move(capture_target_list)); + ll->add_widget(create_change_image_resolution_section()); + return std::make_unique<Subsection>("Record area", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<List> ScreenshotSettingsPage::create_image_quality_section() { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image quality:", get_color_theme().text_color)); + + auto image_quality_box = std::make_unique<ComboBox>(&get_theme().body_font); + image_quality_box->add_item("Medium", "medium"); + image_quality_box->add_item("High", "high"); + image_quality_box->add_item("Very high (Recommended)", "very_high"); + image_quality_box->add_item("Ultra", "ultra"); + image_quality_box->set_selected_item("very_high"); + + image_quality_box_ptr = image_quality_box.get(); + list->add_widget(std::move(image_quality_box)); + + return list; + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_cursor_section() { + auto record_cursor_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record cursor"); + record_cursor_checkbox->set_checked(true); + record_cursor_checkbox_ptr = record_cursor_checkbox.get(); + return record_cursor_checkbox; + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_image_section() { + auto image_section_list = std::make_unique<List>(List::Orientation::VERTICAL); + image_section_list->add_widget(create_image_quality_section()); + image_section_list->add_widget(create_record_cursor_section()); + return std::make_unique<Subsection>("Image", std::move(image_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<List> ScreenshotSettingsPage::create_save_directory(const char *label) { + auto save_directory_list = std::make_unique<List>(List::Orientation::VERTICAL); + save_directory_list->add_widget(std::make_unique<Label>(&get_theme().body_font, label, get_color_theme().text_color)); + auto save_directory_button = std::make_unique<Button>(&get_theme().body_font, get_pictures_dir().c_str(), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + save_directory_button_ptr = save_directory_button.get(); + save_directory_button->on_click = [this]() { + auto select_directory_page = std::make_unique<GsrPage>("File", "Settings"); + select_directory_page->add_button("Save", "save", get_color_theme().tint_color); + select_directory_page->add_button("Cancel", "cancel", get_color_theme().page_bg_color); + + auto file_chooser = std::make_unique<FileChooser>(save_directory_button_ptr->get_text().c_str(), select_directory_page->get_inner_size()); + FileChooser *file_chooser_ptr = file_chooser.get(); + 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") { + save_directory_button_ptr->set_text(file_chooser_ptr->get_current_directory()); + page_stack->pop(); + } else if(id == "cancel") { + page_stack->pop(); + } + }; + + page_stack->push(std::move(select_directory_page)); + }; + save_directory_list->add_widget(std::move(save_directory_button)); + return save_directory_list; + } + + std::unique_ptr<ComboBox> ScreenshotSettingsPage::create_image_format_box() { + auto box = std::make_unique<ComboBox>(&get_theme().body_font); + if(gsr_info->supported_image_formats.jpeg) + box->add_item("jpg", "jpg"); + if(gsr_info->supported_image_formats.png) + box->add_item("png", "png"); + image_format_box_ptr = box.get(); + return box; + } + + std::unique_ptr<List> ScreenshotSettingsPage::create_image_format_section() { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image format:", get_color_theme().text_color)); + list->add_widget(create_image_format_box()); + return list; + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_file_info_section() { + 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 screenshot:")); + file_info_data_list->add_widget(create_image_format_section()); + return std::make_unique<Subsection>("File info", std::move(file_info_data_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_save_screenshot_in_game_folder() { + char text[256]; + snprintf(text, sizeof(text), "Save screenshot 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_screenshot_in_game_folder_checkbox_ptr = checkbox.get(); + return checkbox; + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_general_section() { + return std::make_unique<Subsection>("General", create_save_screenshot_in_game_folder(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_notifications_section() { + auto show_screenshot_saved_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show screenshot saved notification"); + show_screenshot_saved_notification_checkbox->set_checked(true); + show_screenshot_saved_notification_checkbox_ptr = show_screenshot_saved_notification_checkbox.get(); + return std::make_unique<Subsection>("Notifications", std::move(show_screenshot_saved_notification_checkbox), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + } + + std::unique_ptr<Widget> ScreenshotSettingsPage::create_settings() { + auto page_list = std::make_unique<List>(List::Orientation::VERTICAL); + page_list->set_spacing(0.018f); + auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size() - mgl::vec2f(0.0f, page_list->get_size().y + 0.018f * get_theme().window_height)); + settings_scrollable_page_ptr = scrollable_page.get(); + page_list->add_widget(std::move(scrollable_page)); + + auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL); + settings_list->set_spacing(0.018f); + settings_list->add_widget(create_capture_target_section()); + settings_list->add_widget(create_image_section()); + settings_list->add_widget(create_file_info_section()); + settings_list->add_widget(create_general_section()); + settings_list->add_widget(create_notifications_section()); + settings_scrollable_page_ptr->add_widget(std::move(settings_list)); + return page_list; + } + + void ScreenshotSettingsPage::add_widgets() { + content_page_ptr->add_widget(create_settings()); + + record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) { + const bool portal_selected = id == "portal"; + image_resolution_list_ptr->set_visible(change_image_resolution_checkbox_ptr->is_checked()); + restore_portal_session_list_ptr->set_visible(portal_selected); + return true; + }; + + change_image_resolution_checkbox_ptr->on_changed = [this](bool checked) { + image_resolution_list_ptr->set_visible(checked); + }; + + if(!capture_options.monitors.empty()) + record_area_box_ptr->set_selected_item("focused_monitor"); + else if(capture_options.portal) + record_area_box_ptr->set_selected_item("portal"); + else if(capture_options.window) + record_area_box_ptr->set_selected_item("window"); + else + record_area_box_ptr->on_selection_changed("", ""); + } + + void ScreenshotSettingsPage::on_navigate_away_from_page() { + save(); + } + + void ScreenshotSettingsPage::load() { + record_area_box_ptr->set_selected_item(config.screenshot_config.record_area_option); + change_image_resolution_checkbox_ptr->set_checked(config.screenshot_config.change_image_resolution); + image_quality_box_ptr->set_selected_item(config.screenshot_config.image_quality); + image_format_box_ptr->set_selected_item(config.screenshot_config.image_format); + record_cursor_checkbox_ptr->set_checked(config.screenshot_config.record_cursor); + restore_portal_session_checkbox_ptr->set_checked(config.screenshot_config.restore_portal_session); + save_directory_button_ptr->set_text(config.screenshot_config.save_directory); + save_screenshot_in_game_folder_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_in_game_folder); + show_screenshot_saved_notification_checkbox_ptr->set_checked(config.screenshot_config.show_screenshot_saved_notifications); + + if(config.screenshot_config.image_width == 0) + config.screenshot_config.image_width = 1920; + + if(config.screenshot_config.image_height == 0) + config.screenshot_config.image_height = 1080; + + if(config.screenshot_config.image_width < 32) + config.screenshot_config.image_width = 32; + image_width_entry_ptr->set_text(std::to_string(config.screenshot_config.image_width)); + + if(config.screenshot_config.image_height < 32) + config.screenshot_config.image_height = 32; + image_height_entry_ptr->set_text(std::to_string(config.screenshot_config.image_height)); + } + + void ScreenshotSettingsPage::save() { + config.screenshot_config.record_area_option = record_area_box_ptr->get_selected_id(); + config.screenshot_config.image_width = atoi(image_width_entry_ptr->get_text().c_str()); + config.screenshot_config.image_height = atoi(image_height_entry_ptr->get_text().c_str()); + config.screenshot_config.change_image_resolution = change_image_resolution_checkbox_ptr->is_checked(); + config.screenshot_config.image_quality = image_quality_box_ptr->get_selected_id(); + config.screenshot_config.image_format = image_format_box_ptr->get_selected_id(); + config.screenshot_config.record_cursor = record_cursor_checkbox_ptr->is_checked(); + config.screenshot_config.restore_portal_session = restore_portal_session_checkbox_ptr->is_checked(); + config.screenshot_config.save_directory = save_directory_button_ptr->get_text(); + config.screenshot_config.save_screenshot_in_game_folder = save_screenshot_in_game_folder_checkbox_ptr->is_checked(); + config.screenshot_config.show_screenshot_saved_notifications = show_screenshot_saved_notification_checkbox_ptr->is_checked(); + + if(config.screenshot_config.image_width == 0) + config.screenshot_config.image_width = 1920; + + if(config.screenshot_config.image_height == 0) + config.screenshot_config.image_height = 1080; + + if(config.screenshot_config.image_width < 32) { + config.screenshot_config.image_width = 32; + image_width_entry_ptr->set_text("32"); + } + + if(config.screenshot_config.image_height < 32) { + config.screenshot_config.image_height = 32; + image_height_entry_ptr->set_text("32"); + } + + save_config(config); + } +}
\ No newline at end of file diff --git a/src/gui/ScrollablePage.cpp b/src/gui/ScrollablePage.cpp index 74dd715..cec20d3 100644 --- a/src/gui/ScrollablePage.cpp +++ b/src/gui/ScrollablePage.cpp @@ -15,19 +15,49 @@ namespace gsr { ScrollablePage::ScrollablePage(mgl::vec2f size) : size(size) {} + ScrollablePage::~ScrollablePage() { + widgets.for_each([this](std::unique_ptr<Widget> &widget) { + if(widget->parent_widget == this) + widget->parent_widget = nullptr; + return true; + }, true); + } + bool ScrollablePage::on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) { 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; @@ -35,8 +65,9 @@ namespace gsr { // Process widgets by visibility (backwards) const bool continue_events = widgets.for_each_reverse([selected_widget, &window, &event, offset](std::unique_ptr<Widget> &widget) { - if(widget.get() != selected_widget) { - if(!widget->on_event(event, window, offset)) + Widget *p = widget.get(); + if(p != selected_widget) { + if(!p->on_event(event, window, offset)) return false; } return true; @@ -51,14 +82,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 +98,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 +158,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..26e7335 100644 --- a/src/gui/SettingsPage.cpp +++ b/src/gui/SettingsPage.cpp @@ -8,31 +8,38 @@ #include "../../include/GsrInfo.hpp" #include "../../include/Utils.hpp" -#include <mglpp/graphics/Rectangle.hpp> -#include <mglpp/graphics/Sprite.hpp> -#include <mglpp/graphics/Text.hpp> -#include <mglpp/window/Window.hpp> - #include <string.h> namespace gsr { + static const char *custom_app_audio_tag = "[custom]"; + enum class AudioTrackType { DEVICE, APPLICATION, APPLICATION_CUSTOM }; - SettingsPage::SettingsPage(Type type, const GsrInfo &gsr_info, Config &config, PageStack *page_stack) : + static const char* settings_page_type_to_title_text(SettingsPage::Type type) { + switch(type) { + case SettingsPage::Type::REPLAY: return "Instant Replay"; + case SettingsPage::Type::RECORD: return "Record"; + case SettingsPage::Type::STREAM: return "Livestream"; + } + return ""; + } + + 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>(); + auto content_page = std::make_unique<GsrPage>(settings_page_type_to_title_text(type), "Settings"); 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") @@ -41,9 +48,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,42 +62,35 @@ 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) - // record_area_box->add_item("Window", "window"); - if(gsr_info.supported_capture_options.focused) + if(capture_options.window) + record_area_box->add_item("Window", "window"); + 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) { + if(capture_options.region) + record_area_box->add_item("Region", "region"); + if(!capture_options.monitors.empty()) + record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor"); + 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; } - std::unique_ptr<List> SettingsPage::create_select_window() { - auto select_window_list = std::make_unique<List>(List::Orientation::VERTICAL); - select_window_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Select window:", get_color_theme().text_color)); - select_window_list->add_widget(std::make_unique<Button>(&get_theme().body_font, "Click here to select a window...", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120))); - select_window_list_ptr = select_window_list.get(); - return select_window_list; - } - std::unique_ptr<Entry> SettingsPage::create_area_width_entry() { auto area_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3); area_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15); @@ -172,12 +172,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_section() { 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_select_window()); + capture_target_list->add_widget(create_record_area()); capture_target_list->add_widget(create_area_size_section()); capture_target_list->add_widget(create_video_resolution_section()); capture_target_list->add_widget(create_restore_portal_session_section()); @@ -187,128 +186,229 @@ namespace gsr { return std::make_unique<Subsection>("Record area", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); } - std::unique_ptr<ComboBox> SettingsPage::create_audio_device_selection_combobox() { + static bool audio_device_is_output(const std::string &audio_device_id) { + return audio_device_id == "default_output" || ends_with(audio_device_id, ".monitor"); + } + + std::unique_ptr<ComboBox> SettingsPage::create_audio_device_selection_combobox(AudioDeviceType device_type) { auto audio_device_box = std::make_unique<ComboBox>(&get_theme().body_font); for(const auto &audio_device : audio_devices) { - audio_device_box->add_item(audio_device.description, audio_device.name); + const bool device_is_output = audio_device_is_output(audio_device.name); + if((device_type == AudioDeviceType::OUTPUT && device_is_output) || (device_type == AudioDeviceType::INPUT && !device_is_output)) { + std::string description = audio_device.description; + if(starts_with(description, "Monitor of ")) + description.erase(0, 11); + audio_device_box->add_item(description, audio_device.name); + } } return audio_device_box; } - std::unique_ptr<Button> SettingsPage::create_remove_audio_device_button(List *audio_device_list_ptr) { - auto remove_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "Remove", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); - remove_audio_track_button->on_click = [this, audio_device_list_ptr]() { - audio_track_list_ptr->remove_widget(audio_device_list_ptr); + static void set_application_audio_options_visible(Subsection *audio_track_subsection, bool visible, const GsrInfo &gsr_info) { + if(!gsr_info.system_info.supports_app_audio) + visible = false; + + List *audio_track_items_list = dynamic_cast<List*>(audio_track_subsection->get_inner_widget()); + + List *buttons_list = dynamic_cast<List*>(audio_track_items_list->get_child_widget_by_index(1)); + Button *add_application_audio_button = dynamic_cast<Button*>(buttons_list->get_child_widget_by_index(2)); + add_application_audio_button->set_visible(visible); + + CheckBox *invert_app_audio_checkbox = dynamic_cast<CheckBox*>(audio_track_items_list->get_child_widget_by_index(3)); + invert_app_audio_checkbox->set_visible(visible); + } + + static void set_application_audio_options_visible(List *audio_track_section_list_ptr, bool visible, const GsrInfo &gsr_info) { + audio_track_section_list_ptr->for_each_child_widget([visible, &gsr_info](std::unique_ptr<Widget> &widget) { + Subsection *audio_track_subsection = dynamic_cast<Subsection*>(widget.get()); + set_application_audio_options_visible(audio_track_subsection, visible, gsr_info); + return true; + }); + } + + std::unique_ptr<Button> SettingsPage::create_remove_audio_device_button(List *audio_input_list_ptr, List *audio_device_list_ptr) { + auto remove_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 0)); + remove_audio_track_button->set_icon(&get_theme().trash_texture); + remove_audio_track_button->set_icon_padding_scale(0.75f); + remove_audio_track_button->on_click = [audio_input_list_ptr, audio_device_list_ptr]() { + audio_input_list_ptr->remove_widget(audio_device_list_ptr); }; return remove_audio_track_button; } - std::unique_ptr<List> SettingsPage::create_audio_device() { + std::unique_ptr<List> SettingsPage::create_audio_device(AudioDeviceType device_type, List *audio_input_list_ptr) { auto audio_device_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); audio_device_list->userdata = (void*)(uintptr_t)AudioTrackType::DEVICE; - audio_device_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Device:", get_color_theme().text_color)); - audio_device_list->add_widget(create_audio_device_selection_combobox()); - audio_device_list->add_widget(create_remove_audio_device_button(audio_device_list.get())); + audio_device_list->add_widget(std::make_unique<Label>(&get_theme().body_font, device_type == AudioDeviceType::OUTPUT ? "Output device:" : "Input device: ", get_color_theme().text_color)); + audio_device_list->add_widget(create_audio_device_selection_combobox(device_type)); + audio_device_list->add_widget(create_remove_audio_device_button(audio_input_list_ptr, audio_device_list.get())); return audio_device_list; } - std::unique_ptr<Button> SettingsPage::create_add_audio_device_button() { - auto add_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "Add audio device", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); - add_audio_track_button->on_click = [this]() { + std::unique_ptr<Button> SettingsPage::create_add_audio_track_button() { + auto button = std::make_unique<Button>(&get_theme().body_font, "Add audio track", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + button->on_click = [this]() { + audio_track_section_list_ptr->add_widget(create_audio_track_section(audio_section_ptr)); + }; + button->set_visible(type != Type::STREAM); + return button; + } + + std::unique_ptr<Button> SettingsPage::create_add_audio_output_device_button(List *audio_input_list_ptr) { + auto button = std::make_unique<Button>(&get_theme().body_font, "Add output device", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + button->on_click = [this, audio_input_list_ptr]() { audio_devices = get_audio_devices(); - audio_track_list_ptr->add_widget(create_audio_device()); + audio_input_list_ptr->add_widget(create_audio_device(AudioDeviceType::OUTPUT, audio_input_list_ptr)); }; - return add_audio_track_button; + return button; } - std::unique_ptr<ComboBox> SettingsPage::create_application_audio_selection_combobox() { + std::unique_ptr<Button> SettingsPage::create_add_audio_input_device_button(List *audio_input_list_ptr) { + auto button = std::make_unique<Button>(&get_theme().body_font, "Add input device", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); + button->on_click = [this, audio_input_list_ptr]() { + audio_devices = get_audio_devices(); + audio_input_list_ptr->add_widget(create_audio_device(AudioDeviceType::INPUT, audio_input_list_ptr)); + }; + return button; + } + + std::unique_ptr<ComboBox> SettingsPage::create_application_audio_selection_combobox(List *application_audio_row) { auto audio_device_box = std::make_unique<ComboBox>(&get_theme().body_font); + ComboBox *audio_device_box_ptr = audio_device_box.get(); for(const auto &app_audio : application_audio) { audio_device_box->add_item(app_audio, app_audio); } + audio_device_box->add_item("Custom...", custom_app_audio_tag); + + audio_device_box->on_selection_changed = [application_audio_row, audio_device_box_ptr](const std::string&, const std::string &id) { + if(id == custom_app_audio_tag) { + application_audio_row->userdata = (void*)(uintptr_t)AudioTrackType::APPLICATION_CUSTOM; + auto custom_app_audio_entry = std::make_unique<Entry>(&get_theme().body_font, "", (int)(get_theme().body_font.get_character_size() * 10.0f)); + application_audio_row->replace_widget(audio_device_box_ptr, std::move(custom_app_audio_entry)); + } + }; + return audio_device_box; } - std::unique_ptr<List> SettingsPage::create_application_audio() { + std::unique_ptr<List> SettingsPage::create_application_audio(List *audio_input_list_ptr) { auto application_audio_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); application_audio_list->userdata = (void*)(uintptr_t)AudioTrackType::APPLICATION; - application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "App: ", get_color_theme().text_color)); - application_audio_list->add_widget(create_application_audio_selection_combobox()); - application_audio_list->add_widget(create_remove_audio_device_button(application_audio_list.get())); + application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Application: ", get_color_theme().text_color)); + application_audio_list->add_widget(create_application_audio_selection_combobox(application_audio_list.get())); + application_audio_list->add_widget(create_remove_audio_device_button(audio_input_list_ptr, application_audio_list.get())); return application_audio_list; } - std::unique_ptr<List> SettingsPage::create_custom_application_audio() { + std::unique_ptr<List> SettingsPage::create_custom_application_audio(List *audio_input_list_ptr) { auto application_audio_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); application_audio_list->userdata = (void*)(uintptr_t)AudioTrackType::APPLICATION_CUSTOM; - application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "App: ", get_color_theme().text_color)); + application_audio_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Application: ", get_color_theme().text_color)); application_audio_list->add_widget(std::make_unique<Entry>(&get_theme().body_font, "", (int)(get_theme().body_font.get_character_size() * 10.0f))); - application_audio_list->add_widget(create_remove_audio_device_button(application_audio_list.get())); + application_audio_list->add_widget(create_remove_audio_device_button(audio_input_list_ptr, application_audio_list.get())); return application_audio_list; } - std::unique_ptr<Button> SettingsPage::create_add_application_audio_button() { + std::unique_ptr<Button> SettingsPage::create_add_application_audio_button(List *audio_input_list_ptr) { auto add_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "Add application audio", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); - add_application_audio_button_ptr = add_audio_track_button.get(); - add_audio_track_button->on_click = [this]() { + add_audio_track_button->on_click = [this, audio_input_list_ptr]() { application_audio = get_application_audio(); - audio_track_list_ptr->add_widget(create_application_audio()); - }; - return add_audio_track_button; - } - - std::unique_ptr<Button> SettingsPage::create_add_custom_application_audio_button() { - auto add_audio_track_button = std::make_unique<Button>(&get_theme().body_font, "Add custom application audio", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); - add_custom_application_audio_button_ptr = add_audio_track_button.get(); - add_audio_track_button->on_click = [this]() { - audio_track_list_ptr->add_widget(create_custom_application_audio()); + if(application_audio.empty()) + audio_input_list_ptr->add_widget(create_custom_application_audio(audio_input_list_ptr)); + else + audio_input_list_ptr->add_widget(create_application_audio(audio_input_list_ptr)); }; return add_audio_track_button; } - std::unique_ptr<List> SettingsPage::create_add_audio_buttons() { + std::unique_ptr<List> SettingsPage::create_add_audio_buttons(List *audio_input_list_ptr) { auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); - list->add_widget(create_add_audio_device_button()); - list->add_widget(create_add_application_audio_button()); - list->add_widget(create_add_custom_application_audio_button()); + list->add_widget(create_add_audio_output_device_button(audio_input_list_ptr)); + list->add_widget(create_add_audio_input_device_button(audio_input_list_ptr)); + list->add_widget(create_add_application_audio_button(audio_input_list_ptr)); return list; } - std::unique_ptr<List> SettingsPage::create_audio_track_track_section() { + std::unique_ptr<List> SettingsPage::create_audio_input_section() { auto list = std::make_unique<List>(List::Orientation::VERTICAL); - audio_track_list_ptr = list.get(); - audio_track_list_ptr->add_widget(create_audio_device()); // Add default_output by default + //list->add_widget(create_audio_device(list.get())); // Add default_output by default return list; } - std::unique_ptr<CheckBox> SettingsPage::create_merge_audio_tracks_checkbox() { - auto merge_audio_tracks_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Merge audio tracks"); - merge_audio_tracks_checkbox->set_checked(true); - merge_audio_tracks_checkbox_ptr = merge_audio_tracks_checkbox.get(); - return merge_audio_tracks_checkbox; - } - std::unique_ptr<CheckBox> SettingsPage::create_application_audio_invert_checkbox() { auto application_audio_invert_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record audio from all applications except the selected ones"); application_audio_invert_checkbox->set_checked(false); - application_audio_invert_checkbox_ptr = application_audio_invert_checkbox.get(); return application_audio_invert_checkbox; } - std::unique_ptr<Widget> SettingsPage::create_audio_track_section() { + static void update_audio_track_titles(List *audio_track_section_list_ptr) { + int index = 0; + audio_track_section_list_ptr->for_each_child_widget([&index](std::unique_ptr<Widget> &widget) { + char audio_track_name[32]; + snprintf(audio_track_name, sizeof(audio_track_name), "Audio track #%d", 1 + index); + ++index; + + Subsection *subsection = dynamic_cast<Subsection*>(widget.get()); + List *subesection_items = dynamic_cast<List*>(subsection->get_inner_widget()); + Label *audio_track_title = dynamic_cast<Label*>(dynamic_cast<List*>(subesection_items->get_child_widget_by_index(0))->get_child_widget_by_index(0)); + audio_track_title->set_text(audio_track_name); + return true; + }); + } + + std::unique_ptr<List> SettingsPage::create_audio_track_title_and_remove(Subsection *audio_track_subsection, const char *title) { + auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER); + list->add_widget(std::make_unique<Label>(&get_theme().title_font, title, get_color_theme().text_color)); + + auto remove_track_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 0)); + remove_track_button->set_icon(&get_theme().trash_texture); + remove_track_button->set_icon_padding_scale(0.75f); + remove_track_button->on_click = [this, audio_track_subsection]() { + audio_track_section_list_ptr->remove_widget(audio_track_subsection); + update_audio_track_titles(audio_track_section_list_ptr); + }; + list->add_widget(std::move(remove_track_button)); + list->set_visible(type != Type::STREAM); + return list; + } + + std::unique_ptr<Subsection> SettingsPage::create_audio_track_section(Widget *parent_widget) { + char audio_track_name[32]; + snprintf(audio_track_name, sizeof(audio_track_name), "Audio track #%d", 1 + (int)audio_track_section_list_ptr->get_num_children()); + + auto audio_input_section = create_audio_input_section(); + List *audio_input_section_ptr = audio_input_section.get(); + auto list = std::make_unique<List>(List::Orientation::VERTICAL); - list->add_widget(create_add_audio_buttons()); - list->add_widget(create_audio_track_track_section()); + List *list_ptr = list.get(); + auto subsection = std::make_unique<Subsection>("", std::move(std::move(list)), mgl::vec2f(parent_widget->get_inner_size().x, 0.0f)); + subsection->set_bg_color(mgl::Color(35, 40, 44)); + + list_ptr->add_widget(create_audio_track_title_and_remove(subsection.get(), audio_track_name)); + list_ptr->add_widget(create_add_audio_buttons(audio_input_section_ptr)); + list_ptr->add_widget(std::move(audio_input_section)); + list_ptr->add_widget(create_application_audio_invert_checkbox()); + + set_application_audio_options_visible(subsection.get(), view_radio_button_ptr->get_selected_id() == "advanced", *gsr_info); + return subsection; + } + + std::unique_ptr<List> SettingsPage::create_audio_track_section_list() { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + audio_track_section_list_ptr = list.get(); return list; } 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()); - 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)); + List *audio_device_section_list_ptr = audio_device_section_list.get(); + + auto subsection = std::make_unique<Subsection>("Audio", std::move(audio_device_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); + audio_section_ptr = subsection.get(); + audio_device_section_list_ptr->add_widget(create_add_audio_track_button()); + audio_device_section_list_ptr->add_widget(create_audio_track_section_list()); + audio_device_section_list_ptr->add_widget(create_audio_codec()); + return subsection; } std::unique_ptr<List> SettingsPage::create_video_quality_box() { @@ -339,16 +439,32 @@ namespace gsr { return list; } - std::unique_ptr<Entry> SettingsPage::create_video_bitrate_entry() { - auto video_bitrate_entry = std::make_unique<Entry>(&get_theme().body_font, "15000", (int)(get_theme().body_font.get_character_size() * 4.0f)); + 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, "8000", (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, "", 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_mbits_per_seconds = (double)atoi(text.c_str()) / 1024.0; + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%.2fMbps", video_bitrate_mbits_per_seconds); + size_mb_label_ptr->set_text(buffer); + }; + } + + return list; } std::unique_ptr<List> SettingsPage::create_video_bitrate() { auto video_bitrate_list = std::make_unique<List>(List::Orientation::VERTICAL); - video_bitrate_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Video bitrate (kbps):", get_color_theme().text_color)); + video_bitrate_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Video bitrate (Kbps):", get_color_theme().text_color)); video_bitrate_list->add_widget(create_video_bitrate_entry()); video_bitrate_list_ptr = video_bitrate_list.get(); return video_bitrate_list; @@ -378,40 +494,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 +593,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,27 +612,25 @@ 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_section()); 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; - const bool window_selected = id == "window"; + record_area_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) { const bool focused_selected = id == "focused"; const bool portal_selected = id == "portal"; - select_window_list_ptr->set_visible(window_selected); area_size_list_ptr->set_visible(focused_selected); 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) { @@ -524,42 +638,37 @@ namespace gsr { video_resolution_list_ptr->set_visible(!focused_selected && checked); }; - video_quality_box_ptr->on_selection_changed = [this](const std::string &text, const std::string &id) { - (void)text; + video_quality_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) { const bool custom_selected = id == "custom"; video_bitrate_list_ptr->set_visible(custom_selected); 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("focused_monitor"); + 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) { - 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; } } @@ -570,7 +679,7 @@ namespace gsr { auto save_directory_button = std::make_unique<Button>(&get_theme().body_font, get_videos_dir().c_str(), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)); save_directory_button_ptr = save_directory_button.get(); save_directory_button->on_click = [this]() { - auto select_directory_page = std::make_unique<GsrPage>(); + auto select_directory_page = std::make_unique<GsrPage>("File", "Settings"); select_directory_page->add_button("Save", "save", get_color_theme().tint_color); select_directory_page->add_button("Cancel", "cancel", get_color_theme().page_bg_color); @@ -579,10 +688,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,72 +719,132 @@ 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, 86400); 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<List> SettingsPage::create_replay_storage() { + auto list = std::make_unique<List>(List::Orientation::VERTICAL); + list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Where should temporary replay data be stored?", get_color_theme().text_color)); + auto replay_storage_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL); + replay_storage_button_ptr = replay_storage_button.get(); + replay_storage_button->add_item("RAM", "ram"); + replay_storage_button->add_item("Disk (Not recommended on SSDs)", "disk"); + + replay_storage_button->on_selection_changed = [this](const std::string&, const std::string &id) { + update_estimated_replay_file_size(id); + return true; + }; + + list->add_widget(std::move(replay_storage_button)); + list->set_visible(gsr_info->system_info.gsr_version >= GsrVersion{5, 5, 0}); + return list; + } + + 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 std::string &replay_storage_type) { 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 %s: %.2fMB.\nChange video bitrate or replay duration to change file size.", replay_storage_type == "ram" ? "in RAM" : "on disk", 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::view_changed(bool advanced_view, Subsection *notifications_subsection_ptr) { + color_range_list_ptr->set_visible(advanced_view); + audio_codec_ptr->set_visible(advanced_view); + video_codec_ptr->set_visible(advanced_view); + framerate_mode_list_ptr->set_visible(advanced_view); + notifications_subsection_ptr->set_visible(advanced_view); + set_application_audio_options_visible(audio_track_section_list_ptr, advanced_view, *gsr_info); + settings_scrollable_page_ptr->reset_scroll(); + } + + 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_replay_storage()); + 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))); + settings_list_ptr->add_widget(std::make_unique<Subsection>("Autostart", create_start_replay_automatically(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); + auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL); auto show_replay_started_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show replay started notification"); @@ -695,44 +866,55 @@ namespace gsr { Subsection *notifications_subsection_ptr = notifications_subsection.get(); settings_list_ptr->add_widget(std::move(notifications_subsection)); - view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string &text, const std::string &id) { - (void)text; - const bool advanced_view = id == "advanced"; - color_range_list_ptr->set_visible(advanced_view); - audio_codec_ptr->set_visible(advanced_view); - video_codec_ptr->set_visible(advanced_view); - framerate_mode_list_ptr->set_visible(advanced_view); - notifications_subsection_ptr->set_visible(advanced_view); - settings_scrollable_page_ptr->reset_scroll(); + view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string&, const std::string &id) { + view_changed(id == "advanced", notifications_subsection_ptr); + 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(replay_storage_button_ptr->get_selected_id()); + update_replay_time_text(); }; video_bitrate_entry_ptr->on_changed = [this](const std::string&) { - update_estimated_file_size(); + update_estimated_replay_file_size(replay_storage_button_ptr->get_selected_id()); }; } - 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; + } - auto general_list = std::make_unique<List>(List::Orientation::VERTICAL); - general_list->add_widget(create_save_recording_in_game_folder(gsr_info)); - 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))); + 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))); + + settings_list_ptr->add_widget(std::make_unique<Subsection>("General", create_save_recording_in_game_folder(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f))); auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL); @@ -746,27 +928,31 @@ namespace gsr { show_video_saved_notification_checkbox_ptr = show_video_saved_notification_checkbox.get(); checkboxes_list->add_widget(std::move(show_video_saved_notification_checkbox)); + auto show_video_paused_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show video paused/unpaused notification"); + show_video_paused_notification_checkbox->set_checked(true); + show_video_paused_notification_checkbox_ptr = show_video_paused_notification_checkbox.get(); + checkboxes_list->add_widget(std::move(show_video_paused_notification_checkbox)); + auto notifications_subsection = std::make_unique<Subsection>("Notifications", std::move(checkboxes_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)); Subsection *notifications_subsection_ptr = notifications_subsection.get(); settings_list_ptr->add_widget(std::move(notifications_subsection)); - view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string &text, const std::string &id) { - (void)text; - const bool advanced_view = id == "advanced"; - color_range_list_ptr->set_visible(advanced_view); - audio_codec_ptr->set_visible(advanced_view); - video_codec_ptr->set_visible(advanced_view); - framerate_mode_list_ptr->set_visible(advanced_view); - notifications_subsection_ptr->set_visible(advanced_view); - settings_scrollable_page_ptr->reset_scroll(); + view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string&, const std::string &id) { + view_changed(id == "advanced", notifications_subsection_ptr); + 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() { auto streaming_service_box = std::make_unique<ComboBox>(&get_theme().body_font); streaming_service_box->add_item("Twitch", "twitch"); streaming_service_box->add_item("YouTube", "youtube"); + streaming_service_box->add_item("Rumble", "rumble"); streaming_service_box->add_item("Custom", "custom"); streaming_service_box_ptr = streaming_service_box.get(); return streaming_service_box; @@ -791,6 +977,10 @@ namespace gsr { youtube_stream_key_entry_ptr = youtube_stream_key_entry.get(); stream_key_list->add_widget(std::move(youtube_stream_key_entry)); + auto rumble_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20); + rumble_stream_key_entry_ptr = rumble_stream_key_entry.get(); + stream_key_list->add_widget(std::move(rumble_stream_key_entry)); + stream_key_list_ptr = stream_key_list.get(); return stream_key_list; } @@ -825,7 +1015,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()); @@ -849,28 +1039,24 @@ namespace gsr { Subsection *notifications_subsection_ptr = notifications_subsection.get(); settings_list_ptr->add_widget(std::move(notifications_subsection)); - streaming_service_box_ptr->on_selection_changed = [this](const std::string &text, const std::string &id) { - (void)text; + streaming_service_box_ptr->on_selection_changed = [this](const std::string&, const std::string &id) { const bool twitch_option = id == "twitch"; const bool youtube_option = id == "youtube"; + const bool rumble_option = id == "rumble"; const bool custom_option = id == "custom"; stream_key_list_ptr->set_visible(!custom_option); stream_url_list_ptr->set_visible(custom_option); 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); + rumble_stream_key_entry_ptr->set_visible(rumble_option); + return true; }; streaming_service_box_ptr->on_selection_changed("Twitch", "twitch"); - view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string &text, const std::string &id) { - (void)text; - const bool advanced_view = id == "advanced"; - color_range_list_ptr->set_visible(advanced_view); - audio_codec_ptr->set_visible(advanced_view); - video_codec_ptr->set_visible(advanced_view); - framerate_mode_list_ptr->set_visible(advanced_view); - notifications_subsection_ptr->set_visible(advanced_view); - settings_scrollable_page_ptr->reset_scroll(); + view_radio_button_ptr->on_selection_changed = [this, notifications_subsection_ptr](const std::string&, const std::string &id) { + view_changed(id == "advanced", notifications_subsection_ptr); + return true; }; view_radio_button_ptr->on_selection_changed("Simple", "simple"); } @@ -879,21 +1065,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 +1093,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) { @@ -916,51 +1106,66 @@ namespace gsr { return nullptr; } - static bool starts_with(std::string_view str, const char *substr) { - size_t len = strlen(substr); - return str.size() >= len && memcmp(str.data(), substr, len) == 0; - } - - void SettingsPage::load_audio_tracks(const RecordOptions &record_options, const GsrInfo &gsr_info) { - 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) - continue; - - std::string audio_track_name = audio_track.substr(4); - const std::string *app_audio = get_application_audio_by_name_case_insensitive(application_audio, audio_track_name); - if(app_audio) { - std::unique_ptr<List> application_audio_widget = create_application_audio(); - ComboBox *application_audio_box = static_cast<ComboBox*>(application_audio_widget->get_child_widget_by_index(1)); - application_audio_box->set_selected_item(*app_audio); - audio_track_list_ptr->add_widget(std::move(application_audio_widget)); + void SettingsPage::load_audio_tracks(const RecordOptions &record_options) { + audio_track_section_list_ptr->clear(); + for(const AudioTrack &audio_track : record_options.audio_tracks_list) { + auto audio_track_section = create_audio_track_section(audio_section_ptr); + List *audio_track_section_items_list_ptr = dynamic_cast<List*>(audio_track_section->get_inner_widget()); + List *audio_input_list_ptr = dynamic_cast<List*>(audio_track_section_items_list_ptr->get_child_widget_by_index(2)); + CheckBox *application_audio_invert_checkbox_ptr = dynamic_cast<CheckBox*>(audio_track_section_items_list_ptr->get_child_widget_by_index(3)); + application_audio_invert_checkbox_ptr->set_checked(audio_track.application_audio_invert); + + audio_input_list_ptr->clear(); + for(const std::string &audio_input : audio_track.audio_inputs) { + if(starts_with(audio_input, "app:")) { + if(!gsr_info->system_info.supports_app_audio) + continue; + + std::string audio_track_name = audio_input.substr(4); + const std::string *app_audio = get_application_audio_by_name_case_insensitive(application_audio, audio_track_name); + if(app_audio) { + std::unique_ptr<List> application_audio_widget = create_application_audio(audio_input_list_ptr); + ComboBox *application_audio_box = dynamic_cast<ComboBox*>(application_audio_widget->get_child_widget_by_index(1)); + application_audio_box->set_selected_item(*app_audio); + audio_input_list_ptr->add_widget(std::move(application_audio_widget)); + } else { + std::unique_ptr<List> application_audio_widget = create_custom_application_audio(audio_input_list_ptr); + Entry *application_audio_entry = dynamic_cast<Entry*>(application_audio_widget->get_child_widget_by_index(1)); + application_audio_entry->set_text(std::move(audio_track_name)); + audio_input_list_ptr->add_widget(std::move(application_audio_widget)); + } + } else if(starts_with(audio_input, "device:")) { + const std::string device_name = audio_input.substr(7); + const AudioDeviceType audio_device_type = audio_device_is_output(device_name) ? AudioDeviceType::OUTPUT : AudioDeviceType::INPUT; + std::unique_ptr<List> audio_track_widget = create_audio_device(audio_device_type, audio_input_list_ptr); + ComboBox *audio_device_box = dynamic_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1)); + audio_device_box->set_selected_item(device_name); + audio_input_list_ptr->add_widget(std::move(audio_track_widget)); } else { - std::unique_ptr<List> application_audio_widget = create_custom_application_audio(); - Entry *application_audio_entry = static_cast<Entry*>(application_audio_widget->get_child_widget_by_index(1)); - application_audio_entry->set_text(std::move(audio_track_name)); - audio_track_list_ptr->add_widget(std::move(application_audio_widget)); + const AudioDeviceType audio_device_type = audio_device_is_output(audio_input) ? AudioDeviceType::OUTPUT : AudioDeviceType::INPUT; + std::unique_ptr<List> audio_track_widget = create_audio_device(audio_device_type, audio_input_list_ptr); + ComboBox *audio_device_box = dynamic_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1)); + audio_device_box->set_selected_item(audio_input); + audio_input_list_ptr->add_widget(std::move(audio_track_widget)); } - } else if(starts_with(audio_track, "device:")) { - std::unique_ptr<List> audio_track_widget = create_audio_device(); - ComboBox *audio_device_box = static_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1)); - audio_device_box->set_selected_item(audio_track.substr(7)); - audio_track_list_ptr->add_widget(std::move(audio_track_widget)); - } else { - std::unique_ptr<List> audio_track_widget = create_audio_device(); - ComboBox *audio_device_box = static_cast<ComboBox*>(audio_track_widget->get_child_widget_by_index(1)); - audio_device_box->set_selected_item(audio_track); - audio_track_list_ptr->add_widget(std::move(audio_track_widget)); } + + audio_track_section_list_ptr->add_widget(std::move(audio_track_section)); + + if(type == Type::STREAM) + break; + } + + if(type == Type::STREAM && audio_track_section_list_ptr->get_num_children() == 0) { + auto audio_track_section = create_audio_track_section(audio_section_ptr); + audio_track_section_list_ptr->add_widget(std::move(audio_track_section)); } } - 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); - 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,63 +1214,80 @@ 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); + replay_storage_button_ptr->set_selected_item(config.replay_config.replay_storage); 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 > 86400) + config.replay_config.replay_time = 86400; 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); + show_video_paused_notification_checkbox_ptr->set_checked(config.record_config.show_video_paused_notifications); save_directory_button_ptr->set_text(config.record_config.save_directory); 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); youtube_stream_key_entry_ptr->set_text(config.streaming_config.youtube.stream_key); twitch_stream_key_entry_ptr->set_text(config.streaming_config.twitch.stream_key); + rumble_stream_key_entry_ptr->set_text(config.streaming_config.rumble.stream_key); stream_url_entry_ptr->set_text(config.streaming_config.custom.url); container_box_ptr->set_selected_item(config.streaming_config.custom.container); } - static void save_audio_tracks(std::vector<std::string> &audio_devices, List *audio_devices_list_ptr) { - audio_devices.clear(); - audio_devices_list_ptr->for_each_child_widget([&audio_devices](std::unique_ptr<Widget> &child_widget) { - List *audio_track_line = static_cast<List*>(child_widget.get()); - const AudioTrackType audio_track_type = (AudioTrackType)(uintptr_t)audio_track_line->userdata; - switch(audio_track_type) { - case AudioTrackType::DEVICE: { - ComboBox *audio_device_box = static_cast<ComboBox*>(audio_track_line->get_child_widget_by_index(1)); - audio_devices.push_back("device:" + audio_device_box->get_selected_id()); - break; + static void save_audio_tracks(std::vector<AudioTrack> &audio_tracks, List *audio_track_section_list_ptr) { + audio_tracks.clear(); + audio_track_section_list_ptr->for_each_child_widget([&audio_tracks](std::unique_ptr<Widget> &child_widget) { + Subsection *audio_subsection = dynamic_cast<Subsection*>(child_widget.get()); + List *audio_track_section_items_list_ptr = dynamic_cast<List*>(audio_subsection->get_inner_widget()); + List *audio_input_list_ptr = dynamic_cast<List*>(audio_track_section_items_list_ptr->get_child_widget_by_index(2)); + CheckBox *application_audio_invert_checkbox_ptr = dynamic_cast<CheckBox*>(audio_track_section_items_list_ptr->get_child_widget_by_index(3)); + + audio_tracks.push_back({std::vector<std::string>{}, application_audio_invert_checkbox_ptr->is_checked()}); + audio_input_list_ptr->for_each_child_widget([&audio_tracks](std::unique_ptr<Widget> &child_widget){ + List *audio_track_line = dynamic_cast<List*>(child_widget.get()); + const AudioTrackType audio_track_type = (AudioTrackType)(uintptr_t)audio_track_line->userdata; + switch(audio_track_type) { + case AudioTrackType::DEVICE: { + ComboBox *audio_device_box = dynamic_cast<ComboBox*>(audio_track_line->get_child_widget_by_index(1)); + audio_tracks.back().audio_inputs.push_back("device:" + audio_device_box->get_selected_id()); + break; + } + case AudioTrackType::APPLICATION: { + ComboBox *application_audio_box = dynamic_cast<ComboBox*>(audio_track_line->get_child_widget_by_index(1)); + audio_tracks.back().audio_inputs.push_back("app:" + application_audio_box->get_selected_id()); + break; + } + case AudioTrackType::APPLICATION_CUSTOM: { + Entry *application_audio_entry = dynamic_cast<Entry*>(audio_track_line->get_child_widget_by_index(1)); + audio_tracks.back().audio_inputs.push_back("app:" + application_audio_entry->get_text()); + break; + } } - case AudioTrackType::APPLICATION: { - ComboBox *application_audio_box = static_cast<ComboBox*>(audio_track_line->get_child_widget_by_index(1)); - audio_devices.push_back("app:" + application_audio_box->get_selected_id()); - break; - } - case AudioTrackType::APPLICATION_CUSTOM: { - Entry *application_audio_entry = static_cast<Entry*>(audio_track_line->get_child_widget_by_index(1)); - audio_devices.push_back("app:" + application_audio_entry->get_text()); - break; - } - } + return true; + }); + return true; }); } @@ -1078,10 +1300,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(); - 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); + save_audio_tracks(record_options.audio_tracks_list, audio_track_section_list_ptr); record_options.color_range = color_range_box_ptr->get_selected_id(); record_options.video_quality = video_quality_box_ptr->get_selected_id(); record_options.video_codec = video_codec_box_ptr->get_selected_id(); @@ -1140,12 +1360,15 @@ 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(); config.replay_config.save_directory = save_directory_button_ptr->get_text(); config.replay_config.container = container_box_ptr->get_selected_id(); config.replay_config.replay_time = atoi(replay_time_entry_ptr->get_text().c_str()); + config.replay_config.replay_storage = replay_storage_button_ptr->get_selected_id(); if(config.replay_config.replay_time < 5) { config.replay_config.replay_time = 5; @@ -1158,6 +1381,7 @@ namespace gsr { config.record_config.save_video_in_game_folder = save_recording_in_game_folder_ptr->is_checked(); config.record_config.show_recording_started_notifications = show_recording_started_notification_checkbox_ptr->is_checked(); config.record_config.show_video_saved_notifications = show_video_saved_notification_checkbox_ptr->is_checked(); + config.record_config.show_video_paused_notifications = show_video_paused_notification_checkbox_ptr->is_checked(); config.record_config.save_directory = save_directory_button_ptr->get_text(); config.record_config.container = container_box_ptr->get_selected_id(); } @@ -1169,6 +1393,7 @@ namespace gsr { config.streaming_config.streaming_service = streaming_service_box_ptr->get_selected_id(); config.streaming_config.youtube.stream_key = youtube_stream_key_entry_ptr->get_text(); config.streaming_config.twitch.stream_key = twitch_stream_key_entry_ptr->get_text(); + config.streaming_config.rumble.stream_key = rumble_stream_key_entry_ptr->get_text(); config.streaming_config.custom.url = stream_url_entry_ptr->get_text(); config.streaming_config.custom.container = container_box_ptr->get_selected_id(); } diff --git a/src/gui/StaticPage.cpp b/src/gui/StaticPage.cpp index a89fc42..5147819 100644 --- a/src/gui/StaticPage.cpp +++ b/src/gui/StaticPage.cpp @@ -20,8 +20,9 @@ namespace gsr { // Process widgets by visibility (backwards) return widgets.for_each_reverse([selected_widget, &window, &event, offset](std::unique_ptr<Widget> &widget) { - if(widget.get() != selected_widget) { - if(!widget->on_event(event, window, offset)) + Widget *p = widget.get(); + if(p != selected_widget) { + if(!p->on_event(event, window, offset)) return false; } return true; @@ -36,14 +37,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 +49,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/Subsection.cpp b/src/gui/Subsection.cpp index c97460e..bc75a9c 100644 --- a/src/gui/Subsection.cpp +++ b/src/gui/Subsection.cpp @@ -12,12 +12,17 @@ namespace gsr { static const float title_spacing_scale = 0.010f; Subsection::Subsection(const char *title, std::unique_ptr<Widget> inner_widget, mgl::vec2f size) : - label(&get_theme().title_font, title, get_color_theme().text_color), + label(&get_theme().title_font, title ? title : "", get_color_theme().text_color), inner_widget(std::move(inner_widget)), size(size) { this->inner_widget->parent_widget = this; } + + Subsection::~Subsection() { + if(inner_widget->parent_widget == this) + inner_widget->parent_widget = nullptr; + } bool Subsection::on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f) { if(!visible) @@ -32,7 +37,7 @@ namespace gsr { mgl::vec2f draw_pos = position + offset; mgl::Rectangle background(draw_pos.floor(), get_size().floor()); - background.set_color(mgl::Color(25, 30, 34)); + background.set_color(bg_color); window.draw(background); draw_pos += mgl::vec2f(margin_left_scale, margin_top_scale) * mgl::vec2f(get_theme().window_height, get_theme().window_height); @@ -69,4 +74,12 @@ namespace gsr { const mgl::vec2f margin_size = mgl::vec2f(margin_left_scale + margin_right_scale, margin_top_scale + margin_bottom_scale) * mgl::vec2f(get_theme().window_height, get_theme().window_height); return get_size() - margin_size; } + + Widget* Subsection::get_inner_widget() { + return inner_widget.get(); + } + + void Subsection::set_bg_color(mgl::Color color) { + bg_color = color; + } }
\ No newline at end of file diff --git a/src/gui/Utils.cpp b/src/gui/Utils.cpp index e000b7a..8f77f17 100644 --- a/src/gui/Utils.cpp +++ b/src/gui/Utils.cpp @@ -50,4 +50,28 @@ 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; + } + + mgl::vec2f clamp_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to) { + if(from.x > to.x || from.y > to.y) + return scale_keep_aspect_ratio(from, to); + else + return from; + } }
\ No newline at end of file diff --git a/src/gui/Widget.cpp b/src/gui/Widget.cpp index 8732bd7..66cf193 100644 --- a/src/gui/Widget.cpp +++ b/src/gui/Widget.cpp @@ -1,14 +1,15 @@ #include "../../include/gui/Widget.hpp" +#include <vector> namespace gsr { + static std::vector<std::unique_ptr<Widget>> widgets_to_remove; + Widget::Widget() { } Widget::~Widget() { remove_widget_as_selected_in_parent(); - // if(parent_widget) - // parent_widget->remove_child_widget(this); } void Widget::set_position(mgl::vec2f position) { @@ -62,4 +63,15 @@ namespace gsr { void Widget::set_visible(bool visible) { this->visible = visible; } + + void add_widget_to_remove(std::unique_ptr<Widget> widget) { + widgets_to_remove.push_back(std::move(widget)); + } + + void remove_widgets_to_be_removed() { + for(size_t i = 0; i < widgets_to_remove.size(); ++i) { + widgets_to_remove[i].reset(); + } + widgets_to_remove.clear(); + } }
\ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 4a36fe7..a68ff7d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,21 +1,19 @@ #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 <malloc.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. @@ -32,30 +30,225 @@ static void sigint_handler(int signal) { running = 0; } +static void signal_ignore(int) { + +} + static void disable_prime_run() { unsetenv("__NV_PRIME_RENDER_OFFLOAD"); unsetenv("__NV_PRIME_RENDER_OFFLOAD_PROVIDER"); unsetenv("__GLX_VENDOR_LIBRARY_NAME"); unsetenv("__VK_LAYER_NV_optimus"); + unsetenv("DRI_PRIME"); +} + +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(); + }); + + rpc->add_handler("replay-save-1-min", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->save_replay_1_min(); + }); + + rpc->add_handler("replay-save-10-min", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->save_replay_10_min(); + }); + + rpc->add_handler("take-screenshot", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->take_screenshot(); + }); + + rpc->add_handler("take-screenshot-region", [overlay](const std::string &name) { + fprintf(stderr, "rpc command executed: %s\n", name.c_str()); + overlay->take_screenshot_region(); + }); +} + +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 set_display_server_environment_variables() { + // Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY) + const char *display = getenv("DISPLAY"); + if(!display) { + display = ":0"; + setenv("DISPLAY", display, true); + } + + const char *wayland_display = getenv("WAYLAND_DISPLAY"); + if(!wayland_display) { + wayland_display = "wayland-1"; + setenv("WAYLAND_DISPLAY", wayland_display, true); + } } -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 usage() { + printf("usage: gsr-ui [action]\n"); + printf("OPTIONS:\n"); + printf(" action The launch action. Should be either \"launch-show\", \"launch-hide\" or \"launch-daemon\". 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. The UI will be opened if the program is already running in another process.\n"); + printf(" If \"launch-daemon\" is used then the program starts but the UI is not opened until Alt+Z is pressed. The UI will not be opened if the program is already running in another process.\n"); + exit(1); } -int main(void) { +enum class LaunchAction { + LAUNCH_SHOW, + LAUNCH_HIDE, + LAUNCH_DAEMON +}; + +int main(int argc, char **argv) { setlocale(LC_ALL, "C"); // Sigh... stupid C + mallopt(M_MMAP_THRESHOLD, 65536); if(geteuid() == 0) { fprintf(stderr, "Error: don't run gsr-ui as the root user\n"); 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 if(strcmp(launch_action_opt, "launch-daemon") == 0) { + launch_action = LaunchAction::LAUNCH_DAEMON; + } else { + printf("error: invalid action \"%s\", expected \"launch-show\", \"launch-hide\" or \"launch-daemon\".\n", launch_action_opt); + usage(); + } + } else { + usage(); + } + + set_display_server_environment_variables(); + + // 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) { + if(launch_action == LaunchAction::LAUNCH_DAEMON) + return 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; + } + + if(gsr::pidof("gpu-screen-recorder", getpid()) != -1) { + const char *args[] = { "gsr-notify", "--text", "GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr }; + gsr::exec_program_daemonized(args); + } + + if(is_flatpak()) + install_flatpak_systemd_service(); + else + remove_flatpak_systemd_service(); // Stop nvidia driver from buffering frames setenv("__GL_MaxFramesAllowed", "1", true); @@ -68,25 +261,42 @@ int main(void) { unsetenv("vblank_mode"); 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); - } + signal(SIGTERM, sigint_handler); + signal(SIGUSR1, signal_ignore); + signal(SIGUSR2, signal_ignore); + signal(SIGRTMIN, signal_ignore); + signal(SIGRTMIN+1, signal_ignore); + signal(SIGRTMIN+2, signal_ignore); + signal(SIGRTMIN+3, signal_ignore); + signal(SIGRTMIN+4, signal_ignore); + signal(SIGRTMIN+5, signal_ignore); + signal(SIGRTMIN+6, signal_ignore); 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); } - 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"); + 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(MGL_WINDOW_SYSTEM_X11) != 0) { + fprintf(stderr, "Error: failed to initialize mgl. Failed to either connect to the X11 server or setup opengl\n"); + exit(1); + } + + gsr::SupportedCaptureOptions capture_options = gsr::get_supported_capture_options(gsr_info); 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 +307,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 +319,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"); - - auto overlay = std::make_unique<gsr::Overlay>(resources_path, gsr_info, egl_funcs); - //overlay.show(); + fprintf(stderr, "Info: gsr ui is now ready, waiting for inputs. Press alt+z to show/hide the overlay\n"); - // 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 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(); - // 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(); - // }); + 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 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(); - // }); + rpc_add_commands(rpc.get(), overlay.get()); - // 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(); - return 0; + if(exit_reason == "back-to-old-ui") { + const char *args[] = { "gpu-screen-recorder-gtk", "use-old-ui", nullptr }; + execvp(args[0], (char* const*)args); + return 0; + } else if(exit_reason == "exit") { + return 0; + } + + return mgl_is_connected_to_display_server() ? 0 : 1; } |