aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--TODO5
m---------depends/mglpp0
-rw-r--r--include/GsrInfo.hpp1
-rw-r--r--include/Overlay.hpp14
-rw-r--r--include/RegionSelector.hpp52
-rw-r--r--include/WindowUtils.hpp10
-rw-r--r--meson.build2
-rw-r--r--project.conf3
-rw-r--r--src/GsrInfo.cpp2
-rw-r--r--src/Overlay.cpp290
-rw-r--r--src/RegionSelector.cpp437
-rw-r--r--src/WindowUtils.cpp211
-rw-r--r--src/gui/ScreenshotSettingsPage.cpp2
-rw-r--r--src/gui/SettingsPage.cpp2
15 files changed, 842 insertions, 191 deletions
diff --git a/README.md b/README.md
index 10e2ead..6bd8422 100644
--- a/README.md
+++ b/README.md
@@ -21,7 +21,7 @@ GPU Screen Recorder UI uses meson build system so you need to install `meson` to
## Build dependencies
These are the dependencies needed to build GPU Screen Recorder UI:
-* x11 (libx11, libxrandr, libxrender, libxcomposite, libxfixes, libxi)
+* x11 (libx11, libxrandr, libxrender, libxcomposite, libxfixes, libxext, libxi)
* libxcursor
* libglvnd (which provides libgl, libglx and libegl)
* linux-api-headers
diff --git a/TODO b/TODO
index ec7ea50..551b095 100644
--- a/TODO
+++ b/TODO
@@ -139,11 +139,6 @@ System startup option should also support runit and some other init systems, not
Allow using a hotkey such as printscreen or any other non-alphanumeric key without a modifier. Allow that in gsr-ui and gsr-global-hotkeys. Update the ui to match that.
-Implement region capture by adding it as a capture target. The region selection should be done in the same way as the gsr-ui overlay works, by doing xi grab and displaying a fullscreen semi-transparent window.
- The window however should covert all monitors combined (use XWidthOfScreen/XHeightOfScreen) and selecting a region should make that region fully transparent to make it clear which region is selected.
- Clicking instead of dragging should select that monitor (by passing region 0x0+X+Y).
- Hide gsr-ui overlay before showing the region capture (the region capture should start when you start recording/screenshot).
-
Use x11 shm instead of XGetImage (https://stackoverflow.com/questions/43442675/how-to-use-xshmgetimage-and-xshmputimage).
Add a hotkey to record/stream/replay/screenshot region.
diff --git a/depends/mglpp b/depends/mglpp
-Subproject 6341a8aa77761a94e6ef073bccd28f880ee61f9
+Subproject 241ebb9d035834b35ae988d1d838b52a69439a1
diff --git a/include/GsrInfo.hpp b/include/GsrInfo.hpp
index b027cc5..b8f478c 100644
--- a/include/GsrInfo.hpp
+++ b/include/GsrInfo.hpp
@@ -47,6 +47,7 @@ namespace gsr {
struct SupportedCaptureOptions {
bool window = false;
+ bool region = false;
bool focused = false;
bool portal = false;
std::vector<GsrMonitor> monitors;
diff --git a/include/Overlay.hpp b/include/Overlay.hpp
index 89747cd..5ed7f51 100644
--- a/include/Overlay.hpp
+++ b/include/Overlay.hpp
@@ -6,9 +6,9 @@
#include "Config.hpp"
#include "window_texture.h"
#include "WindowUtils.hpp"
-#include "GlobalHotkeysLinux.hpp"
#include "GlobalHotkeysJoystick.hpp"
#include "AudioPlayer.hpp"
+#include "RegionSelector.hpp"
#include <mglpp/window/Window.hpp>
#include <mglpp/window/Event.hpp>
@@ -78,7 +78,6 @@ namespace gsr {
void process_key_bindings(mgl::Event &event);
void grab_mouse_and_keyboard();
void xi_setup_fake_cursor();
- void xi_grab_all_mouse_devices();
void close_gpu_screen_recorder_output();
@@ -109,10 +108,10 @@ namespace gsr {
void update_ui_replay_stopped();
void on_press_save_replay();
- bool on_press_start_replay(bool disable_notification);
- void on_press_start_record();
- void on_press_start_stream();
- void on_press_take_screenshot();
+ bool on_press_start_replay(bool disable_notification, bool finished_region_selection);
+ void on_press_start_record(bool finished_region_selection);
+ void on_press_start_stream(bool finished_region_selection);
+ void on_press_take_screenshot(bool finished_region_selection);
bool update_compositor_texture(const Monitor &monitor);
void force_window_on_top();
@@ -202,5 +201,8 @@ namespace gsr {
bool try_replay_startup = true;
AudioPlayer audio_player;
+ RegionSelector region_selector;
+ bool start_region_capture = false;
+ std::function<void()> on_region_selected;
};
} \ No newline at end of file
diff --git a/include/RegionSelector.hpp b/include/RegionSelector.hpp
new file mode 100644
index 0000000..7f3041a
--- /dev/null
+++ b/include/RegionSelector.hpp
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "WindowUtils.hpp"
+#include <mglpp/system/vec.hpp>
+#include <mglpp/graphics/Color.hpp>
+#include <vector>
+
+#include <X11/Xlib.h>
+
+namespace gsr {
+ struct Region {
+ mgl::vec2i pos;
+ mgl::vec2i size;
+ };
+
+ class RegionSelector {
+ public:
+ RegionSelector();
+ RegionSelector(const RegionSelector&) = delete;
+ RegionSelector& operator=(const RegionSelector&) = delete;
+ ~RegionSelector();
+
+ bool start(mgl::Color border_color);
+ void stop();
+ bool is_started() const;
+
+ bool failed() const;
+ bool poll_events();
+ bool is_selected() const;
+ bool take_selection();
+ Region get_selection() const;
+ private:
+ void on_button_press(const void *de);
+ void on_button_release(const void *de);
+ void on_mouse_motion(const void *de);
+ private:
+ Display *dpy = nullptr;
+ unsigned long region_window = 0;
+ unsigned long cursor_window = 0;
+ unsigned long region_window_colormap = 0;
+ int xi_opcode = 0;
+ GC region_gc = nullptr;
+ GC cursor_gc = nullptr;
+
+ Region region;
+ bool selecting_region = false;
+ bool selected = false;
+ bool is_wayland = false;
+ std::vector<Monitor> monitors;
+ mgl::vec2i cursor_pos;
+ };
+} \ No newline at end of file
diff --git a/include/WindowUtils.hpp b/include/WindowUtils.hpp
index 99b45e9..e31eeb2 100644
--- a/include/WindowUtils.hpp
+++ b/include/WindowUtils.hpp
@@ -22,9 +22,19 @@ namespace gsr {
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type);
std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window);
std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window);
+ void set_window_size_not_resizable(Display *dpy, Window window, int width, int height);
mgl::vec2i get_cursor_position(Display *dpy, Window *window);
mgl::vec2i create_window_get_center_position(Display *display);
std::string get_window_manager_name(Display *display);
bool is_compositor_running(Display *dpy, int screen);
std::vector<Monitor> get_monitors(Display *dpy);
+ void xi_grab_all_mouse_devices(Display *dpy);
+ void xi_ungrab_all_mouse_devices(Display *dpy);
+ void xi_warp_all_mouse_devices(Display *dpy, mgl::vec2i position);
+ void window_set_fullscreen(Display *dpy, Window window, bool fullscreen);
+ bool window_is_fullscreen(Display *display, Window window);
+ bool set_window_wm_state(Display *dpy, Window window, Atom atom);
+ void make_window_click_through(Display *display, Window window);
+ bool make_window_sticky(Display *dpy, Window window);
+ bool hide_window_from_taskbar(Display *dpy, Window window);
} \ No newline at end of file
diff --git a/meson.build b/meson.build
index 2a0d71a..2acdfb0 100644
--- a/meson.build
+++ b/meson.build
@@ -33,6 +33,7 @@ src = [
'src/gui/Subsection.cpp',
'src/Utils.cpp',
'src/WindowUtils.cpp',
+ 'src/RegionSelector.cpp',
'src/Config.cpp',
'src/GsrInfo.cpp',
'src/Process.cpp',
@@ -65,6 +66,7 @@ executable(
dependency('threads'),
dependency('xcomposite'),
dependency('xfixes'),
+ dependency('xext'),
dependency('xi'),
dependency('xcursor'),
dependency('libpulse-simple'),
diff --git a/project.conf b/project.conf
index 879f3ab..9ddb369 100644
--- a/project.conf
+++ b/project.conf
@@ -13,6 +13,7 @@ ignore_dirs = ["build", "tools"]
[dependencies]
xcomposite = ">=0"
xfixes = ">=0"
+xext = ">=0"
xi = ">=0"
xcursor = ">=1"
-libpulse-simple = ">=0"
+libpulse-simple = ">=0" \ No newline at end of file
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 &region_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 &region_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(&region_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, &region_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) {