aboutsummaryrefslogtreecommitdiff
path: root/src/RegionSelector.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/RegionSelector.cpp')
-rw-r--r--src/RegionSelector.cpp454
1 files changed, 454 insertions, 0 deletions
diff --git a/src/RegionSelector.cpp b/src/RegionSelector.cpp
new file mode 100644
index 0000000..5b7243b
--- /dev/null
+++ b/src/RegionSelector.cpp
@@ -0,0 +1,454 @@
+#include "../include/RegionSelector.hpp"
+
+#include <stdio.h>
+#include <string.h>
+
+#include <X11/extensions/XInput2.h>
+#include <X11/extensions/Xrandr.h>
+#include <X11/extensions/shape.h>
+
+namespace gsr {
+ static const int cursor_window_size = 32;
+ static const int cursor_thickness = 5;
+ static const int region_border_size = 2;
+
+ static bool xinput_is_supported(Display *dpy, int *xi_opcode) {
+ *xi_opcode = 0;
+ int query_event = 0;
+ int query_error = 0;
+ if(!XQueryExtension(dpy, "XInputExtension", xi_opcode, &query_event, &query_error)) {
+ fprintf(stderr, "error: RegionSelector: X Input extension not available\n");
+ return false;
+ }
+
+ int major = 2;
+ int minor = 1;
+ int retval = XIQueryVersion(dpy, &major, &minor);
+ if(retval != Success) {
+ fprintf(stderr, "error: RegionSelector: XInput 2.1 is not supported\n");
+ return false;
+ }
+
+ return true;
+ }
+
+ static int max_int(int a, int b) {
+ return a >= b ? a : b;
+ }
+
+ static void set_region_rectangle(Display *dpy, Window window, int x, int y, int width, int height, int border_size) {
+ if(width < 0) {
+ x += width;
+ width = abs(width);
+ }
+
+ if(height < 0) {
+ y += height;
+ height = abs(height);
+ }
+
+ XRectangle rectangles[] = {
+ {
+ (short)max_int(0, x), (short)max_int(0, y),
+ (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
+ }, // Left
+ {
+ (short)max_int(0, x + width - border_size), (short)max_int(0, y),
+ (unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
+ }, // Right
+ {
+ (short)max_int(0, x + border_size), (short)max_int(0, y),
+ (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
+ }, // Top
+ {
+ (short)max_int(0, x + border_size), (short)max_int(0, y + height - border_size),
+ (unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
+ }, // Bottom
+ };
+ XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted);
+ XFlush(dpy);
+ }
+
+ static void set_window_shape_cross(Display *dpy, Window window, int window_width, int window_height, int thickness) {
+ XRectangle rectangles[] = {
+ {
+ (short)(window_width / 2 - thickness / 2), (short)0,
+ (unsigned short)thickness, (unsigned short)window_height
+ }, // Vertical
+ {
+ (short)(0), (short)(window_height / 2 - thickness / 2),
+ (unsigned short)window_width, (unsigned short)thickness
+ }, // Horizontal
+ };
+ XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 2, ShapeSet, Unsorted);
+ XFlush(dpy);
+ }
+
+ static void draw_rectangle(Display *dpy, Window window, GC gc, int x, int y, int width, int height) {
+ if(width < 0) {
+ x += width;
+ width = abs(width);
+ }
+
+ if(height < 0) {
+ y += height;
+ height = abs(height);
+ }
+
+ XDrawRectangle(dpy, window, gc, x, y, width, height);
+ }
+
+ static Window create_cursor_window(Display *dpy, int width, int height, XVisualInfo *vinfo, unsigned long background_pixel) {
+ XSetWindowAttributes window_attr;
+ window_attr.background_pixel = background_pixel;
+ window_attr.border_pixel = 0;
+ window_attr.override_redirect = true;
+ window_attr.event_mask = StructureNotifyMask | PointerMotionMask;
+ window_attr.colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo->visual, AllocNone);
+ const Window window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, width, height, 0, vinfo->depth, InputOutput, vinfo->visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr);
+ if(window) {
+ set_window_size_not_resizable(dpy, window, width, height);
+ set_window_shape_cross(dpy, window, width, height, cursor_thickness);
+ make_window_click_through(dpy, window);
+ }
+ return window;
+ }
+
+ static void draw_rectangle_around_selected_monitor(Display *dpy, Window window, GC region_gc, int region_border_size, bool is_wayland, const std::vector<Monitor> &monitors, mgl::vec2i cursor_pos) {
+ const Monitor *focused_monitor = nullptr;
+ for(const Monitor &monitor : monitors) {
+ if(cursor_pos.x >= monitor.position.x && cursor_pos.x <= monitor.position.x + monitor.size.x
+ && cursor_pos.y >= monitor.position.y && cursor_pos.y <= monitor.position.y + monitor.size.y)
+ {
+ focused_monitor = &monitor;
+ break;
+ }
+ }
+
+ int x = 0;
+ int y = 0;
+ int width = 0;
+ int height = 0;
+ if(focused_monitor) {
+ x = focused_monitor->position.x;
+ y = focused_monitor->position.y;
+ width = focused_monitor->size.x;
+ height = focused_monitor->size.y;
+ }
+
+ if(is_wayland)
+ draw_rectangle(dpy, window, region_gc, x, y, width, height);
+ else
+ set_region_rectangle(dpy, window, x, y, width, height, region_border_size);
+ }
+
+ static void update_cursor_window(Display *dpy, Window window, Window cursor_window, bool is_wayland, int cursor_x, int cursor_y, int cursor_window_size, int thickness, GC cursor_gc) {
+ if(is_wayland) {
+ const int x = cursor_x - cursor_window_size / 2;
+ const int y = cursor_y - cursor_window_size / 2;
+ XFillRectangle(dpy, window, cursor_gc, x + cursor_window_size / 2 - thickness / 2 , y, thickness, cursor_window_size);
+ XFillRectangle(dpy, window, cursor_gc, x, y + cursor_window_size / 2 - thickness / 2, cursor_window_size, thickness);
+ } else if(cursor_window) {
+ XMoveWindow(dpy, cursor_window, cursor_x - cursor_window_size / 2, cursor_y - cursor_window_size / 2);
+ }
+ XFlush(dpy);
+ }
+
+ static bool is_xwayland(Display *dpy) {
+ int opcode, event, error;
+ return XQueryExtension(dpy, "XWAYLAND", &opcode, &event, &error);
+ }
+
+ static unsigned long mgl_color_to_x11_color(mgl::Color color) {
+ if(color.a == 0)
+ return 0;
+ return ((uint32_t)color.a << 24) | (((uint32_t)color.r * color.a / 0xFF) << 16) | (((uint32_t)color.g * color.a / 0xFF) << 8) | ((uint32_t)color.b * color.a / 0xFF);
+ }
+
+ RegionSelector::RegionSelector() {
+
+ }
+
+ RegionSelector::~RegionSelector() {
+ stop();
+ }
+
+ bool RegionSelector::start(mgl::Color border_color) {
+ if(dpy)
+ return false;
+
+ const unsigned long border_color_x11 = mgl_color_to_x11_color(border_color);
+ dpy = XOpenDisplay(nullptr);
+ if(!dpy) {
+ fprintf(stderr, "Error: RegionSelector::start: failed to connect to the X11 server\n");
+ return false;
+ }
+
+ xi_opcode = 0;
+ if(!xinput_is_supported(dpy, &xi_opcode)) {
+ fprintf(stderr, "Error: RegionSelector::start: xinput not supported on your system\n");
+ stop();
+ return false;
+ }
+
+ is_wayland = is_xwayland(dpy);
+ monitors = get_monitors(dpy);
+
+ Window x11_cursor_window = None;
+ cursor_pos = get_cursor_position(dpy, &x11_cursor_window);
+ region.pos = {0, 0};
+ region.size = {0, 0};
+
+ XVisualInfo vinfo;
+ memset(&vinfo, 0, sizeof(vinfo));
+ XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo);
+ region_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone);
+
+ XSetWindowAttributes window_attr;
+ window_attr.background_pixel = is_wayland ? 0 : border_color_x11;
+ window_attr.border_pixel = 0;
+ window_attr.override_redirect = true;
+ window_attr.event_mask = StructureNotifyMask | PointerMotionMask;
+ 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);
+ XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync, CurrentTime);
+ xi_grab_all_mouse_devices(dpy);
+ XFlush(dpy);
+
+ window_set_fullscreen(dpy, region_window, true);
+
+ if(!is_wayland || x11_cursor_window)
+ update_cursor_window(dpy, region_window, cursor_window, is_wayland, cursor_pos.x, cursor_pos.y, cursor_window_size, cursor_thickness, cursor_gc);
+
+ if(cursor_window) {
+ XMapWindow(dpy, cursor_window);
+ make_window_sticky(dpy, cursor_window);
+ hide_window_from_taskbar(dpy, cursor_window);
+ }
+
+ draw_rectangle_around_selected_monitor(dpy, region_window, region_gc, region_border_size, is_wayland, monitors, cursor_pos);
+
+ XFlush(dpy);
+ selected = false;
+ canceled = false;
+ return true;
+ }
+
+ void RegionSelector::stop() {
+ if(!dpy)
+ return;
+
+ XWarpPointer(dpy, DefaultRootWindow(dpy), DefaultRootWindow(dpy), 0, 0, 0, 0, cursor_pos.x, cursor_pos.y);
+ xi_warp_all_mouse_devices(dpy, cursor_pos);
+ XFixesShowCursor(dpy, region_window);
+
+ XUngrabPointer(dpy, CurrentTime);
+ XUngrabKeyboard(dpy, CurrentTime);
+ xi_ungrab_all_mouse_devices(dpy);
+ XFlush(dpy);
+
+ if(region_gc) {
+ XFreeGC(dpy, region_gc);
+ region_gc = nullptr;
+ }
+
+ if(cursor_gc) {
+ XFreeGC(dpy, cursor_gc);
+ cursor_gc = nullptr;
+ }
+
+ if(region_window_colormap) {
+ XFreeColormap(dpy, region_window_colormap);
+ region_window_colormap = 0;
+ }
+
+ if(region_window) {
+ XDestroyWindow(dpy, region_window);
+ region_window = 0;
+ }
+
+ XCloseDisplay(dpy);
+ dpy = nullptr;
+ selecting_region = false;
+ }
+
+ bool RegionSelector::is_started() const {
+ return dpy != nullptr;
+ }
+
+ bool RegionSelector::failed() const {
+ return !dpy;
+ }
+
+ bool RegionSelector::poll_events() {
+ if(!dpy || selected)
+ return false;
+
+ XEvent xev;
+ while(XPending(dpy)) {
+ XNextEvent(dpy, &xev);
+
+ if(xev.type == KeyRelease && XKeycodeToKeysym(dpy, xev.xkey.keycode, 0) == XK_Escape) {
+ canceled = true;
+ selected = false;
+ stop();
+ break;
+ }
+
+ XGenericEventCookie *cookie = &xev.xcookie;
+ if(cookie->type != GenericEvent || cookie->extension != xi_opcode || !XGetEventData(dpy, cookie))
+ continue;
+
+ const XIDeviceEvent *de = (XIDeviceEvent*)cookie->data;
+ switch(cookie->evtype) {
+ case XI_ButtonPress: {
+ on_button_press(de);
+ break;
+ }
+ case XI_ButtonRelease: {
+ on_button_release(de);
+ break;
+ }
+ case XI_Motion: {
+ on_mouse_motion(de);
+ break;
+ }
+ }
+ XFreeEventData(dpy, cookie);
+
+ if(selected) {
+ stop();
+ break;
+ }
+ }
+ return true;
+ }
+
+ bool RegionSelector::is_selected() const {
+ return selected;
+ }
+
+ bool RegionSelector::take_selection() {
+ const bool result = selected;
+ selected = false;
+ return result;
+ }
+
+ bool RegionSelector::take_canceled() {
+ const bool result = canceled;
+ canceled = false;
+ return result;
+ }
+
+ Region RegionSelector::get_selection() const {
+ return region;
+ }
+
+ void RegionSelector::on_button_press(const void *de) {
+ const XIDeviceEvent *device_event = (XIDeviceEvent*)de;
+ if(device_event->detail != Button1)
+ return;
+
+ region.pos = { (int)device_event->root_x, (int)device_event->root_y };
+ selecting_region = true;
+ }
+
+ void RegionSelector::on_button_release(const void *de) {
+ const XIDeviceEvent *device_event = (XIDeviceEvent*)de;
+ if(device_event->detail != Button1)
+ return;
+
+ if(!selecting_region)
+ return;
+
+ if(is_wayland) {
+ XClearWindow(dpy, region_window);
+ XFlush(dpy);
+ } else {
+ set_region_rectangle(dpy, region_window, 0, 0, 0, 0, 0);
+ }
+ selecting_region = false;
+
+ cursor_pos = region.pos + region.size;
+
+ if(region.size.x < 0) {
+ region.pos.x += region.size.x;
+ region.size.x = abs(region.size.x);
+ }
+
+ if(region.size.y < 0) {
+ region.pos.y += region.size.y;
+ region.size.y = abs(region.size.y);
+ }
+
+ if(region.size.x > 0)
+ region.size.x += 1;
+
+ if(region.size.y > 0)
+ region.size.y += 1;
+
+ selected = true;
+ }
+
+ void RegionSelector::on_mouse_motion(const void *de) {
+ const XIDeviceEvent *device_event = (XIDeviceEvent*)de;
+ XClearWindow(dpy, region_window);
+ if(selecting_region) {
+ region.size.x = device_event->root_x - region.pos.x;
+ region.size.y = device_event->root_y - region.pos.y;
+ cursor_pos = region.pos + region.size;
+
+ if(is_wayland)
+ draw_rectangle(dpy, region_window, region_gc, region.pos.x, region.pos.y, region.size.x, region.size.y);
+ else
+ set_region_rectangle(dpy, region_window, region.pos.x, region.pos.y, region.size.x, region.size.y, region_border_size);
+ } else {
+ cursor_pos = { (int)device_event->root_x, (int)device_event->root_y };
+ draw_rectangle_around_selected_monitor(dpy, region_window, region_gc, region_border_size, is_wayland, monitors, cursor_pos);
+ }
+ update_cursor_window(dpy, region_window, cursor_window, is_wayland, cursor_pos.x, cursor_pos.y, cursor_window_size, cursor_thickness, cursor_gc);
+ XFlush(dpy);
+ }
+} \ No newline at end of file