From 189736c1a96a1ad0e571ad69f01039e96455011a Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 22 Feb 2025 13:31:51 +0100 Subject: Add option to take a screenshot (default hotkey: alt+f1) --- src/Config.cpp | 118 +++++++++++-- src/GsrInfo.cpp | 14 ++ src/Hotplug.cpp | 3 +- src/Overlay.cpp | 288 +++++++++++++++++++++++-------- src/Theme.cpp | 3 + src/Utils.cpp | 8 + src/WindowUtils.cpp | 12 +- src/gui/Button.cpp | 67 +++++--- src/gui/DropdownButton.cpp | 9 + src/gui/GlobalSettingsPage.cpp | 126 ++++++-------- src/gui/GsrPage.cpp | 13 +- src/gui/ScreenshotSettingsPage.cpp | 339 +++++++++++++++++++++++++++++++++++++ src/gui/SettingsPage.cpp | 26 +-- src/main.cpp | 7 + 14 files changed, 838 insertions(+), 195 deletions(-) create mode 100644 src/gui/ScreenshotSettingsPage.cpp (limited to 'src') diff --git a/src/Config.cpp b/src/Config.cpp index b9e4cb7..ebdc0ca 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #define FORMAT_I32 "%" PRIi32 @@ -13,6 +14,37 @@ #define FORMAT_U32 "%" PRIu32 namespace gsr { + static std::vector hotkey_modifiers_to_mgl_keys(uint32_t modifiers) { + std::vector 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()); + } + } + bool ConfigHotkey::operator==(const ConfigHotkey &other) const { return key == other.key && modifiers == other.modifiers; } @@ -21,36 +53,83 @@ namespace gsr { return !operator==(other); } + std::string ConfigHotkey::to_string(bool spaces, bool modifier_side) const { + std::string result; + + const std::vector 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; + } + Config::Config(const SupportedCaptureOptions &capture_options) { - const std::string default_save_directory = get_videos_dir(); + 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.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT}; streaming_config.record_options.video_quality = "custom"; streaming_config.record_options.audio_tracks.push_back("default_output"); streaming_config.record_options.video_bitrate = 15000; - record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT}; - record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT}; - record_config.save_directory = default_save_directory; + record_config.save_directory = default_videos_save_directory; record_config.record_options.audio_tracks.push_back("default_output"); record_config.record_options.video_bitrate = 45000; - replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT}; - replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT}; replay_config.record_options.video_quality = "custom"; - replay_config.save_directory = default_save_directory; + replay_config.save_directory = default_videos_save_directory; replay_config.record_options.audio_tracks.push_back("default_output"); replay_config.record_options.video_bitrate = 45000; - main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT}; + screenshot_config.save_directory = default_pictures_save_directory; if(!capture_options.monitors.empty()) { streaming_config.record_options.record_area_option = capture_options.monitors.front().name; record_config.record_options.record_area_option = capture_options.monitors.front().name; replay_config.record_options.record_area_option = capture_options.monitors.front().name; + screenshot_config.record_area_option = capture_options.monitors.front().name; } } + 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}; + + screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::F1, HOTKEY_MOD_LALT}; + + main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT}; + } + static std::optional parse_key_value(std::string_view line) { const size_t space_index = line.find(' '); if(space_index == std::string_view::npos) @@ -156,7 +235,20 @@ namespace gsr { {"replay.container", &config.replay_config.container}, {"replay.time", &config.replay_config.replay_time}, {"replay.start_stop_hotkey", &config.replay_config.start_stop_hotkey}, - {"replay.save_hotkey", &config.replay_config.save_hotkey} + {"replay.save_hotkey", &config.replay_config.save_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} }; } @@ -183,6 +275,8 @@ namespace gsr { } else if(std::holds_alternative*>(it.second)) { if(*std::get*>(it.second) != *std::get*>(it_other->second)) return false; + } else { + assert(false); } } return true; @@ -245,6 +339,8 @@ namespace gsr { } else if(std::holds_alternative*>(it->second)) { std::string array_value(key_value->value); std::get*>(it->second)->push_back(std::move(array_value)); + } else { + assert(false); } return true; @@ -294,6 +390,8 @@ namespace gsr { for(const std::string &value : *array) { fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), value.c_str()); } + } else { + assert(false); } } diff --git a/src/GsrInfo.cpp b/src/GsrInfo.cpp index 033757c..c9373ae 100644 --- a/src/GsrInfo.cpp +++ b/src/GsrInfo.cpp @@ -157,11 +157,19 @@ namespace gsr { gsr_info->supported_video_codecs.vp9 = true; } + 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 { UNKNOWN, SYSTEM_INFO, GPU_INFO, VIDEO_CODECS, + IMAGE_FORMATS, CAPTURE_OPTIONS }; @@ -194,6 +202,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 @@ -217,6 +227,10 @@ 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: { // Intentionally ignore, get capture options with get_supported_capture_options instead break; diff --git a/src/Hotplug.cpp b/src/Hotplug.cpp index 84ed5bb..0f5155c 100644 --- a/src/Hotplug.cpp +++ b/src/Hotplug.cpp @@ -44,9 +44,10 @@ namespace gsr { } void Hotplug::process_event_data(int fd, const HotplugEventCallback &callback) { - const int bytes_read = read(fd, event_data, sizeof(event_data)); + 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; diff --git a/src/Overlay.cpp b/src/Overlay.cpp index 919117d..ef4e630 100644 --- a/src/Overlay.cpp +++ b/src/Overlay.cpp @@ -7,10 +7,10 @@ #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/gui/GsrPage.hpp" #include "../include/WindowUtils.hpp" #include "../include/GlobalHotkeys.hpp" @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -377,6 +378,13 @@ namespace gsr { fprintf(stderr, "pressed %s\n", id.c_str()); overlay->save_replay(); }); + + 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(); + }); } static std::unique_ptr register_linux_hotkeys(Overlay *overlay, GlobalHotkeysLinux::GrabType grab_type) { @@ -470,6 +478,16 @@ 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(); @@ -674,6 +692,7 @@ namespace gsr { update_notification_process_status(); update_gsr_replay_save(); update_gsr_process_status(); + update_gsr_screenshot_process_status(); replay_status_update_status(); if(!visible) @@ -939,6 +958,71 @@ namespace gsr { 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->get_system_handle()); + + 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_left_ptr); + XFlush(display); + + grab_mouse_and_keyboard(); + + // The real cursor doesn't move when all devices are grabbed, so we create our own cursor and diplay that while grabbed + xi_setup_fake_cursor(); + + // 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(); + + if(!is_wlroots) + 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(); + + // 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); @@ -976,8 +1060,8 @@ namespace gsr { auto button = std::make_unique(&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)); button->add_item("Settings", "settings"); button->set_item_icon("start", &get_theme().play_texture); button->set_item_icon("save", &get_theme().save_texture); @@ -1002,8 +1086,8 @@ namespace gsr { auto button = std::make_unique(&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); @@ -1028,7 +1112,7 @@ namespace gsr { auto button = std::make_unique(&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_small_texture); @@ -1053,7 +1137,7 @@ namespace gsr { { const mgl::vec2f main_buttons_size = main_buttons_list_ptr->get_size(); - const int settings_button_size = main_buttons_size.y * 0.2f; + const int settings_button_size = main_buttons_size.y * 0.33f; auto button = std::make_unique