Skip to content

Commit 058e647

Browse files
committed
Add window swallow plugin
1 parent 619bcae commit 058e647

File tree

4 files changed

+306
-0
lines changed

4 files changed

+306
-0
lines changed

Diff for: metadata/meson.build

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ install_data('mag.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadi
1313
install_data('showrepaint.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir'))
1414
install_data('view-shot.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir'))
1515
install_data('water.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir'))
16+
install_data('window-swallow.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir'))
1617
install_data('window-zoom.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir'))
1718
install_data('workspace-names.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir'))
1819
install_data('hinge.xml', install_dir: wayfire.get_variable(pkgconfig: 'metadatadir'))

Diff for: metadata/window-swallow.xml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0"?>
2+
<wayfire>
3+
<plugin name="window-swallow">
4+
<_short>Window Swallow</_short>
5+
<category>Window Management</category>
6+
<option name="swallower_views" type="string">
7+
<_short>Swallower Views</_short>
8+
<_long>Views matching this criteria will swallow other mapped views when focused.</_long>
9+
<default>app_id is "foot"</default>
10+
</option>
11+
</plugin>
12+
</wayfire>

Diff for: src/meson.build

+4
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ water = shared_module('water', 'water.cpp',
6666
dependencies: [wayfire],
6767
install: true, install_dir: join_paths(get_option('libdir'), 'wayfire'))
6868

69+
window_swallow = shared_module('window-swallow', 'window-swallow.cpp',
70+
dependencies: [wayfire],
71+
install: true, install_dir: join_paths(get_option('libdir'), 'wayfire'))
72+
6973
window_zoom = shared_module('winzoom', 'window-zoom.cpp',
7074
dependencies: [wayfire],
7175
install: true, install_dir: join_paths(get_option('libdir'), 'wayfire'))

Diff for: src/window-swallow.cpp

+289
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
/*
2+
* Copyright © 2023 Scott Moreau
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining
5+
* a copy of this software and associated documentation files (the "Software"),
6+
* to deal in the Software without restriction, including without limitation
7+
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
8+
* and/or sell copies of the Software, and to permit persons to whom the
9+
* Software is furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included
12+
* in all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
16+
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17+
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18+
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19+
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
20+
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21+
*/
22+
23+
#include <wayfire/per-output-plugin.hpp>
24+
#include <wayfire/workspace-manager.hpp>
25+
#include <wayfire/matcher.hpp>
26+
27+
28+
namespace wayfire_window_swallow
29+
{
30+
std::map<wayfire_view, std::vector<std::pair<wayfire_view, wf::geometry_t>>> swallowed_views;
31+
std::map<wayfire_view, wf::geometry_t> swallowed_geometries;
32+
wayfire_view last_focus_view = nullptr;
33+
wayfire_view current_focus_view = nullptr;
34+
35+
/* Hack: When we swallow a view, we want the size to match the swallower size,
36+
* so we call set_seometry() to set it. However, the geometry might be changed
37+
* if the swallowed view has server side decorations, because adding decoration
38+
* triggers a set_geometry() call as well. However it sets it to the size of
39+
* the current geometry and not the geometry of what we intended. So to work
40+
* around this problem, we set a bool to true on mapped and set it to false
41+
* a short time later. Then if the geometry was changed right after we set it,
42+
* change it back to the geometry we intended. */
43+
wf::wl_idle_call idle_set_geometry;
44+
wf::wl_timer no_longer_newly_mapped;
45+
bool newly_mapped = false;
46+
47+
wf::view_matcher_t swallower_views{"window-swallow/swallower_views"};
48+
49+
class window_swallow : public wf::per_output_plugin_instance_t
50+
{
51+
private:
52+
53+
wf::signal::connection_t<wf::focus_view_signal> view_focused = [=] (wf::focus_view_signal *ev)
54+
{
55+
if (!ev->view)
56+
{
57+
return;
58+
}
59+
60+
if (swallower_views.matches(ev->view))
61+
{
62+
if (current_focus_view && swallower_views.matches(current_focus_view))
63+
{
64+
last_focus_view = current_focus_view;
65+
}
66+
67+
current_focus_view = ev->view;
68+
}
69+
};
70+
71+
void hide_view(wayfire_view hiding, wayfire_view swallowed)
72+
{
73+
if (!hiding || !swallowed)
74+
{
75+
return;
76+
}
77+
78+
output->workspace->remove_view(hiding);
79+
std::vector<std::pair<wayfire_view, wf::geometry_t>> v;
80+
try {
81+
v = swallowed_views.at(hiding);
82+
} catch (const std::out_of_range&)
83+
{}
84+
85+
std::pair<wayfire_view, wf::geometry_t> p(hiding, hiding->get_wm_geometry());
86+
v.push_back(p);
87+
swallowed_views[swallowed] = v;
88+
swallowed->connect(&view_geometry_changed);
89+
hiding->set_output(nullptr);
90+
}
91+
92+
wf::signal::connection_t<wf::view_geometry_changed_signal> view_geometry_changed{[this] (wf::
93+
view_geometry_changed_signal
94+
*ev)
95+
{
96+
auto view = ev->view;
97+
wf::geometry_t g;
98+
try {
99+
g = swallowed_geometries.at(view);
100+
} catch (const std::out_of_range&)
101+
{
102+
return;
103+
}
104+
105+
if (newly_mapped)
106+
{
107+
idle_set_geometry.run_once([=] ()
108+
{
109+
view->disconnect(&view_geometry_changed);
110+
view->set_geometry(g);
111+
view->connect(&view_geometry_changed);
112+
});
113+
} else
114+
{
115+
swallowed_geometries[view] = view->get_wm_geometry();
116+
}
117+
}
118+
};
119+
120+
wf::signal::connection_t<wf::view_mapped_signal> view_mapped{[this] (wf::view_mapped_signal *ev)
121+
{
122+
auto view = ev->view;
123+
124+
if (last_focus_view)
125+
{
126+
current_focus_view = last_focus_view;
127+
last_focus_view = nullptr;
128+
}
129+
130+
if ((view == current_focus_view) || !view || (view->role != wf::VIEW_ROLE_TOPLEVEL) ||
131+
!swallower_views.matches(current_focus_view))
132+
{
133+
return;
134+
}
135+
136+
/* swallow */
137+
if (current_focus_view->get_decoration())
138+
{
139+
auto g1 = current_focus_view->get_wm_geometry();
140+
auto g2 = current_focus_view->get_decoration()->expand_wm_geometry(
141+
current_focus_view->get_wm_geometry());
142+
143+
view->set_geometry({g2.x - (g2.x - g1.x), g2.y - (g2.y - g1.y), g1.width, g1.height});
144+
swallowed_geometries[view] =
145+
{g2.x - (g2.x - g1.x), g2.y - (g2.y - g1.y), g1.width, g1.height};
146+
} else
147+
{
148+
view->set_geometry(current_focus_view->get_wm_geometry());
149+
swallowed_geometries[view] = current_focus_view->get_wm_geometry();
150+
}
151+
152+
ev->is_positioned = true;
153+
hide_view(current_focus_view, view);
154+
current_focus_view = view;
155+
newly_mapped = true;
156+
no_longer_newly_mapped.disconnect();
157+
no_longer_newly_mapped.set_timeout(250, [=] ()
158+
{
159+
newly_mapped = false;
160+
return false; // disconnect
161+
});
162+
}
163+
};
164+
165+
wayfire_view unhide_view(std::vector<std::pair<wayfire_view, wf::geometry_t>> & v, wayfire_view swallowed)
166+
{
167+
auto p = v.back();
168+
auto unhiding = p.first;
169+
unhiding->set_output(output);
170+
wf::get_core().move_view_to_output(unhiding, output, true);
171+
output->workspace->add_view(unhiding, wf::LAYER_WORKSPACE);
172+
auto g = p.second;
173+
auto saved_geometry = g;
174+
try {
175+
saved_geometry = swallowed_geometries.at(swallowed);
176+
swallowed_geometries.erase(swallowed_geometries.find(swallowed));
177+
} catch (const std::out_of_range&)
178+
{}
179+
180+
g.x = saved_geometry.x;
181+
g.y = saved_geometry.y;
182+
if (g.width != saved_geometry.width)
183+
{
184+
g.x += (saved_geometry.width - g.width) / 2;
185+
}
186+
187+
if (g.height != saved_geometry.height)
188+
{
189+
g.y += (saved_geometry.height - g.height) / 2;
190+
}
191+
192+
unhiding->set_geometry(g);
193+
current_focus_view = unhiding;
194+
v.erase(std::remove(v.begin(), v.end(), p), v.end());
195+
if (!v.empty())
196+
{
197+
swallowed_views[unhiding] = v;
198+
}
199+
200+
return unhiding;
201+
}
202+
203+
void prune()
204+
{
205+
auto all_views = wf::get_core().get_all_views();
206+
for (auto & [swallowed, v] : swallowed_views)
207+
{
208+
for (auto & p : v)
209+
{
210+
if (std::find(all_views.begin(), all_views.end(), p.first) == all_views.end())
211+
{
212+
/* Prune view in swallowed_views but not in all_views */
213+
v.erase(std::remove(v.begin(), v.end(), p), v.end());
214+
}
215+
}
216+
}
217+
}
218+
219+
wf::signal::connection_t<wf::view_unmapped_signal> view_unmapped{[this] (wf::view_unmapped_signal *ev)
220+
{
221+
auto view = ev->view;
222+
223+
if (view == current_focus_view)
224+
{
225+
current_focus_view = nullptr;
226+
}
227+
228+
if (view == last_focus_view)
229+
{
230+
last_focus_view = nullptr;
231+
}
232+
233+
if (!view)
234+
{
235+
return;
236+
}
237+
238+
prune();
239+
240+
std::vector<std::pair<wayfire_view, wf::geometry_t>> v;
241+
try {
242+
v = swallowed_views.at(view);
243+
} catch (const std::out_of_range&)
244+
{
245+
return;
246+
}
247+
248+
if (v.empty())
249+
{
250+
swallowed_views.erase(swallowed_views.find(view));
251+
return;
252+
}
253+
254+
/* unswallow */
255+
unhide_view(v, view);
256+
swallowed_views.erase(swallowed_views.find(view));
257+
}
258+
};
259+
260+
public:
261+
void init() override
262+
{
263+
output->connect(&view_mapped);
264+
output->connect(&view_focused);
265+
output->connect(&view_unmapped);
266+
}
267+
268+
void fini() override
269+
{
270+
for (auto & [s, hidden_views] : swallowed_views)
271+
{
272+
auto swallowed = s;
273+
while (!hidden_views.empty())
274+
{
275+
swallowed = unhide_view(hidden_views, swallowed);
276+
}
277+
}
278+
279+
swallowed_views.clear();
280+
281+
view_mapped.disconnect();
282+
view_focused.disconnect();
283+
view_unmapped.disconnect();
284+
view_geometry_changed.disconnect();
285+
}
286+
};
287+
288+
DECLARE_WAYFIRE_PLUGIN(wf::per_output_plugin_t<window_swallow>);
289+
}

0 commit comments

Comments
 (0)