Skip to content

Commit

Permalink
preserve-output: rewrite plugin to make use of workspace sets
Browse files Browse the repository at this point in the history
Workspace sets are the best choice for saving and restoring the view
geometry when an output is removed and re-added later.

Hopefully fixes #1707
  • Loading branch information
ammen99 committed Aug 10, 2023
1 parent 80b3864 commit f616a42
Showing 1 changed file with 67 additions and 269 deletions.
336 changes: 67 additions & 269 deletions plugins/single_plugins/preserve-output.cpp
Original file line number Diff line number Diff line change
@@ -1,331 +1,129 @@
#include "wayfire/nonstd/observer_ptr.h"
#include "wayfire/object.hpp"
#include "wayfire/output.hpp"
#include "wayfire/toplevel-view.hpp"
#include "wayfire/util.hpp"
#include "wayfire/view-helpers.hpp"
#include <sys/types.h>
#include <chrono>
#include <wayfire/per-output-plugin.hpp>
#include <wayfire/view.hpp>
#include <wayfire/core.hpp>
#include "wayfire/core.hpp"
#include "wayfire/plugin.hpp"
#include <wayfire/workspace-set.hpp>
#include <wayfire/signal-definitions.hpp>
#include <wayfire/output.hpp>
#include <wayfire/output-layout.hpp>
#include <wayfire/nonstd/wlroots-full.hpp>
#include <wayfire/util/log.hpp>
#include <wlr/util/edges.h>
#include <wayfire/window-manager.hpp>

#include <wayfire/plugins/common/shared-core-data.hpp>

/**
* View last output info
*/
#include <chrono>

class last_output_info_t : public wf::custom_data_t
namespace wf
{
public:
std::string output_identifier;
wf::geometry_t geometry;
bool fullscreen = false;
bool minimized = false;
uint32_t tiled_edges = 0;
uint z_order;
bool focused = false;
};

std::string make_output_identifier(wf::output_t *output)
namespace preserve_output
{
static std::string make_output_identifier(wf::output_t *output)
{
std::string identifier = "";
identifier += output->handle->make;
identifier += nonull(output->handle->make);
identifier += "|";
identifier += output->handle->model;
identifier += nonull(output->handle->model);
identifier += "|";
identifier += output->handle->serial;
identifier += nonull(output->handle->serial);
return identifier;
}

void view_store_data(wayfire_toplevel_view view, wf::output_t *output, int z_order)
{
auto view_data = view->get_data_safe<last_output_info_t>();
view_data->output_identifier = make_output_identifier(output);
view_data->geometry = view->get_pending_geometry();
view_data->fullscreen = view->pending_fullscreen();
view_data->minimized = view->minimized;
view_data->tiled_edges = view->pending_tiled_edges();
view_data->z_order = z_order;
if (view == output->get_active_view())
{
view_data->focused = true;
}
}

nonstd::observer_ptr<last_output_info_t> view_get_data(wayfire_toplevel_view view)
{
return view->get_data<last_output_info_t>();
}

bool view_has_data(wayfire_view view)
{
return view->has_data<last_output_info_t>();
}

void view_erase_data(wayfire_view view)
{
view->erase_data<last_output_info_t>();
}

/**
* Core preserve-output info
*/

wf::option_wrapper_t<int> last_output_focus_timeout{
"preserve-output/last_output_focus_timeout"};

class preserve_output_t
struct per_output_state_t
{
public:
int instances = 0;
std::string last_focused_output_identifier = "";
std::chrono::time_point<std::chrono::steady_clock> last_focused_output_timestamp;

std::map<std::string, wf::point_t> output_saved_workspace;

preserve_output_t() = default;
~preserve_output_t()
{
LOGD("This is last instance - deleting all data");
// Delete data from all views
for (auto& view : wf::get_core().get_all_views())
{
view_erase_data(view);
}
}

preserve_output_t(preserve_output_t&&) = delete;
preserve_output_t& operator =(preserve_output_t&&) = delete;

preserve_output_t(const preserve_output_t&) = delete;
preserve_output_t& operator =(const preserve_output_t&) = delete;
std::shared_ptr<wf::workspace_set_t> workspace_set;
std::chrono::time_point<std::chrono::steady_clock> destroy_timestamp;
bool was_focused = false;
};

/**
* preserve-output plugin
*/

class wayfire_preserve_output : public wf::per_output_plugin_instance_t
class preserve_output_t : public wf::plugin_interface_t
{
bool outputs_being_removed = false;
wf::shared_data::ref_ptr_t<preserve_output_t> core_data;
wf::option_wrapper_t<int> last_output_focus_timeout{"preserve-output/last_output_focus_timeout"};
std::map<std::string, per_output_state_t> saved_outputs;

bool focused_output_expired()
bool focused_output_expired(const per_output_state_t& state) const
{
using namespace std::chrono;
const auto now = steady_clock::now();
const auto last_focus_ts = core_data->last_focused_output_timestamp;
const auto elapsed_since_focus =
duration_cast<milliseconds>(now - last_focus_ts).count();

const auto elapsed_since_focus = duration_cast<milliseconds>(now - state.destroy_timestamp).count();
return elapsed_since_focus > last_output_focus_timeout;
}

void store_focused_output(wf::output_t *output)
{
auto& last_focused_output = core_data->last_focused_output_identifier;
// Store the output as last focused if no other output has been stored as
// last
// focused in the last 10 seconds
if ((last_focused_output == "") || focused_output_expired())
{
LOGD("Setting last focused output to: ", output->to_string());
last_focused_output = make_output_identifier(output);
core_data->last_focused_output_timestamp =
std::chrono::steady_clock::now();
}
}

wf::signal::connection_t<wf::output_pre_remove_signal> output_pre_remove =
[=] (wf::output_pre_remove_signal *ev)
void save_output(wf::output_t *output)
{
LOGD("Received pre-remove event: ", ev->output->to_string());
outputs_being_removed = true;

if (ev->output != output)
{
// This event is not for this output
return;
}

// This output is being destroyed
std::string identifier = make_output_identifier(output);
auto ident = make_output_identifier(output);
auto& data = saved_outputs[ident];

// Store this output as the focused one
if (wf::get_core().get_active_output() == output)
{
store_focused_output(output);
}
data.was_focused = (output == wf::get_core().get_active_output());
data.destroy_timestamp = std::chrono::steady_clock::now();
data.workspace_set = output->wset();

core_data->output_saved_workspace[identifier] =
output->wset()->get_current_workspace();
LOGD("Saving workspace set ", data.workspace_set->get_index(), " from output ", output->to_string(),
" with identifier ", ident);

auto views = output->wset()->get_views();
for (size_t i = 0; i < views.size(); i++)
{
auto view = views[i];
if ((view->role != wf::VIEW_ROLE_TOPLEVEL) || !view->is_mapped())
{
continue;
}
// Set a dummy workspace set with no views at all.
output->set_workspace_set(wf::workspace_set_t::create());

// Set current output and geometry in the view's last output data
if (!view_has_data(view))
{
view_store_data(view, output, i);
}
}
};

wf::signal::connection_t<wf::output_removed_signal> output_removed = [=] (wf::output_removed_signal *ev)
{
LOGD("Received output-removed event: ", ev->output->to_string());
outputs_being_removed = false;
};
// Detach workspace set from its old output
data.workspace_set->attach_to_output(nullptr);
}

void restore_views_to_output()
void try_restore_output(wf::output_t *output)
{
std::string identifier = make_output_identifier(output);

// Restore active workspace on the output
// We do this first so that when restoring view's geometries, they land
// directly on the correct workspace.
if (core_data->output_saved_workspace.count(identifier))
std::string ident = make_output_identifier(output);
if (!saved_outputs.count(ident))
{
output->wset()->set_workspace(
core_data->output_saved_workspace[identifier]);
LOGD("No saved identifier for ", output->to_string());
return;
}

// Focus this output if it was the last one focused
if (core_data->last_focused_output_identifier == identifier)
{
LOGD("This is last focused output, refocusing: ", output->to_string());
wf::get_core().focus_output(output);
core_data->last_focused_output_identifier.clear();
}
auto& data = saved_outputs[ident];

// Make a list of views to move to this output
auto views = std::vector<wayfire_toplevel_view>();
for (auto& view : wf::get_core().get_all_views())
auto new_output = data.workspace_set->get_attached_output();
if (new_output && (new_output->wset() == data.workspace_set))
{
if (!view->is_mapped() || !toplevel_cast(view))
{
continue;
}

if (!view_has_data(view))
{
continue;
}

auto last_output_info = view_get_data(toplevel_cast(view));
if (last_output_info->output_identifier == identifier)
{
views.push_back(wf::toplevel_cast(view));
}
// The wset was moved to a different output => We should leave it where it is
LOGD("Saved workspace for ", output->to_string(), " has been remapped to another output.");
return;
}

// Sorts with the views closest to front last
std::sort(views.begin(), views.end(),
[=] (wayfire_toplevel_view & view1, wayfire_toplevel_view & view2)
{
return view_get_data(view1)->z_order > view_get_data(view2)->z_order;
});

// Move views to this output
for (auto& view : views)
LOGD("Restoring workspace set ", data.workspace_set->get_index(), " to output ", output->to_string());
output->set_workspace_set(data.workspace_set);
if (data.was_focused && !focused_output_expired(data))
{
auto last_output_info = view_get_data(view);
LOGD("Restoring view: ",
view->get_title(), " to: ", output->to_string());

move_view_to_output(view, output, false);
view->toplevel()->pending().fullscreen = last_output_info->fullscreen;
view->set_minimized(last_output_info->minimized);
if (last_output_info->tiled_edges != 0)
{
wf::get_core().default_wm->tile_request(view, last_output_info->tiled_edges);
}

view->set_geometry(last_output_info->geometry);

// Focus
if (last_output_info->focused)
{
LOGD("Focusing view: ", view->get_title());
output->focus_view(view, false);
}

// Z Order
wf::view_bring_to_front(view);

// Remove all last output info from views
view_erase_data(view);
wf::get_core().focus_output(output);
}

// Start listening for view resize events AFTER this callback has finished
output->connect(&view_moved);
saved_outputs.erase(ident);
}

wf::signal::connection_t<wf::view_geometry_changed_signal> view_moved =
[=] (wf::view_geometry_changed_signal *signal_data)
wf::signal::connection_t<output_pre_remove_signal> output_pre_remove = [=] (output_pre_remove_signal *ev)
{
// Triggered whenever a view on this output's geometry changed
auto view = signal_data->view;

// Ignore event if geometry didn't actually change
if (signal_data->old_geometry == view->get_geometry())
if (wlr_output_is_headless(ev->output->handle))
{
// For example, NOOP-1
return;
}

if (view_has_data(view))
if (wf::get_core().get_current_state() == compositor_state_t::RUNNING)
{
// Remove a view's last output info if it is deliberately moved
// by user
if (!outputs_being_removed)
{
LOGD("View moved, deleting last output info for: ",
view->get_title());
view_erase_data(view);
}
LOGD("Received pre-remove event: ", ev->output->to_string());
save_output(ev->output);
}
};

wf::wl_idle_call idle_restore_views;

public:
void init() override
wf::signal::connection_t<output_added_signal> on_new_output = [=] (output_added_signal *ev)
{
if (wlr_output_is_headless(output->handle))
if (wlr_output_is_headless(ev->output->handle))
{
// Don't do anything for NO-OP outputs
// For example, NOOP-1
return;
}

// Call restore_views_to_output() after returning to main loop
idle_restore_views.run_once([=] ()
{
restore_views_to_output();
});

wf::get_core().output_layout->connect(&output_pre_remove);
wf::get_core().output_layout->connect(&output_removed);
}
try_restore_output(ev->output);
};

void fini() override
public:
void init() override
{
// Nothing to do
wf::get_core().output_layout->connect(&on_new_output);
wf::get_core().output_layout->connect(&output_pre_remove);
}
};
}
}

DECLARE_WAYFIRE_PLUGIN(wf::per_output_plugin_t<wayfire_preserve_output>);
DECLARE_WAYFIRE_PLUGIN(wf::preserve_output::preserve_output_t);

0 comments on commit f616a42

Please sign in to comment.