diff options
author | dec05eba <dec05eba@protonmail.com> | 2025-03-15 00:39:37 +0100 |
---|---|---|
committer | dec05eba <dec05eba@protonmail.com> | 2025-03-15 00:56:38 +0100 |
commit | 63b2b6cbc34b9e34208f3bff96686b9bd3f54521 (patch) | |
tree | b20334166fd064c6b35d29daea7350841a219897 /src | |
parent | 6c7158c06d41fd7c77a8a8b9d186440904950f8c (diff) |
Add region capture option
Diffstat (limited to 'src')
-rw-r--r-- | src/GsrInfo.cpp | 2 | ||||
-rw-r--r-- | src/Overlay.cpp | 290 | ||||
-rw-r--r-- | src/RegionSelector.cpp | 437 | ||||
-rw-r--r-- | src/WindowUtils.cpp | 211 | ||||
-rw-r--r-- | src/gui/ScreenshotSettingsPage.cpp | 2 | ||||
-rw-r--r-- | src/gui/SettingsPage.cpp | 2 |
6 files changed, 766 insertions, 178 deletions
diff --git a/src/GsrInfo.cpp b/src/GsrInfo.cpp index 5126d13..73f54ee 100644 --- a/src/GsrInfo.cpp +++ b/src/GsrInfo.cpp @@ -310,6 +310,8 @@ namespace gsr { 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") diff --git a/src/Overlay.cpp b/src/Overlay.cpp index ad1fb10..d98bc54 100644 --- a/src/Overlay.cpp +++ b/src/Overlay.cpp @@ -13,6 +13,7 @@ #include "../include/gui/PageStack.hpp" #include "../include/WindowUtils.hpp" #include "../include/GlobalHotkeys.hpp" +#include "../include/GlobalHotkeysLinux.hpp" #include <string.h> #include <assert.h> @@ -30,7 +31,7 @@ #include <X11/cursorfont.h> #include <X11/extensions/Xfixes.h> #include <X11/extensions/XInput2.h> -#include <X11/extensions/shape.h> +#include <X11/extensions/shapeconst.h> #include <X11/Xcursor/Xcursor.h> #include <mglpp/system/Rect.hpp> #include <mglpp/window/Event.hpp> @@ -203,77 +204,6 @@ 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; - } - } - - XFree(properties); - return is_fullscreen; - } - - #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 void make_window_click_through(Display *display, Window window) { - XRectangle rect; - memset(&rect, 0, sizeof(rect)); - XserverRegion region = XFixesCreateRegion(display, &rect, 1); - XFixesSetWindowShapeRegion(display, window, ShapeInput, 0, 0, region); - XFixesDestroyRegion(display, region); - } - - static Bool make_window_sticky(Display *dpy, Window window) { - return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_STICKY", False)); - } - - 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)); - } - // Returns the first monitor if not found. Assumes there is at least one monitor connected. static const Monitor* find_monitor_at_position(const std::vector<Monitor> &monitors, mgl::vec2i pos) { assert(!monitors.empty()); @@ -661,6 +591,11 @@ namespace gsr { global_hotkeys_js->poll_events(); handle_keyboard_mapping_event(); + region_selector.poll_events(); + if(region_selector.take_selection() && on_region_selected) { + on_region_selected(); + on_region_selected = nullptr; + } if(!visible || !window) return; @@ -696,6 +631,20 @@ namespace gsr { 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, 0), mgl::Color(255, 0, 0, 0), NotificationType::RECORD); + on_region_selected = nullptr; + } + } + + if(region_selector.is_started()) { + usleep(5 * 1000); // 5 ms + return true; + } + if(!visible) return false; @@ -821,52 +770,13 @@ namespace gsr { XcursorImageDestroy(cursor_image); } - static bool device_is_mouse(const XIDeviceInfo *dev) { - for(int i = 0; i < dev->num_classes; ++i) { - if(dev->classes[i]->type == XIMasterPointer || dev->classes[i]->type == XISlavePointer) - return true; - } - return false; - } - - void Overlay::xi_grab_all_mouse_devices() { - if(!xi_display) - return; - - int num_devices = 0; - XIDeviceInfo *info = XIQueryDevice(xi_display, XIAllDevices, &num_devices); - if(!info) - return; - - unsigned char mask[XIMaskLen(XI_LASTEVENT)]; - memset(mask, 0, sizeof(mask)); - XISetMask(mask, XI_Motion); - //XISetMask(mask, XI_RawMotion); - XISetMask(mask, XI_ButtonPress); - XISetMask(mask, XI_ButtonRelease); - XISetMask(mask, XI_KeyPress); - XISetMask(mask, XI_KeyRelease); - - for (int i = 0; i < num_devices; ++i) { - const XIDeviceInfo *dev = &info[i]; - if(!device_is_mouse(dev)) - continue; - - XIEventMask xi_masks; - xi_masks.deviceid = dev->deviceid; - xi_masks.mask_len = sizeof(mask); - xi_masks.mask = mask; - XIGrabDevice(xi_display, dev->deviceid, window->get_system_handle(), CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, XIOwnerEvents, &xi_masks); - } - - XFlush(xi_display); - XIFreeDeviceInfo(info); - } - void Overlay::show() { if(visible) return; + if(region_selector.is_started()) + return; + drawn_first_frame = false; window.reset(); window = std::make_unique<mgl::Window>(); @@ -990,7 +900,7 @@ namespace gsr { // We want to grab all devices to prevent any other application below the UI from receiving events. // Owlboy seems to use xi events and XGrabPointer doesn't prevent owlboy from receiving events. - xi_grab_all_mouse_devices(); + xi_grab_all_mouse_devices(xi_display); if(!is_wlroots) window->set_fullscreen(true); @@ -1079,7 +989,7 @@ namespace gsr { } else if(id == "save") { on_press_save_replay(); } else if(id == "start") { - on_press_start_replay(false); + on_press_start_replay(false, false); } }; main_buttons_list->add_widget(std::move(button)); @@ -1105,7 +1015,7 @@ namespace gsr { } else if(id == "pause") { toggle_pause(); } else if(id == "start") { - on_press_start_record(); + on_press_start_record(false); } }; main_buttons_list->add_widget(std::move(button)); @@ -1127,7 +1037,7 @@ namespace gsr { }; 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)); @@ -1284,6 +1194,7 @@ namespace gsr { visible = false; drawn_first_frame = false; + start_region_capture = false; if(xi_input_xev) { free(xi_input_xev); @@ -1296,20 +1207,21 @@ namespace gsr { } if(xi_display) { - XCloseDisplay(xi_display); - xi_display = nullptr; - if(window) { mgl_context *context = mgl_get_context(); Display *display = (Display*)context->connection; const mgl::vec2i new_cursor_position = mgl::vec2i(window->internal_window()->pos.x, window->internal_window()->pos.y) + window->get_mouse_position(); XWarpPointer(display, DefaultRootWindow(display), DefaultRootWindow(display), 0, 0, 0, 0, new_cursor_position.x, new_cursor_position.y); + 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) { @@ -1345,7 +1257,7 @@ namespace gsr { } void Overlay::toggle_record() { - on_press_start_record(); + on_press_start_record(false); } void Overlay::toggle_pause() { @@ -1365,11 +1277,11 @@ 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() { @@ -1377,7 +1289,7 @@ namespace gsr { } void Overlay::take_screenshot() { - on_press_take_screenshot(); + on_press_take_screenshot(false); } static const char* notification_type_to_string(NotificationType notification_type) { @@ -1711,9 +1623,9 @@ namespace gsr { if(focused_window_is_fullscreen != prev_focused_window_is_fullscreen) { if(recording_status == RecordingStatus::NONE && focused_window_is_fullscreen) { if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks)) - on_press_start_replay(false); + on_press_start_replay(false, false); } else if(recording_status == RecordingStatus::REPLAY && !focused_window_is_fullscreen) { - on_press_start_replay(true); + on_press_start_replay(true, false); } } } @@ -1728,9 +1640,9 @@ namespace gsr { if(power_supply_connected != prev_power_supply_status) { if(recording_status == RecordingStatus::NONE && power_supply_connected) { if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks)) - on_press_start_replay(false); + on_press_start_replay(false, false); } else if(recording_status == RecordingStatus::REPLAY && !power_supply_connected) { - on_press_start_replay(false); + on_press_start_replay(false, false); } } } @@ -1740,7 +1652,7 @@ namespace gsr { return; if(are_all_audio_tracks_available_to_capture(config.replay_config.record_options.audio_tracks)) - on_press_start_replay(true); + on_press_start_replay(true, false); } void Overlay::on_stop_recording(int exit_code) { @@ -1884,7 +1796,18 @@ namespace gsr { return result; } - 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_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; + } + 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, 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"); @@ -1916,12 +1839,17 @@ namespace gsr { 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 GsrInfo &gsr_info, const std::string &capture_target) { const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info); // TODO: Also check x11 window when enabled (check if capture_target is a decminal/hex number) - if(capture_target == "focused") { + if(capture_target == "region") { + return capture_options.region; + } else if(capture_target == "focused") { return capture_options.focused; } else if(capture_target == "portal") { return capture_options.portal; @@ -1943,7 +1871,10 @@ namespace gsr { kill(gpu_screen_recorder_process, SIGUSR1); } - bool Overlay::on_press_start_replay(bool disable_notification) { + bool Overlay::on_press_start_replay(bool disable_notification, bool finished_region_selection) { + if(region_selector.is_started()) + return false; + switch(recording_status) { case RecordingStatus::NONE: case RecordingStatus::REPLAY: @@ -1991,6 +1922,14 @@ namespace gsr { return false; } + if(config.replay_config.record_options.record_area_option == "region" && !finished_region_selection) { + start_region_capture = true; + on_region_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); @@ -2006,13 +1945,13 @@ namespace gsr { encoder = "cpu"; } - char region[64]; - region[0] = '\0'; + char size[64]; + size[0] = '\0'; if(config.replay_config.record_options.record_area_option == "focused") - snprintf(region, sizeof(region), "%dx%d", (int)config.replay_config.record_options.record_area_width, (int)config.replay_config.record_options.record_area_height); + 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(), @@ -2034,7 +1973,8 @@ namespace gsr { args.push_back("yes"); } - add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); + char region_str[128]; + add_common_gpu_screen_recorder_args(args, config.replay_config.record_options, audio_tracks, video_bitrate, size, audio_tracks_merged, region_str, sizeof(region_str), region_selector); args.push_back(nullptr); @@ -2067,7 +2007,10 @@ namespace gsr { return true; } - void Overlay::on_press_start_record() { + void Overlay::on_press_start_record(bool finished_region_selection) { + if(region_selector.is_started()) + return; + switch(recording_status) { case RecordingStatus::NONE: case RecordingStatus::RECORD: @@ -2112,6 +2055,14 @@ namespace gsr { return; } + if(config.record_config.record_options.record_area_option == "region" && !finished_region_selection) { + start_region_capture = true; + on_region_selected = [this]() { + on_press_start_record(true); + }; + return; + } + record_filepath.clear(); // TODO: Validate input, fallback to valid values @@ -2128,13 +2079,13 @@ namespace gsr { encoder = "cpu"; } - char region[64]; - region[0] = '\0'; + char size[64]; + size[0] = '\0'; if(config.record_config.record_options.record_area_option == "focused") - snprintf(region, sizeof(region), "%dx%d", (int)config.record_config.record_options.record_area_width, (int)config.record_config.record_options.record_area_height); + 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(), @@ -2150,7 +2101,8 @@ 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); + char region_str[128]; + add_common_gpu_screen_recorder_args(args, config.record_config.record_options, audio_tracks, video_bitrate, size, audio_tracks_merged, region_str, sizeof(region_str), region_selector); args.push_back(nullptr); @@ -2205,7 +2157,10 @@ namespace gsr { return url; } - void Overlay::on_press_start_stream() { + void Overlay::on_press_start_stream(bool finished_region_selection) { + if(region_selector.is_started()) + return; + switch(recording_status) { case RecordingStatus::NONE: case RecordingStatus::STREAM: @@ -2248,6 +2203,14 @@ namespace gsr { return; } + if(config.streaming_config.record_options.record_area_option == "region" && !finished_region_selection) { + start_region_capture = true; + on_region_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); @@ -2267,13 +2230,13 @@ namespace gsr { const std::string url = streaming_get_url(config); - char region[64]; - region[0] = '\0'; + char size[64]; + size[0] = '\0'; if(config.record_config.record_options.record_area_option == "focused") - snprintf(region, sizeof(region), "%dx%d", (int)config.streaming_config.record_options.record_area_width, (int)config.streaming_config.record_options.record_area_height); + 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(), @@ -2289,7 +2252,8 @@ namespace gsr { }; config.streaming_config.record_options.merge_audio_tracks = true; - add_common_gpu_screen_recorder_args(args, config.streaming_config.record_options, audio_tracks, video_bitrate, region, audio_tracks_merged); + char region_str[128]; + add_common_gpu_screen_recorder_args(args, config.streaming_config.record_options, audio_tracks, video_bitrate, size, audio_tracks_merged, region_str, sizeof(region_str), region_selector); args.push_back(nullptr); @@ -2314,7 +2278,10 @@ namespace gsr { show_notification("Streaming has started", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM); } - void Overlay::on_press_take_screenshot() { + void Overlay::on_press_take_screenshot(bool finished_region_selection) { + if(region_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; @@ -2327,6 +2294,15 @@ namespace gsr { return; } + if(config.screenshot_config.record_area_option == "region" && !finished_region_selection) { + start_region_capture = true; + on_region_selected = [this]() { + 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); + }; + 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 @@ -2338,12 +2314,12 @@ namespace gsr { "-o", output_file.c_str() }; - char region[64]; - region[0] = '\0'; + char size[64]; + size[0] = '\0'; if(config.screenshot_config.change_image_resolution) { - snprintf(region, sizeof(region), "%dx%d", (int)config.screenshot_config.image_width, (int)config.screenshot_config.image_height); + 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(region); + args.push_back(size); } if(config.screenshot_config.restore_portal_session) { @@ -2351,6 +2327,10 @@ namespace gsr { args.push_back("yes"); } + char region_str[128]; + if(config.screenshot_config.record_area_option == "region") + add_region_command(args, region_str, sizeof(region_str), region_selector); + args.push_back(nullptr); screenshot_filepath = output_file; diff --git a/src/RegionSelector.cpp b/src/RegionSelector.cpp new file mode 100644 index 0000000..5d838f1 --- /dev/null +++ b/src/RegionSelector.cpp @@ -0,0 +1,437 @@ +#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, 5); + 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 { + 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; + 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); + 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; + 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); + 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); + 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::is_selected() const { + return selected; + } + + bool RegionSelector::take_selection() { + const bool result = selected; + selected = 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/WindowUtils.cpp b/src/WindowUtils.cpp index d588374..49fd65b 100644 --- a/src/WindowUtils.cpp +++ b/src/WindowUtils.cpp @@ -1,8 +1,10 @@ #include "../include/WindowUtils.hpp" -#include <X11/Xlib.h> #include <X11/Xatom.h> #include <X11/Xutil.h> +#include <X11/extensions/XInput2.h> +#include <X11/extensions/Xfixes.h> +#include <X11/extensions/shapeconst.h> #include <mglpp/system/Utf8.hpp> @@ -301,6 +303,21 @@ namespace gsr { 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; @@ -348,17 +365,7 @@ namespace gsr { XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L); window_set_decorations_visible(display, window, false); - - XSizeHints *size_hints = XAllocSizeHints(); - size_hints->width = size; - size_hints->height = size; - size_hints->min_width = size; - size_hints->min_height = size; - size_hints->max_width = size; - size_hints->max_height = size; - size_hints->flags = PSize | PMinSize | PMaxSize; - XSetWMNormalHints(display, window, size_hints); - XFree(size_hints); + set_window_size_not_resizable(display, window, size, size); XMapWindow(display, window); XFlush(display); @@ -412,17 +419,7 @@ namespace gsr { XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L); window_set_decorations_visible(display, window, false); - - XSizeHints *size_hints = XAllocSizeHints(); - size_hints->width = size; - size_hints->height = size; - size_hints->min_width = size; - size_hints->min_height = size; - size_hints->max_width = size; - size_hints->max_height = size; - size_hints->flags = PSize | PMinSize | PMaxSize; - XSetWMNormalHints(display, window, size_hints); - XFree(size_hints); + set_window_size_not_resizable(display, window, size, size); XMapWindow(display, window); XFlush(display); @@ -531,4 +528,172 @@ namespace gsr { mgl_for_each_active_monitor_output(dpy, get_monitors_callback, &monitors); 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/ScreenshotSettingsPage.cpp b/src/gui/ScreenshotSettingsPage.cpp index f2f4730..fd75660 100644 --- a/src/gui/ScreenshotSettingsPage.cpp +++ b/src/gui/ScreenshotSettingsPage.cpp @@ -38,6 +38,8 @@ namespace gsr { // TODO: Enable this //if(capture_options.window) // record_area_box->add_item("Window", "window"); + if(capture_options.region) + record_area_box->add_item("Region", "region"); 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); diff --git a/src/gui/SettingsPage.cpp b/src/gui/SettingsPage.cpp index e4319ce..f29f4af 100644 --- a/src/gui/SettingsPage.cpp +++ b/src/gui/SettingsPage.cpp @@ -66,6 +66,8 @@ namespace gsr { // TODO: Enable this //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.focused) record_area_box->add_item("Follow focused window", "focused"); for(const auto &monitor : capture_options.monitors) { |