--- 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 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() : 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 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(); + 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()) + { + 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>(existing)) + { + auto option = std::make_shared>( + 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 @@ + +