Add option to start replay on fullscreen

This commit is contained in:
dec05eba
2024-11-14 00:25:37 +01:00
parent 4ba1e814b7
commit d2f6b0738b
12 changed files with 218 additions and 41 deletions

6
TODO
View File

@@ -79,3 +79,9 @@ Add profile option. Convert view to profile, add an option at the bottom that sa
Verify monitor/audio when starting recording. Give an error if the options are no longer valid.
Add option to record focused monitor. This is less error prone when plugging in monitors, etc.
Get focused window when opening gsr-ui and pass that to the save replay script, to ignore gsr-ui when getting game name.
gsr ui window has _NET_WM_STATE _NET_WM_STATE_ABOVE, not _NET_WM_STATE_FULLSCREEN
For replay on fullscreen detect focused fullscreen window by checking if the window size is the same as the monitor size instead of _NET_WM_STATE_FULLSCREEN.

View File

@@ -81,7 +81,7 @@ namespace gsr {
struct ReplayConfig {
RecordOptions record_options;
bool start_replay_automatically = false;
std::string turn_on_replay_automatically_mode = "dont_turn_on_automatically";
bool save_video_in_game_folder = false;
bool show_replay_started_notifications = true;
bool show_replay_stopped_notifications = true;

View File

@@ -59,6 +59,8 @@ namespace gsr {
void update_notification_process_status();
void update_gsr_process_status();
void update_focused_fullscreen_status();
void update_ui_recording_paused();
void update_ui_recording_unpaused();
@@ -115,6 +117,8 @@ namespace gsr {
RecordingStatus recording_status = RecordingStatus::NONE;
bool paused = false;
mgl::Clock focused_fullscreen_clock;
std::array<KeyBinding, 1> key_bindings;
};
}

View File

@@ -5,11 +5,11 @@
namespace gsr {
class LineSeparator : public Widget {
public:
enum class Type {
enum class Orientation {
HORIZONTAL
};
LineSeparator(Type type, float width);
LineSeparator(Orientation orientation, float width);
LineSeparator(const LineSeparator&) = delete;
LineSeparator& operator=(const LineSeparator&) = delete;
@@ -18,7 +18,7 @@ namespace gsr {
mgl::vec2f get_size() override;
private:
Type type;
Orientation orientation;
float width;
};
}

View File

@@ -9,7 +9,12 @@
namespace gsr {
class RadioButton : public Widget {
public:
RadioButton(mgl::Font *font);
enum class Orientation {
VERTICAL,
HORIZONTAL
};
RadioButton(mgl::Font *font, Orientation orientation);
RadioButton(const RadioButton&) = delete;
RadioButton& operator=(const RadioButton&) = delete;
@@ -32,6 +37,7 @@ namespace gsr {
};
mgl::Font *font;
Orientation orientation;
std::vector<Item> items;
size_t selected_item = 0;
bool dirty = true;

View File

@@ -92,7 +92,7 @@ namespace gsr {
std::unique_ptr<List> create_container_section();
std::unique_ptr<Entry> create_replay_time_entry();
std::unique_ptr<List> create_replay_time();
std::unique_ptr<CheckBox> create_start_replay_on_startup();
std::unique_ptr<RadioButton> create_start_replay_automatically();
std::unique_ptr<CheckBox> create_save_replay_in_game_folder();
std::unique_ptr<Label> create_estimated_file_size();
void update_estimated_file_size();
@@ -165,7 +165,6 @@ namespace gsr {
List *stream_key_list_ptr = nullptr;
List *stream_url_list_ptr = nullptr;
List *container_list_ptr = nullptr;
CheckBox *start_replay_automatically_ptr = nullptr;
CheckBox *save_replay_in_game_folder_ptr = nullptr;
Label *estimated_file_size_ptr = nullptr;
CheckBox *show_replay_started_notification_checkbox_ptr = nullptr;
@@ -181,6 +180,7 @@ namespace gsr {
Entry *youtube_stream_key_entry_ptr = nullptr;
Entry *stream_url_entry_ptr = nullptr;
Entry *replay_time_entry_ptr = nullptr;
RadioButton *turn_on_replay_automatically_mode_ptr = nullptr;
PageStack *page_stack = nullptr;

View File

@@ -132,7 +132,7 @@ namespace gsr {
{"replay.record_options.overclock", &config.replay_config.record_options.overclock},
{"replay.record_options.record_cursor", &config.replay_config.record_options.record_cursor},
{"replay.record_options.restore_portal_session", &config.replay_config.record_options.restore_portal_session},
{"replay.start_replay_automatically", &config.replay_config.start_replay_automatically},
{"replay.turn_on_replay_automatically_mode", &config.replay_config.turn_on_replay_automatically_mode},
{"replay.save_video_in_game_folder", &config.replay_config.save_video_in_game_folder},
{"replay.show_replay_started_notifications", &config.replay_config.show_replay_started_notifications},
{"replay.show_replay_stopped_notifications", &config.replay_config.show_replay_stopped_notifications},

View File

@@ -30,6 +30,66 @@ extern "C" {
namespace gsr {
static const mgl::Color bg_color(0, 0, 0, 100);
static const double force_window_on_top_timeout_seconds = 1.0;
static const double focused_window_fullscreen_check_timeout_seconds = 1.0;
static bool window_has_atom(Display *dpy, Window window, Atom atom) {
Atom type;
unsigned long len, bytes_left;
int format;
unsigned char *properties = NULL;
if(XGetWindowProperty(dpy, window, atom, 0, 1024, False, AnyPropertyType, &type, &format, &len, &bytes_left, &properties) < Success)
return false;
if(properties)
XFree(properties);
return type != None;
}
static bool window_is_user_program(Display *dpy, Window window) {
const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False);
const Atom wm_state_atom = XInternAtom(dpy, "WM_STATE", False);
return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom);
}
static Window get_window_at_cursor_position(Display *dpy) {
Window root_window = None;
Window window = None;
int dummy_i;
unsigned int dummy_u;
int cursor_pos_x = 0;
int cursor_pos_y = 0;
XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &dummy_i, &dummy_i, &cursor_pos_x, &cursor_pos_y, &dummy_u);
return window;
}
static Window get_focused_window(Display *dpy) {
const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
Window focused_window = None;
// Atom type = None;
// int format = 0;
// unsigned long num_items = 0;
// unsigned long bytes_left = 0;
// unsigned char *data = NULL;
// XGetWindowProperty(dpy, DefaultRootWindow(dpy), net_active_window_atom, 0, 1, False, XA_WINDOW, &type, &format, &num_items, &bytes_left, &data);
// fprintf(stderr, "focused window: %p\n", (void*)data);
// if(type == XA_WINDOW && num_items == 1 && data)
// return *(Window*)data;
int revert_to = 0;
XGetInputFocus(dpy, &focused_window, &revert_to);
if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window))
return focused_window;
focused_window = get_window_at_cursor_position(dpy);
if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window))
return focused_window;
return None;
}
static mgl::Texture texture_from_ximage(XImage *img) {
uint8_t *texture_data = (uint8_t*)malloc(img->width * img->height * 3);
@@ -82,17 +142,6 @@ namespace gsr {
return result;
}
static Window get_window_at_cursor_position(Display *display) {
Window root_window = None;
Window window = None;
int dummy_i;
unsigned int dummy_u;
int cursor_pos_x = 0;
int cursor_pos_y = 0;
XQueryPointer(display, DefaultRootWindow(display), &root_window, &window, &dummy_i, &dummy_i, &cursor_pos_x, &cursor_pos_y, &dummy_u);
return window;
}
struct DrawableGeometry {
int x, y, width, height;
};
@@ -130,6 +179,57 @@ namespace gsr {
&& diff_int(geometry.width, monitor->size.x, margin) && diff_int(geometry.height, monitor->size.y, margin);
}
/*static bool is_window_fullscreen_on_monitor(Display *display, Window window, const mgl_monitor *monitors, int num_monitors) {
if(!window)
return false;
DrawableGeometry geometry;
if(!get_drawable_geometry(display, window, &geometry))
return false;
const int margin = 2;
for(int i = 0; i < num_monitors; ++i) {
const mgl_monitor *mon = &monitors[i];
if(diff_int(geometry.x, mon->pos.x, margin) && diff_int(geometry.y, mon->pos.y, margin)
&& diff_int(geometry.width, mon->size.x, margin) && diff_int(geometry.height, mon->size.y, margin))
{
return true;
}
}
return false;
}*/
static bool window_is_fullscreen(Display *display, Window window) {
const Atom wm_state_atom = XInternAtom(display, "_NET_WM_STATE", False);
const Atom wm_state_fullscreen_atom = XInternAtom(display, "_NET_WM_STATE_FULLSCREEN", False);
Atom type = None;
int format = 0;
unsigned long num_items = 0;
unsigned long bytes_after = 0;
unsigned char *properties = nullptr;
if(XGetWindowProperty(display, window, wm_state_atom, 0, 1024, False, XA_ATOM, &type, &format, &num_items, &bytes_after, &properties) < Success) {
fprintf(stderr, "Failed to get window wm state property\n");
return false;
}
if(!properties)
return false;
bool is_fullscreen = false;
Atom *atoms = (Atom*)properties;
for(unsigned long i = 0; i < num_items; ++i) {
if(atoms[i] == wm_state_fullscreen_atom) {
is_fullscreen = true;
break;
}
}
XFree(properties);
return is_fullscreen;
}
static void set_focused_window(Display *dpy, Window window) {
XSetInputFocus(dpy, window, RevertToParent, CurrentTime);
@@ -220,7 +320,7 @@ namespace gsr {
const std::string notify_bg_color_str = color_to_hex_str(get_color_theme().tint_color);
setenv("GSR_NOTIFY_BG_COLOR", notify_bg_color_str.c_str(), true);
if(config.replay_config.start_replay_automatically)
if(config.replay_config.turn_on_replay_automatically_mode == "turn_on_at_system_startup")
on_press_start_replay(true);
}
@@ -289,6 +389,7 @@ namespace gsr {
bool Overlay::draw() {
update_notification_process_status();
update_gsr_process_status();
update_focused_fullscreen_status();
if(!visible)
return false;
@@ -753,6 +854,30 @@ namespace gsr {
recording_status = RecordingStatus::NONE;
}
void Overlay::update_focused_fullscreen_status() {
if(focused_fullscreen_clock.get_elapsed_time_seconds() < focused_window_fullscreen_check_timeout_seconds)
return;
focused_fullscreen_clock.restart();
if(config.replay_config.turn_on_replay_automatically_mode != "turn_on_at_fullscreen")
return;
mgl_context *context = mgl_get_context();
Display *display = (Display*)context->connection;
const Window focused_window = get_focused_window(display);
if(window && focused_window == window->get_system_handle())
return;
if(recording_status == RecordingStatus::NONE) {
if(focused_window != 0 && window_is_fullscreen(display, focused_window))
on_press_start_replay(false);
} else if(recording_status == RecordingStatus::REPLAY) {
if(focused_window == 0 || !window_is_fullscreen(display, focused_window))
on_press_start_replay(false);
}
}
void Overlay::update_ui_recording_paused() {
if(!visible || recording_status != RecordingStatus::RECORD)
return;

View File

@@ -16,7 +16,7 @@ namespace gsr {
};
}
LineSeparator::LineSeparator(Type type, float width) : type(type), width(width) {
LineSeparator::LineSeparator(Orientation orientation, float width) : orientation(orientation), width(width) {
}

View File

@@ -14,7 +14,7 @@ namespace gsr {
static const float spacing_scale = 0.007f;
static const float border_scale = 0.0015f;
RadioButton::RadioButton(mgl::Font *font) : font(font) {
RadioButton::RadioButton(mgl::Font *font, Orientation orientation) : font(font), orientation(orientation) {
}
@@ -44,7 +44,14 @@ namespace gsr {
return false;
}
switch(orientation) {
case Orientation::VERTICAL:
draw_pos.y += item_size.y + spacing_scale * get_theme().window_height;
break;
case Orientation::HORIZONTAL:
draw_pos.x += item_size.x + spacing_scale * get_theme().window_height;
break;
}
}
}
return true;
@@ -85,7 +92,14 @@ namespace gsr {
item.text.set_position((draw_pos + item_size * 0.5f - item.text.get_bounds().size * 0.5f).floor());
window.draw(item.text);
switch(orientation) {
case Orientation::VERTICAL:
draw_pos.y += item_size.y + spacing_scale * get_theme().window_height;
break;
case Orientation::HORIZONTAL:
draw_pos.x += item_size.x + spacing_scale * get_theme().window_height;
break;
}
}
}
@@ -98,13 +112,32 @@ namespace gsr {
const int padding_left = padding_left_scale * get_theme().window_height;
const int padding_right = padding_right_scale * get_theme().window_height;
size = { 0.0f, font->get_character_size() + (float)padding_top + (float)padding_bottom };
size = { 0.0f, 0.0f };
for(Item &item : items) {
const mgl::vec2f bounds = item.text.get_bounds().size;
switch(orientation) {
case Orientation::VERTICAL:
size.x = std::max(size.x, bounds.x + padding_left + padding_right);
size.y += bounds.y + padding_top + padding_bottom;
break;
case Orientation::HORIZONTAL:
size.x += bounds.x + padding_left + padding_right;
size.y = font->get_character_size() + (float)padding_top + (float)padding_bottom;
break;
}
if(items.size() > 1)
}
if(items.size() > 1) {
switch(orientation) {
case Orientation::VERTICAL:
size.y += (items.size() - 1) * spacing_scale * get_theme().window_height;
break;
case Orientation::HORIZONTAL:
size.x += (items.size() - 1) * spacing_scale * get_theme().window_height;
break;
}
}
dirty = false;
}

View File

@@ -42,7 +42,7 @@ namespace gsr {
}
std::unique_ptr<RadioButton> SettingsPage::create_view_radio_button() {
auto view_radio_button = std::make_unique<RadioButton>(&get_theme().body_font);
auto view_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
view_radio_button->add_item("Simple view", "simple");
view_radio_button->add_item("Advanced view", "advanced");
view_radio_button->set_horizontal_alignment(Widget::Alignment::CENTER);
@@ -228,7 +228,7 @@ namespace gsr {
}
std::unique_ptr<RadioButton> SettingsPage::create_audio_type_button() {
auto audio_type_radio_button = std::make_unique<RadioButton>(&get_theme().body_font);
auto audio_type_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
audio_type_radio_button->add_item("Audio devices", "audio_devices");
audio_type_radio_button->add_item("Application audio", "app_audio");
audio_type_radio_button_ptr = audio_type_radio_button.get();
@@ -321,10 +321,10 @@ namespace gsr {
auto audio_section = std::make_unique<Subsection>("Audio", std::move(audio_device_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
audio_device_section_list_ptr->add_widget(create_audio_type_button());
audio_device_section_list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Type::HORIZONTAL, audio_section->get_inner_size().x));
audio_device_section_list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, audio_section->get_inner_size().x));
audio_device_section_list_ptr->add_widget(create_audio_device_section());
audio_device_section_list_ptr->add_widget(create_application_audio_section());
//audio_device_section_list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Type::HORIZONTAL, audio_section->get_inner_size().x));
//audio_device_section_list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, audio_section->get_inner_size().x));
audio_device_section_list_ptr->add_widget(create_merge_audio_tracks_checkbox());
audio_device_section_list_ptr->add_widget(create_audio_codec());
return audio_section;
@@ -646,10 +646,13 @@ namespace gsr {
return replay_time_list;
}
std::unique_ptr<CheckBox> SettingsPage::create_start_replay_on_startup() {
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Turn on replay automatically");
start_replay_automatically_ptr = checkbox.get();
return checkbox;
std::unique_ptr<RadioButton> SettingsPage::create_start_replay_automatically() {
auto radiobutton = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::VERTICAL);
radiobutton->add_item("Don't turn on replay automatically", "dont_turn_on_automatically");
radiobutton->add_item("Turn on replay at system startup", "turn_on_at_system_startup");
radiobutton->add_item("Turn on replay when starting a fullscreen application", "turn_on_at_fullscreen");
turn_on_replay_automatically_mode_ptr = radiobutton.get();
return radiobutton;
}
std::unique_ptr<CheckBox> SettingsPage::create_save_replay_in_game_folder() {
@@ -685,7 +688,7 @@ namespace gsr {
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_start_replay_on_startup());
general_list->add_widget(create_start_replay_automatically());
general_list->add_widget(create_save_replay_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)));
@@ -1015,7 +1018,7 @@ namespace gsr {
void SettingsPage::load_replay() {
load_common(config.replay_config.record_options);
start_replay_automatically_ptr->set_checked(config.replay_config.start_replay_automatically);
turn_on_replay_automatically_mode_ptr->set_selected_item(config.replay_config.turn_on_replay_automatically_mode);
save_replay_in_game_folder_ptr->set_checked(config.replay_config.save_video_in_game_folder);
show_replay_started_notification_checkbox_ptr->set_checked(config.replay_config.show_replay_started_notifications);
show_replay_stopped_notification_checkbox_ptr->set_checked(config.replay_config.show_replay_stopped_notifications);
@@ -1142,7 +1145,7 @@ namespace gsr {
void SettingsPage::save_replay() {
save_common(config.replay_config.record_options);
config.replay_config.start_replay_automatically = start_replay_automatically_ptr->is_checked();
config.replay_config.turn_on_replay_automatically_mode = turn_on_replay_automatically_mode_ptr->get_selected_id();
config.replay_config.save_video_in_game_folder = save_replay_in_game_folder_ptr->is_checked();
config.replay_config.show_replay_started_notifications = show_replay_started_notification_checkbox_ptr->is_checked();
config.replay_config.show_replay_stopped_notifications = show_replay_stopped_notification_checkbox_ptr->is_checked();

View File

@@ -156,7 +156,7 @@ int main(void) {
global_hotkeys.poll_events();
overlay->handle_events();
if(!overlay->draw())
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
fprintf(stderr, "info: shutting down!\n");