diff --git a/metadata/meson.build b/metadata/meson.build index 57174c3..4d0934c 100644 --- a/metadata/meson.build +++ b/metadata/meson.build @@ -13,6 +13,7 @@ install_data('mag.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadi install_data('showrepaint.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir')) install_data('view-shot.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir')) install_data('water.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir')) +install_data('window-swallow.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir')) install_data('window-zoom.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir')) install_data('workspace-names.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir')) install_data('hinge.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir')) diff --git a/metadata/window-swallow.xml b/metadata/window-swallow.xml new file mode 100644 index 0000000..dac5783 --- /dev/null +++ b/metadata/window-swallow.xml @@ -0,0 +1,12 @@ + + + + <_short>Window Swallow + Window Management + + + diff --git a/src/meson.build b/src/meson.build index 6177a49..dcce95b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -66,6 +66,10 @@ water = shared_module('water', 'water.cpp', dependencies: [wayfire], install: true, install_dir: join_paths(get_option('libdir'), 'wayfire')) +window_swallow = shared_module('window-swallow', 'window-swallow.cpp', + dependencies: [wayfire], + install: true, install_dir: join_paths(get_option('libdir'), 'wayfire')) + window_zoom = shared_module('winzoom', 'window-zoom.cpp', dependencies: [wayfire], install: true, install_dir: join_paths(get_option('libdir'), 'wayfire')) diff --git a/src/window-swallow.cpp b/src/window-swallow.cpp new file mode 100644 index 0000000..8ea5d9d --- /dev/null +++ b/src/window-swallow.cpp @@ -0,0 +1,289 @@ +/* + * Copyright © 2023 Scott Moreau + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include + + +namespace wayfire_window_swallow +{ +std::map>> swallowed_views; +std::map swallowed_geometries; +wayfire_view last_focus_view = nullptr; +wayfire_view current_focus_view = nullptr; + +/* Hack: When we swallow a view, we want the size to match the swallower size, + * so we call set_seometry() to set it. However, the geometry might be changed + * if the swallowed view has server side decorations, because adding decoration + * triggers a set_geometry() call as well. However it sets it to the size of + * the current geometry and not the geometry of what we intended. So to work + * around this problem, we set a bool to true on mapped and set it to false + * a short time later. Then if the geometry was changed right after we set it, + * change it back to the geometry we intended. */ +wf::wl_idle_call idle_set_geometry; +wf::wl_timer no_longer_newly_mapped; +bool newly_mapped = false; + +wf::view_matcher_t swallower_views{"window-swallow/swallower_views"}; + +class window_swallow : public wf::per_output_plugin_instance_t +{ + private: + + wf::signal::connection_t view_focused = [=] (wf::focus_view_signal *ev) + { + if (!ev->view) + { + return; + } + + if (swallower_views.matches(ev->view)) + { + if (current_focus_view && swallower_views.matches(current_focus_view)) + { + last_focus_view = current_focus_view; + } + + current_focus_view = ev->view; + } + }; + + void hide_view(wayfire_view hiding, wayfire_view swallowed) + { + if (!hiding || !swallowed) + { + return; + } + + output->workspace->remove_view(hiding); + std::vector> v; + try { + v = swallowed_views.at(hiding); + } catch (const std::out_of_range&) + {} + + std::pair p(hiding, hiding->get_wm_geometry()); + v.push_back(p); + swallowed_views[swallowed] = v; + swallowed->connect(&view_geometry_changed); + hiding->set_output(nullptr); + } + + wf::signal::connection_t view_geometry_changed{[this] (wf:: + view_geometry_changed_signal + *ev) + { + auto view = ev->view; + wf::geometry_t g; + try { + g = swallowed_geometries.at(view); + } catch (const std::out_of_range&) + { + return; + } + + if (newly_mapped) + { + idle_set_geometry.run_once([=] () + { + view->disconnect(&view_geometry_changed); + view->set_geometry(g); + view->connect(&view_geometry_changed); + }); + } else + { + swallowed_geometries[view] = view->get_wm_geometry(); + } + } + }; + + wf::signal::connection_t view_mapped{[this] (wf::view_mapped_signal *ev) + { + auto view = ev->view; + + if (last_focus_view) + { + current_focus_view = last_focus_view; + last_focus_view = nullptr; + } + + if ((view == current_focus_view) || !view || (view->role != wf::VIEW_ROLE_TOPLEVEL) || + !swallower_views.matches(current_focus_view)) + { + return; + } + + /* swallow */ + if (current_focus_view->get_decoration()) + { + auto g1 = current_focus_view->get_wm_geometry(); + auto g2 = current_focus_view->get_decoration()->expand_wm_geometry( + current_focus_view->get_wm_geometry()); + + view->set_geometry({g2.x - (g2.x - g1.x), g2.y - (g2.y - g1.y), g1.width, g1.height}); + swallowed_geometries[view] = + {g2.x - (g2.x - g1.x), g2.y - (g2.y - g1.y), g1.width, g1.height}; + } else + { + view->set_geometry(current_focus_view->get_wm_geometry()); + swallowed_geometries[view] = current_focus_view->get_wm_geometry(); + } + + ev->is_positioned = true; + hide_view(current_focus_view, view); + current_focus_view = view; + newly_mapped = true; + no_longer_newly_mapped.disconnect(); + no_longer_newly_mapped.set_timeout(250, [=] () + { + newly_mapped = false; + return false; // disconnect + }); + } + }; + + wayfire_view unhide_view(std::vector> & v, wayfire_view swallowed) + { + auto p = v.back(); + auto unhiding = p.first; + unhiding->set_output(output); + wf::get_core().move_view_to_output(unhiding, output, true); + output->workspace->add_view(unhiding, wf::LAYER_WORKSPACE); + auto g = p.second; + auto saved_geometry = g; + try { + saved_geometry = swallowed_geometries.at(swallowed); + swallowed_geometries.erase(swallowed_geometries.find(swallowed)); + } catch (const std::out_of_range&) + {} + + g.x = saved_geometry.x; + g.y = saved_geometry.y; + if (g.width != saved_geometry.width) + { + g.x += (saved_geometry.width - g.width) / 2; + } + + if (g.height != saved_geometry.height) + { + g.y += (saved_geometry.height - g.height) / 2; + } + + unhiding->set_geometry(g); + current_focus_view = unhiding; + v.erase(std::remove(v.begin(), v.end(), p), v.end()); + if (!v.empty()) + { + swallowed_views[unhiding] = v; + } + + return unhiding; + } + + void prune() + { + auto all_views = wf::get_core().get_all_views(); + for (auto & [swallowed, v] : swallowed_views) + { + for (auto & p : v) + { + if (std::find(all_views.begin(), all_views.end(), p.first) == all_views.end()) + { + /* Prune view in swallowed_views but not in all_views */ + v.erase(std::remove(v.begin(), v.end(), p), v.end()); + } + } + } + } + + wf::signal::connection_t view_unmapped{[this] (wf::view_unmapped_signal *ev) + { + auto view = ev->view; + + if (view == current_focus_view) + { + current_focus_view = nullptr; + } + + if (view == last_focus_view) + { + last_focus_view = nullptr; + } + + if (!view) + { + return; + } + + prune(); + + std::vector> v; + try { + v = swallowed_views.at(view); + } catch (const std::out_of_range&) + { + return; + } + + if (v.empty()) + { + swallowed_views.erase(swallowed_views.find(view)); + return; + } + + /* unswallow */ + unhide_view(v, view); + swallowed_views.erase(swallowed_views.find(view)); + } + }; + + public: + void init() override + { + output->connect(&view_mapped); + output->connect(&view_focused); + output->connect(&view_unmapped); + } + + void fini() override + { + for (auto & [s, hidden_views] : swallowed_views) + { + auto swallowed = s; + while (!hidden_views.empty()) + { + swallowed = unhide_view(hidden_views, swallowed); + } + } + + swallowed_views.clear(); + + view_mapped.disconnect(); + view_focused.disconnect(); + view_unmapped.disconnect(); + view_geometry_changed.disconnect(); + } +}; + +DECLARE_WAYFIRE_PLUGIN(wf::per_output_plugin_t); +}