Compare commits

...

5 Commits

Author SHA1 Message Date
dec05eba
fde1b438df Improve window selection (dont show selection when no window is selected) 2026-02-15 18:22:10 +01:00
dec05eba
1d96b73e1a 1.10.8 2026-02-15 18:05:39 +01:00
dec05eba
1ce12067aa Fix window capture selection not working if the cursor is hidden and grabbed when starting capture 2026-02-15 18:04:56 +01:00
dec05eba
728ccc40a6 Strip window title when using kde/hyprland as well 2026-02-12 01:25:59 +01:00
dec05eba
02db186232 aur -> official repo 2026-02-06 19:04:43 +01:00
13 changed files with 219 additions and 370 deletions

View File

@@ -5,7 +5,7 @@ A fullscreen overlay UI for [GPU Screen Recorder](https://git.dec05eba.com/gpu-s
The application is currently primarly designed for X11 but it can run on Wayland as well through XWayland, with some caveats because of Wayland limitations.
# Installation
If you are using an Arch Linux based distro then you can find gpu screen recorder ui on aur under the name gpu-screen-recorder-ui (`yay -S gpu-screen-recorder-ui`).\
If you are running an Arch Linux based distro then you can find gpu screen recorder ui in the official repositories under the name gpu-screen-recorder-ui (`sudo pacman -S gpu-screen-recorder-ui`).\
If you are running another distro then you can run `sudo ./install.sh`, but you need to manually install the dependencies, as described below.\
You can also install gpu screen recorder from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder) which includes this UI.

2
TODO
View File

@@ -117,8 +117,6 @@ System startup option should also support runit and some other init systems, not
Use x11 shm instead of XGetImage (https://stackoverflow.com/questions/43442675/how-to-use-xshmgetimage-and-xshmputimage).
Add a hotkey to record/stream/replay region.
Do xi grab for keys as well. Otherwise the ui cant be used for keyboard input if a program has grabbed the keyboard, and there could possibly be a game that grabs the keyboard as well.
Make inactive buttons gray (in dropdown boxes and in the front page with save, etc when replay is not running).

View File

@@ -9,7 +9,6 @@
#include "GlobalHotkeys/GlobalHotkeysJoystick.hpp"
#include "AudioPlayer.hpp"
#include "RegionSelector.hpp"
#include "WindowSelector.hpp"
#include "ClipboardFile.hpp"
#include "LedIndicator.hpp"
#include "CursorTracker/CursorTracker.hpp"
@@ -275,9 +274,7 @@ namespace gsr {
bool start_region_capture = false;
std::function<void()> on_region_selected;
WindowSelector window_selector;
bool start_window_capture = false;
std::function<void()> on_window_selected;
std::string recording_capture_target;
std::string screenshot_capture_target;

View File

@@ -15,14 +15,26 @@ namespace gsr {
mgl::vec2i size;
};
struct RegionWindow {
Window window = None;
mgl::vec2i pos;
mgl::vec2i size;
};
class RegionSelector {
public:
enum class SelectionType {
NONE,
REGION,
WINDOW
};
RegionSelector();
RegionSelector(const RegionSelector&) = delete;
RegionSelector& operator=(const RegionSelector&) = delete;
~RegionSelector();
bool start(mgl::Color border_color);
bool start(SelectionType selection_type, mgl::Color border_color);
void stop();
bool is_started() const;
@@ -30,7 +42,11 @@ namespace gsr {
bool poll_events();
bool take_selection();
bool take_canceled();
Region get_selection(Display *x11_dpy, struct wl_display *wayland_dpy) const;
Region get_region_selection(Display *x11_dpy, struct wl_display *wayland_dpy) const;
// Returns None (0) if none is selected
Window get_window_selection() const;
SelectionType get_selection_type() const;
private:
void on_button_press(const void *de);
void on_button_release(const void *de);
@@ -50,6 +66,10 @@ namespace gsr {
bool canceled = false;
bool is_wayland = false;
std::vector<Monitor> monitors;
std::vector<RegionWindow> windows; // First window is the window that is on top
std::optional<RegionWindow> focused_window;
mgl::vec2i cursor_pos;
SelectionType selection_type = SelectionType::NONE;
};
}

View File

@@ -1,33 +0,0 @@
#pragma once
#include <X11/Xlib.h>
#include <mglpp/graphics/Color.hpp>
namespace gsr {
class WindowSelector {
public:
WindowSelector();
WindowSelector(const WindowSelector&) = delete;
WindowSelector& operator=(const WindowSelector&) = delete;
~WindowSelector();
bool start(mgl::Color border_color);
void stop();
bool is_started() const;
bool failed() const;
bool poll_events();
bool take_selection();
bool take_canceled();
Window get_selection() const;
private:
Display *dpy = nullptr;
Cursor crosshair_cursor = None;
Colormap border_window_colormap = None;
Window border_window = None;
Window selected_window = None;
bool selected = false;
bool canceled = false;
};
}

View File

@@ -27,6 +27,7 @@ namespace gsr {
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);
Window window_get_target_window_child(Display *display, Window window);
unsigned char* window_get_property(Display *dpy, Window window, Atom property_type, const char *property_name, unsigned int *property_size);
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);

View File

@@ -1,4 +1,4 @@
project('gsr-ui', ['c', 'cpp'], version : '1.10.7', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.10.8', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
add_project_arguments('-D_FILE_OFFSET_BITS=64', language : ['c', 'cpp'])
@@ -45,7 +45,6 @@ src = [
'src/KwinWorkaround.cpp',
'src/WindowUtils.cpp',
'src/RegionSelector.cpp',
'src/WindowSelector.cpp',
'src/Config.cpp',
'src/GsrInfo.cpp',
'src/Process.cpp',
@@ -71,7 +70,7 @@ gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui')
icons_path = join_paths(prefix, datadir, 'icons')
add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.12.1"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.12.2"', language: ['c', 'cpp'])
add_project_arguments('-DKWIN_HELPER_SCRIPT_PATH="' + gsr_ui_resources_path + '/gsrkwinhelper.js"', language: ['c', 'cpp'])

View File

@@ -1,7 +1,7 @@
[package]
name = "gsr-ui"
type = "executable"
version = "1.10.7"
version = "1.10.8"
platforms = ["posix"]
[lang.cpp]

View File

@@ -804,24 +804,28 @@ namespace gsr {
if(region_selector.take_canceled()) {
on_region_selected = nullptr;
} else if(region_selector.take_selection() && on_region_selected) {
on_region_selected();
on_region_selected = nullptr;
}
switch(region_selector.get_selection_type()) {
case RegionSelector::SelectionType::NONE: {
break;
}
case RegionSelector::SelectionType::REGION: {
on_region_selected();
break;
}
case RegionSelector::SelectionType::WINDOW: {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
window_selector.poll_events();
if(window_selector.take_canceled()) {
on_window_selected = nullptr;
} else if(window_selector.take_selection() && on_window_selected) {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
const Window selected_window = window_selector.get_selection();
if(selected_window && selected_window != DefaultRootWindow(display)) {
on_window_selected();
} else {
show_notification(TR("No window selected"), notification_timeout_seconds, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0), NotificationType::NOTICE, nullptr, NotificationLevel::ERROR);
const Window selected_window = region_selector.get_window_selection();
if(selected_window && selected_window != DefaultRootWindow(display)) {
on_region_selected();
} else {
show_notification(TR("No window selected"), notification_timeout_seconds, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0), NotificationType::NOTICE, nullptr, NotificationLevel::ERROR);
}
break;
}
}
on_window_selected = nullptr;
on_region_selected = nullptr;
}
if(!visible || !window)
@@ -875,7 +879,7 @@ namespace gsr {
if(start_region_capture) {
start_region_capture = false;
hide();
if(!region_selector.start(get_color_theme().tint_color)) {
if(!region_selector.start(RegionSelector::SelectionType::REGION, get_color_theme().tint_color)) {
show_notification(TR("Failed to start region capture"), notification_error_timeout_seconds, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0), NotificationType::NOTICE, nullptr, NotificationLevel::ERROR);
on_region_selected = nullptr;
}
@@ -884,13 +888,13 @@ namespace gsr {
if(start_window_capture) {
start_window_capture = false;
hide();
if(!window_selector.start(get_color_theme().tint_color)) {
if(!region_selector.start(RegionSelector::SelectionType::WINDOW, get_color_theme().tint_color)) {
show_notification(TR("Failed to start window capture"), notification_error_timeout_seconds, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0), NotificationType::NOTICE, nullptr, NotificationLevel::ERROR);
on_window_selected = nullptr;
on_region_selected = nullptr;
}
}
if(region_selector.is_started() || window_selector.is_started()) {
if(region_selector.is_started()) {
usleep(5 * 1000); // 5 ms
return true;
}
@@ -1025,7 +1029,7 @@ namespace gsr {
if(visible)
return;
if(region_selector.is_started() || window_selector.is_started())
if(region_selector.is_started())
return;
drawn_first_frame = false;
@@ -2041,6 +2045,7 @@ namespace gsr {
if(focused_window_name.empty())
focused_window_name = "Game";
focused_window_name = strip(focused_window_name);
string_replace_characters(focused_window_name.data(), "/\\", ' ');
std::string video_directory = filepath_get_directory(video_filepath.c_str()) + "/" + focused_window_name;
@@ -2591,7 +2596,7 @@ namespace gsr {
}
void Overlay::add_region_command(std::vector<const char*> &args, char *region_str, int region_str_size) {
Region region = region_selector.get_selection(x11_dpy, wayland_dpy);
Region region = region_selector.get_region_selection(x11_dpy, wayland_dpy);
if(region.size.x <= 32 && region.size.y <= 32) {
region.size.x = 0;
region.size.y = 0;
@@ -2675,7 +2680,7 @@ namespace gsr {
std::string Overlay::get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options) {
if(capture_target == "window") {
return std::to_string(window_selector.get_selection());
return std::to_string(region_selector.get_window_selection());
} else if(capture_target == "focused_monitor") {
std::optional<CursorInfo> cursor_info;
if(cursor_tracker) {
@@ -2870,7 +2875,7 @@ namespace gsr {
}
bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) {
if(region_selector.is_started() || window_selector.is_started())
if(region_selector.is_started())
return false;
switch(recording_status) {
@@ -2913,15 +2918,6 @@ namespace gsr {
return true;
}
const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
recording_capture_target = get_capture_target(config.replay_config.record_options.record_area_option, capture_options);
if(!validate_capture_target(config.replay_config.record_options.record_area_option, capture_options)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), TR("Failed to start replay, capture target \"%s\" is invalid.\nPlease change capture target in settings"), recording_capture_target.c_str());
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY, nullptr, NotificationLevel::ERROR);
return false;
}
if(config.replay_config.record_options.record_area_option == "region" && !finished_selection) {
start_region_capture = true;
on_region_selected = [disable_notification, this]() {
@@ -2932,12 +2928,21 @@ namespace gsr {
if(config.replay_config.record_options.record_area_option == "window" && !finished_selection) {
start_window_capture = true;
on_window_selected = [disable_notification, this]() {
on_region_selected = [disable_notification, this]() {
on_press_start_replay(disable_notification, true);
};
return false;
}
const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
recording_capture_target = get_capture_target(config.replay_config.record_options.record_area_option, capture_options);
if(!validate_capture_target(config.replay_config.record_options.record_area_option, capture_options)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), TR("Failed to start replay, capture target \"%s\" is invalid.\nPlease change capture target in settings"), recording_capture_target.c_str());
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::REPLAY, nullptr, NotificationLevel::ERROR);
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);
@@ -3039,7 +3044,7 @@ namespace gsr {
}
void Overlay::on_press_start_record(bool finished_selection, RecordForceType force_type) {
if(region_selector.is_started() || window_selector.is_started())
if(region_selector.is_started())
return;
switch(recording_status) {
@@ -3153,15 +3158,6 @@ namespace gsr {
break;
}
const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
recording_capture_target = get_capture_target(record_area_option, capture_options);
if(!validate_capture_target(record_area_option, capture_options)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), TR("Failed to start recording, capture target \"%s\" is invalid.\nPlease change capture target in settings"), recording_capture_target.c_str());
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD, nullptr, NotificationLevel::ERROR);
return;
}
if(record_area_option == "region" && !finished_selection) {
start_region_capture = true;
on_region_selected = [this, force_type]() {
@@ -3172,12 +3168,21 @@ namespace gsr {
if(record_area_option == "window" && !finished_selection) {
start_window_capture = true;
on_window_selected = [this, force_type]() {
on_region_selected = [this, force_type]() {
on_press_start_record(true, force_type);
};
return;
}
const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
recording_capture_target = get_capture_target(record_area_option, capture_options);
if(!validate_capture_target(record_area_option, capture_options)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), TR("Failed to start recording, capture target \"%s\" is invalid.\nPlease change capture target in settings"), recording_capture_target.c_str());
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::RECORD, nullptr, NotificationLevel::ERROR);
return;
}
record_filepath.clear();
// TODO: Validate input, fallback to valid values
@@ -3311,7 +3316,7 @@ namespace gsr {
}
void Overlay::on_press_start_stream(bool finished_selection) {
if(region_selector.is_started() || window_selector.is_started())
if(region_selector.is_started())
return;
switch(recording_status) {
@@ -3351,15 +3356,6 @@ namespace gsr {
return;
}
const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
recording_capture_target = get_capture_target(config.streaming_config.record_options.record_area_option, capture_options);
if(!validate_capture_target(config.streaming_config.record_options.record_area_option, capture_options)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), TR("Failed to start streaming, capture target \"%s\" is invalid.\nPlease change capture target in settings"), recording_capture_target.c_str());
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM, nullptr, NotificationLevel::ERROR);
return;
}
if(config.streaming_config.record_options.record_area_option == "region" && !finished_selection) {
start_region_capture = true;
on_region_selected = [this]() {
@@ -3370,12 +3366,21 @@ namespace gsr {
if(config.streaming_config.record_options.record_area_option == "window" && !finished_selection) {
start_window_capture = true;
on_window_selected = [this]() {
on_region_selected = [this]() {
on_press_start_stream(true);
};
return;
}
const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
recording_capture_target = get_capture_target(config.streaming_config.record_options.record_area_option, capture_options);
if(!validate_capture_target(config.streaming_config.record_options.record_area_option, capture_options)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), TR("Failed to start streaming, capture target \"%s\" is invalid.\nPlease change capture target in settings"), recording_capture_target.c_str());
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::STREAM, nullptr, NotificationLevel::ERROR);
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);
@@ -3468,7 +3473,7 @@ namespace gsr {
}
void Overlay::on_press_take_screenshot(bool finished_selection, ScreenshotForceType force_type) {
if(region_selector.is_started() || window_selector.is_started())
if(region_selector.is_started())
return;
if(gpu_screen_recorder_screenshot_process > 0) {
@@ -3489,15 +3494,6 @@ namespace gsr {
break;
}
const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
screenshot_capture_target = get_capture_target(record_area_option, capture_options);
if(!validate_capture_target(record_area_option, capture_options)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), TR("Failed to take a screenshot, capture target \"%s\" is invalid.\nPlease change capture target in settings"), screenshot_capture_target.c_str());
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT, nullptr, NotificationLevel::ERROR);
return;
}
if(record_area_option == "region" && !finished_selection) {
start_region_capture = true;
on_region_selected = [this, force_type]() {
@@ -3508,12 +3504,21 @@ namespace gsr {
if(record_area_option == "window" && !finished_selection) {
start_window_capture = true;
on_window_selected = [this, force_type]() {
on_region_selected = [this, force_type]() {
on_press_take_screenshot(true, force_type);
};
return;
}
const SupportedCaptureOptions capture_options = get_supported_capture_options(gsr_info);
screenshot_capture_target = get_capture_target(record_area_option, capture_options);
if(!validate_capture_target(record_area_option, capture_options)) {
char err_msg[256];
snprintf(err_msg, sizeof(err_msg), TR("Failed to take a screenshot, capture target \"%s\" is invalid.\nPlease change capture target in settings"), screenshot_capture_target.c_str());
show_notification(err_msg, notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), NotificationType::SCREENSHOT, nullptr, NotificationLevel::ERROR);
return;
}
// TODO: Validate input, fallback to valid values
std::string output_file;
if(config.screenshot_config.save_screenshot_to_disk)

View File

@@ -2,11 +2,13 @@
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <X11/Xatom.h>
#include <X11/extensions/XInput2.h>
#include <X11/extensions/Xrandr.h>
#include <X11/extensions/shape.h>
#include <mglpp/system/Rect.hpp>
namespace gsr {
static const int cursor_window_size = 32;
@@ -66,7 +68,11 @@ namespace gsr {
(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);
if(width == 0 && height == 0)
XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 0, ShapeSet, Unsorted);
else
XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted);
XFlush(dpy);
}
@@ -96,7 +102,8 @@ namespace gsr {
height = abs(height);
}
XDrawRectangle(dpy, window, gc, x, y, width, height);
if(width != 0 && height != 0)
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) {
@@ -115,6 +122,13 @@ namespace gsr {
return window;
}
static void draw_rectangle_or_region(Display *dpy, Window window, GC region_gc, int region_border_size, bool is_wayland, mgl::vec2i pos, mgl::vec2i size) {
if(is_wayland)
draw_rectangle(dpy, window, region_gc, pos.x, pos.y, size.x, size.y);
else
set_region_rectangle(dpy, window, pos.x, pos.y, size.x, size.y, region_border_size);
}
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) {
@@ -137,10 +151,7 @@ namespace gsr {
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);
draw_rectangle_or_region(dpy, window, region_gc, region_border_size, is_wayland, mgl::vec2i(x, y), mgl::vec2i(width, height));
}
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) {
@@ -222,6 +233,42 @@ namespace gsr {
};
}
static std::vector<RegionWindow> query_windows(Display *dpy) {
std::vector<RegionWindow> windows;
Window root_return = None;
Window parent_return = None;
Window *children_return = nullptr;
unsigned int num_children_return = 0;
if(!XQueryTree(dpy, DefaultRootWindow(dpy), &root_return, &parent_return, &children_return, &num_children_return) || !children_return)
return windows;
for(int i = (int)num_children_return - 1; i >= 0; --i) {
const Window child_window = children_return[i];
XWindowAttributes win_attr;
if(XGetWindowAttributes(dpy, child_window, &win_attr) && !win_attr.override_redirect && win_attr.c_class == InputOutput && win_attr.map_state == IsViewable) {
windows.push_back(
RegionWindow{
child_window,
mgl::vec2i(win_attr.x, win_attr.y),
mgl::vec2i(win_attr.width, win_attr.height)
}
);
}
}
XFree(children_return);
return windows;
}
static std::optional<RegionWindow> get_window_by_position(const std::vector<RegionWindow> &windows, mgl::vec2i pos) {
for(const RegionWindow &window : windows) {
if(mgl::IntRect(window.pos, window.size).contains(pos))
return window;
}
return std::nullopt;
}
RegionSelector::RegionSelector() {
}
@@ -230,7 +277,7 @@ namespace gsr {
stop();
}
bool RegionSelector::start(mgl::Color border_color) {
bool RegionSelector::start(SelectionType selection_type, mgl::Color border_color) {
if(dpy)
return false;
@@ -328,11 +375,20 @@ namespace gsr {
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);
windows = query_windows(dpy);
if(selection_type == SelectionType::WINDOW) {
focused_window = get_window_by_position(windows, cursor_pos);
if(focused_window)
draw_rectangle_or_region(dpy, region_window, region_gc, region_border_size, is_wayland, focused_window->pos, focused_window->size);
} else if(selection_type == SelectionType::REGION) {
draw_rectangle_around_selected_monitor(dpy, region_window, region_gc, region_border_size, is_wayland, monitors, cursor_pos);
}
XFlush(dpy);
selected = false;
canceled = false;
this->selection_type = selection_type;
return true;
}
@@ -375,6 +431,8 @@ namespace gsr {
XCloseDisplay(dpy);
dpy = nullptr;
selecting_region = false;
monitors.clear();
windows.clear();
}
bool RegionSelector::is_started() const {
@@ -441,20 +499,35 @@ namespace gsr {
return result;
}
Region RegionSelector::get_selection(Display *x11_dpy, struct wl_display *wayland_dpy) const {
Region RegionSelector::get_region_selection(Display *x11_dpy, struct wl_display *wayland_dpy) const {
assert(selection_type == SelectionType::REGION);
Region returned_region = region;
if(is_wayland && x11_dpy && wayland_dpy)
returned_region = x11_region_to_wayland_region(x11_dpy, wayland_dpy, returned_region);
return returned_region;
}
Window RegionSelector::get_window_selection() const {
assert(selection_type == SelectionType::WINDOW);
if(focused_window)
return focused_window->window;
else
return None;
}
RegionSelector::SelectionType RegionSelector::get_selection_type() const {
return selection_type;
}
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;
if(selection_type == SelectionType::REGION) {
region.pos = { (int)device_event->root_x, (int)device_event->root_y };
selecting_region = true;
}
}
void RegionSelector::on_button_release(const void *de) {
@@ -462,8 +535,23 @@ namespace gsr {
if(device_event->detail != Button1)
return;
if(!selecting_region)
return;
if(selection_type == SelectionType::WINDOW) {
focused_window = get_window_by_position(windows, mgl::vec2i(device_event->root_x, device_event->root_y));
if(focused_window) {
const Window real_window = window_get_target_window_child(dpy, focused_window->window);
XWindowAttributes win_attr;
if(XGetWindowAttributes(dpy, real_window, &win_attr)) {
focused_window = RegionWindow{
real_window,
mgl::vec2i(win_attr.x, win_attr.y),
mgl::vec2i(win_attr.width, win_attr.height)
};
}
}
} else if(selection_type == SelectionType::REGION) {
if(!selecting_region)
return;
}
if(is_wayland) {
XClearWindow(dpy, region_window);
@@ -473,7 +561,11 @@ namespace gsr {
}
selecting_region = false;
cursor_pos = region.pos + region.size;
if(selection_type == SelectionType::WINDOW) {
cursor_pos = { (int)device_event->root_x, (int)device_event->root_y };
} else if(selection_type == SelectionType::REGION) {
cursor_pos = region.pos + region.size;
}
if(region.size.x < 0) {
region.pos.x += region.size.x;
@@ -497,19 +589,26 @@ namespace gsr {
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);
draw_rectangle_or_region(dpy, region_window, region_gc, region_border_size, is_wayland, region.pos, region.size);
} else if(selection_type == SelectionType::WINDOW) {
cursor_pos = { (int)device_event->root_x, (int)device_event->root_y };
focused_window = get_window_by_position(windows, cursor_pos);
if(focused_window)
draw_rectangle_or_region(dpy, region_window, region_gc, region_border_size, is_wayland, focused_window->pos, focused_window->size);
else
set_region_rectangle(dpy, region_window, region.pos.x, region.pos.y, region.size.x, region.size.y, region_border_size);
} else {
draw_rectangle_or_region(dpy, region_window, region_gc, region_border_size, is_wayland, mgl::vec2i(0, 0), mgl::vec2i(0, 0));
} else if(selection_type == SelectionType::REGION) {
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);
}

View File

@@ -1,7 +1,6 @@
#include "../include/Translation.hpp"
#include <cstdio>
#include <cstring>
#include <iostream>
#include <unordered_map>
#include <fstream>

View File

@@ -1,236 +0,0 @@
#include "../include/WindowSelector.hpp"
#include "../include/WindowUtils.hpp"
#include <stdio.h>
#include <string.h>
#include <X11/Xatom.h>
#include <X11/extensions/shape.h>
#include <X11/cursorfont.h>
#include <X11/keysym.h>
namespace gsr {
static const int rectangle_border_size = 2;
static int max_int(int a, int b) {
return a >= b ? a : b;
}
static void set_region_rectangle(Display *dpy, Window window, int x, int y, int width, int height, int border_size) {
if(width < 0) {
x += width;
width = abs(width);
}
if(height < 0) {
y += height;
height = abs(height);
}
XRectangle rectangles[] = {
{
(short)max_int(0, x), (short)max_int(0, y),
(unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
}, // Left
{
(short)max_int(0, x + width - border_size), (short)max_int(0, y),
(unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
}, // Right
{
(short)max_int(0, x + border_size), (short)max_int(0, y),
(unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
}, // Top
{
(short)max_int(0, x + border_size), (short)max_int(0, y + height - border_size),
(unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
}, // Bottom
};
XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted);
XFlush(dpy);
}
static unsigned long mgl_color_to_x11_color(mgl::Color color) {
if(color.a == 0)
return 0;
return ((uint32_t)color.a << 24) | (((uint32_t)color.r * color.a / 0xFF) << 16) | (((uint32_t)color.g * color.a / 0xFF) << 8) | ((uint32_t)color.b * color.a / 0xFF);
}
static Window get_cursor_window(Display *dpy) {
Window root_window = None;
Window window = None;
int dummy_i;
unsigned int dummy_u;
mgl::vec2i root_pos;
XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u);
return window;
}
static void get_window_geometry(Display *dpy, Window window, mgl::vec2i &pos, mgl::vec2i &size) {
Window root_window;
int x = 0;
int y = 0;
unsigned int w = 0;
unsigned int h = 0;
unsigned int dummy_border, dummy_depth;
XGetGeometry(dpy, window, &root_window, &x, &y, &w, &h, &dummy_border, &dummy_depth);
pos.x = x;
pos.y = y;
size.x = w;
size.y = h;
}
WindowSelector::WindowSelector() {
}
WindowSelector::~WindowSelector() {
stop();
}
bool WindowSelector::start(mgl::Color border_color) {
if(dpy)
return false;
const unsigned long border_color_x11 = mgl_color_to_x11_color(border_color);
dpy = XOpenDisplay(nullptr);
if(!dpy) {
fprintf(stderr, "Error: WindowSelector::start: failed to connect to the X11 server\n");
return false;
}
const Window cursor_window = get_cursor_window(dpy);
mgl::vec2i cursor_window_pos, cursor_window_size;
get_window_geometry(dpy, cursor_window, cursor_window_pos, cursor_window_size);
XVisualInfo vinfo;
memset(&vinfo, 0, sizeof(vinfo));
XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo);
border_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone);
XSetWindowAttributes window_attr;
window_attr.background_pixel = border_color_x11;
window_attr.border_pixel = 0;
window_attr.override_redirect = true;
window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask;
window_attr.colormap = border_window_colormap;
Screen *screen = XDefaultScreenOfDisplay(dpy);
border_window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, XWidthOfScreen(screen), XHeightOfScreen(screen), 0,
vinfo.depth, InputOutput, vinfo.visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr);
if(!border_window) {
fprintf(stderr, "Error: WindowSelector::start: failed to create region window\n");
stop();
return false;
}
set_window_size_not_resizable(dpy, border_window, XWidthOfScreen(screen), XHeightOfScreen(screen));
unsigned char data = 2; // Prefer being composed to allow transparency. Do this to prevent the compositor from getting turned on/off when taking a screenshot
XChangeProperty(dpy, border_window, XInternAtom(dpy, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
if(cursor_window && cursor_window != DefaultRootWindow(dpy))
set_region_rectangle(dpy, border_window, cursor_window_pos.x, cursor_window_pos.y, cursor_window_size.x, cursor_window_size.y, rectangle_border_size);
else
set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0);
make_window_click_through(dpy, border_window);
XMapWindow(dpy, border_window);
crosshair_cursor = XCreateFontCursor(dpy, XC_crosshair);
XGrabPointer(dpy, DefaultRootWindow(dpy), True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, crosshair_cursor, CurrentTime);
XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync, CurrentTime);
XFlush(dpy);
selected = false;
canceled = false;
selected_window = None;
return true;
}
void WindowSelector::stop() {
if(!dpy)
return;
XUngrabPointer(dpy, CurrentTime);
XUngrabKeyboard(dpy, CurrentTime);
if(border_window_colormap) {
XFreeColormap(dpy, border_window_colormap);
border_window_colormap = 0;
}
if(border_window) {
XDestroyWindow(dpy, border_window);
border_window = 0;
}
if(crosshair_cursor) {
XFreeCursor(dpy, crosshair_cursor);
crosshair_cursor = None;
}
XFlush(dpy);
XSync(dpy, False);
XCloseDisplay(dpy);
dpy = nullptr;
}
bool WindowSelector::is_started() const {
return dpy != nullptr;
}
bool WindowSelector::failed() const {
return !dpy;
}
bool WindowSelector::poll_events() {
if(!dpy || selected)
return false;
XEvent xev;
while(XPending(dpy)) {
XNextEvent(dpy, &xev);
if(xev.type == MotionNotify) {
const Window motion_window = xev.xmotion.subwindow;
mgl::vec2i motion_window_pos, motion_window_size;
get_window_geometry(dpy, motion_window, motion_window_pos, motion_window_size);
if(motion_window && motion_window != DefaultRootWindow(dpy))
set_region_rectangle(dpy, border_window, motion_window_pos.x, motion_window_pos.y, motion_window_size.x, motion_window_size.y, rectangle_border_size);
else
set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0);
XFlush(dpy);
} else if(xev.type == ButtonRelease && xev.xbutton.button == Button1) {
selected_window = xev.xbutton.subwindow;
const Window clicked_window_real = window_get_target_window_child(dpy, selected_window);
if(clicked_window_real)
selected_window = clicked_window_real;
selected = true;
stop();
break;
} else if(xev.type == KeyRelease && XKeycodeToKeysym(dpy, xev.xkey.keycode, 0) == XK_Escape) {
canceled = true;
selected = false;
stop();
break;
}
}
return true;
}
bool WindowSelector::take_selection() {
const bool result = selected;
selected = false;
return result;
}
bool WindowSelector::take_canceled() {
const bool result = canceled;
canceled = false;
return result;
}
Window WindowSelector::get_selection() const {
return selected_window;
}
}

View File

@@ -229,7 +229,7 @@ namespace gsr {
wl_display_roundtrip(dpy);
}
static unsigned char* window_get_property(Display *dpy, Window window, Atom property_type, const char *property_name, unsigned int *property_size) {
unsigned char* window_get_property(Display *dpy, Window window, Atom property_type, const char *property_name, unsigned int *property_size) {
Atom ret_property_type = None;
int ret_format = 0;
unsigned long num_items = 0;
@@ -284,14 +284,14 @@ namespace gsr {
return None;
Window found_window = None;
for(int i = num_children - 1; i >= 0; --i) {
for(int i = (int)num_children - 1; i >= 0; --i) {
if(children[i] && window_is_user_program(display, children[i])) {
found_window = children[i];
goto finished;
}
}
for(int i = num_children - 1; i >= 0; --i) {
for(int i = (int)num_children - 1; i >= 0; --i) {
if(children[i]) {
Window win = window_get_target_window_child(display, children[i]);
if(win) {