-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
preserve-output: rewrite plugin to make use of workspace sets
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
Showing
1 changed file
with
67 additions
and
269 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |