Compare commits

..

23 Commits
1.7.2 ... 1.7.7

Author SHA1 Message Date
dec05eba
5ee2b95384 Workaround amd driver bug: kill notifications with SIGINT instead of SIGKILL 2025-09-24 18:38:22 +02:00
dec05eba
d610a980f8 Update flatpak version reference 2025-09-23 19:43:43 +02:00
dec05eba
70780ae14e 1.7.6 2025-09-21 03:32:19 +02:00
dec05eba
5f7cb94f4e Only do override-redirect on wayland if the focused x11 application is fullscreen (fixes input focus issue on cosmic when clicking on a window behind the overlay 2025-09-19 14:14:46 +02:00
dec05eba
748c51e2b6 Reorder README.md 2025-09-17 18:02:30 +02:00
dec05eba
3ba9ce771b Update flatpak version reference 2025-09-10 21:36:19 +02:00
dec05eba
c18b062180 README 2025-09-09 16:45:59 +02:00
dec05eba
705da21363 README 2025-09-09 16:43:25 +02:00
dec05eba
609a3e54fd 1.7.5 2025-09-06 19:16:09 +02:00
dec05eba
4e62d12e8c Allow 'sync to content' framerate mode option on wayland (only desktop portal) 2025-09-06 01:58:28 +02:00
dec05eba
b4e003c8f7 Only use mallopt M_MMAP_THRESHOLD if glibc 2025-09-03 18:21:58 +02:00
dec05eba
9efe9d3c91 Add content framerate mode (for x11) 2025-09-01 18:11:16 +02:00
dec05eba
ef4a0fe7cb Update flatpak version reference 2025-08-25 22:30:10 +02:00
dec05eba
dacf6126bf Screenshot: add option to save screenshot to clipboard 2025-08-25 22:26:54 +02:00
dec05eba
9bbec944de GlobalSettings: Add notification speed setting, change recording start notification speed 2025-08-24 22:12:34 +02:00
dec05eba
6a55338b12 Entry: update selection caret when changing masked state 2025-08-07 21:20:05 +02:00
dec05eba
2d3abace0e Fix build 2025-08-07 20:47:14 +02:00
dec05eba
47c02fc6c8 1.7.3 2025-08-07 20:22:20 +02:00
dec05eba
5f8c366b43 Show video codec error messages from gsr
let gsr choose video codec automatically for us when using auto (prefer h264, then hevc and then av1),
fallback to software encoding in gsr ui if none of them are available.
2025-08-07 20:21:22 +02:00
dec05eba
f4ed622510 Entry: add more delimiter for moving 2025-08-07 02:51:55 +02:00
dec05eba
f1ee19d014 Mask stream keys, add button to unmask it 2025-08-07 02:00:35 +02:00
dec05eba
67a8040e57 Entry: use text32 (utf32) instead of text (utf8). This simplifies text editing and other features such as text masking (password) 2025-08-07 00:13:59 +02:00
dec05eba
ff00be30df Entry: implement moving care by word with ctrl+arrow keys 2025-08-06 14:54:25 +02:00
28 changed files with 787 additions and 183 deletions

View File

@@ -4,18 +4,18 @@
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.
# Usage
You can start the overlay UI and make it start automatically on system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
Alternatively you can run `gsr-ui` and go into settings and enable start on system startup setting.\
Press `Left Alt+Z` to show/hide the UI. Go into settings to view all of the different hotkeys configured.\
If you use a non-systemd distro and want to start the UI on system startup then you have to manually add `gsr-ui` to your system startup script.\
A program called `gsr-ui-cli` is also installed when installing this software. This can be used to remotely control the UI. Run `gsr-ui-cli --help` to list the available commands.
# 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 from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder) which includes this UI.
# Usage
Press `Left Alt+Z` to show/hide the UI. Go into settings (the icon on the right) to view all of the different hotkeys configured.\
You can start the overlay UI and make it start automatically on system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
Alternatively you can run `gsr-ui` and go into settings and enable start on system startup setting.\
If you use a non-systemd distro and want to start the UI on system startup then you have to manually add `gsr-ui launch-daemon` to your system startup script.\
A program called `gsr-ui-cli` is also installed when installing this software. This can be used to remotely control the UI. Run `gsr-ui-cli --help` to list the available commands.
# Dependencies
GPU Screen Recorder UI uses meson build system so you need to install `meson` to build GPU Screen Recorder UI.

9
TODO
View File

@@ -25,8 +25,6 @@ Have different modes. Overlay, window and side menu. Overlay can be used on x11,
Show navigation breadcrumbs for settings and deeper navigation (such as selecting a directory to save videos).
Add option to hide stream key like a password input.
Add global setting. In that setting there should be an option to enable/disable gsr-ui from system startup (the systemd service).
Add profiles and hotkey to switch between profiles (show notification when switching profile).
@@ -173,8 +171,6 @@ Add a bug report page that automatically includes system info (make this clear t
Make it possible to change controller hotkeys. Also read from /dev/input/eventN instead of /dev/input/jsN. This is readable for controllers.
Add option to copy screenshot to clipboard. Does it work properly on Wayland compositors? Maybe need to wait until the application becomes Wayland native instead of XWayland.
Show message that replay/streaming has to be restarted if recording settings are changed while replay/streaming is ongoing.
Support vector graphics. Maybe support svg, rendering it to a texture for better performance.
@@ -210,3 +206,8 @@ Support localization.
Add option to not capture cursor in screenshot when doing region/window capture.
Window selection doesn't work when a window is fullscreen on x11.
Make it possible to change replay duration of the "save 1 min" and "save 10 min" by adding them to the replay settings as options.
If replay duration is set below the "save 1 min" or "save 10 min" then gray them out and when hovering over those buttons
show a tooltip that says that those buttons cant be used because the replay duration in replay settings is set to a lower value than that (and display the replay duration there).

BIN
images/masked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

BIN
images/unmasked.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

60
include/ClipboardFile.hpp Normal file
View File

@@ -0,0 +1,60 @@
#pragma once
#include <string>
#include <thread>
#include <mutex>
#include <vector>
#include <X11/Xlib.h>
namespace gsr {
struct ClipboardCopy {
Window requestor = None;
uint64_t file_offset = 0;
Atom property = None;
Atom requestor_target = None;
};
class ClipboardFile {
public:
enum class FileType {
JPG,
PNG
};
ClipboardFile();
~ClipboardFile();
ClipboardFile(const ClipboardFile&) = delete;
ClipboardFile& operator=(const ClipboardFile&) = delete;
// Set this to an empty string to unset clipboard
void set_current_file(const std::string &filepath, FileType file_type);
private:
bool file_type_matches_request_atom(FileType file_type, Atom request_atom);
const char* file_type_clipboard_get_name(Atom request_atom);
const char* file_type_get_name(FileType file_type);
void send_clipboard_start(XSelectionRequestEvent *xselectionrequest);
void transfer_clipboard_data(XSelectionRequestEvent *xselectionrequest, ClipboardCopy *clipboard_copy);
ClipboardCopy* get_clipboard_copy_by_requestor(Window requestor);
void remove_clipboard_copy(Window requestor);
private:
Display *dpy = nullptr;
Window clipboard_window = None;
int file_fd = -1;
uint64_t file_size = 0;
FileType file_type = FileType::JPG;
Atom incr_atom = None;
Atom targets_atom = None;
Atom clipboard_atom = None;
Atom image_jpg_atom = None;
Atom image_jpeg_atom = None;
Atom image_png_atom = None;
std::thread event_thread;
std::mutex mutex;
bool running = true;
std::vector<ClipboardCopy> clipboard_copies;
bool should_clear_selection = false;
};
}

View File

@@ -68,6 +68,7 @@ namespace gsr {
std::string hotkeys_enable_option = "enable_hotkeys";
std::string joystick_hotkeys_enable_option = "disable_hotkeys";
std::string tint_color;
std::string notification_speed = "normal";
ConfigHotkey show_hide_hotkey;
};
@@ -142,6 +143,7 @@ namespace gsr {
bool restore_portal_session = true;
bool save_screenshot_in_game_folder = false;
bool save_screenshot_to_clipboard = false;
bool show_screenshot_saved_notifications = true;
std::string save_directory;
ConfigHotkey take_screenshot_hotkey;

View File

@@ -10,6 +10,7 @@
#include "AudioPlayer.hpp"
#include "RegionSelector.hpp"
#include "WindowSelector.hpp"
#include "ClipboardFile.hpp"
#include "CursorTracker/CursorTracker.hpp"
#include <mglpp/window/Window.hpp>
@@ -47,6 +48,11 @@ namespace gsr {
WINDOW
};
enum class NotificationSpeed {
NORMAL,
FAST
};
class Overlay {
public:
Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs);
@@ -81,6 +87,8 @@ namespace gsr {
void unbind_all_keyboard_hotkeys();
void rebind_all_keyboard_hotkeys();
void set_notification_speed(NotificationSpeed notification_speed);
private:
void handle_keyboard_mapping_event();
void on_event(mgl::Event &event);
@@ -245,5 +253,7 @@ namespace gsr {
mgl::Clock cursor_tracker_update_clock;
bool hide_ui = false;
double notification_duration_multiplier = 1.0;
ClipboardFile clipboard_file;
};
}

View File

@@ -44,6 +44,8 @@ namespace gsr {
mgl::Texture save_texture;
mgl::Texture screenshot_texture;
mgl::Texture trash_texture;
mgl::Texture masked_texture;
mgl::Texture unmasked_texture;
mgl::Texture ps4_home_texture;
mgl::Texture ps4_options_texture;

View File

@@ -19,7 +19,9 @@ namespace gsr {
void draw(mgl::Window &window, mgl::vec2f offset) override;
void add_item(const std::string &text, const std::string &id);
// The item can only be selected if it's enabled
void set_selected_item(const std::string &id, bool trigger_event = true, bool trigger_event_even_if_selection_not_changed = true);
void set_item_enabled(const std::string &id, bool enabled);
const std::string& get_selected_id() const;
mgl::vec2f get_size() override;
@@ -36,6 +38,7 @@ namespace gsr {
mgl::Text text;
std::string id;
mgl::vec2f position;
bool enabled = true;
};
mgl::vec2f max_size;

View File

@@ -4,7 +4,7 @@
#include <functional>
#include <mglpp/graphics/Color.hpp>
#include <mglpp/graphics/Text.hpp>
#include <mglpp/graphics/Text32.hpp>
#include <mglpp/graphics/Rectangle.hpp>
namespace gsr {
@@ -15,10 +15,20 @@ namespace gsr {
ALLOW,
REPLACED
};
using EntryValidateHandler = std::function<EntryValidateHandlerResult(Entry &entry, const std::string &str)>;
using EntryValidateHandler = std::function<EntryValidateHandlerResult(Entry &entry, const std::u32string &str)>;
struct CaretIndexPos {
int index;
mgl::vec2f pos;
};
class Entry : public Widget {
public:
enum class Direction {
LEFT,
RIGHT
};
Entry(mgl::Font *font, const char *text, float max_width);
Entry(const Entry&) = delete;
Entry& operator=(const Entry&) = delete;
@@ -28,11 +38,11 @@ namespace gsr {
mgl::vec2f get_size() override;
EntryValidateHandlerResult set_text(std::string str);
const std::string& get_text() const;
EntryValidateHandlerResult set_text(const std::string &str);
std::string get_text() const;
// Also updates the cursor position
void replace_text(size_t index, size_t size, const std::string &replacement);
void set_masked(bool masked);
bool is_masked() const;
// Return false to specify that the string should not be accepted. This reverts the string back to its previous value.
// The input can be changed by changing the input parameter and returning true.
@@ -40,24 +50,28 @@ namespace gsr {
std::function<void(const std::string &text)> on_changed;
private:
EntryValidateHandlerResult set_text_internal(std::string str);
// Also updates the cursor position
void replace_text(size_t index, size_t size, const std::u32string &replacement);
void move_caret_word(Direction direction, size_t max_codepoints);
EntryValidateHandlerResult set_text_internal(std::u32string str);
void draw_caret(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size);
void draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size);
mgl_index_codepoint_pair find_closest_caret_index_by_position(mgl::vec2f position);
CaretIndexPos find_closest_caret_index_by_position(mgl::vec2f position);
private:
struct Caret {
float offset_x = 0.0f;
int utf8_index = 0;
int byte_index = 0;
int index = 0;
};
mgl::Rectangle background;
mgl::Text text;
mgl::Text32 text;
mgl::Text32 masked_text;
float max_width;
bool selected = false;
bool selecting_text = false;
bool selecting_with_keyboard = false;
bool show_selection = false;
bool masked = false;
Caret caret;
Caret selection_start_caret;
float text_overflow = 0.0f;

View File

@@ -70,6 +70,7 @@ namespace gsr {
std::unique_ptr<Subsection> create_controller_hotkey_subsection(ScrollablePage *parent_page);
std::unique_ptr<Button> create_exit_program_button();
std::unique_ptr<Button> create_go_back_to_old_ui_button();
std::unique_ptr<List> create_notification_speed();
std::unique_ptr<Subsection> create_application_options_subsection(ScrollablePage *parent_page);
std::unique_ptr<Subsection> create_application_info_subsection(ScrollablePage *parent_page);
void add_widgets();
@@ -103,6 +104,7 @@ namespace gsr {
Button *take_screenshot_region_button_ptr = nullptr;
Button *take_screenshot_window_button_ptr = nullptr;
Button *show_hide_button_ptr = nullptr;
RadioButton *notification_speed_button_ptr = nullptr;
ConfigHotkey configure_config_hotkey;
ConfigureHotkeyType configure_hotkey_type = ConfigureHotkeyType::NONE;

View File

@@ -42,6 +42,7 @@ namespace gsr {
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<CheckBox> create_save_screenshot_to_clipboard();
std::unique_ptr<Widget> create_general_section();
std::unique_ptr<Widget> create_notifications_section();
std::unique_ptr<Widget> create_settings();
@@ -69,6 +70,7 @@ namespace gsr {
ComboBox *image_format_box_ptr = nullptr;
Button *save_directory_button_ptr = nullptr;
CheckBox *save_screenshot_in_game_folder_checkbox_ptr = nullptr;
CheckBox *save_screenshot_to_clipboard_checkbox_ptr = nullptr;
CheckBox *show_screenshot_saved_notification_checkbox_ptr = nullptr;
PageStack *page_stack = nullptr;

View File

@@ -118,9 +118,11 @@ namespace gsr {
std::unique_ptr<ComboBox> create_streaming_service_box();
std::unique_ptr<List> create_streaming_service_section();
std::unique_ptr<List> create_stream_key_section();
std::unique_ptr<List> create_stream_custom_url();
std::unique_ptr<List> create_stream_custom_key();
std::unique_ptr<List> create_stream_custom_section();
std::unique_ptr<ComboBox> create_stream_container_box();
std::unique_ptr<List> create_stream_container_section();
std::unique_ptr<List> create_stream_container();
void add_stream_widgets();
void load_audio_tracks(const RecordOptions &record_options);
@@ -173,8 +175,7 @@ namespace gsr {
ComboBox *container_box_ptr = nullptr;
ComboBox *streaming_service_box_ptr = nullptr;
List *stream_key_list_ptr = nullptr;
List *stream_url_list_ptr = nullptr;
List *container_list_ptr = nullptr;
List *custom_stream_list_ptr = nullptr;
CheckBox *save_replay_in_game_folder_ptr = nullptr;
CheckBox *restart_replay_on_save = nullptr;
Label *estimated_file_size_ptr = nullptr;

View File

@@ -45,6 +45,8 @@ namespace gsr {
void set_visible(bool visible);
Widget* get_parent_widget();
void *userdata = nullptr;
protected:
void set_widget_as_selected_in_parent();

View File

@@ -1,4 +1,6 @@
project('gsr-ui', ['c', 'cpp'], version : '1.7.2', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
project('gsr-ui', ['c', 'cpp'], version : '1.7.7', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
add_project_arguments('-D_FILE_OFFSET_BITS=64', language : ['c', 'cpp'])
if get_option('buildtype') == 'debug'
add_project_arguments('-g3', language : ['c', 'cpp'])
@@ -47,6 +49,7 @@ src = [
'src/Overlay.cpp',
'src/AudioPlayer.cpp',
'src/Hotplug.cpp',
'src/ClipboardFile.cpp',
'src/Rpc.cpp',
'src/main.cpp',
]
@@ -62,7 +65,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.7.5"', language: ['c', 'cpp'])
add_project_arguments('-DGSR_FLATPAK_VERSION="5.8.2"', language: ['c', 'cpp'])
executable(
meson.project_name(),

View File

@@ -1,7 +1,7 @@
[package]
name = "gsr-ui"
type = "executable"
version = "1.7.2"
version = "1.7.7"
platforms = ["posix"]
[lang.cpp]
@@ -10,6 +10,9 @@ version = "c++17"
[config]
ignore_dirs = ["build", "tools"]
[define]
_FILE_OFFSET_BITS = "64"
[dependencies]
xcomposite = ">=0"
xfixes = ">=0"

294
src/ClipboardFile.cpp Normal file
View File

@@ -0,0 +1,294 @@
#include "../include/ClipboardFile.hpp"
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <inttypes.h>
#include <poll.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <X11/Xatom.h>
#define FORMAT_I64 "%" PRIi64
#define FORMAT_U64 "%" PRIu64
namespace gsr {
ClipboardFile::ClipboardFile() {
dpy = XOpenDisplay(nullptr);
if(!dpy) {
fprintf(stderr, "gsr ui: error: ClipboardFile: failed to connect to the X11 server\n");
return;
}
clipboard_window = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 8, 8, 0, 0, 0);
if(!clipboard_window) {
fprintf(stderr, "gsr ui: error: ClipboardFile: failed to create clipboard window\n");
XCloseDisplay(dpy);
dpy = nullptr;
return;
}
incr_atom = XInternAtom(dpy, "INCR", False);
targets_atom = XInternAtom(dpy, "TARGETS", False);
clipboard_atom = XInternAtom(dpy, "CLIPBOARD", False);
image_jpg_atom = XInternAtom(dpy, "image/jpg", False);
image_jpeg_atom = XInternAtom(dpy, "image/jpeg", False);
image_png_atom = XInternAtom(dpy, "image/png", False);
event_thread = std::thread([&]() {
pollfd poll_fds[1];
poll_fds[0].fd = ConnectionNumber(dpy);
poll_fds[0].events = POLLIN;
poll_fds[0].revents = 0;
XEvent xev;
while(running) {
poll(poll_fds, 1, 100);
while(XPending(dpy)) {
XNextEvent(dpy, &xev);
switch(xev.type) {
case SelectionClear: {
should_clear_selection = true;
if(clipboard_copies.empty()) {
should_clear_selection = false;
set_current_file("", file_type);
}
break;
}
case SelectionRequest:
send_clipboard_start(&xev.xselectionrequest);
break;
case PropertyNotify: {
if(xev.xproperty.state == PropertyDelete) {
std::lock_guard<std::mutex> lock(mutex);
ClipboardCopy *clipboard_copy = get_clipboard_copy_by_requestor(xev.xproperty.window);
if(!clipboard_copy || xev.xproperty.atom != clipboard_copy->property)
return;
XSelectionRequestEvent xselectionrequest;
xselectionrequest.display = xev.xproperty.display;;
xselectionrequest.requestor = xev.xproperty.window;
xselectionrequest.selection = clipboard_atom;
xselectionrequest.target = clipboard_copy->requestor_target;
xselectionrequest.property = clipboard_copy->property;
xselectionrequest.time = xev.xproperty.time;
transfer_clipboard_data(&xselectionrequest, clipboard_copy);
}
break;
}
}
}
}
});
}
ClipboardFile::~ClipboardFile() {
running = false;
if(event_thread.joinable())
event_thread.join();
if(file_fd > 0)
close(file_fd);
if(dpy) {
XDestroyWindow(dpy, clipboard_window);
XCloseDisplay(dpy);
}
}
bool ClipboardFile::file_type_matches_request_atom(FileType file_type, Atom request_atom) {
switch(file_type) {
case FileType::JPG:
return request_atom == image_jpg_atom || request_atom == image_jpeg_atom;
case FileType::PNG:
return request_atom == image_png_atom;
}
return false;
}
const char* ClipboardFile::file_type_clipboard_get_name(Atom request_atom) {
if(request_atom == image_jpg_atom)
return "image/jpg";
else if(request_atom == image_jpeg_atom)
return "image/jpeg";
else if(request_atom == image_png_atom)
return "image/png";
return "Unknown";
}
const char* ClipboardFile::file_type_get_name(FileType file_type) {
switch(file_type) {
case FileType::JPG:
return "image/jpeg";
case FileType::PNG:
return "image/png";
}
return "Unknown";
}
void ClipboardFile::send_clipboard_start(XSelectionRequestEvent *xselectionrequest) {
std::lock_guard<std::mutex> lock(mutex);
if(file_fd <= 0) {
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to get clipboard from us but we don't have any clipboard file open\n", (int64_t)xselectionrequest->requestor);
return;
}
if(xselectionrequest->selection != clipboard_atom) {
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to non-clipboard selection from us\n", (int64_t)xselectionrequest->requestor);
return;
}
XSelectionEvent selection_event;
selection_event.type = SelectionNotify;
selection_event.display = xselectionrequest->display;
selection_event.requestor = xselectionrequest->requestor;
selection_event.selection = xselectionrequest->selection;
selection_event.property = xselectionrequest->property;
selection_event.time = xselectionrequest->time;
selection_event.target = xselectionrequest->target;
if(xselectionrequest->target == targets_atom) {
int num_targets = 1;
Atom targets[3];
targets[0] = targets_atom;
switch(file_type) {
case FileType::JPG:
num_targets = 3;
targets[1] = image_jpg_atom;
targets[2] = image_jpeg_atom;
break;
case FileType::PNG:
num_targets = 2;
targets[1] = image_png_atom;
break;
}
XChangeProperty(dpy, selection_event.requestor, selection_event.property, XA_ATOM, 32, PropModeReplace, (unsigned char*)targets, num_targets);
} else if(xselectionrequest->target == image_jpg_atom || xselectionrequest->target == image_jpeg_atom || xselectionrequest->target == image_png_atom) {
if(!file_type_matches_request_atom(file_type, xselectionrequest->target)) {
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to request clipboard of type %s, but %s was expected\n", (int64_t)xselectionrequest->requestor, file_type_clipboard_get_name(xselectionrequest->target), file_type_get_name(file_type));
return;
}
ClipboardCopy *clipboard_copy = get_clipboard_copy_by_requestor(xselectionrequest->requestor);
if(!clipboard_copy) {
clipboard_copies.push_back({ xselectionrequest->requestor, 0 });
clipboard_copy = &clipboard_copies.back();
}
*clipboard_copy = { xselectionrequest->requestor, 0 };
clipboard_copy->property = selection_event.property;
clipboard_copy->requestor_target = selection_event.target;
XSelectInput(dpy, selection_event.requestor, PropertyChangeMask);
const long lower_bound = std::min((uint64_t)1<<16, file_size);
XChangeProperty(dpy, selection_event.requestor, selection_event.property, incr_atom, 32, PropModeReplace, (const unsigned char*)&lower_bound, 1);
} else {
char *target_clipboard_name = XGetAtomName(dpy, xselectionrequest->target);
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to request clipboard of type %s, expected TARGETS, image/jpg, image/jpeg or image/png\n", (int64_t)xselectionrequest->requestor, target_clipboard_name ? target_clipboard_name : "Unknown");
if(target_clipboard_name)
XFree(target_clipboard_name);
selection_event.property = None;
}
XSendEvent(dpy, selection_event.requestor, False, NoEventMask, (XEvent*)&selection_event);
XFlush(dpy);
}
void ClipboardFile::transfer_clipboard_data(XSelectionRequestEvent *xselectionrequest, ClipboardCopy *clipboard_copy) {
uint8_t file_buffer[1<<16];
ssize_t file_bytes_read = 0;
if(lseek(file_fd, clipboard_copy->file_offset, SEEK_SET) == -1) {
fprintf(stderr, "gsr ui: error: ClipboardFile::send_clipboard: failed to seek in clipboard file to offset " FORMAT_U64 " for requestor window " FORMAT_I64 ", error: %s\n", (uint64_t)clipboard_copy->file_offset, (int64_t)xselectionrequest->requestor, strerror(errno));
clipboard_copy->file_offset = 0;
// TODO: Cancel transfer
return;
}
file_bytes_read = read(file_fd, file_buffer, sizeof(file_buffer));
if(file_bytes_read < 0) {
fprintf(stderr, "gsr ui: error: ClipbaordFile::send_clipboard: failed to read data from offset " FORMAT_U64 " for requestor window " FORMAT_I64 ", error: %s\n", (uint64_t)clipboard_copy->file_offset, (int64_t)xselectionrequest->requestor, strerror(errno));
clipboard_copy->file_offset = 0;
// TODO: Cancel transfer
return;
}
XChangeProperty(dpy, xselectionrequest->requestor, xselectionrequest->property, xselectionrequest->target, 8, PropModeReplace, (const unsigned char*)file_buffer, file_bytes_read);
XSendEvent(dpy, xselectionrequest->requestor, False, NoEventMask, (XEvent*)xselectionrequest);
XFlush(dpy);
clipboard_copy->file_offset += file_bytes_read;
if(file_bytes_read == 0)
remove_clipboard_copy(clipboard_copy->requestor);
}
ClipboardCopy* ClipboardFile::get_clipboard_copy_by_requestor(Window requestor) {
for(ClipboardCopy &clipboard_copy : clipboard_copies) {
if(clipboard_copy.requestor == requestor)
return &clipboard_copy;
}
return nullptr;
}
void ClipboardFile::remove_clipboard_copy(Window requestor) {
for(auto it = clipboard_copies.begin(), end = clipboard_copies.end(); it != end; ++it) {
if(it->requestor == requestor) {
clipboard_copies.erase(it);
XSelectInput(dpy, requestor, 0);
if(clipboard_copies.empty() && should_clear_selection) {
should_clear_selection = false;
set_current_file("", file_type);
}
return;
}
}
}
void ClipboardFile::set_current_file(const std::string &filepath, FileType file_type) {
if(!dpy)
return;
std::lock_guard<std::mutex> lock(mutex);
for(ClipboardCopy &clipboard_copy : clipboard_copies) {
XSelectInput(dpy, clipboard_copy.requestor, 0);
}
clipboard_copies.clear();
if(filepath.empty()) {
if(file_fd > 0) {
close(file_fd);
file_fd = -1;
}
file_size = 0;
return;
}
if(file_fd > 0) {
close(file_fd);
file_fd = -1;
file_size = 0;
}
file_fd = open(filepath.c_str(), O_RDONLY);
if(file_fd <= 0) {
fprintf(stderr, "gsr ui: error: ClipboardFile::set_current_file: failed to open file %s, error: %s\n", filepath.c_str(), strerror(errno));
return;
}
struct stat64 stat;
if(fstat64(file_fd, &stat) == -1) {
fprintf(stderr, "gsr ui: error: ClipboardFile::set_current_file: failed to get file size for file %s, error: %s\n", filepath.c_str(), strerror(errno));
close(file_fd);
file_fd = -1;
return;
}
file_size = stat.st_size;
this->file_type = file_type;
XSetSelectionOwner(dpy, clipboard_atom, clipboard_window, CurrentTime);
XFlush(dpy);
}
}

View File

@@ -174,6 +174,7 @@ namespace gsr {
{"main.hotkeys_enable_option", &config.main_config.hotkeys_enable_option},
{"main.joystick_hotkeys_enable_option", &config.main_config.joystick_hotkeys_enable_option},
{"main.tint_color", &config.main_config.tint_color},
{"main.notification_speed", &config.main_config.notification_speed},
{"main.show_hide_hotkey", &config.main_config.show_hide_hotkey},
{"streaming.record_options.record_area_option", &config.streaming_config.record_options.record_area_option},
@@ -283,6 +284,7 @@ namespace gsr {
{"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.save_screenshot_to_clipboard", &config.screenshot_config.save_screenshot_to_clipboard},
{"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},

View File

@@ -50,6 +50,7 @@ namespace gsr {
static const double force_window_on_top_timeout_seconds = 1.0;
static const double replay_status_update_check_timeout_seconds = 1.5;
static const double replay_saving_notification_timeout_seconds = 0.5;
static const double short_notification_timeout_seconds = 2.0;
static const double notification_timeout_seconds = 3.0;
static const double notification_error_timeout_seconds = 5.0;
static const double cursor_tracker_update_timeout_sec = 0.1;
@@ -446,6 +447,17 @@ namespace gsr {
return global_hotkeys_js;
}
static NotificationSpeed to_notification_speed(const std::string &notification_speed_str) {
if(notification_speed_str == "normal")
return NotificationSpeed::NORMAL;
else if(notification_speed_str == "fast")
return NotificationSpeed::FAST;
else {
assert(false);
return NotificationSpeed::NORMAL;
}
}
Overlay::Overlay(std::string resources_path, GsrInfo gsr_info, SupportedCaptureOptions capture_options, egl_functions egl_funcs) :
resources_path(std::move(resources_path)),
gsr_info(std::move(gsr_info)),
@@ -474,6 +486,7 @@ namespace gsr {
power_supply_online_filepath = get_power_supply_online_filepath();
replay_startup_mode = replay_startup_string_to_type(config.replay_config.turn_on_replay_automatically_mode.c_str());
set_notification_speed(to_notification_speed(config.main_config.notification_speed));
if(config.main_config.hotkeys_enable_option == "enable_hotkeys")
global_hotkeys = register_linux_hotkeys(this, GlobalHotkeysLinux::GrabType::ALL);
@@ -499,7 +512,7 @@ namespace gsr {
hide();
if(notification_process > 0) {
kill(notification_process, SIGKILL);
kill(notification_process, SIGINT);
int status;
if(waitpid(notification_process, &status, 0) == -1) {
perror("waitpid failed");
@@ -946,6 +959,7 @@ namespace gsr {
const bool is_kwin = wm_name == "KWin";
const bool is_wlroots = wm_name.find("wlroots") != std::string::npos;
const bool is_hyprland = wm_name.find("Hyprland") != std::string::npos;
//const bool is_smithay = wm_name.find("Smithay") != std::string::npos;
const bool hyprland_waybar_is_dock = is_hyprland && is_hyprland_waybar_running_as_dock();
std::optional<CursorInfo> cursor_info;
@@ -973,8 +987,7 @@ namespace gsr {
// Wayland doesn't allow XGrabPointer/XGrabKeyboard when a wayland application is focused.
// If the focused window is a wayland application then don't use override redirect and instead create
// a fullscreen window for the ui.
// TODO: (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor))
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || x11_cursor_window || is_wlroots || is_hyprland;
const bool prevent_game_minimizing = gsr_info.system_info.display_server != DisplayServer::WAYLAND || (x11_cursor_window && is_window_fullscreen_on_monitor(display, x11_cursor_window, *focused_monitor)) || is_wlroots || is_hyprland;
if(prevent_game_minimizing) {
window_pos = focused_monitor->position;
@@ -1645,6 +1658,8 @@ namespace gsr {
}
void Overlay::show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target) {
timeout_seconds *= notification_duration_multiplier;
char timeout_seconds_str[32];
snprintf(timeout_seconds_str, sizeof(timeout_seconds_str), "%f", timeout_seconds);
@@ -1685,7 +1700,7 @@ namespace gsr {
notification_args[arg_index++] = nullptr;
if(notification_process > 0) {
kill(notification_process, SIGKILL);
kill(notification_process, SIGINT);
int status = 0;
waitpid(notification_process, &status, 0);
}
@@ -1737,6 +1752,17 @@ namespace gsr {
bind_linux_hotkeys(static_cast<GlobalHotkeysLinux*>(global_hotkeys.get()), this);
}
void Overlay::set_notification_speed(NotificationSpeed notification_speed) {
switch(notification_speed) {
case NotificationSpeed::NORMAL:
notification_duration_multiplier = 1.0;
break;
case NotificationSpeed::FAST:
notification_duration_multiplier = 0.3;
break;
}
}
void Overlay::update_notification_process_status() {
if(notification_process <= 0)
return;
@@ -1802,8 +1828,6 @@ namespace gsr {
result += std::to_string(seconds) + " second" + (seconds == 1 ? "" : "s");
}
fprintf(stderr, "to duration string: %f, %d, %d, %d\n", duration_sec, seconds, minutes, hours);
return result;
}
@@ -1816,6 +1840,15 @@ namespace gsr {
return replay_duration_sec;
}
static ClipboardFile::FileType filename_to_clipboard_file_type(const std::string &filename) {
if(ends_with(filename, ".jpg") || ends_with(filename, ".jpeg"))
return ClipboardFile::FileType::JPG;
else if(ends_with(filename, ".png"))
return ClipboardFile::FileType::PNG;
assert(false);
return ClipboardFile::FileType::PNG;
}
void Overlay::save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type) {
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
@@ -1870,6 +1903,9 @@ namespace gsr {
snprintf(msg, sizeof(msg), "Saved a screenshot of %s\nto \"%s\"",
capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str(), focused_window_name.c_str());
capture_target = screenshot_capture_target.c_str();
if(config.screenshot_config.save_screenshot_to_clipboard)
clipboard_file.set_current_file(new_video_filepath, filename_to_clipboard_file_type(new_video_filepath));
break;
}
case NotificationType::NONE:
@@ -1951,14 +1987,24 @@ namespace gsr {
void Overlay::on_gsr_process_error(int exit_code, NotificationType notification_type) {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_process, exit_code);
if(exit_code == 50) {
show_notification("Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
show_notification("Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 51) {
show_notification("Monitor capture failed.\nThe monitor you are trying to capture is invalid.\nPlease validate your capture settings.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 52) {
show_notification("Capture failed. Neither H264, HEVC nor AV1 video codecs are supported\non your system or you are trying to capture at a resolution higher than your\nsystem supports for each video codec.", 10.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 53) {
show_notification("Capture failed. Your system doesn't support the resolution you are trying to\nrecord at with the video codec you have chosen.\nChange capture resolution or video codec and try again.\nNote: AV1 supports the highest resolution, then HEVC and then H264.", 10.0, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 54) {
show_notification("Capture failed. Your system doesn't support the video codec you have chosen.\nChange video codec and try again.", notification_error_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else if(exit_code == 60) {
show_notification("Stopped capture because the user canceled the desktop portal", notification_timeout_seconds, mgl::Color(255, 0, 0), mgl::Color(255, 0, 0), notification_type);
} else {
const char *prefix = "";
switch(notification_type) {
case NotificationType::NONE:
break;
case NotificationType::SCREENSHOT:
prefix = "Failed to take a screenshot";
break;
case NotificationType::RECORD:
prefix = "Failed to start/save recording";
@@ -2001,7 +2047,7 @@ namespace gsr {
update_ui_replay_stopped();
if(exit_code == 0) {
if(config.replay_config.show_replay_stopped_notifications)
show_notification("Replay stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
show_notification("Replay stopped", short_notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::REPLAY);
} else {
on_gsr_process_error(exit_code, NotificationType::REPLAY);
}
@@ -2016,7 +2062,7 @@ namespace gsr {
update_ui_streaming_stopped();
if(exit_code == 0) {
if(config.streaming_config.show_streaming_stopped_notifications)
show_notification("Streaming has stopped", notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
show_notification("Streaming has stopped", short_notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::STREAM);
} else {
on_gsr_process_error(exit_code, NotificationType::STREAM);
}
@@ -2050,6 +2096,9 @@ namespace gsr {
snprintf(msg, sizeof(msg), "Saved a screenshot of %s",
capture_target_get_notification_name(screenshot_capture_target.c_str(), true).c_str());
show_notification(msg, notification_timeout_seconds, mgl::Color(255, 255, 255), get_color_theme().tint_color, NotificationType::SCREENSHOT, screenshot_capture_target.c_str());
if(config.screenshot_config.save_screenshot_to_clipboard)
clipboard_file.set_current_file(screenshot_filepath, filename_to_clipboard_file_type(screenshot_filepath));
}
} else {
fprintf(stderr, "Warning: gpu-screen-recorder (%d) exited with exit status %d\n", (int)gpu_screen_recorder_screenshot_process, exit_code);
@@ -2468,7 +2517,7 @@ namespace gsr {
kill(gpu_screen_recorder_process, SIGRTMIN+5);
}
static const char* switch_video_codec_to_usable_hardware_encoder(const GsrInfo &gsr_info) {
static const char* get_first_usable_hardware_video_codec_name(const GsrInfo &gsr_info) {
if(gsr_info.supported_video_codecs.h264)
return "h264";
else if(gsr_info.supported_video_codecs.hevc)
@@ -2501,8 +2550,7 @@ namespace gsr {
*video_codec = "h264";
*encoder = "cpu";
} else if(strcmp(*video_codec, "auto") == 0) {
*video_codec = switch_video_codec_to_usable_hardware_encoder(gsr_info);
if(!*video_codec) {
if(!get_first_usable_hardware_video_codec_name(gsr_info)) {
*video_codec = "h264";
*encoder = "cpu";
}
@@ -2510,6 +2558,14 @@ namespace gsr {
*container = change_container_if_codec_not_supported(*video_codec, *container);
}
static std::string get_framerate_mode_validate(const RecordOptions &record_options, const GsrInfo &gsr_info) {
(void)gsr_info;
std::string framerate_mode = record_options.framerate_mode;
if(framerate_mode == "auto")
framerate_mode = "vfr";
return framerate_mode;
}
bool Overlay::on_press_start_replay(bool disable_notification, bool finished_selection) {
if(region_selector.is_started() || window_selector.is_started())
return false;
@@ -2582,7 +2638,7 @@ namespace gsr {
const std::string video_bitrate = std::to_string(config.replay_config.record_options.video_bitrate);
const std::string output_directory = config.replay_config.save_directory;
const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.replay_config.record_options.audio_tracks_list, gsr_info);
const std::string framerate_mode = config.replay_config.record_options.framerate_mode == "auto" ? "vfr" : config.replay_config.record_options.framerate_mode;
const std::string framerate_mode = get_framerate_mode_validate(config.replay_config.record_options, gsr_info);
const std::string replay_time = std::to_string(config.replay_config.replay_time);
const char *container = config.replay_config.container.c_str();
const char *video_codec = config.replay_config.record_options.video_codec.c_str();
@@ -2658,7 +2714,7 @@ namespace gsr {
if(!disable_notification && config.replay_config.show_replay_started_notifications) {
char msg[256];
snprintf(msg, sizeof(msg), "Started replaying %s", capture_target_get_notification_name(recording_capture_target.c_str(), false).c_str());
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
show_notification(msg, short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::REPLAY, recording_capture_target.c_str());
}
if(config.replay_config.record_options.record_area_option == "portal")
@@ -2686,7 +2742,7 @@ namespace gsr {
if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
if(!replay_recording) {
if(config.record_config.show_recording_started_notifications)
show_notification("Started recording in the replay session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
show_notification("Started recording in the replay session", short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
update_ui_recording_started();
// TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has
@@ -2707,7 +2763,7 @@ namespace gsr {
if(gsr_info.system_info.gsr_version >= GsrVersion{5, 4, 0}) {
if(!replay_recording) {
if(config.record_config.show_recording_started_notifications)
show_notification("Started recording in the streaming session", notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
show_notification("Started recording in the streaming session", short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD);
update_ui_recording_started();
// TODO: This will be incorrect if the user uses portal capture, as capture wont start until the user has
@@ -2779,7 +2835,7 @@ namespace gsr {
const std::string video_bitrate = std::to_string(config.record_config.record_options.video_bitrate);
const std::string output_file = config.record_config.save_directory + "/Video_" + get_date_str() + "." + container_to_file_extension(config.record_config.container.c_str());
const std::vector<std::string> audio_tracks = create_audio_tracks_cli_args(config.record_config.record_options.audio_tracks_list, gsr_info);
const std::string framerate_mode = config.record_config.record_options.framerate_mode == "auto" ? "vfr" : config.record_config.record_options.framerate_mode;
const std::string framerate_mode = get_framerate_mode_validate(config.record_config.record_options, gsr_info);
const char *container = config.record_config.container.c_str();
const char *video_codec = config.record_config.record_options.video_codec.c_str();
const char *encoder = "gpu";
@@ -2833,7 +2889,7 @@ namespace gsr {
if(config.record_config.show_recording_started_notifications) {
char msg[256];
snprintf(msg, sizeof(msg), "Started recording %s", capture_target_get_notification_name(recording_capture_target.c_str(), false).c_str());
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
show_notification(msg, short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::RECORD, recording_capture_target.c_str());
}
if(config.record_config.record_options.record_area_option == "portal")
@@ -2955,7 +3011,7 @@ namespace gsr {
// But we check it anyways as streaming on some sites can fail if there is more than one audio track
if(audio_tracks.size() > 1)
audio_tracks.resize(1);
const std::string framerate_mode = config.streaming_config.record_options.framerate_mode == "auto" ? "vfr" : config.streaming_config.record_options.framerate_mode;
const std::string framerate_mode = get_framerate_mode_validate(config.streaming_config.record_options, gsr_info);
const char *container = "flv";
if(config.streaming_config.streaming_service == "custom")
container = config.streaming_config.custom.container.c_str();
@@ -3019,7 +3075,7 @@ namespace gsr {
if(config.streaming_config.show_streaming_started_notifications) {
char msg[256];
snprintf(msg, sizeof(msg), "Started streaming %s", capture_target_get_notification_name(recording_capture_target.c_str(), false).c_str());
show_notification(msg, notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM, recording_capture_target.c_str());
show_notification(msg, short_notification_timeout_seconds, get_color_theme().tint_color, get_color_theme().tint_color, NotificationType::STREAM, recording_capture_target.c_str());
}
if(config.streaming_config.record_options.record_area_option == "portal")

View File

@@ -120,6 +120,12 @@ namespace gsr {
if(!theme->trash_texture.load_from_file((resources_path + "images/trash.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->masked_texture.load_from_file((resources_path + "images/masked.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->unmasked_texture.load_from_file((resources_path + "images/unmasked.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;
if(!theme->ps4_home_texture.load_from_file((resources_path + "images/ps4_home.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
goto error;

View File

@@ -36,7 +36,7 @@ namespace gsr {
for(size_t i = 0; i < items.size(); ++i) {
Item &item = items[i];
item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom;
if(mgl::FloatRect(item.position, item_size).contains(mouse_pos)) {
if(mgl::FloatRect(item.position, item_size).contains(mouse_pos) && item.enabled) {
const size_t prev_selected_item = selected_item;
selected_item = i;
show_dropdown = false;
@@ -93,7 +93,7 @@ namespace gsr {
void ComboBox::set_selected_item(const std::string &id, bool trigger_event, bool trigger_event_even_if_selection_not_changed) {
for(size_t i = 0; i < items.size(); ++i) {
auto &item = items[i];
if(item.id == id) {
if(item.id == id && item.enabled) {
const size_t prev_selected_item = selected_item;
selected_item = i;
dirty = true;
@@ -106,6 +106,22 @@ namespace gsr {
}
}
void ComboBox::set_item_enabled(const std::string &id, bool enabled) {
for(size_t i = 0; i < items.size(); ++i) {
auto &item = items[i];
if(item.id == id) {
item.enabled = enabled;
item.text.set_color(item.enabled ? mgl::Color(255, 255, 255, 255) : mgl::Color(255, 255, 255, 80));
if(selected_item == i) {
selected_item = 0;
show_dropdown = false;
dirty = true;
}
return;
}
}
}
const std::string& ComboBox::get_selected_id() const {
if(items.empty()) {
static std::string dummy;
@@ -150,7 +166,7 @@ namespace gsr {
Item &item = items[i];
item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom;
if(!cursor_inside) {
if(!cursor_inside && item.enabled) {
cursor_inside = mgl::FloatRect(items_draw_pos, item_size).contains(mouse_pos);
if(cursor_inside) {
mgl::Rectangle item_background(items_draw_pos.floor(), item_size.floor());

View File

@@ -6,6 +6,7 @@
#include <mglpp/system/FloatRect.hpp>
#include <mglpp/system/Utf8.hpp>
#include <optional>
#include <string.h>
namespace gsr {
static const float padding_top_scale = 0.004629f;
@@ -22,8 +23,13 @@ namespace gsr {
}
}
Entry::Entry(mgl::Font *font, const char *text, float max_width) : text("", *font), max_width(max_width) {
Entry::Entry(mgl::Font *font, const char *text, float max_width) :
text(std::u32string(), *font),
masked_text(std::u32string(), *font),
max_width(max_width)
{
this->text.set_color(get_color_theme().text_color);
this->masked_text.set_color(get_color_theme().text_color);
set_text(text);
}
@@ -31,6 +37,8 @@ namespace gsr {
if(!visible)
return true;
mgl::Text32 &active_text = masked ? masked_text : text;
if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
const mgl::vec2f mouse_pos = { (float)event.mouse_button.x, (float)event.mouse_button.y };
selected = mgl::FloatRect(position + offset, get_size()).contains(mouse_pos);
@@ -38,9 +46,8 @@ namespace gsr {
selecting_text = true;
const auto caret_index_mouse = find_closest_caret_index_by_position(mouse_pos);
caret.byte_index = caret_index_mouse.byte_index;
caret.utf8_index = caret_index_mouse.utf8_index;
caret.offset_x = caret_index_mouse.pos.x - this->text.get_position().x;
caret.index = caret_index_mouse.index;
caret.offset_x = caret_index_mouse.pos.x - active_text.get_position().x;
selection_start_caret = caret;
show_selection = true;
} else {
@@ -50,89 +57,72 @@ namespace gsr {
}
} else if(event.type == mgl::Event::MouseButtonReleased && event.mouse_button.button == mgl::Mouse::Left) {
selecting_text = false;
if(caret.byte_index == selection_start_caret.byte_index)
if(caret.index == selection_start_caret.index)
show_selection = false;
} else if(event.type == mgl::Event::MouseMoved && selected) {
if(selecting_text) {
const auto caret_index_mouse = find_closest_caret_index_by_position(mgl::vec2f(event.mouse_move.x, event.mouse_move.y));
caret.byte_index = caret_index_mouse.byte_index;
caret.utf8_index = caret_index_mouse.utf8_index;
caret.offset_x = caret_index_mouse.pos.x - this->text.get_position().x;
caret.index = caret_index_mouse.index;
caret.offset_x = caret_index_mouse.pos.x - active_text.get_position().x;
return false;
}
} else if(event.type == mgl::Event::KeyPressed && selected) {
int selection_start_byte = caret.byte_index;
int selection_end_byte = caret.byte_index;
int selection_start_byte = caret.index;
int selection_end_byte = caret.index;
if(show_selection) {
selection_start_byte = std::min(caret.byte_index, selection_start_caret.byte_index);
selection_end_byte = std::max(caret.byte_index, selection_start_caret.byte_index);
selection_start_byte = std::min(caret.index, selection_start_caret.index);
selection_end_byte = std::max(caret.index, selection_start_caret.index);
}
if(event.key.code == mgl::Keyboard::Backspace) {
if(selection_start_byte == selection_end_byte && caret.byte_index > 0)
selection_start_byte = mgl::utf8_get_start_of_codepoint((const unsigned char*)text.get_string().c_str(), text.get_string().size(), caret.byte_index - 1);
if(selection_start_byte == selection_end_byte && caret.index > 0)
selection_start_byte -= 1;
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, "");
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::u32string());
} else if(event.key.code == mgl::Keyboard::Delete) {
if(selection_start_byte == selection_end_byte && caret.byte_index < (int)text.get_string().size()) {
size_t codepoint_length = 1;
mgl::utf8_get_codepoint_length(((const unsigned char*)text.get_string().c_str())[caret.byte_index], &codepoint_length);
selection_end_byte = selection_start_byte + codepoint_length;
}
if(selection_start_byte == selection_end_byte && caret.index < (int)active_text.get_string().size())
selection_end_byte += 1;
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, "");
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::u32string());
} else if(event.key.code == mgl::Keyboard::C && event.key.control) {
const size_t selection_num_bytes = selection_end_byte - selection_start_byte;
if(selection_num_bytes > 0)
window.set_clipboard(text.get_string().substr(selection_start_byte, selection_num_bytes));
window.set_clipboard(mgl::utf32_to_utf8(text.get_string().substr(selection_start_byte, selection_num_bytes)));
} else if(event.key.code == mgl::Keyboard::V && event.key.control) {
std::string clipboard_string = window.get_clipboard_string();
string_replace_all(clipboard_string, '\n', ' ');
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::move(clipboard_string));
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, mgl::utf8_to_utf32(clipboard_string));
} else if(event.key.code == mgl::Keyboard::A && event.key.control) {
selection_start_caret.byte_index = 0;
selection_start_caret.utf8_index = 0;
selection_start_caret.index = 0;
selection_start_caret.offset_x = 0.0f;
caret.byte_index = text.get_string().size();
caret.utf8_index = mgl::utf8_get_character_count((const unsigned char*)text.get_string().data(), text.get_string().size());
caret.index = active_text.get_string().size();
// TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
show_selection = true;
} else if(event.key.code == mgl::Keyboard::Left && caret.byte_index > 0) {
if(!selecting_with_keyboard && show_selection) {
} else if(event.key.code == mgl::Keyboard::Left) {
if(!selecting_with_keyboard && show_selection)
show_selection = false;
} else {
caret.byte_index = mgl::utf8_get_start_of_codepoint((const unsigned char*)text.get_string().data(), text.get_string().size(), caret.byte_index - 1);
caret.utf8_index -= 1;
// TODO: Move left by one character instead of calculating every character to caret index
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
}
else
move_caret_word(Direction::LEFT, event.key.control ? 999999 : 1);
if(!selecting_with_keyboard) {
selection_start_caret = caret;
show_selection = false;
}
} else if(event.key.code == mgl::Keyboard::Right) {
if(!selecting_with_keyboard && show_selection) {
if(!selecting_with_keyboard && show_selection)
show_selection = false;
} else {
const int caret_byte_index_before = caret.byte_index;
caret.byte_index = mgl::utf8_index_to_byte_index((const unsigned char*)text.get_string().data(), text.get_string().size(), caret.utf8_index + 1);
if(caret.byte_index != caret_byte_index_before)
caret.utf8_index += 1;
// TODO: Move right by one character instead of calculating every character to caret index
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
}
else
move_caret_word(Direction::RIGHT, event.key.control ? 999999 : 1);
if(!selecting_with_keyboard) {
selection_start_caret = caret;
show_selection = false;
}
} else if(event.key.code == mgl::Keyboard::Home) {
caret.byte_index = 0;
caret.utf8_index = 0;
caret.index = 0;
caret.offset_x = 0.0f;
if(!selecting_with_keyboard) {
@@ -140,10 +130,9 @@ namespace gsr {
show_selection = false;
}
} else if(event.key.code == mgl::Keyboard::End) {
caret.byte_index = text.get_string().size();
caret.utf8_index = mgl::utf8_get_character_count((const unsigned char*)text.get_string().data(), text.get_string().size());
caret.index = active_text.get_string().size();
// TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
if(!selecting_with_keyboard) {
selection_start_caret = caret;
@@ -164,14 +153,14 @@ namespace gsr {
return false;
} else if(event.type == mgl::Event::TextEntered && selected && event.text.codepoint >= 32 && event.text.codepoint != 127) {
int selection_start_byte = caret.byte_index;
int selection_end_byte = caret.byte_index;
int selection_start_byte = caret.index;
int selection_end_byte = caret.index;
if(show_selection) {
selection_start_byte = std::min(caret.byte_index, selection_start_caret.byte_index);
selection_end_byte = std::max(caret.byte_index, selection_start_caret.byte_index);
selection_start_byte = std::min(caret.index, selection_start_caret.index);
selection_end_byte = std::max(caret.index, selection_start_caret.index);
}
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::string(event.text.str, event.text.size));
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, mgl::utf8_to_utf32((const unsigned char*)event.text.str, event.text.size));
return false;
}
@@ -189,13 +178,15 @@ namespace gsr {
const int padding_left = padding_left_scale * get_theme().window_height;
const int padding_right = padding_right_scale * get_theme().window_height;
mgl::Text32 &active_text = masked ? masked_text : text;
background.set_size(get_size());
background.set_position(draw_pos.floor());
background.set_color(selected ? mgl::Color(0, 0, 0, 255) : mgl::Color(0, 0, 0, 120));
window.draw(background);
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
const mgl::vec2f caret_size = mgl::vec2f(caret_width, text.get_bounds().size.y).floor();
const mgl::vec2f caret_size = mgl::vec2f(caret_width, active_text.get_bounds().size.y).floor();
const float overflow_left = (caret.offset_x + padding_left) - (padding_left + text_overflow);
if(overflow_left < 0.0f)
@@ -205,18 +196,18 @@ namespace gsr {
if(overflow_right - text_overflow > 0.0f)
text_overflow = overflow_right;
text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - text.get_bounds().size.y * 0.5f) - mgl::vec2f(text_overflow, 0.0f)).floor());
active_text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - active_text.get_bounds().size.y * 0.5f) - mgl::vec2f(text_overflow, 0.0f)).floor());
const auto text_bounds = text.get_bounds();
const auto text_bounds = active_text.get_bounds();
const bool text_larger_than_background = text_bounds.size.x > (background.get_size().x - padding_left - padding_right);
const float text_overflow_right = (text_bounds.position.x + text_bounds.size.x) - (background.get_position().x + background.get_size().x - padding_right);
if(text_larger_than_background) {
if(text_overflow_right < 0.0f) {
text_overflow += text_overflow_right;
text.set_position(text.get_position() + mgl::vec2f(-text_overflow_right, 0.0f));
active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow_right, 0.0f));
}
} else {
text.set_position(text.get_position() + mgl::vec2f(-text_overflow, 0.0f));
active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow, 0.0f));
text_overflow = 0.0f;
}
@@ -234,7 +225,7 @@ namespace gsr {
});
window.set_scissor(scissor);
window.draw(text);
window.draw(active_text);
if(show_selection)
draw_caret_selection(window, draw_pos, caret_size);
@@ -254,11 +245,16 @@ namespace gsr {
}
void Entry::draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size) {
if(selection_start_caret.index == caret.index)
return;
const int padding_top = padding_top_scale * get_theme().window_height;
const int padding_left = padding_left_scale * get_theme().window_height;
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
const int offset = caret.index < selection_start_caret.index ? caret_width : 0;
mgl::Rectangle caret_selection_rect(mgl::vec2f(std::abs(selection_start_caret.offset_x - caret.offset_x), caret_size.y).floor());
caret_selection_rect.set_position((draw_pos + mgl::vec2f(padding_left + std::min(caret.offset_x, selection_start_caret.offset_x) - text_overflow, padding_top)).floor());
mgl::Rectangle caret_selection_rect(mgl::vec2f(std::abs(selection_start_caret.offset_x - caret.offset_x) - offset, caret_size.y).floor());
caret_selection_rect.set_position((draw_pos + mgl::vec2f(padding_left + std::min(caret.offset_x, selection_start_caret.offset_x) - text_overflow + offset, padding_top)).floor());
mgl::Color caret_select_color = get_color_theme().tint_color;
caret_select_color.a = 100;
caret_selection_rect.set_color(caret_select_color);
@@ -274,13 +270,43 @@ namespace gsr {
return { max_width, text.get_bounds().size.y + padding_top + padding_bottom };
}
EntryValidateHandlerResult Entry::set_text(std::string str) {
EntryValidateHandlerResult validate_result = set_text_internal(std::move(str));
void Entry::move_caret_word(Direction direction, size_t max_codepoints) {
mgl::Text32 &active_text = masked ? masked_text : text;
const int dir_step = direction == Direction::LEFT ? -1 : 1;
const int num_delimiter_chars = 15;
const char delimiter_chars[num_delimiter_chars + 1] = " \t\n/.,:;\\[](){}";
const char32_t *text_str = active_text.get_string().data();
int num_non_delimiter_chars_found = 0;
for(size_t i = 0; i < max_codepoints; ++i) {
const uint32_t codepoint = text_str[caret.index];
const bool is_delimiter_char = codepoint < 127 && !!memchr(delimiter_chars, codepoint, num_delimiter_chars);
if(is_delimiter_char) {
if(num_non_delimiter_chars_found > 0)
break;
} else {
++num_non_delimiter_chars_found;
}
if(caret.index + dir_step < 0 || caret.index + dir_step > (int)active_text.get_string().size())
break;
caret.index += dir_step;
}
// TODO: Move right by some characters instead of calculating every character to caret index
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
}
EntryValidateHandlerResult Entry::set_text(const std::string &str) {
EntryValidateHandlerResult validate_result = set_text_internal(mgl::utf8_to_utf32(str));
if(validate_result == EntryValidateHandlerResult::ALLOW) {
caret.byte_index = text.get_string().size();
caret.utf8_index = mgl::utf8_get_character_count((const unsigned char*)text.get_string().data(), text.get_string().size());
mgl::Text32 &active_text = masked ? masked_text : text;
caret.index = active_text.get_string().size();
// TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
selection_start_caret = caret;
selecting_text = false;
@@ -290,40 +316,59 @@ namespace gsr {
return validate_result;
}
EntryValidateHandlerResult Entry::set_text_internal(std::string str) {
EntryValidateHandlerResult Entry::set_text_internal(std::u32string str) {
EntryValidateHandlerResult validate_result = EntryValidateHandlerResult::ALLOW;
if(validate_handler)
validate_result = validate_handler(*this, str);
if(validate_result == EntryValidateHandlerResult::ALLOW) {
text.set_string(std::move(str));
if(masked)
masked_text.set_string(std::u32string(text.get_string().size(), '*'));
// TODO: Call callback with utf32 instead?
if(on_changed)
on_changed(text.get_string());
on_changed(mgl::utf32_to_utf8(text.get_string()));
}
return validate_result;
}
const std::string& Entry::get_text() const {
return text.get_string();
std::string Entry::get_text() const {
return mgl::utf32_to_utf8(text.get_string());
}
void Entry::replace_text(size_t index, size_t size, const std::string &replacement) {
void Entry::set_masked(bool masked) {
if(masked == this->masked)
return;
this->masked = masked;
if(masked)
masked_text.set_string(std::u32string(text.get_string().size(), '*'));
else
masked_text.set_string(std::u32string());
mgl::Text32 &active_text = masked ? masked_text : text;
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
selection_start_caret.offset_x = active_text.find_character_pos(selection_start_caret.index).x - active_text.get_position().x;
}
bool Entry::is_masked() const {
return masked;
}
void Entry::replace_text(size_t index, size_t size, const std::u32string &replacement) {
if(index + size > text.get_string().size())
return;
const auto prev_caret = caret;
if((int)index >= caret.byte_index) {
caret.utf8_index += mgl::utf8_get_character_count((const unsigned char*)replacement.c_str(), replacement.size());
caret.byte_index += replacement.size();
} else {
caret.utf8_index -= mgl::utf8_get_character_count((const unsigned char*)(text.get_string().c_str() + caret.byte_index - size), size);
caret.utf8_index += mgl::utf8_get_character_count((const unsigned char*)replacement.c_str(), replacement.size());
caret.byte_index = caret.byte_index - size + replacement.size();
}
if((int)index >= caret.index)
caret.index += replacement.size();
else
caret.index = caret.index - size + replacement.size();
std::string str = text.get_string();
std::u32string str = text.get_string();
str.replace(index, size, replacement);
const EntryValidateHandlerResult validate_result = set_text_internal(std::move(str));
if(validate_result == EntryValidateHandlerResult::DENY) {
@@ -333,8 +378,9 @@ namespace gsr {
return;
}
mgl::Text32 &active_text = masked ? masked_text : text;
// TODO: Optimize
caret.offset_x = text.find_character_pos(caret.utf8_index).x - this->text.get_position().x;
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
selection_start_caret = caret;
selecting_text = false;
@@ -342,17 +388,14 @@ namespace gsr {
show_selection = false;
}
mgl_index_codepoint_pair Entry::find_closest_caret_index_by_position(mgl::vec2f position) {
const std::string &str = text.get_string();
mgl::Font *font = text.get_font();
CaretIndexPos Entry::find_closest_caret_index_by_position(mgl::vec2f position) {
mgl::Text32 &active_text = masked ? masked_text : text;
const std::u32string &str = active_text.get_string();
mgl::Font *font = active_text.get_font();
mgl_index_codepoint_pair result = {0, 0, {text.get_position().x, text.get_position().y}};
for(; result.byte_index < str.size();) {
uint32_t codepoint = ' ';
size_t clen = 1;
if(!mgl::utf8_decode((const unsigned char*)&str[result.byte_index], str.size() - result.byte_index, &codepoint, &clen))
clen = 1;
CaretIndexPos result = {0, {active_text.get_position().x, active_text.get_position().y}};
for(result.index = 0; result.index < (int)str.size(); ++result.index) {
const uint32_t codepoint = str[result.index];
float glyph_width = 0.0f;
if(codepoint == '\t') {
@@ -368,8 +411,6 @@ namespace gsr {
break;
result.pos.x += glyph_width;
result.byte_index += clen;
result.utf8_index += 1;
}
return result;
@@ -379,7 +420,7 @@ namespace gsr {
return c >= '0' && c <= '9';
}
static std::optional<int> to_integer(const std::string &str) {
static std::optional<int> to_integer(const std::u32string &str) {
if(str.empty())
return std::nullopt;
@@ -406,7 +447,7 @@ namespace gsr {
}
EntryValidateHandler create_entry_validator_integer_in_range(int min, int max) {
return [min, max](Entry &entry, const std::string &str) {
return [min, max](Entry &entry, const std::u32string &str) {
if(str.empty())
return EntryValidateHandlerResult::ALLOW;

View File

@@ -467,13 +467,42 @@ namespace gsr {
return exit_program_button;
}
std::unique_ptr<List> GlobalSettingsPage::create_notification_speed() {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Notification speed", get_color_theme().text_color));
auto radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
notification_speed_button_ptr = radio_button.get();
radio_button->add_item("Normal", "normal");
radio_button->add_item("Fast", "fast");
radio_button->on_selection_changed = [this](const std::string&, const std::string &id) {
if(id == "normal")
overlay->set_notification_speed(NotificationSpeed::NORMAL);
else if(id == "fast")
overlay->set_notification_speed(NotificationSpeed::FAST);
return true;
};
list->add_widget(std::move(radio_button));
return list;
}
std::unique_ptr<Subsection> GlobalSettingsPage::create_application_options_subsection(ScrollablePage *parent_page) {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
List *list_ptr = list.get();
auto subsection = std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
list_ptr->add_widget(create_notification_speed());
list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x));
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL);
list->add_widget(create_exit_program_button());
auto navigate_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
navigate_list->add_widget(create_exit_program_button());
if(inside_flatpak)
list->add_widget(create_go_back_to_old_ui_button());
return std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
navigate_list->add_widget(create_go_back_to_old_ui_button());
list_ptr->add_widget(std::move(navigate_list));
return subsection;
}
std::unique_ptr<Subsection> GlobalSettingsPage::create_application_info_subsection(ScrollablePage *parent_page) {
@@ -535,6 +564,8 @@ namespace gsr {
enable_keyboard_hotkeys_radio_button_ptr->set_selected_item(config.main_config.hotkeys_enable_option, false, false);
enable_joystick_hotkeys_radio_button_ptr->set_selected_item(config.main_config.joystick_hotkeys_enable_option, false, false);
notification_speed_button_ptr->set_selected_item(config.main_config.notification_speed);
load_hotkeys();
}
@@ -561,6 +592,7 @@ namespace gsr {
config.main_config.tint_color = tint_color_radio_button_ptr->get_selected_id();
config.main_config.hotkeys_enable_option = enable_keyboard_hotkeys_radio_button_ptr->get_selected_id();
config.main_config.joystick_hotkeys_enable_option = enable_joystick_hotkeys_radio_button_ptr->get_selected_id();
config.main_config.notification_speed = notification_speed_button_ptr->get_selected_id();
save_config(config);
}

View File

@@ -215,8 +215,17 @@ namespace gsr {
return checkbox;
}
std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_save_screenshot_to_clipboard() {
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Save screenshot to clipboard");
save_screenshot_to_clipboard_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));
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
list->add_widget(create_save_screenshot_in_game_folder());
list->add_widget(create_save_screenshot_to_clipboard());
return std::make_unique<Subsection>("General", std::move(list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
}
std::unique_ptr<Widget> ScreenshotSettingsPage::create_notifications_section() {
@@ -281,6 +290,7 @@ namespace gsr {
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);
save_screenshot_to_clipboard_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_to_clipboard);
show_screenshot_saved_notification_checkbox_ptr->set_checked(config.screenshot_config.show_screenshot_saved_notifications);
if(config.screenshot_config.image_width == 0)
@@ -309,6 +319,7 @@ namespace gsr {
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.save_screenshot_to_clipboard = save_screenshot_to_clipboard_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)

View File

@@ -4,6 +4,7 @@
#include "../../include/gui/PageStack.hpp"
#include "../../include/gui/FileChooser.hpp"
#include "../../include/gui/Subsection.hpp"
#include "../../include/gui/Image.hpp"
#include "../../include/Theme.hpp"
#include "../../include/GsrInfo.hpp"
#include "../../include/Utils.hpp"
@@ -567,6 +568,10 @@ namespace gsr {
framerate_mode_box->add_item("Auto (Recommended)", "auto");
framerate_mode_box->add_item("Constant", "cfr");
framerate_mode_box->add_item("Variable", "vfr");
if(gsr_info->system_info.display_server == DisplayServer::X11)
framerate_mode_box->add_item("Sync to content", "content");
else
framerate_mode_box->add_item("Sync to content (Only X11 or desktop portal capture)", "content");
framerate_mode_box_ptr = framerate_mode_box.get();
return framerate_mode_box;
}
@@ -965,42 +970,75 @@ namespace gsr {
return streaming_service_list;
}
static std::unique_ptr<Button> create_mask_toggle_button(Entry *entry_to_toggle, mgl::vec2f size) {
auto button = std::make_unique<Button>(&get_theme().body_font, "", size, mgl::Color(0, 0, 0, 0));
Button *button_ptr = button.get();
button->set_icon(&get_theme().masked_texture);
button->on_click = [entry_to_toggle, button_ptr]() {
const bool is_masked = entry_to_toggle->is_masked();
button_ptr->set_icon(is_masked ? &get_theme().unmasked_texture : &get_theme().masked_texture);
entry_to_toggle->set_masked(!is_masked);
};
return button;
}
static Entry* add_stream_key_entry_to_list(List *stream_key_list) {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
auto key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
key_entry->set_masked(true);
Entry *key_entry_ptr = key_entry.get();
const float mask_icon_size = key_entry_ptr->get_size().y * 0.9f;
list->add_widget(std::move(key_entry));
list->add_widget(create_mask_toggle_button(key_entry_ptr, mgl::vec2f(mask_icon_size, mask_icon_size)));
stream_key_list->add_widget(std::move(list));
return key_entry_ptr;
}
std::unique_ptr<List> SettingsPage::create_stream_key_section() {
auto stream_key_list = std::make_unique<List>(List::Orientation::VERTICAL);
stream_key_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color));
auto twitch_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
twitch_stream_key_entry_ptr = twitch_stream_key_entry.get();
stream_key_list->add_widget(std::move(twitch_stream_key_entry));
auto youtube_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
youtube_stream_key_entry_ptr = youtube_stream_key_entry.get();
stream_key_list->add_widget(std::move(youtube_stream_key_entry));
auto rumble_stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
rumble_stream_key_entry_ptr = rumble_stream_key_entry.get();
stream_key_list->add_widget(std::move(rumble_stream_key_entry));
twitch_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
youtube_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
rumble_stream_key_entry_ptr = add_stream_key_entry_to_list(stream_key_list.get());
stream_key_list_ptr = stream_key_list.get();
return stream_key_list;
}
std::unique_ptr<List> SettingsPage::create_stream_custom_section() {
auto stream_url_list = std::make_unique<List>(List::Orientation::VERTICAL);
stream_url_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream URL:", get_color_theme().text_color));
std::unique_ptr<List> SettingsPage::create_stream_custom_url() {
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
auto stream_url_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
stream_url_entry_ptr = stream_url_entry.get();
stream_url_list->add_widget(std::move(stream_url_entry));
stream_url_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color));
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream URL:", get_color_theme().text_color));
list->add_widget(std::move(stream_url_entry));
return list;
}
std::unique_ptr<List> SettingsPage::create_stream_custom_key() {
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
auto stream_key_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
stream_key_entry->set_masked(true);
stream_key_entry_ptr = stream_key_entry.get();
stream_url_list->add_widget(std::move(stream_key_entry));
const float mask_icon_size = stream_key_entry_ptr->get_size().y * 0.9f;
list->add_widget(std::move(stream_key_entry));
list->add_widget(create_mask_toggle_button(stream_key_entry_ptr, mgl::vec2f(mask_icon_size, mask_icon_size)));
return list;
}
stream_url_list_ptr = stream_url_list.get();
return stream_url_list;
std::unique_ptr<List> SettingsPage::create_stream_custom_section() {
auto custom_stream_list = std::make_unique<List>(List::Orientation::VERTICAL);
auto stream_url_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
stream_url_list->add_widget(create_stream_custom_url());
stream_url_list->add_widget(create_stream_container());
custom_stream_list->add_widget(std::move(stream_url_list));
custom_stream_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Stream key:", get_color_theme().text_color));
custom_stream_list->add_widget(create_stream_custom_key());
custom_stream_list_ptr = custom_stream_list.get();
return custom_stream_list;
}
std::unique_ptr<ComboBox> SettingsPage::create_stream_container_box() {
@@ -1013,11 +1051,10 @@ namespace gsr {
return container_box;
}
std::unique_ptr<List> SettingsPage::create_stream_container_section() {
std::unique_ptr<List> SettingsPage::create_stream_container() {
auto container_list = std::make_unique<List>(List::Orientation::VERTICAL);
container_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Container:", get_color_theme().text_color));
container_list->add_widget(create_stream_container_box());
container_list_ptr = container_list.get();
return container_list;
}
@@ -1026,7 +1063,6 @@ namespace gsr {
streaming_info_list->add_widget(create_streaming_service_section());
streaming_info_list->add_widget(create_stream_key_section());
streaming_info_list->add_widget(create_stream_custom_section());
streaming_info_list->add_widget(create_stream_container_section());
settings_list_ptr->add_widget(std::make_unique<Subsection>("Streaming info", std::move(streaming_info_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f)));
auto checkboxes_list = std::make_unique<List>(List::Orientation::VERTICAL);
@@ -1051,11 +1087,10 @@ namespace gsr {
const bool rumble_option = id == "rumble";
const bool custom_option = id == "custom";
stream_key_list_ptr->set_visible(!custom_option);
stream_url_list_ptr->set_visible(custom_option);
container_list_ptr->set_visible(custom_option);
twitch_stream_key_entry_ptr->set_visible(twitch_option);
youtube_stream_key_entry_ptr->set_visible(youtube_option);
rumble_stream_key_entry_ptr->set_visible(rumble_option);
custom_stream_list_ptr->set_visible(custom_option);
twitch_stream_key_entry_ptr->get_parent_widget()->set_visible(twitch_option);
youtube_stream_key_entry_ptr->get_parent_widget()->set_visible(youtube_option);
rumble_stream_key_entry_ptr->get_parent_widget()->set_visible(rumble_option);
return true;
};
streaming_service_box_ptr->on_selection_changed("Twitch", "twitch");

View File

@@ -64,6 +64,10 @@ namespace gsr {
this->visible = visible;
}
Widget* Widget::get_parent_widget() {
return parent_widget;
}
void add_widget_to_remove(std::unique_ptr<Widget> widget) {
widgets_to_remove.push_back(std::move(widget));
}

View File

@@ -195,7 +195,9 @@ enum class LaunchAction {
int main(int argc, char **argv) {
setlocale(LC_ALL, "C"); // Sigh... stupid C
#ifdef __GLIBC__
mallopt(M_MMAP_THRESHOLD, 65536);
#endif
if(geteuid() == 0) {
fprintf(stderr, "Error: don't run gsr-ui as the root user\n");