Compare commits

..

25 Commits
1.1.7 ... 1.3.0

Author SHA1 Message Date
dec05eba
d8a0b49bc2 flatpak 2025-03-15 01:03:06 +01:00
dec05eba
34b9aad24b 1.3.0 2025-03-15 01:02:36 +01:00
dec05eba
63b2b6cbc3 Add region capture option 2025-03-15 00:56:38 +01:00
dec05eba
6c7158c06d Support more keys for hotkeys (media keys) 2025-03-14 00:20:08 +01:00
dec05eba
7d1f6f9a25 Update flatpak version reference 2025-03-08 14:08:23 +01:00
dec05eba
3c8dd9c4db 1.2.2 2025-03-07 20:22:13 +01:00
dec05eba
c7fcf251e3 Fix shortcut keys changed 2025-03-07 20:21:25 +01:00
dec05eba
6d58b2495d Update screenshot image 2025-03-07 20:14:40 +01:00
dec05eba
347eced060 Change notification timeout 2025-03-07 17:38:50 +01:00
dec05eba
6449133c57 Update mglpp 2025-03-05 22:10:30 +01:00
dec05eba
1168e68278 1.2.1 2025-02-27 15:57:25 +01:00
dec05eba
4836c661ce 1.2.0 2025-02-27 15:56:35 +01:00
dec05eba
f0bbbbe4a9 Replay on startup: wait until audio devices are available before turning replay on 2025-02-25 17:37:25 +01:00
dec05eba
d9a1e5c2eb Add option to press backspace to remove hotkey 2025-02-25 01:21:23 +01:00
dec05eba
b6c59e1049 Update README 2025-02-22 23:01:06 +01:00
dec05eba
af6984cd7e 1.2.0 2025-02-22 14:39:40 +01:00
dec05eba
51a47193d7 Show correct process on screenshot failure 2025-02-22 13:39:06 +01:00
dec05eba
189736c1a9 Add option to take a screenshot (default hotkey: alt+f1) 2025-02-22 13:31:51 +01:00
dec05eba
8003c209fe m 2025-02-11 22:40:11 +01:00
dec05eba
1734d48af6 window get title: cleanup data 2025-02-10 19:41:56 +01:00
dec05eba
fc2f6f4c50 Better detect focused x11 window on wayland, properly get focused game name on wayland 2025-02-10 19:31:27 +01:00
dec05eba
f4e44cbef5 Prepare for sound. Fix game name being gsr-ui on wayland in some cases when saving video when the ui is open 2025-02-10 18:22:21 +01:00
dec05eba
3d6354c642 m 2025-02-08 03:18:16 +01:00
dec05eba
efb5fc53c1 Show notification when saving a large replay that is taking some time 2025-02-07 19:41:39 +01:00
dec05eba
51367ac078 Change replay duration max limit to 3 hours 2025-02-06 02:17:56 +01:00
42 changed files with 2275 additions and 506 deletions

View File

@@ -2,9 +2,7 @@
# GPU Screen Recorder UI
A fullscreen overlay UI for [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/about/) in the style of ShadowPlay.\
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.\
Note: This software is still in early alpha. Expect bugs, and please report any if you experience them. Some are already known, but it doesn't hurt to report them anyways.\
You can report an issue by emailing the issue to dec05eba@protonmail.com.
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.
# Usage
Run `gsr-ui` and press `Left Alt+Z` to show/hide the UI. You can start the overlay UI at system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
@@ -15,7 +13,7 @@ A program called `gsr-ui-cli` is also installed when installing this software. T
# 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 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 (the gtk gui version) from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder). This flatpak includes both this UI and gpu-screen-recorder so no need to install that first.
You can also install gpu screen recorder from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder). This flatpak includes both this UI and gpu-screen-recorder so no need to install that first.
# Dependencies
GPU Screen Recorder UI uses meson build system so you need to install `meson` to build GPU Screen Recorder UI.
@@ -23,10 +21,11 @@ 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
* libpulse (libpulse-simple)
## Runtime dependencies
There are also additional dependencies needed at runtime:
@@ -41,6 +40,9 @@ This might cause issues for you if you use input remapping software. To workarou
# License
This software is licensed under GPL3.0-only. Files under `fonts/` directory belong to the Noto Sans Google fonts project and they are licensed under `SIL Open Font License`. `images/default.cur` it part of the [Adwaita icon theme](https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/tree/master) which is licensed under `Creative Commons Attribution-Share Alike 3.0`.
# Reporting bugs, contributing patches, questions or donation
See [https://git.dec05eba.com/?p=about](https://git.dec05eba.com/?p=about).
# Demo
[![Click here to watch a demo video on youtube](https://img.youtube.com/vi/SOqXusCTXXA/0.jpg)](https://www.youtube.com/watch?v=SOqXusCTXXA)
@@ -48,15 +50,10 @@ This software is licensed under GPL3.0-only. Files under `fonts/` directory belo
![](https://dec05eba.com/images/front_page.jpg)
![](https://dec05eba.com/images/settings_page.jpg)
# Donations
If you want to donate you can donate via bitcoin or monero.
* Bitcoin: bc1qqvuqnwrdyppf707ge27fqz2n9y9gu7lf5ypyuf
* Monero: 4An9kp2qW1C9Gah7ewv4JzcNFQ5TAX7ineGCqXWK6vQnhsGGcRpNgcn8r9EC3tMcgY7vqCKs3nSRXhejMHBaGvFdN2egYet
# Known issues
* When the UI is open the wallpaper is shown instead of the game on Hyprland. This is an issue with Hyprland. It cant be fixed until the UI is redesigned to not be a fullscreen overlay.
* Opening the UI when a game is fullscreened can mess up the game window a bit on Hyprland. I believe this is an issue with Hyprland.
# FAQ
## I get an error when trying to start the gpu-screen-recorder-ui.service systemd service
If you have previously used the flatpak version of GPU Screen Recorder with the new UI then non-flatpak version of the systemd service will conflict with that. Run `gsr-ui` to fix that.
If you have previously used the flatpak version of GPU Screen Recorder with the new UI then the non-flatpak version of the systemd service will conflict with that. Run `gsr-ui` to fix that.

33
TODO
View File

@@ -78,8 +78,6 @@ Dont allow autostart of replay if capture option is window recording (when windo
Use global shortcuts desktop portal protocol on wayland when available.
When support for window capture is enabled on x11 then make sure to not save the window except temporary while the program is open.
Support CJK.
Move ui hover code from ::draw to ::on_event, to properly handle widget event stack.
@@ -114,4 +112,33 @@ For keyboards that report supporting mice the keyboard grab will be delayed unti
Instead of installing gsr-global-hotkeys in flatpak use kms-server-proxy to launch gsr-global-hotkeys inside the flatpak with root, just like gsr-kms-server. This removes the need to update gsr-global-hotkeys everytime there is an update.
Check if "modprobe uinput" is needed on some systems (old fedora?).
Check if "modprobe uinput" is needed on some systems (old fedora?).
Add recording timer to see duration of recording/streaming.
Make folder with window name work when using gamescope. Gamescope runs x11 itself so to get the window name inside that we have to connect to the gamescope X11 server (DISPLAY=:1 on x11 and DISPLAY=:2 on wayland, but not always).
When clicking on current directory in file manager show a dropdown menu where you can select common directories (HOME, Videos, Downloads and mounted drives) for quick navigation. Maybe even button to search.
Maybe change gsr-ui startup retry time in the systemd service, from 5 seconds to 2 seconds.
Add support for window capture. This should not prompt for window selection directly but instead prompt for window selection when recording starts and hide the ui first.
For screenshots window capture should exist but "follow focused" option should not exist.
Improve audio design. It should have a button to add/remove audio tracks and button to add audio into each audio track separately and "record audio from all applications except the selected ones" for each audio track. Then also remove the "merge audio tracks" option.
Make it possible to take a screenshot through a button in the ui instead of having to use hotkey.
Handle failing to save a replay. gsr should output "failed to save replay, or something like that" to make it possible to detect that.
Dont allow saving replay while a replay save is in progress.
Make input work with cjk input systems (such as fcitx).
System startup option should also support runit and some other init systems, not only soystemd.
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.
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.

BIN
images/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

22
include/AudioPlayer.hpp Normal file
View File

@@ -0,0 +1,22 @@
#pragma once
#include <thread>
namespace gsr {
// Only plays raw stereo PCM audio in 48000hz in s16le format.
// Use this command to convert an audio file (input.wav) to a format playable by this class (output.pcm):
// ffmpeg -i input.wav -f s16le -acodec pcm_s16le -ar 48000 output.pcm
class AudioPlayer {
public:
AudioPlayer() = default;
~AudioPlayer();
AudioPlayer(const AudioPlayer&) = delete;
AudioPlayer& operator=(const AudioPlayer&) = delete;
bool play(const char *filepath);
private:
std::thread thread;
bool stop_playing_audio = false;
int audio_file_fd = -1;
};
}

View File

@@ -11,12 +11,23 @@
namespace gsr {
struct SupportedCaptureOptions;
enum class ReplayStartupMode {
DONT_TURN_ON_AUTOMATICALLY,
TURN_ON_AT_SYSTEM_STARTUP,
TURN_ON_AT_FULLSCREEN,
TURN_ON_AT_POWER_SUPPLY_CONNECTED
};
ReplayStartupMode replay_startup_string_to_type(const char *startup_mode_str);
struct ConfigHotkey {
int64_t key = 0; // Mgl key
uint32_t modifiers = 0; // HotkeyModifier
bool operator==(const ConfigHotkey &other) const;
bool operator!=(const ConfigHotkey &other) const;
std::string to_string(bool spaces = true, bool modifier_side = true) const;
};
struct RecordOptions {
@@ -101,15 +112,34 @@ namespace gsr {
ConfigHotkey save_hotkey;
};
struct ScreenshotConfig {
std::string record_area_option = "screen";
int32_t image_width = 0;
int32_t image_height = 0;
bool change_image_resolution = false;
std::string image_quality = "very_high";
std::string image_format = "jpg";
bool record_cursor = true;
bool restore_portal_session = true;
bool save_screenshot_in_game_folder = false;
bool show_screenshot_saved_notifications = true;
std::string save_directory;
ConfigHotkey take_screenshot_hotkey;
};
struct Config {
Config(const SupportedCaptureOptions &capture_options);
bool operator==(const Config &other);
bool operator!=(const Config &other);
void set_hotkeys_to_default();
MainConfig main_config;
StreamingConfig streaming_config;
RecordConfig record_config;
ReplayConfig replay_config;
ScreenshotConfig screenshot_config;
};
std::optional<Config> read_config(const SupportedCaptureOptions &capture_options);

View File

@@ -20,6 +20,11 @@ namespace gsr {
bool vp9 = false;
};
struct SupportedImageFormats {
bool jpeg = false;
bool png = false;
};
struct GsrMonitor {
std::string name;
mgl::vec2i size;
@@ -42,6 +47,7 @@ namespace gsr {
struct SupportedCaptureOptions {
bool window = false;
bool region = false;
bool focused = false;
bool portal = false;
std::vector<GsrMonitor> monitors;
@@ -75,6 +81,7 @@ namespace gsr {
SystemInfo system_info;
GpuInfo gpu_info;
SupportedVideoCodecs supported_video_codecs;
SupportedImageFormats supported_image_formats;
};
enum class GsrInfoExitStatus {

View File

@@ -6,8 +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>
@@ -34,7 +35,8 @@ namespace gsr {
NONE,
RECORD,
REPLAY,
STREAM
STREAM,
SCREENSHOT
};
class Overlay {
@@ -56,6 +58,7 @@ namespace gsr {
void toggle_stream();
void toggle_replay();
void save_replay();
void take_screenshot();
void show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type);
bool is_open() const;
bool should_exit(std::string &reason) const;
@@ -69,23 +72,26 @@ namespace gsr {
void handle_keyboard_mapping_event();
void on_event(mgl::Event &event);
void create_frontpage_ui_components();
void xi_setup();
void handle_xi_events();
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();
void update_notification_process_status();
void save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type);
void on_replay_saved(const char *replay_saved_filepath);
void update_gsr_replay_save();
void update_gsr_process_status();
void update_gsr_screenshot_process_status();
void replay_status_update_status();
void update_focused_fullscreen_status();
void update_power_supply_status();
void update_system_startup_status();
void on_stop_recording(int exit_code);
@@ -102,9 +108,10 @@ namespace gsr {
void update_ui_replay_stopped();
void on_press_save_replay();
void on_press_start_replay(bool disable_notification);
void on_press_start_record();
void on_press_start_stream();
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();
@@ -147,6 +154,7 @@ namespace gsr {
pid_t notification_process = -1;
int gpu_screen_recorder_process_output_fd = -1;
FILE *gpu_screen_recorder_process_output_file = nullptr;
pid_t gpu_screen_recorder_screenshot_process = -1;
DropdownButton *replay_dropdown_button_ptr = nullptr;
DropdownButton *record_dropdown_button_ptr = nullptr;
@@ -163,6 +171,7 @@ namespace gsr {
bool focused_window_is_fullscreen = false;
std::string record_filepath;
std::string screenshot_filepath;
Display *xi_display = nullptr;
int xi_opcode = 0;
@@ -185,5 +194,15 @@ namespace gsr {
std::unique_ptr<GlobalHotkeysJoystick> global_hotkeys_js = nullptr;
Display *x11_mapping_display = nullptr;
XEvent x11_mapping_xev;
mgl::Clock replay_save_clock;
bool replay_save_show_notification = false;
ReplayStartupMode replay_startup_mode = ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP;
bool try_replay_startup = true;
AudioPlayer audio_player;
RegionSelector region_selector;
bool start_region_capture = false;
std::function<void()> on_region_selected;
};
}

View File

@@ -12,14 +12,14 @@ namespace gsr {
};
// Arguments ending with NULL
bool exec_program_daemonized(const char **args);
bool exec_program_daemonized(const char **args, bool debug = true);
// Arguments ending with NULL. |read_fd| can be NULL
pid_t exec_program(const char **args, int *read_fd);
pid_t exec_program(const char **args, int *read_fd, bool debug = true);
// Arguments ending with NULL. Returns the exit status of the program or -1 on error
int exec_program_get_stdout(const char **args, std::string &result);
int exec_program_get_stdout(const char **args, std::string &result, bool debug = true);
// Arguments ending with NULL. Returns the exit status of the program or -1 on error.
// This works the same as |exec_program_get_stdout|, except on flatpak where this runs the program on the
// host machine with flatpak-spawn --host
int exec_program_on_host_get_stdout(const char **args, std::string &result);
int exec_program_on_host_get_stdout(const char **args, std::string &result, bool debug = true);
pid_t pidof(const char *process_name, pid_t ignore_pid);
}

View File

@@ -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;
};
}

View File

@@ -41,6 +41,7 @@ namespace gsr {
mgl::Texture stop_texture;
mgl::Texture pause_texture;
mgl::Texture save_texture;
mgl::Texture screenshot_texture;
double double_click_timeout_seconds = 0.4;

View File

@@ -24,6 +24,8 @@ namespace gsr {
std::map<std::string, std::string> get_xdg_variables();
std::string get_videos_dir();
std::string get_pictures_dir();
// Returns 0 on success
int create_directory_recursive(char *path);
bool file_get_content(const char *filepath, std::string &file_content);

View File

@@ -3,6 +3,7 @@
#include <mglpp/system/vec.hpp>
#include <string>
#include <vector>
#include <optional>
#include <X11/Xlib.h>
namespace gsr {
@@ -16,11 +17,24 @@ namespace gsr {
mgl::vec2i size;
};
std::optional<std::string> get_window_title(Display *dpy, Window window);
Window get_focused_window(Display *dpy, WindowCaptureType cap_type);
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);
}

View File

@@ -30,6 +30,7 @@ namespace gsr {
std::function<void()> on_click;
private:
void scale_sprite_to_button_size();
float get_button_height();
private:
mgl::vec2f size;
mgl::Color bg_color;

View File

@@ -20,6 +20,7 @@ namespace gsr {
void add_item(const std::string &text, const std::string &id, const std::string &description = "");
void set_item_label(const std::string &id, const std::string &new_label);
void set_item_icon(const std::string &id, mgl::Texture *texture);
void set_item_description(const std::string &id, const std::string &new_description);
void set_description(std::string description_text);
void set_activated(bool activated);

View File

@@ -25,6 +25,7 @@ namespace gsr {
RECORD_START_STOP,
RECORD_PAUSE_UNPAUSE,
STREAM_START_STOP,
TAKE_SCREENSHOT,
SHOW_HIDE
};
@@ -44,6 +45,7 @@ namespace gsr {
std::function<void(const char *reason)> on_click_exit_program_button;
std::function<void(const char *hotkey_option)> on_keyboard_hotkey_changed;
std::function<void(const char *hotkey_option)> on_joystick_hotkey_changed;
std::function<void()> on_page_closed;
private:
void load_hotkeys();
@@ -55,6 +57,7 @@ namespace gsr {
std::unique_ptr<List> create_replay_hotkey_options();
std::unique_ptr<List> create_record_hotkey_options();
std::unique_ptr<List> create_stream_hotkey_options();
std::unique_ptr<List> create_screenshot_hotkey_options();
std::unique_ptr<List> create_hotkey_control_buttons();
std::unique_ptr<Subsection> create_hotkey_subsection(ScrollablePage *parent_page);
std::unique_ptr<Button> create_exit_program_button();
@@ -86,6 +89,7 @@ namespace gsr {
Button *start_stop_recording_button_ptr = nullptr;
Button *pause_unpause_recording_button_ptr = nullptr;
Button *start_stop_streaming_button_ptr = nullptr;
Button *take_screenshot_button_ptr = nullptr;
Button *show_hide_button_ptr = nullptr;
ConfigHotkey configure_config_hotkey;

View File

@@ -9,7 +9,7 @@
namespace gsr {
class GsrPage : public Page {
public:
GsrPage();
GsrPage(const char *top_text, const char *bottom_text);
GsrPage(const GsrPage&) = delete;
GsrPage& operator=(const GsrPage&) = delete;
@@ -42,7 +42,8 @@ namespace gsr {
float margin_bottom_scale = 0.0f;
float margin_left_scale = 0.0f;
float margin_right_scale = 0.0f;
mgl::Text label_text;
mgl::Text top_text;
mgl::Text bottom_text;
std::vector<ButtonItem> buttons;
};
}

View File

@@ -0,0 +1,78 @@
#pragma once
#include "StaticPage.hpp"
#include "List.hpp"
#include "ComboBox.hpp"
#include "Entry.hpp"
#include "CheckBox.hpp"
#include "../GsrInfo.hpp"
#include "../Config.hpp"
namespace gsr {
class PageStack;
class GsrPage;
class ScrollablePage;
class Button;
class ScreenshotSettingsPage : public StaticPage {
public:
ScreenshotSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack);
ScreenshotSettingsPage(const ScreenshotSettingsPage&) = delete;
ScreenshotSettingsPage& operator=(const ScreenshotSettingsPage&) = delete;
void load();
void save();
void on_navigate_away_from_page() override;
private:
std::unique_ptr<ComboBox> create_record_area_box();
std::unique_ptr<Widget> create_record_area();
std::unique_ptr<List> create_select_window();
std::unique_ptr<Entry> create_image_width_entry();
std::unique_ptr<Entry> create_image_height_entry();
std::unique_ptr<List> create_image_resolution();
std::unique_ptr<List> create_image_resolution_section();
std::unique_ptr<CheckBox> create_restore_portal_session_checkbox();
std::unique_ptr<List> create_restore_portal_session_section();
std::unique_ptr<Widget> create_change_image_resolution_section();
std::unique_ptr<Widget> create_capture_target_section();
std::unique_ptr<List> create_image_quality_section();
std::unique_ptr<Widget> create_record_cursor_section();
std::unique_ptr<Widget> create_image_section();
std::unique_ptr<List> create_save_directory(const char *label);
std::unique_ptr<ComboBox> create_image_format_box();
std::unique_ptr<List> create_image_format_section();
std::unique_ptr<Widget> create_file_info_section();
std::unique_ptr<CheckBox> create_save_screenshot_in_game_folder();
std::unique_ptr<Widget> create_general_section();
std::unique_ptr<Widget> create_notifications_section();
std::unique_ptr<Widget> create_settings();
void add_widgets();
void save(RecordOptions &record_options);
private:
Config &config;
const GsrInfo *gsr_info = nullptr;
SupportedCaptureOptions capture_options;
GsrPage *content_page_ptr = nullptr;
ScrollablePage *settings_scrollable_page_ptr = nullptr;
List *select_window_list_ptr = nullptr;
List *image_resolution_list_ptr = nullptr;
List *restore_portal_session_list_ptr = nullptr;
List *color_range_list_ptr = nullptr;
Widget *image_format_ptr = nullptr;
ComboBox *record_area_box_ptr = nullptr;
Entry *image_width_entry_ptr = nullptr;
Entry *image_height_entry_ptr = nullptr;
CheckBox *record_cursor_checkbox_ptr = nullptr;
CheckBox *restore_portal_session_checkbox_ptr = nullptr;
CheckBox *change_image_resolution_checkbox_ptr = nullptr;
ComboBox *image_quality_box_ptr = nullptr;
ComboBox *image_format_box_ptr = nullptr;
Button *save_directory_button_ptr = nullptr;
CheckBox *save_screenshot_in_game_folder_checkbox_ptr = nullptr;
CheckBox *show_screenshot_saved_notification_checkbox_ptr = nullptr;
PageStack *page_stack = nullptr;
};
}

View File

@@ -52,7 +52,7 @@ namespace gsr {
std::unique_ptr<CheckBox> create_restore_portal_session_checkbox();
std::unique_ptr<List> create_restore_portal_session_section();
std::unique_ptr<Widget> create_change_video_resolution_section();
std::unique_ptr<Widget> create_capture_target();
std::unique_ptr<Widget> create_capture_target_section();
std::unique_ptr<ComboBox> create_audio_device_selection_combobox();
std::unique_ptr<Button> create_remove_audio_device_button(List *audio_device_list_ptr);
std::unique_ptr<List> create_audio_device();
@@ -93,13 +93,14 @@ namespace gsr {
std::unique_ptr<List> create_save_directory(const char *label);
std::unique_ptr<ComboBox> create_container_box();
std::unique_ptr<List> create_container_section();
std::unique_ptr<Entry> create_replay_time_entry();
std::unique_ptr<List> create_replay_time_entry();
std::unique_ptr<List> create_replay_time();
std::unique_ptr<RadioButton> create_start_replay_automatically();
std::unique_ptr<CheckBox> create_save_replay_in_game_folder();
std::unique_ptr<CheckBox> create_restart_replay_on_save();
std::unique_ptr<Label> create_estimated_replay_file_size();
void update_estimated_replay_file_size();
void update_replay_time_text();
std::unique_ptr<CheckBox> create_save_recording_in_game_folder();
std::unique_ptr<Label> create_estimated_record_file_size();
void update_estimated_record_file_size();
@@ -186,6 +187,7 @@ namespace gsr {
Entry *youtube_stream_key_entry_ptr = nullptr;
Entry *stream_url_entry_ptr = nullptr;
Entry *replay_time_entry_ptr = nullptr;
Label *replay_time_label_ptr = nullptr;
RadioButton *turn_on_replay_automatically_mode_ptr = nullptr;
PageStack *page_stack = nullptr;

View File

@@ -1,4 +1,4 @@
project('gsr-ui', ['c', 'cpp'], version : '1.1.7', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.3.0', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
if get_option('buildtype') == 'debug'
add_project_arguments('-g3', language : ['c', 'cpp'])
@@ -27,11 +27,13 @@ src = [
'src/gui/CustomRendererWidget.cpp',
'src/gui/FileChooser.cpp',
'src/gui/SettingsPage.cpp',
'src/gui/ScreenshotSettingsPage.cpp',
'src/gui/GlobalSettingsPage.cpp',
'src/gui/GsrPage.cpp',
'src/gui/Subsection.cpp',
'src/Utils.cpp',
'src/WindowUtils.cpp',
'src/RegionSelector.cpp',
'src/Config.cpp',
'src/GsrInfo.cpp',
'src/Process.cpp',
@@ -39,6 +41,7 @@ src = [
'src/GlobalHotkeysX11.cpp',
'src/GlobalHotkeysLinux.cpp',
'src/GlobalHotkeysJoystick.cpp',
'src/AudioPlayer.cpp',
'src/Hotplug.cpp',
'src/Rpc.cpp',
'src/main.cpp',
@@ -52,7 +55,7 @@ datadir = get_option('datadir')
gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui')
add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.1.4"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.2.0"', language: ['c', 'cpp'])
executable(
meson.project_name(),
@@ -63,8 +66,10 @@ executable(
dependency('threads'),
dependency('xcomposite'),
dependency('xfixes'),
dependency('xext'),
dependency('xi'),
dependency('xcursor'),
dependency('libpulse-simple'),
],
cpp_args : '-DGSR_UI_RESOURCES_PATH="' + gsr_ui_resources_path + '"',
)

View File

@@ -1,7 +1,7 @@
[package]
name = "gsr-ui"
type = "executable"
version = "1.1.7"
version = "1.3.0"
platforms = ["posix"]
[lang.cpp]
@@ -13,5 +13,7 @@ ignore_dirs = ["build", "tools"]
[dependencies]
xcomposite = ">=0"
xfixes = ">=0"
xext = ">=0"
xi = ">=0"
xcursor = ">=1"
libpulse-simple = ">=0"

86
src/AudioPlayer.cpp Normal file
View File

@@ -0,0 +1,86 @@
#include "../include/AudioPlayer.hpp"
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <pulse/simple.h>
#include <pulse/error.h>
#define BUFSIZE 4096
namespace gsr {
AudioPlayer::~AudioPlayer() {
if(thread.joinable()) {
stop_playing_audio = true;
thread.join();
}
if(audio_file_fd > 0)
close(audio_file_fd);
}
bool AudioPlayer::play(const char *filepath) {
if(thread.joinable()) {
stop_playing_audio = true;
thread.join();
}
stop_playing_audio = false;
audio_file_fd = open(filepath, O_RDONLY);
if(audio_file_fd == -1)
return false;
thread = std::thread([this]() {
pa_sample_spec ss;
ss.format = PA_SAMPLE_S16LE;
ss.rate = 48000;
ss.channels = 2;
pa_simple *s = NULL;
int error;
/* Create a new playback stream */
if(!(s = pa_simple_new(NULL, "gsr-ui-audio-playback", PA_STREAM_PLAYBACK, NULL, "playback", &ss, NULL, NULL, &error))) {
fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error));
goto finish;
}
uint8_t buf[BUFSIZE];
for(;;) {
ssize_t r;
if(stop_playing_audio)
goto finish;
if((r = read(audio_file_fd, buf, sizeof(buf))) <= 0) {
if(r == 0) /* EOF */
break;
fprintf(stderr, __FILE__": read() failed: %s\n", strerror(errno));
goto finish;
}
if(pa_simple_write(s, buf, (size_t) r, &error) < 0) {
fprintf(stderr, __FILE__": pa_simple_write() failed: %s\n", pa_strerror(error));
goto finish;
}
}
if(pa_simple_drain(s, &error) < 0) {
fprintf(stderr, __FILE__": pa_simple_drain() failed: %s\n", pa_strerror(error));
goto finish;
}
finish:
if(s)
pa_simple_free(s);
close(audio_file_fd);
audio_file_fd = -1;
});
return true;
}
}

View File

@@ -6,6 +6,8 @@
#include <limits.h>
#include <inttypes.h>
#include <libgen.h>
#include <string.h>
#include <assert.h>
#include <mglpp/window/Keyboard.hpp>
#define FORMAT_I32 "%" PRIi32
@@ -13,6 +15,50 @@
#define FORMAT_U32 "%" PRIu32
namespace gsr {
static std::vector<mgl::Keyboard::Key> hotkey_modifiers_to_mgl_keys(uint32_t modifiers) {
std::vector<mgl::Keyboard::Key> result;
if(modifiers & HOTKEY_MOD_LCTRL)
result.push_back(mgl::Keyboard::LControl);
if(modifiers & HOTKEY_MOD_LSHIFT)
result.push_back(mgl::Keyboard::LShift);
if(modifiers & HOTKEY_MOD_LALT)
result.push_back(mgl::Keyboard::LAlt);
if(modifiers & HOTKEY_MOD_LSUPER)
result.push_back(mgl::Keyboard::LSystem);
if(modifiers & HOTKEY_MOD_RCTRL)
result.push_back(mgl::Keyboard::RControl);
if(modifiers & HOTKEY_MOD_RSHIFT)
result.push_back(mgl::Keyboard::RShift);
if(modifiers & HOTKEY_MOD_RALT)
result.push_back(mgl::Keyboard::RAlt);
if(modifiers & HOTKEY_MOD_RSUPER)
result.push_back(mgl::Keyboard::RSystem);
return result;
}
static void string_remove_all(std::string &str, const std::string &substr) {
size_t index = 0;
while(true) {
index = str.find(substr, index);
if(index == std::string::npos)
break;
str.erase(index, substr.size());
}
}
ReplayStartupMode replay_startup_string_to_type(const char *startup_mode_str) {
if(strcmp(startup_mode_str, "dont_turn_on_automatically") == 0)
return ReplayStartupMode::DONT_TURN_ON_AUTOMATICALLY;
else if(strcmp(startup_mode_str, "turn_on_at_system_startup") == 0)
return ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP;
else if(strcmp(startup_mode_str, "turn_on_at_fullscreen") == 0)
return ReplayStartupMode::TURN_ON_AT_FULLSCREEN;
else if(strcmp(startup_mode_str, "turn_on_at_power_supply_connected") == 0)
return ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED;
else
return ReplayStartupMode::DONT_TURN_ON_AUTOMATICALLY;
}
bool ConfigHotkey::operator==(const ConfigHotkey &other) const {
return key == other.key && modifiers == other.modifiers;
}
@@ -21,36 +67,83 @@ namespace gsr {
return !operator==(other);
}
Config::Config(const SupportedCaptureOptions &capture_options) {
const std::string default_save_directory = get_videos_dir();
std::string ConfigHotkey::to_string(bool spaces, bool modifier_side) const {
std::string result;
const std::vector<mgl::Keyboard::Key> modifier_keys = hotkey_modifiers_to_mgl_keys(modifiers);
std::string modifier_str;
for(const mgl::Keyboard::Key modifier_key : modifier_keys) {
if(!result.empty()) {
if(spaces)
result += " + ";
else
result += "+";
}
modifier_str = mgl::Keyboard::key_to_string(modifier_key);
if(!modifier_side) {
string_remove_all(modifier_str, "Left");
string_remove_all(modifier_str, "Right");
}
result += modifier_str;
}
if(key != 0) {
if(!result.empty()) {
if(spaces)
result += " + ";
else
result += "+";
}
result += mgl::Keyboard::key_to_string((mgl::Keyboard::Key)key);
}
return result;
}
Config::Config(const SupportedCaptureOptions &capture_options) {
const std::string default_videos_save_directory = get_videos_dir();
const std::string default_pictures_save_directory = get_pictures_dir();
set_hotkeys_to_default();
streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT};
streaming_config.record_options.video_quality = "custom";
streaming_config.record_options.audio_tracks.push_back("default_output");
streaming_config.record_options.video_bitrate = 15000;
record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT};
record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT};
record_config.save_directory = default_save_directory;
record_config.save_directory = default_videos_save_directory;
record_config.record_options.audio_tracks.push_back("default_output");
record_config.record_options.video_bitrate = 45000;
replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT};
replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT};
replay_config.record_options.video_quality = "custom";
replay_config.save_directory = default_save_directory;
replay_config.save_directory = default_videos_save_directory;
replay_config.record_options.audio_tracks.push_back("default_output");
replay_config.record_options.video_bitrate = 45000;
main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT};
screenshot_config.save_directory = default_pictures_save_directory;
if(!capture_options.monitors.empty()) {
streaming_config.record_options.record_area_option = capture_options.monitors.front().name;
record_config.record_options.record_area_option = capture_options.monitors.front().name;
replay_config.record_options.record_area_option = capture_options.monitors.front().name;
screenshot_config.record_area_option = capture_options.monitors.front().name;
}
}
void Config::set_hotkeys_to_default() {
streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT};
record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT};
record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT};
replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT};
replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT};
screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::F1, HOTKEY_MOD_LALT};
main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT};
}
static std::optional<KeyValue> parse_key_value(std::string_view line) {
const size_t space_index = line.find(' ');
if(space_index == std::string_view::npos)
@@ -156,7 +249,20 @@ namespace gsr {
{"replay.container", &config.replay_config.container},
{"replay.time", &config.replay_config.replay_time},
{"replay.start_stop_hotkey", &config.replay_config.start_stop_hotkey},
{"replay.save_hotkey", &config.replay_config.save_hotkey}
{"replay.save_hotkey", &config.replay_config.save_hotkey},
{"screenshot.record_area_option", &config.screenshot_config.record_area_option},
{"screenshot.image_width", &config.screenshot_config.image_width},
{"screenshot.image_height", &config.screenshot_config.image_height},
{"screenshot.change_image_resolution", &config.screenshot_config.change_image_resolution},
{"screenshot.image_quality", &config.screenshot_config.image_quality},
{"screenshot.image_format", &config.screenshot_config.image_format},
{"screenshot.record_cursor", &config.screenshot_config.record_cursor},
{"screenshot.restore_portal_session", &config.screenshot_config.restore_portal_session},
{"screenshot.save_screenshot_in_game_folder", &config.screenshot_config.save_screenshot_in_game_folder},
{"screenshot.show_screenshot_saved_notifications", &config.screenshot_config.show_screenshot_saved_notifications},
{"screenshot.save_directory", &config.screenshot_config.save_directory},
{"screenshot.take_screenshot_hotkey", &config.screenshot_config.take_screenshot_hotkey}
};
}
@@ -183,6 +289,8 @@ namespace gsr {
} else if(std::holds_alternative<std::vector<std::string>*>(it.second)) {
if(*std::get<std::vector<std::string>*>(it.second) != *std::get<std::vector<std::string>*>(it_other->second))
return false;
} else {
assert(false);
}
}
return true;
@@ -245,6 +353,8 @@ namespace gsr {
} else if(std::holds_alternative<std::vector<std::string>*>(it->second)) {
std::string array_value(key_value->value);
std::get<std::vector<std::string>*>(it->second)->push_back(std::move(array_value));
} else {
assert(false);
}
return true;
@@ -294,6 +404,8 @@ namespace gsr {
for(const std::string &value : *array) {
fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), value.c_str());
}
} else {
assert(false);
}
}

View File

@@ -157,11 +157,19 @@ namespace gsr {
gsr_info->supported_video_codecs.vp9 = true;
}
static void parse_image_formats_line(GsrInfo *gsr_info, std::string_view line) {
if(line == "jpeg")
gsr_info->supported_image_formats.jpeg = true;
else if(line == "png")
gsr_info->supported_image_formats.png = true;
}
enum class GsrInfoSection {
UNKNOWN,
SYSTEM_INFO,
GPU_INFO,
VIDEO_CODECS,
IMAGE_FORMATS,
CAPTURE_OPTIONS
};
@@ -194,6 +202,8 @@ namespace gsr {
section = GsrInfoSection::GPU_INFO;
else if(section_name == "video_codecs")
section = GsrInfoSection::VIDEO_CODECS;
else if(section_name == "image_formats")
section = GsrInfoSection::IMAGE_FORMATS;
else if(section_name == "capture_options")
section = GsrInfoSection::CAPTURE_OPTIONS;
else
@@ -217,6 +227,10 @@ namespace gsr {
parse_video_codecs_line(gsr_info, line);
break;
}
case GsrInfoSection::IMAGE_FORMATS: {
parse_image_formats_line(gsr_info, line);
break;
}
case GsrInfoSection::CAPTURE_OPTIONS: {
// Intentionally ignore, get capture options with get_supported_capture_options instead
break;
@@ -244,7 +258,7 @@ namespace gsr {
std::string stdout_str;
const char *args[] = { "gpu-screen-recorder", "--list-audio-devices", nullptr };
if(exec_program_get_stdout(args, stdout_str) != 0) {
if(exec_program_get_stdout(args, stdout_str, false) != 0) {
fprintf(stderr, "error: 'gpu-screen-recorder --list-audio-devices' failed\n");
return audio_devices;
}
@@ -296,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")

View File

@@ -44,9 +44,10 @@ namespace gsr {
}
void Hotplug::process_event_data(int fd, const HotplugEventCallback &callback) {
const int bytes_read = read(fd, event_data, sizeof(event_data));
const int bytes_read = read(fd, event_data, sizeof(event_data) - 1);
if(bytes_read <= 0)
return;
event_data[bytes_read] = '\0';
/* Hotplug data ends with a newline and a null terminator */
int data_index = 0;

File diff suppressed because it is too large Load Diff

View File

@@ -40,12 +40,13 @@ namespace gsr {
return num_args;
}
bool exec_program_daemonized(const char **args) {
bool exec_program_daemonized(const char **args, bool debug) {
/* 1 argument */
if(args[0] == nullptr)
return false;
debug_print_args(args);
if(debug)
debug_print_args(args);
const pid_t pid = vfork();
if(pid == -1) {
@@ -72,7 +73,7 @@ namespace gsr {
return true;
}
pid_t exec_program(const char **args, int *read_fd) {
pid_t exec_program(const char **args, int *read_fd, bool debug) {
if(read_fd)
*read_fd = -1;
@@ -84,7 +85,8 @@ namespace gsr {
if(pipe(fds) == -1)
return -1;
debug_print_args(args);
if(debug)
debug_print_args(args);
const pid_t pid = vfork();
if(pid == -1) {
@@ -110,10 +112,10 @@ namespace gsr {
}
}
int exec_program_get_stdout(const char **args, std::string &result) {
int exec_program_get_stdout(const char **args, std::string &result, bool debug) {
result.clear();
int read_fd = -1;
const pid_t process_id = exec_program(args, &read_fd);
const pid_t process_id = exec_program(args, &read_fd, debug);
if(process_id == -1)
return -1;
@@ -152,7 +154,7 @@ namespace gsr {
return exit_status;
}
int exec_program_on_host_get_stdout(const char **args, std::string &result) {
int exec_program_on_host_get_stdout(const char **args, std::string &result, bool debug) {
if(count_num_args(args) > 64 - 3) {
fprintf(stderr, "Error: too many arguments when trying to launch \"%s\"\n", args[0]);
return -1;
@@ -170,9 +172,9 @@ namespace gsr {
}
modified_args[i] = arg;
}
return exec_program_get_stdout(modified_args, result);
return exec_program_get_stdout(modified_args, result, debug);
} else {
return exec_program_get_stdout(args, result);
return exec_program_get_stdout(args, result, debug);
}
}

437
src/RegionSelector.cpp Normal file
View File

@@ -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);
}
}

View File

@@ -108,6 +108,9 @@ namespace gsr {
if(!theme->save_texture.load_from_file((resources_path + "images/save.png").c_str()))
goto error;
if(!theme->screenshot_texture.load_from_file((resources_path + "images/screenshot.png").c_str()))
goto error;
return true;
error:

View File

@@ -114,6 +114,14 @@ namespace gsr {
return xdg_videos_dir;
}
std::string get_pictures_dir() {
auto xdg_vars = get_xdg_variables();
std::string xdg_videos_dir = xdg_vars["XDG_PICTURES_DIR"];
if(xdg_videos_dir.empty())
xdg_videos_dir = get_home_dir() + "/Pictures";
return xdg_videos_dir;
}
int create_directory_recursive(char *path) {
int path_len = strlen(path);
char *p = path;

View File

@@ -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>
@@ -16,8 +18,6 @@ extern "C" {
#include <string.h>
#include <poll.h>
#include <optional>
#define MAX_PROPERTY_VALUE_LEN 4096
namespace gsr {
@@ -105,8 +105,17 @@ namespace gsr {
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);
if(window)
*window = window_get_target_window_child(dpy, *window);
const Window direct_window = *window;
*window = window_get_target_window_child(dpy, *window);
// HACK: Count some other x11 windows as having an x11 window focused. Some games seem to create an Input window and that gets focused.
if(!*window) {
XWindowAttributes attr;
memset(&attr, 0, sizeof(attr));
XGetWindowAttributes(dpy, direct_window, &attr);
if(attr.c_class == InputOnly && !get_window_title(dpy, direct_window))
*window = direct_window;
}
return root_pos;
}
@@ -149,7 +158,7 @@ namespace gsr {
std::string result;
for(int i = 0; i < size;) {
// Some games such as the finals has utf8-bom between each character, wtf?
if(i + 3 < size && memcmp(str + i, "\xEF\xBB\xBF", 3) == 0) {
if(i + 3 <= size && memcmp(str + i, "\xEF\xBB\xBF", 3) == 0) {
i += 3;
continue;
}
@@ -163,7 +172,8 @@ namespace gsr {
return result;
}
static std::optional<std::string> get_window_title(Display *dpy, Window window) {
std::optional<std::string> get_window_title(Display *dpy, Window window) {
std::optional<std::string> result;
const Atom net_wm_name_atom = XInternAtom(dpy, "_NET_WM_NAME", False);
const Atom wm_name_atom = XInternAtom(dpy, "WM_NAME", False);
const Atom utf8_string_atom = XInternAtom(dpy, "UTF8_STRING", False);
@@ -175,8 +185,13 @@ namespace gsr {
unsigned char *data = NULL;
XGetWindowProperty(dpy, window, net_wm_name_atom, 0, 1024, False, utf8_string_atom, &type, &format, &num_items, &bytes_left, &data);
if(type == utf8_string_atom && format == 8 && data)
return utf8_sanitize(data, num_items);
if(type == utf8_string_atom && format == 8 && data) {
result = utf8_sanitize(data, num_items);
goto done;
}
if(data)
XFree(data);
type = None;
format = 0;
@@ -185,10 +200,15 @@ namespace gsr {
data = NULL;
XGetWindowProperty(dpy, window, wm_name_atom, 0, 1024, False, 0, &type, &format, &num_items, &bytes_left, &data);
if((type == XA_STRING || type == utf8_string_atom) && data)
return utf8_sanitize(data, num_items);
if((type == XA_STRING || type == utf8_string_atom) && data) {
result = utf8_sanitize(data, num_items);
goto done;
}
return std::nullopt;
done:
if(data)
XFree(data);
return result;
}
static std::string strip(const std::string &str) {
@@ -228,14 +248,76 @@ namespace gsr {
XClassHint class_hint = {nullptr, nullptr};
XGetClassHint(dpy, focused_window, &class_hint);
if(class_hint.res_class) {
if(class_hint.res_class)
result = strip(class_hint.res_class);
return result;
}
if(class_hint.res_name)
XFree(class_hint.res_name);
if(class_hint.res_class)
XFree(class_hint.res_class);
return result;
}
std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window) {
std::string result;
Window root;
Window parent;
Window *children = nullptr;
unsigned int num_children = 0;
if(!XQueryTree(dpy, DefaultRootWindow(dpy), &root, &parent, &children, &num_children) || !children)
return result;
for(int i = (int)num_children - 1; i >= 0; --i) {
if(children[i] == ignore_window)
continue;
XWindowAttributes attr;
memset(&attr, 0, sizeof(attr));
XGetWindowAttributes(dpy, children[i], &attr);
if(attr.override_redirect || attr.c_class != InputOutput || attr.map_state != IsViewable)
continue;
if(position.x >= attr.x && position.x <= attr.x + attr.width && position.y >= attr.y && position.y <= attr.y + attr.height) {
const Window real_window = window_get_target_window_child(dpy, children[i]);
if(!real_window || real_window == ignore_window)
continue;
const std::optional<std::string> window_title = get_window_title(dpy, real_window);
if(window_title)
result = strip(window_title.value());
break;
}
}
XFree(children);
return result;
}
std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window) {
Window cursor_window;
const mgl::vec2i cursor_position = get_cursor_position(dpy, &cursor_window);
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;
@@ -283,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);
@@ -347,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);
@@ -466,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));
}
}

View File

@@ -15,8 +15,8 @@ namespace gsr {
// These are relative to the button size
static const float padding_top_icon_scale = 0.25f;
static const float padding_bottom_icon_scale = 0.25f;
static const float padding_left_icon_scale = 0.25f;
static const float padding_right_icon_scale = 0.25f;
//static const float padding_left_icon_scale = 0.25f;
static const float padding_right_icon_scale = 0.15f;
Button::Button(mgl::Font *font, const char *text, mgl::vec2f size, mgl::Color bg_color) :
size(size), bg_color(bg_color), bg_hover_color(bg_color), text(text, *font)
@@ -53,13 +53,21 @@ namespace gsr {
background.set_color(mouse_inside ? bg_hover_color : bg_color);
window.draw(background);
text.set_position((draw_pos + item_size * 0.5f - text.get_bounds().size * 0.5f).floor());
window.draw(text);
if(sprite.get_texture() && sprite.get_texture()->is_valid()) {
scale_sprite_to_button_size();
sprite.set_position((background.get_position() + background.get_size() * 0.5f - sprite.get_size() * 0.5f).floor());
const int padding_left = padding_left_scale * get_theme().window_height;
if(text.get_string().empty()) // Center
sprite.set_position((background.get_position() + background.get_size() * 0.5f - sprite.get_size() * 0.5f).floor());
else // Left
sprite.set_position((draw_pos + mgl::vec2f(padding_left, background.get_size().y * 0.5f - sprite.get_size().y * 0.5f)).floor());
window.draw(sprite);
const int padding_icon_right = padding_right_icon_scale * get_button_height();
text.set_position((sprite.get_position() + mgl::vec2f(sprite.get_size().x + padding_icon_right, sprite.get_size().y * 0.5f - text.get_bounds().size.y * 0.5f)).floor());
window.draw(text);
} else {
text.set_position((draw_pos + item_size * 0.5f - text.get_bounds().size * 0.5f).floor());
window.draw(text);
}
if(mouse_inside) {
@@ -72,18 +80,25 @@ namespace gsr {
if(!visible)
return {0.0f, 0.0f};
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
const int padding_left = padding_left_scale * get_theme().window_height;
const int padding_right = padding_right_scale * get_theme().window_height;
const mgl::vec2f text_bounds = text.get_bounds().size;
mgl::vec2f s = size;
if(s.x < 0.0001f)
s.x = padding_left + text_bounds.x + padding_right;
if(s.y < 0.0001f)
s.y = padding_top + text_bounds.y + padding_bottom;
return s;
mgl::vec2f widget_size = size;
if(widget_size.y < 0.0001f)
widget_size.y = get_button_height();
if(widget_size.x < 0.0001f) {
widget_size.x = padding_left + text_bounds.x + padding_right;
if(sprite.get_texture() && sprite.get_texture()->is_valid()) {
scale_sprite_to_button_size();
const int padding_icon_right = text_bounds.x > 0.001f ? padding_right_icon_scale * widget_size.y : 0.0f;
widget_size.x += sprite.get_size().x + padding_icon_right;
}
}
return widget_size;
}
void Button::set_border_scale(float scale) {
@@ -110,13 +125,23 @@ namespace gsr {
if(!sprite.get_texture() || !sprite.get_texture()->is_valid())
return;
const mgl::vec2f button_size = get_size();
const int padding_icon_top = padding_top_icon_scale * button_size.y;
const int padding_icon_bottom = padding_bottom_icon_scale * button_size.y;
const int padding_icon_left = padding_left_icon_scale * button_size.y;
const int padding_icon_right = padding_right_icon_scale * button_size.y;
const float widget_height = get_button_height();
const mgl::vec2f desired_size = button_size - mgl::vec2f(padding_icon_left + padding_icon_right, padding_icon_top + padding_icon_bottom);
sprite.set_size(scale_keep_aspect_ratio(sprite.get_texture()->get_size().to_vec2f(), desired_size).floor());
const int padding_icon_top = padding_top_icon_scale * widget_height;
const int padding_icon_bottom = padding_bottom_icon_scale * widget_height;
const float desired_height = widget_height - (padding_icon_top + padding_icon_bottom);
sprite.set_height((int)desired_height);
}
float Button::get_button_height() {
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
float widget_height = size.y;
if(widget_height < 0.0001f)
widget_height = padding_top + text.get_bounds().size.y + padding_bottom;
return widget_height;
}
}

View File

@@ -201,6 +201,15 @@ namespace gsr {
}
}
void DropdownButton::set_item_description(const std::string &id, const std::string &new_description) {
for(auto &item : items) {
if(item.id == id) {
item.description_text.set_string(new_description);
return;
}
}
}
void DropdownButton::set_description(std::string description_text) {
description.set_string(std::move(description_text));
}

View File

@@ -67,46 +67,6 @@ namespace gsr {
return 0;
}
static std::vector<mgl::Keyboard::Key> hotkey_modifiers_to_mgl_keys(uint32_t modifiers) {
std::vector<mgl::Keyboard::Key> result;
if(modifiers & HOTKEY_MOD_LCTRL)
result.push_back(mgl::Keyboard::LControl);
if(modifiers & HOTKEY_MOD_LSHIFT)
result.push_back(mgl::Keyboard::LShift);
if(modifiers & HOTKEY_MOD_LALT)
result.push_back(mgl::Keyboard::LAlt);
if(modifiers & HOTKEY_MOD_LSUPER)
result.push_back(mgl::Keyboard::LSystem);
if(modifiers & HOTKEY_MOD_RCTRL)
result.push_back(mgl::Keyboard::RControl);
if(modifiers & HOTKEY_MOD_RSHIFT)
result.push_back(mgl::Keyboard::RShift);
if(modifiers & HOTKEY_MOD_RALT)
result.push_back(mgl::Keyboard::RAlt);
if(modifiers & HOTKEY_MOD_RSUPER)
result.push_back(mgl::Keyboard::RSystem);
return result;
}
static std::string config_hotkey_to_string(ConfigHotkey config_hotkey) {
std::string result;
const std::vector<mgl::Keyboard::Key> modifier_keys = hotkey_modifiers_to_mgl_keys(config_hotkey.modifiers);
for(const mgl::Keyboard::Key modifier_key : modifier_keys) {
if(!result.empty())
result += " + ";
result += mgl::Keyboard::key_to_string(modifier_key);
}
if(config_hotkey.key != 0) {
if(!result.empty())
result += " + ";
result += mgl::Keyboard::key_to_string((mgl::Keyboard::Key)config_hotkey.key);
}
return result;
}
GlobalSettingsPage::GlobalSettingsPage(Overlay *overlay, const GsrInfo *gsr_info, Config &config, PageStack *page_stack) :
StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()),
overlay(overlay),
@@ -114,7 +74,7 @@ namespace gsr {
gsr_info(gsr_info),
page_stack(page_stack)
{
auto content_page = std::make_unique<GsrPage>();
auto content_page = std::make_unique<GsrPage>("Global", "Settings");
content_page->add_button("Back", "back", get_color_theme().page_bg_color);
content_page->on_click = [page_stack](const std::string &id) {
if(id == "back")
@@ -134,7 +94,7 @@ namespace gsr {
mgl::Text title_text("Press a key combination to use for the hotkey \"" + hotkey_configure_action_name + "\":", get_theme().title_font);
mgl::Text hotkey_text(configure_hotkey_button->get_text(), get_theme().top_bar_font);
mgl::Text description_text("The hotkey has to contain one or more of these keys: Alt, Ctrl, Shift and Super. Press Esc to cancel.", get_theme().body_font);
mgl::Text description_text("The hotkey has to contain one or more of these keys: Alt, Ctrl, Shift and Super. Press Esc to cancel or Backspace to remove the hotkey.", get_theme().body_font);
const float text_max_width = std::max(title_text.get_bounds().size.x, std::max(hotkey_text.get_bounds().size.x, description_text.get_bounds().size.x));
const float padding_horizontal = int(get_theme().window_height * 0.01f);
@@ -171,7 +131,7 @@ namespace gsr {
std::unique_ptr<Subsection> GlobalSettingsPage::create_appearance_subsection(ScrollablePage *parent_page) {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Tint color", get_color_theme().text_color));
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Accent color", get_color_theme().text_color));
auto tint_color_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
tint_color_radio_button_ptr = tint_color_radio_button.get();
tint_color_radio_button->add_item("Red", "amd");
@@ -322,30 +282,41 @@ namespace gsr {
return list;
}
std::unique_ptr<List> GlobalSettingsPage::create_screenshot_hotkey_options() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Take a screenshot:", get_color_theme().text_color));
auto take_screenshot_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
take_screenshot_button_ptr = take_screenshot_button.get();
list->add_widget(std::move(take_screenshot_button));
take_screenshot_button_ptr->on_click = [this] {
configure_hotkey_start(ConfigureHotkeyType::TAKE_SCREENSHOT);
};
return list;
}
std::unique_ptr<List> GlobalSettingsPage::create_hotkey_control_buttons() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
// auto clear_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Clear hotkeys", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
// clear_hotkeys_button->on_click = [this] {
// config.streaming_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
// config.record_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
// config.record_config.pause_unpause_hotkey = {mgl::Keyboard::Unknown, 0};
// config.replay_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
// config.replay_config.save_hotkey = {mgl::Keyboard::Unknown, 0};
// config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0};
// load_hotkeys();
// overlay->rebind_all_keyboard_hotkeys();
// };
// list->add_widget(std::move(clear_hotkeys_button));
auto clear_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Clear hotkeys", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
clear_hotkeys_button->on_click = [this] {
config.streaming_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
config.record_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
config.record_config.pause_unpause_hotkey = {mgl::Keyboard::Unknown, 0};
config.replay_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
config.replay_config.save_hotkey = {mgl::Keyboard::Unknown, 0};
config.screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Unknown, 0};
config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0};
load_hotkeys();
overlay->rebind_all_keyboard_hotkeys();
};
list->add_widget(std::move(clear_hotkeys_button));
auto reset_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Reset hotkeys to default", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
reset_hotkeys_button->on_click = [this] {
config.streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT};
config.record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT};
config.record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT};
config.replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT};
config.replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT};
config.main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT};
config.set_hotkeys_to_default();
load_hotkeys();
overlay->rebind_all_keyboard_hotkeys();
};
@@ -368,6 +339,7 @@ namespace gsr {
list_ptr->add_widget(create_replay_hotkey_options());
list_ptr->add_widget(create_record_hotkey_options());
list_ptr->add_widget(create_stream_hotkey_options());
list_ptr->add_widget(create_screenshot_hotkey_options());
list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Double-click the controller share button to save a replay", get_color_theme().text_color));
list_ptr->add_widget(create_hotkey_control_buttons());
return subsection;
@@ -440,6 +412,8 @@ namespace gsr {
void GlobalSettingsPage::on_navigate_away_from_page() {
save();
if(on_page_closed)
on_page_closed();
}
void GlobalSettingsPage::load() {
@@ -460,15 +434,17 @@ namespace gsr {
}
void GlobalSettingsPage::load_hotkeys() {
turn_replay_on_off_button_ptr->set_text(config_hotkey_to_string(config.replay_config.start_stop_hotkey));
save_replay_button_ptr->set_text(config_hotkey_to_string(config.replay_config.save_hotkey));
turn_replay_on_off_button_ptr->set_text(config.replay_config.start_stop_hotkey.to_string());
save_replay_button_ptr->set_text(config.replay_config.save_hotkey.to_string());
start_stop_recording_button_ptr->set_text(config_hotkey_to_string(config.record_config.start_stop_hotkey));
pause_unpause_recording_button_ptr->set_text(config_hotkey_to_string(config.record_config.pause_unpause_hotkey));
start_stop_recording_button_ptr->set_text(config.record_config.start_stop_hotkey.to_string());
pause_unpause_recording_button_ptr->set_text(config.record_config.pause_unpause_hotkey.to_string());
start_stop_streaming_button_ptr->set_text(config_hotkey_to_string(config.streaming_config.start_stop_hotkey));
start_stop_streaming_button_ptr->set_text(config.streaming_config.start_stop_hotkey.to_string());
show_hide_button_ptr->set_text(config_hotkey_to_string(config.main_config.show_hide_hotkey));
take_screenshot_button_ptr->set_text(config.screenshot_config.take_screenshot_hotkey.to_string());
show_hide_button_ptr->set_text(config.main_config.show_hide_hotkey.to_string());
}
void GlobalSettingsPage::save() {
@@ -494,12 +470,19 @@ namespace gsr {
if(event.key.code == mgl::Keyboard::Escape)
return false;
if(event.key.code == mgl::Keyboard::Backspace) {
configure_config_hotkey = {mgl::Keyboard::Unknown, 0};
configure_hotkey_button->set_text("");
configure_hotkey_stop_and_save();
return false;
}
if(mgl::Keyboard::key_is_modifier(event.key.code)) {
configure_config_hotkey.modifiers |= mgl_modifier_to_hotkey_modifier(event.key.code);
configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey));
configure_hotkey_button->set_text(configure_config_hotkey.to_string());
} else if(configure_config_hotkey.modifiers != 0) {
configure_config_hotkey.key = event.key.code;
configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey));
configure_hotkey_button->set_text(configure_config_hotkey.to_string());
configure_hotkey_stop_and_save();
}
@@ -512,7 +495,7 @@ namespace gsr {
if(mgl::Keyboard::key_is_modifier(event.key.code)) {
configure_config_hotkey.modifiers &= ~mgl_modifier_to_hotkey_modifier(event.key.code);
configure_hotkey_button->set_text(config_hotkey_to_string(configure_config_hotkey));
configure_hotkey_button->set_text(configure_config_hotkey.to_string());
}
return false;
@@ -535,6 +518,8 @@ namespace gsr {
return pause_unpause_recording_button_ptr;
case ConfigureHotkeyType::STREAM_START_STOP:
return start_stop_streaming_button_ptr;
case ConfigureHotkeyType::TAKE_SCREENSHOT:
return take_screenshot_button_ptr;
case ConfigureHotkeyType::SHOW_HIDE:
return show_hide_button_ptr;
}
@@ -555,6 +540,8 @@ namespace gsr {
return &config.record_config.pause_unpause_hotkey;
case ConfigureHotkeyType::STREAM_START_STOP:
return &config.streaming_config.start_stop_hotkey;
case ConfigureHotkeyType::TAKE_SCREENSHOT:
return &config.screenshot_config.take_screenshot_hotkey;
case ConfigureHotkeyType::SHOW_HIDE:
return &config.main_config.show_hide_hotkey;
}
@@ -568,6 +555,7 @@ namespace gsr {
&config.record_config.start_stop_hotkey,
&config.record_config.pause_unpause_hotkey,
&config.streaming_config.start_stop_hotkey,
&config.screenshot_config.take_screenshot_hotkey,
&config.main_config.show_hide_hotkey
};
for(ConfigHotkey *config_hotkey : config_hotkeys) {
@@ -604,6 +592,9 @@ namespace gsr {
case ConfigureHotkeyType::STREAM_START_STOP:
hotkey_configure_action_name = "Start/stop streaming";
break;
case ConfigureHotkeyType::TAKE_SCREENSHOT:
hotkey_configure_action_name = "Take a screenshot";
break;
case ConfigureHotkeyType::SHOW_HIDE:
hotkey_configure_action_name = "Show/hide UI";
break;
@@ -614,7 +605,7 @@ namespace gsr {
Button *config_hotkey_button = configure_hotkey_get_button_by_active_type();
ConfigHotkey *config_hotkey = configure_hotkey_get_config_by_active_type();
if(config_hotkey_button && config_hotkey)
config_hotkey_button->set_text(config_hotkey_to_string(*config_hotkey));
config_hotkey_button->set_text(config_hotkey->to_string());
configure_config_hotkey = {0, 0};
configure_hotkey_type = ConfigureHotkeyType::NONE;
@@ -628,15 +619,17 @@ namespace gsr {
ConfigHotkey *config_hotkey = configure_hotkey_get_config_by_active_type();
if(config_hotkey_button && config_hotkey) {
bool hotkey_used_by_another_action = false;
for_each_config_hotkey([&](ConfigHotkey *config_hotkey_item) {
if(config_hotkey_item != config_hotkey && *config_hotkey_item == configure_config_hotkey)
hotkey_used_by_another_action = true;
});
if(configure_config_hotkey.key != mgl::Keyboard::Unknown) {
for_each_config_hotkey([&](ConfigHotkey *config_hotkey_item) {
if(config_hotkey_item != config_hotkey && *config_hotkey_item == configure_config_hotkey)
hotkey_used_by_another_action = true;
});
}
if(hotkey_used_by_another_action) {
const std::string error_msg = "The hotkey \"" + config_hotkey_to_string(configure_config_hotkey) + " is already used for something else";
const std::string error_msg = "The hotkey \"" + configure_config_hotkey.to_string() + " is already used for something else";
overlay->show_notification(error_msg.c_str(), 3.0, mgl::Color(255, 0, 0, 255), mgl::Color(255, 0, 0, 255), NotificationType::NONE);
config_hotkey_button->set_text(config_hotkey_to_string(*config_hotkey));
config_hotkey_button->set_text(config_hotkey->to_string());
configure_config_hotkey = {0, 0};
return;
}

View File

@@ -8,8 +8,9 @@
namespace gsr {
static const float button_spacing_scale = 0.015f;
GsrPage::GsrPage() :
label_text("Settings", get_theme().title_font)
GsrPage::GsrPage(const char *top_text, const char *bottom_text) :
top_text(top_text, get_theme().title_font),
bottom_text(bottom_text, get_theme().title_font)
{
const float margin = 0.02f;
set_margins(margin, margin, margin, margin);
@@ -80,13 +81,17 @@ namespace gsr {
window.draw(background);
const int text_margin = background.get_size().y * 0.085;
label_text.set_position((background.get_position() + mgl::vec2f(background.get_size().x * 0.5f - label_text.get_bounds().size.x * 0.5f, text_margin)).floor());
window.draw(label_text);
top_text.set_position((background.get_position() + mgl::vec2f(background.get_size().x * 0.5f - top_text.get_bounds().size.x * 0.5f, text_margin)).floor());
window.draw(top_text);
mgl::Sprite icon(&get_theme().settings_texture);
icon.set_height((int)(background.get_size().y * 0.5f));
icon.set_position((background.get_position() + background.get_size() * 0.5f - icon.get_size() * 0.5f).floor());
window.draw(icon);
bottom_text.set_position((background.get_position() + mgl::vec2f(background.get_size().x * 0.5f - bottom_text.get_bounds().size.x * 0.5f, background.get_size().y - bottom_text.get_bounds().size.y - text_margin)).floor());
window.draw(bottom_text);
}
void GsrPage::draw_buttons(mgl::Window &window, mgl::vec2f body_pos, mgl::vec2f body_size) {

View File

@@ -0,0 +1,343 @@
#include "../../include/gui/ScreenshotSettingsPage.hpp"
#include "../../include/gui/GsrPage.hpp"
#include "../../include/gui/PageStack.hpp"
#include "../../include/Theme.hpp"
#include "../../include/GsrInfo.hpp"
#include "../../include/Utils.hpp"
#include "../../include/gui/List.hpp"
#include "../../include/gui/ScrollablePage.hpp"
#include "../../include/gui/Label.hpp"
#include "../../include/gui/Subsection.hpp"
#include "../../include/gui/FileChooser.hpp"
namespace gsr {
ScreenshotSettingsPage::ScreenshotSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack) :
StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()),
config(config),
gsr_info(gsr_info),
page_stack(page_stack)
{
capture_options = get_supported_capture_options(*gsr_info);
auto content_page = std::make_unique<GsrPage>("Screenshot", "Settings");
content_page->add_button("Back", "back", get_color_theme().page_bg_color);
content_page->on_click = [page_stack](const std::string &id) {
if(id == "back")
page_stack->pop();
};
content_page_ptr = content_page.get();
add_widget(std::move(content_page));
add_widgets();
load();
}
std::unique_ptr<ComboBox> ScreenshotSettingsPage::create_record_area_box() {
auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font);
// TODO: Show options not supported but disable them
// 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);
record_area_box->add_item(name, monitor.name);
}
if(capture_options.portal)
record_area_box->add_item("Desktop portal", "portal");
record_area_box_ptr = record_area_box.get();
return record_area_box;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_area() {
auto record_area_list = std::make_unique<List>(List::Orientation::VERTICAL);
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture target:", get_color_theme().text_color));
record_area_list->add_widget(create_record_area_box());
return record_area_list;
}
std::unique_ptr<List> ScreenshotSettingsPage::create_select_window() {
auto select_window_list = std::make_unique<List>(List::Orientation::VERTICAL);
select_window_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Select window:", get_color_theme().text_color));
select_window_list->add_widget(std::make_unique<Button>(&get_theme().body_font, "Click here to select a window...", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120)));
select_window_list_ptr = select_window_list.get();
return select_window_list;
}
std::unique_ptr<Entry> ScreenshotSettingsPage::create_image_width_entry() {
auto image_width_entry = std::make_unique<Entry>(&get_theme().body_font, "1920", get_theme().body_font.get_character_size() * 3);
image_width_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15);
image_width_entry_ptr = image_width_entry.get();
return image_width_entry;
}
std::unique_ptr<Entry> ScreenshotSettingsPage::create_image_height_entry() {
auto image_height_entry = std::make_unique<Entry>(&get_theme().body_font, "1080", get_theme().body_font.get_character_size() * 3);
image_height_entry->validate_handler = create_entry_validator_integer_in_range(1, 1 << 15);
image_height_entry_ptr = image_height_entry.get();
return image_height_entry;
}
std::unique_ptr<List> ScreenshotSettingsPage::create_image_resolution() {
auto area_size_params_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
area_size_params_list->add_widget(create_image_width_entry());
area_size_params_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "x", get_color_theme().text_color));
area_size_params_list->add_widget(create_image_height_entry());
return area_size_params_list;
}
std::unique_ptr<List> ScreenshotSettingsPage::create_image_resolution_section() {
auto image_resolution_list = std::make_unique<List>(List::Orientation::VERTICAL);
image_resolution_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image resolution limit:", get_color_theme().text_color));
image_resolution_list->add_widget(create_image_resolution());
image_resolution_list_ptr = image_resolution_list.get();
return image_resolution_list;
}
std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_restore_portal_session_checkbox() {
auto restore_portal_session_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Restore portal session");
restore_portal_session_checkbox->set_checked(true);
restore_portal_session_checkbox_ptr = restore_portal_session_checkbox.get();
return restore_portal_session_checkbox;
}
std::unique_ptr<List> ScreenshotSettingsPage::create_restore_portal_session_section() {
auto restore_portal_session_list = std::make_unique<List>(List::Orientation::VERTICAL);
restore_portal_session_list->add_widget(std::make_unique<Label>(&get_theme().body_font, " ", get_color_theme().text_color));
restore_portal_session_list->add_widget(create_restore_portal_session_checkbox());
restore_portal_session_list_ptr = restore_portal_session_list.get();
return restore_portal_session_list;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_change_image_resolution_section() {
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Change image resolution");
change_image_resolution_checkbox_ptr = checkbox.get();
return checkbox;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_capture_target_section() {
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
capture_target_list->add_widget(create_record_area());
capture_target_list->add_widget(create_select_window());
capture_target_list->add_widget(create_image_resolution_section());
capture_target_list->add_widget(create_restore_portal_session_section());
ll->add_widget(std::move(capture_target_list));
ll->add_widget(create_change_image_resolution_section());
return std::make_unique<Subsection>("Record area", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<List> ScreenshotSettingsPage::create_image_quality_section() {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image quality:", get_color_theme().text_color));
auto image_quality_box = std::make_unique<ComboBox>(&get_theme().body_font);
image_quality_box->add_item("Medium", "medium");
image_quality_box->add_item("High", "high");
image_quality_box->add_item("Very high (Recommended)", "very_high");
image_quality_box->add_item("Ultra", "ultra");
image_quality_box->set_selected_item("very_high");
image_quality_box_ptr = image_quality_box.get();
list->add_widget(std::move(image_quality_box));
return list;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_cursor_section() {
auto record_cursor_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record cursor");
record_cursor_checkbox->set_checked(true);
record_cursor_checkbox_ptr = record_cursor_checkbox.get();
return record_cursor_checkbox;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_image_section() {
auto image_section_list = std::make_unique<List>(List::Orientation::VERTICAL);
image_section_list->add_widget(create_image_quality_section());
image_section_list->add_widget(create_record_cursor_section());
return std::make_unique<Subsection>("Image", std::move(image_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<List> ScreenshotSettingsPage::create_save_directory(const char *label) {
auto save_directory_list = std::make_unique<List>(List::Orientation::VERTICAL);
save_directory_list->add_widget(std::make_unique<Label>(&get_theme().body_font, label, get_color_theme().text_color));
auto save_directory_button = std::make_unique<Button>(&get_theme().body_font, get_pictures_dir().c_str(), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
save_directory_button_ptr = save_directory_button.get();
save_directory_button->on_click = [this]() {
auto select_directory_page = std::make_unique<GsrPage>("File", "Settings");
select_directory_page->add_button("Save", "save", get_color_theme().tint_color);
select_directory_page->add_button("Cancel", "cancel", get_color_theme().page_bg_color);
auto file_chooser = std::make_unique<FileChooser>(save_directory_button_ptr->get_text().c_str(), select_directory_page->get_inner_size());
FileChooser *file_chooser_ptr = file_chooser.get();
select_directory_page->add_widget(std::move(file_chooser));
select_directory_page->on_click = [this, file_chooser_ptr](const std::string &id) {
if(id == "save") {
save_directory_button_ptr->set_text(file_chooser_ptr->get_current_directory());
page_stack->pop();
} else if(id == "cancel") {
page_stack->pop();
}
};
page_stack->push(std::move(select_directory_page));
};
save_directory_list->add_widget(std::move(save_directory_button));
return save_directory_list;
}
std::unique_ptr<ComboBox> ScreenshotSettingsPage::create_image_format_box() {
auto box = std::make_unique<ComboBox>(&get_theme().body_font);
if(gsr_info->supported_image_formats.jpeg)
box->add_item("jpg", "jpg");
if(gsr_info->supported_image_formats.png)
box->add_item("png", "png");
image_format_box_ptr = box.get();
return box;
}
std::unique_ptr<List> ScreenshotSettingsPage::create_image_format_section() {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image format:", get_color_theme().text_color));
list->add_widget(create_image_format_box());
return list;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_file_info_section() {
auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
file_info_data_list->add_widget(create_save_directory("Directory to save the screenshot:"));
file_info_data_list->add_widget(create_image_format_section());
return std::make_unique<Subsection>("File info", std::move(file_info_data_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_save_screenshot_in_game_folder() {
char text[256];
snprintf(text, sizeof(text), "Save screenshot in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)");
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text);
save_screenshot_in_game_folder_checkbox_ptr = checkbox.get();
return checkbox;
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_general_section() {
return std::make_unique<Subsection>("General", create_save_screenshot_in_game_folder(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_notifications_section() {
auto show_screenshot_saved_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show screenshot saved notification");
show_screenshot_saved_notification_checkbox->set_checked(true);
show_screenshot_saved_notification_checkbox_ptr = show_screenshot_saved_notification_checkbox.get();
return std::make_unique<Subsection>("Notifications", std::move(show_screenshot_saved_notification_checkbox), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_settings() {
auto page_list = std::make_unique<List>(List::Orientation::VERTICAL);
page_list->set_spacing(0.018f);
auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size() - mgl::vec2f(0.0f, page_list->get_size().y + 0.018f * get_theme().window_height));
settings_scrollable_page_ptr = scrollable_page.get();
page_list->add_widget(std::move(scrollable_page));
auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL);
settings_list->set_spacing(0.018f);
settings_list->add_widget(create_capture_target_section());
settings_list->add_widget(create_image_section());
settings_list->add_widget(create_file_info_section());
settings_list->add_widget(create_general_section());
settings_list->add_widget(create_notifications_section());
settings_scrollable_page_ptr->add_widget(std::move(settings_list));
return page_list;
}
void ScreenshotSettingsPage::add_widgets() {
content_page_ptr->add_widget(create_settings());
record_area_box_ptr->on_selection_changed = [this](const std::string &text, const std::string &id) {
(void)text;
const bool window_selected = id == "window";
const bool portal_selected = id == "portal";
select_window_list_ptr->set_visible(window_selected);
image_resolution_list_ptr->set_visible(change_image_resolution_checkbox_ptr->is_checked());
restore_portal_session_list_ptr->set_visible(portal_selected);
return true;
};
change_image_resolution_checkbox_ptr->on_changed = [this](bool checked) {
image_resolution_list_ptr->set_visible(checked);
};
if(!capture_options.monitors.empty())
record_area_box_ptr->set_selected_item(capture_options.monitors.front().name);
else if(capture_options.portal)
record_area_box_ptr->set_selected_item("portal");
else if(capture_options.window)
record_area_box_ptr->set_selected_item("window");
else
record_area_box_ptr->on_selection_changed("", "");
}
void ScreenshotSettingsPage::on_navigate_away_from_page() {
save();
}
void ScreenshotSettingsPage::load() {
record_area_box_ptr->set_selected_item(config.screenshot_config.record_area_option);
change_image_resolution_checkbox_ptr->set_checked(config.screenshot_config.change_image_resolution);
image_quality_box_ptr->set_selected_item(config.screenshot_config.image_quality);
image_format_box_ptr->set_selected_item(config.screenshot_config.image_format);
record_cursor_checkbox_ptr->set_checked(config.screenshot_config.record_cursor);
restore_portal_session_checkbox_ptr->set_checked(config.screenshot_config.restore_portal_session);
save_directory_button_ptr->set_text(config.screenshot_config.save_directory);
save_screenshot_in_game_folder_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_in_game_folder);
show_screenshot_saved_notification_checkbox_ptr->set_checked(config.screenshot_config.show_screenshot_saved_notifications);
if(config.screenshot_config.image_width == 0)
config.screenshot_config.image_width = 1920;
if(config.screenshot_config.image_height == 0)
config.screenshot_config.image_height = 1080;
if(config.screenshot_config.image_width < 32)
config.screenshot_config.image_width = 32;
image_width_entry_ptr->set_text(std::to_string(config.screenshot_config.image_width));
if(config.screenshot_config.image_height < 32)
config.screenshot_config.image_height = 32;
image_height_entry_ptr->set_text(std::to_string(config.screenshot_config.image_height));
}
void ScreenshotSettingsPage::save() {
config.screenshot_config.record_area_option = record_area_box_ptr->get_selected_id();
config.screenshot_config.image_width = atoi(image_width_entry_ptr->get_text().c_str());
config.screenshot_config.image_height = atoi(image_height_entry_ptr->get_text().c_str());
config.screenshot_config.change_image_resolution = change_image_resolution_checkbox_ptr->is_checked();
config.screenshot_config.image_quality = image_quality_box_ptr->get_selected_id();
config.screenshot_config.image_format = image_format_box_ptr->get_selected_id();
config.screenshot_config.record_cursor = record_cursor_checkbox_ptr->is_checked();
config.screenshot_config.restore_portal_session = restore_portal_session_checkbox_ptr->is_checked();
config.screenshot_config.save_directory = save_directory_button_ptr->get_text();
config.screenshot_config.save_screenshot_in_game_folder = save_screenshot_in_game_folder_checkbox_ptr->is_checked();
config.screenshot_config.show_screenshot_saved_notifications = show_screenshot_saved_notification_checkbox_ptr->is_checked();
if(config.screenshot_config.image_width == 0)
config.screenshot_config.image_width = 1920;
if(config.screenshot_config.image_height == 0)
config.screenshot_config.image_height = 1080;
if(config.screenshot_config.image_width < 32) {
config.screenshot_config.image_width = 32;
image_width_entry_ptr->set_text("32");
}
if(config.screenshot_config.image_height < 32) {
config.screenshot_config.image_height = 32;
image_height_entry_ptr->set_text("32");
}
save_config(config);
}
}

View File

@@ -8,11 +8,6 @@
#include "../../include/GsrInfo.hpp"
#include "../../include/Utils.hpp"
#include <mglpp/graphics/Rectangle.hpp>
#include <mglpp/graphics/Sprite.hpp>
#include <mglpp/graphics/Text.hpp>
#include <mglpp/window/Window.hpp>
#include <string.h>
namespace gsr {
@@ -22,6 +17,15 @@ namespace gsr {
APPLICATION_CUSTOM
};
static const char* settings_page_type_to_title_text(SettingsPage::Type type) {
switch(type) {
case SettingsPage::Type::REPLAY: return "Instant Replay";
case SettingsPage::Type::RECORD: return "Record";
case SettingsPage::Type::STREAM: return "Livestream";
}
return "";
}
SettingsPage::SettingsPage(Type type, const GsrInfo *gsr_info, Config &config, PageStack *page_stack) :
StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()),
type(type),
@@ -33,7 +37,7 @@ namespace gsr {
application_audio = get_application_audio();
capture_options = get_supported_capture_options(*gsr_info);
auto content_page = std::make_unique<GsrPage>();
auto content_page = std::make_unique<GsrPage>(settings_page_type_to_title_text(type), "Settings");
content_page->add_button("Back", "back", get_color_theme().page_bg_color);
content_page->on_click = [page_stack](const std::string &id) {
if(id == "back")
@@ -62,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) {
@@ -171,7 +177,7 @@ namespace gsr {
return checkbox;
}
std::unique_ptr<Widget> SettingsPage::create_capture_target() {
std::unique_ptr<Widget> SettingsPage::create_capture_target_section() {
auto ll = std::make_unique<List>(List::Orientation::VERTICAL);
auto capture_target_list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
@@ -347,14 +353,14 @@ namespace gsr {
list->add_widget(std::move(video_bitrate_entry));
if(type == Type::STREAM) {
auto size_mb_label = std::make_unique<Label>(&get_theme().body_font, "1.92MB", get_color_theme().text_color);
auto size_mb_label = std::make_unique<Label>(&get_theme().body_font, "1.64MB", get_color_theme().text_color);
Label *size_mb_label_ptr = size_mb_label.get();
list->add_widget(std::move(size_mb_label));
video_bitrate_entry_ptr->on_changed = [size_mb_label_ptr](const std::string &text) {
const double video_bitrate_mb_per_seconds = (double)atoi(text.c_str()) / 1000LL / 8LL * 1.024;
const double video_bitrate_mbits_per_seconds = (double)atoi(text.c_str()) / 1024.0;
char buffer[32];
snprintf(buffer, sizeof(buffer), "%.2fMB", video_bitrate_mb_per_seconds);
snprintf(buffer, sizeof(buffer), "%.2fMbps", video_bitrate_mbits_per_seconds);
size_mb_label_ptr->set_text(buffer);
};
}
@@ -512,7 +518,7 @@ namespace gsr {
auto settings_list = std::make_unique<List>(List::Orientation::VERTICAL);
settings_list->set_spacing(0.018f);
settings_list->add_widget(create_capture_target());
settings_list->add_widget(create_capture_target_section());
settings_list->add_widget(create_audio_section());
settings_list->add_widget(create_video_section());
settings_list_ptr = settings_list.get();
@@ -589,7 +595,7 @@ namespace gsr {
auto save_directory_button = std::make_unique<Button>(&get_theme().body_font, get_videos_dir().c_str(), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
save_directory_button_ptr = save_directory_button.get();
save_directory_button->on_click = [this]() {
auto select_directory_page = std::make_unique<GsrPage>();
auto select_directory_page = std::make_unique<GsrPage>("File", "Settings");
select_directory_page->add_button("Save", "save", get_color_theme().tint_color);
select_directory_page->add_button("Cancel", "cancel", get_color_theme().page_bg_color);
@@ -629,16 +635,24 @@ namespace gsr {
return container_list;
}
std::unique_ptr<Entry> SettingsPage::create_replay_time_entry() {
std::unique_ptr<List> SettingsPage::create_replay_time_entry() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
auto replay_time_entry = std::make_unique<Entry>(&get_theme().body_font, "60", get_theme().body_font.get_character_size() * 3);
replay_time_entry->validate_handler = create_entry_validator_integer_in_range(1, 1200);
replay_time_entry->validate_handler = create_entry_validator_integer_in_range(1, 10800);
replay_time_entry_ptr = replay_time_entry.get();
return replay_time_entry;
list->add_widget(std::move(replay_time_entry));
auto replay_time_label = std::make_unique<Label>(&get_theme().body_font, "00h:00m:00s", get_color_theme().text_color);
replay_time_label_ptr = replay_time_label.get();
list->add_widget(std::move(replay_time_label));
return list;
}
std::unique_ptr<List> SettingsPage::create_replay_time() {
auto replay_time_list = std::make_unique<List>(List::Orientation::VERTICAL);
replay_time_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Replay time in seconds:", get_color_theme().text_color));
replay_time_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Replay duration in seconds:", get_color_theme().text_color));
replay_time_list->add_widget(create_replay_time_entry());
return replay_time_list;
}
@@ -681,11 +695,25 @@ namespace gsr {
const int64_t video_bitrate_bps = atoi(video_bitrate_entry_ptr->get_text().c_str()) * 1000LL / 8LL;
const double video_filesize_mb = ((double)replay_time_seconds * (double)video_bitrate_bps) / 1000.0 / 1000.0 * 1.024;
char buffer[512];
snprintf(buffer, sizeof(buffer), "Estimated video max file size in RAM: %.2fMB", video_filesize_mb);
char buffer[256];
snprintf(buffer, sizeof(buffer), "Estimated video max file size in RAM: %.2fMB.\nChange video bitrate or replay duration to change file size.", video_filesize_mb);
estimated_file_size_ptr->set_text(buffer);
}
void SettingsPage::update_replay_time_text() {
int seconds = atoi(replay_time_entry_ptr->get_text().c_str());
const int hours = seconds / 60 / 60;
seconds -= (hours * 60 * 60);
const int minutes = seconds / 60;
seconds -= (minutes * 60);
char buffer[256];
snprintf(buffer, sizeof(buffer), "%02dh:%02dm:%02ds", hours, minutes, seconds);
replay_time_label_ptr->set_text(buffer);
}
void SettingsPage::add_replay_widgets() {
auto file_info_list = std::make_unique<List>(List::Orientation::VERTICAL);
auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
@@ -739,6 +767,7 @@ namespace gsr {
replay_time_entry_ptr->on_changed = [this](const std::string&) {
update_estimated_replay_file_size();
update_replay_time_text();
};
video_bitrate_entry_ptr->on_changed = [this](const std::string&) {
@@ -778,9 +807,7 @@ namespace gsr {
file_info_list->add_widget(create_estimated_record_file_size());
settings_list_ptr->add_widget(std::make_unique<Subsection>("File info", std::move(file_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto general_list = std::make_unique<List>(List::Orientation::VERTICAL);
general_list->add_widget(create_save_recording_in_game_folder());
settings_list_ptr->add_widget(std::make_unique<Subsection>("General", std::move(general_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
settings_list_ptr->add_widget(std::make_unique<Subsection>("General", create_save_recording_in_game_folder(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL);
@@ -1081,8 +1108,10 @@ namespace gsr {
save_directory_button_ptr->set_text(config.replay_config.save_directory);
container_box_ptr->set_selected_item(config.replay_config.container);
if(config.replay_config.replay_time < 5)
config.replay_config.replay_time = 5;
if(config.replay_config.replay_time < 2)
config.replay_config.replay_time = 2;
if(config.replay_config.replay_time > 10800)
config.replay_config.replay_time = 10800;
replay_time_entry_ptr->set_text(std::to_string(config.replay_config.replay_time));
}

View File

@@ -8,6 +8,7 @@
#include <signal.h>
#include <string.h>
#include <limits.h>
#include <malloc.h>
#include <mglpp/mglpp.hpp>
#include <mglpp/system/Clock.hpp>
@@ -72,6 +73,11 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->save_replay();
});
rpc->add_handler("take-screenshot", [overlay](const std::string &name) {
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
overlay->take_screenshot();
});
}
static bool is_gsr_ui_virtual_keyboard_running() {
@@ -150,6 +156,7 @@ enum class LaunchAction {
int main(int argc, char **argv) {
setlocale(LC_ALL, "C"); // Sigh... stupid C
mallopt(M_MMAP_THRESHOLD, 65536);
if(geteuid() == 0) {
fprintf(stderr, "Error: don't run gsr-ui as the root user\n");
@@ -182,6 +189,7 @@ int main(int argc, char **argv) {
// that uses pid sandboxing. Replace this with a better method once we no longer rely on linux global hotkeys on some platform.
// TODO: This method doesn't work when disabling hotkeys and the method below with pidof gsr-ui doesn't work in flatpak.
// What do? creating a pid file doesn't work in flatpak either.
// TODO: This doesn't work in flatpak when disabling hotkeys.
if(is_gsr_ui_virtual_keyboard_running() || gsr::pidof("gsr-ui", getpid()) != -1) {
gsr::Rpc rpc;
if(rpc.open("gsr-ui") && rpc.write("show_ui\n", 8)) {

View File

@@ -0,0 +1,21 @@
# About
Global hotkeys for X11 and all Wayland compositors by using linux device api. Keyboards are grabbed and only the non-hotkey keys are passed through to the system.
The program accepts text commands as input. Run the program with the option `--virtual` to only grab virtual devices. This is useful when using keyboard input mapping software such as
kanata, otherwise kanata may fail to launch or this program may fail to launch.
# Commands
## Bind
To add a key send `bind <action> <keycode+keycode+...><newline>` to the programs stdin, for example:
```
bind show_hide 56+44
```
which will bind alt+z. When alt+z is pressed the program will output `show_hide` (and a newline) to stdout.
The program only accepts one key for each keybind command but accepts a multiple modifier keys.
The keybinding requires at least one modifier key (ctrl, alt, super or shift) and a key to be used.
The keycodes are values from `<linux/input-event-codes.h>` linux api header (which is the same as X11 keycode value minus 8).
## Unbind
To unbind all keys send `unbind_all<newline>` to the programs stdin, for example:
```
unbind_all
```

View File

@@ -65,9 +65,10 @@ static void hotplug_event_parse_netlink_data(hotplug_event *self, const char *li
/* Netlink uevent structure is documented here: https://web.archive.org/web/20160127215232/https://www.kernel.org/doc/pending/hotplug.txt */
void hotplug_event_process_event_data(hotplug_event *self, int fd, hotplug_device_added_callback callback, void *userdata) {
const int bytes_read = read(fd, self->event_data, sizeof(self->event_data));
const int bytes_read = read(fd, self->event_data, sizeof(self->event_data) - 1);
if(bytes_read <= 0)
return;
self->event_data[bytes_read] = '\0';
/* Hotplug data ends with a newline and a null terminator */
int data_index = 0;

View File

@@ -294,7 +294,11 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
unsigned char key_bits[KEY_MAX/8 + 1] = {0};
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(key_bits)), &key_bits);
const bool supports_key_events = key_bits[KEY_A/8] & (1 << (KEY_A % 8));
const bool supports_key_a = key_bits[KEY_A/8] & (1 << (KEY_A % 8));
const bool supports_key_esc = key_bits[KEY_ESC/8] & (1 << (KEY_ESC % 8));
const bool supports_key_volume_up = key_bits[KEY_VOLUMEUP/8] & (1 << (KEY_VOLUMEUP % 8));
const bool supports_key_events = supports_key_a || supports_key_esc || supports_key_volume_up;
const bool supports_mouse_events = key_bits[BTN_MOUSE/8] & (1 << (BTN_MOUSE % 8));
//const bool supports_touch_events = key_bits[BTN_TOUCH/8] & (1 << (BTN_TOUCH % 8));
const bool supports_joystick_events = key_bits[BTN_JOYSTICK/8] & (1 << (BTN_JOYSTICK % 8));
@@ -523,6 +527,7 @@ void keyboard_event_deinit(keyboard_event *self) {
self->num_global_hotkeys = 0;
if(self->uinput_fd > 0) {
ioctl(self->uinput_fd, UI_DEV_DESTROY);
close(self->uinput_fd);
self->uinput_fd = -1;
}

View File

@@ -50,6 +50,7 @@ static void usage(void) {
printf(" toggle-stream Start/stop streaming.\n");
printf(" toggle-replay Start/stop replay.\n");
printf(" replay-save Save replay.\n");
printf(" take-screenshot Take a screenshot.\n");
printf("\n");
printf("EXAMPLES:\n");
printf(" gsr-ui-cli toggle-show\n");
@@ -65,6 +66,7 @@ static bool is_valid_command(const char *command) {
"toggle-stream",
"toggle-replay",
"replay-save",
"take-screenshot",
NULL
};