1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
--- a/plugins/grid/grid.cpp
+++ b/plugins/grid/grid.cpp
@@ -85,6 +85,7 @@
wf::ipc_activator_t toggle_scope{"grid/toggle_scope"};
wf::ipc_activator_t use_virtual{"grid/use_virtual"}, use_physical{"grid/use_physical"},
reset_scope{"grid/reset_scope"}, next_monitor{"grid/next_monitor"};
+ wf::ipc_activator_t left_monitor, right_monitor;
wf::option_wrapper_t<std::string> default_scope{"core/window_scope"}, scope_rules{"core/window_scope_rules"};
wf::geometry_t slot_dimensions(wayfire_toplevel_view view, int n)
{
@@ -200,10 +201,117 @@
adjust_for_workspace(get_view_wset(view), zone.geometry, workspace), wf::TILED_EDGES_ALL);
}
+ void translate_floating_family(wayfire_toplevel_view view, wf::pointf_t delta)
+ {
+ if (!view->is_mapped()) return;
+ if (!view->pending_tiled_edges() && !view->pending_fullscreen())
+ view->set_geometry(view->get_pending_geometry() + delta);
+ for (auto child : view->children) translate_floating_family(child, delta);
+ }
+
+ bool move_to_monitor(wayfire_view focused, int direction)
+ {
+ auto top = wf::toplevel_cast(wf::find_topmost_parent(focused));
+ auto output = top ? top->get_output() : nullptr;
+ auto layout = output ? output->get_data<wf::output_stitch_layout_t>() : nullptr;
+ auto wset = top ? top->get_wset() : nullptr;
+ if (!top || !top->is_mapped() || !layout || layout->zones.size() < 2 ||
+ !wset || wset != output->wset() ||
+ wset->get_view_main_workspace(top) != wset->get_current_workspace() ||
+ !output->can_activate_plugin(&grab_interface)) return false;
+
+ auto current = wf::window_scope_zone(top);
+ if (!current)
+ {
+ // A VIRTUAL window has no selected physical region. Use its
+ // center, independently of which monitor contains the pointer.
+ auto geometry = top->get_pending_geometry();
+ wf::pointf_t center{geometry.x + geometry.width / 2, geometry.y + geometry.height / 2};
+ for (auto& zone : layout->zones)
+ if (center.x >= zone.geometry.x && center.x < zone.geometry.x + zone.geometry.width &&
+ center.y >= zone.geometry.y && center.y < zone.geometry.y + zone.geometry.height)
+ current = zone;
+ }
+ if (!current) current = zone_for_window(top);
+ if (!current) return false;
+
+ std::optional<wf::output_stitch_zone_t> destination;
+ double nearest = INFINITY;
+ double origin = current->geometry.x + current->geometry.width / 2;
+ for (auto& zone : layout->zones)
+ {
+ double distance = direction * (zone.geometry.x + zone.geometry.width / 2 - origin);
+ double vertical_overlap = std::min(zone.geometry.y + zone.geometry.height,
+ current->geometry.y + current->geometry.height) - std::max(zone.geometry.y, current->geometry.y);
+ if (zone.name != current->name && vertical_overlap > 0 && distance > 0 && distance < nearest)
+ {
+ destination = zone;
+ nearest = distance;
+ }
+ }
+ // Window movement stops at the edges of this face.
+ if (!destination) return true;
+ auto source_area = find_zone(output, current->name);
+ auto destination_area = find_zone(output, destination->name);
+ if (!source_area || !destination_area) return false;
+
+ const auto workspace = wset->get_current_workspace();
+ const auto policy = wf::window_scope_policy(top);
+ auto slot = top->get_data<wf_grid_slot_data>();
+ if (slot && !slot->zone.empty()) slot->zone = destination->name;
+ translate_floating_family(top, wf::origin(destination_area->geometry) - wf::origin(source_area->geometry));
+ if (policy == "physical")
+ {
+ wf::select_window_scope_monitor(top, destination->name);
+ wf::apply_window_scope(top, workspace);
+ } else
+ {
+ // An explicit move selects physical scope for a desktop-wide or
+ // connector-pinned window without changing application rules.
+ wf::set_window_scope(top, destination->name);
+ wf::set_window_scope(top, "physical");
+ }
+ if (auto remembered = top->get_data<grid_scope_toggle_data_t>())
+ {
+ remembered->monitor = destination->name;
+ remembered->physical_policy = "physical";
+ }
+
+ auto active = wf::get_core().seat->get_active_view();
+ if (wf::find_topmost_parent(active) == top)
+ {
+ auto box = top->get_pending_geometry();
+ auto area = destination_area->geometry;
+ auto global = output->get_layout_geometry();
+ wf::get_core().warp_cursor({global.x + std::clamp(box.x + box.width / 2, area.x, area.x + area.width - 1),
+ global.y + std::clamp(box.y + box.height / 2, area.y, area.y + area.height - 1)});
+ wf::get_core().default_wm->focus_request(active);
+ }
+ return true;
+ }
+
public:
void init() override
{
init_output_tracking();
+ // Older metadata leaves new ini options untyped, or absent. Promote
+ // them before loading activators, preserving any configured binding.
+ auto section = wf::get_core().config->get_section("grid");
+ for (const auto& name : {"left_monitor", "right_monitor"})
+ {
+ auto existing = section->get_option_or(name);
+ if (!std::dynamic_pointer_cast<wf::config::option_t<wf::activatorbinding_t>>(existing))
+ {
+ auto option = std::make_shared<wf::config::option_t<wf::activatorbinding_t>>(
+ name, wf::activatorbinding_t{});
+ if (existing) option->set_value_str(existing->get_value_str());
+ section->register_new_option(option);
+ }
+ }
+ left_monitor.load_from_xml_option("grid/left_monitor");
+ right_monitor.load_from_xml_option("grid/right_monitor");
+ left_monitor.set_handler([this] (wf::output_t*, wayfire_view view) { return move_to_monitor(view, -1); });
+ right_monitor.set_handler([this] (wf::output_t*, wayfire_view view) { return move_to_monitor(view, 1); });
default_scope.set_callback([this] { reset_policy_zones(); });
scope_rules.set_callback([this] { reset_policy_zones(); });
auto scope_handler = [this] (std::string scope)
--- a/metadata/grid.xml
+++ b/metadata/grid.xml
@@ -12,6 +12,8 @@
<option name="use_virtual" type="activator"><_short>Use VIRTUAL for this window</_short><_long>Override this window's scope to span the combined desktop.</_long><default></default></option>
<option name="use_physical" type="activator"><_short>Use physical monitor for this window</_short><_long>Override this window's scope to its current physical monitor.</_long><default></default></option>
<option name="reset_scope" type="activator"><_short>Restore application scope policy</_short><_long>Remove this window's manual scope override.</_long><default></default></option>
+ <option name="left_monitor" type="activator"><_short>Move window to left monitor</_short><_long>Move the focused window and its dialogs to the adjacent physical monitor on the current workspace. Preserve floating placement, snapped positions, tiling and fullscreen. Stop at the outer monitor.</_long><default></default></option>
+ <option name="right_monitor" type="activator"><_short>Move window to right monitor</_short><_long>Move the focused window and its dialogs to the adjacent physical monitor on the current workspace. Preserve floating placement, snapped positions, tiling and fullscreen. Stop at the outer monitor.</_long><default></default></option>
<option name="next_monitor" type="activator"><_short>Move to next physical monitor</_short><_long>Move this window to the next member of the combined desktop and constrain it there.</_long><default></default></option>
<option name="zone_maximize" type="activator">
<_short>Maximize within a monitor zone</_short>
|