--- a/plugins/ipc-rules/ipc-rules.cpp +++ b/plugins/ipc-rules/ipc-rules.cpp @@ -17,6 +17,7 @@ #include "ipc-input-methods.hpp" #include "ipc-utility-methods.hpp" #include "ipc-events.hpp" +#include "ipc-picker.hpp" class ipc_rules_t : public wf::plugin_interface_t, public wf::ipc_rules_input_methods_t, @@ -60,10 +61,12 @@ init_input_methods(method_repository.get()); init_utility_methods(method_repository.get()); init_events(method_repository.get()); + picker.init(method_repository.get()); } void fini() override { + picker.fini(); method_repository->unregister_method("window-rules/set-window-scope"); method_repository->unregister_method("window-rules/list-views"); method_repository->unregister_method("window-rules/list-outputs"); @@ -76,7 +79,7 @@ method_repository->unregister_method("window-rules/focus-view"); method_repository->unregister_method("window-rules/get-focused-view"); method_repository->unregister_method("window-rules/get-focused-output"); - method_repository->unregister_method("window-rules/get-cursor-position"); + method_repository->unregister_method("window-rules/get_cursor_position"); method_repository->unregister_method("window-rules/close-view"); method_repository->unregister_method("window-rules/set-view-property"); method_repository->unregister_method("window-rules/get-view-property"); @@ -451,6 +454,7 @@ }; private: + ipc_rules_picker_t picker; wf::shared_data::ref_ptr_t method_repository; }; --- a/plugins/ipc-rules/ipc-rules-common.hpp +++ b/plugins/ipc-rules/ipc-rules-common.hpp @@ -230,6 +230,13 @@ description["max-size"] = wf::ipc::dimensions_to_json( toplevel ? toplevel->toplevel()->get_max_size() : wf::dimensions_t{0, 0}); description["focusable"] = view->is_focusable(); + description["focused"] = output && wf::get_active_view_for_output(output) == view; + bool xwayland = false; +#if WF_HAS_XWAYLAND + auto surface = view->get_wlr_surface(); + xwayland = surface && wlr_xwayland_surface_try_from_wlr_surface(surface); +#endif + description["xwayland"] = xwayland; description["type"] = get_view_type(view); description["always-on-top"] = view->has_data("wm-actions-above"); --- a/plugins/wm-actions/wm-actions.cpp +++ b/plugins/wm-actions/wm-actions.cpp @@ -480,6 +480,7 @@ { init_output_tracking(); ipc_repo->register_method("wm-actions/set-minimized", ipc_minimize); + ipc_repo->register_method("wm-actions/set-maximized", ipc_maximize); ipc_repo->register_method("wm-actions/set-always-on-top", ipc_set_always_on_top); ipc_repo->register_method("wm-actions/set-fullscreen", ipc_set_fullscreen); ipc_repo->register_method("wm-actions/set-sticky", ipc_set_sticky); @@ -492,6 +493,7 @@ { fini_output_tracking(); ipc_repo->unregister_method("wm-actions/set-minimized"); + ipc_repo->unregister_method("wm-actions/set-maximized"); ipc_repo->unregister_method("wm-actions/set-always-on-top"); ipc_repo->unregister_method("wm-actions/set-fullscreen"); ipc_repo->unregister_method("wm-actions/set-sticky"); --- /dev/null +++ b/plugins/ipc-rules/ipc-picker.hpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT +// Asynchronous window selection for the window-rules IPC API. +#pragma once +#include +#include +#include +#include +#include +#include +#include "ipc-rules-common.hpp" + +namespace +{ +class ipc_rules_picker_t : public wf::pointer_interaction_t, public wf::keyboard_interaction_t +{ + wf::ipc::method_repository_t *repo = nullptr; + wf::ipc::client_interface_t *picker = nullptr; + std::map> grabs; + wf::plugin_activation_data_t activation{ + .name = "ipc-rules-pick", .capabilities = wf::CAPABILITY_GRAB_INPUT, + }; + wf::wl_idle_call finish_idle; + wf::wl_timer pick_timeout; + bool completing = false; + + void release_grabs() + { + for (auto& [output, grab] : grabs) + { + grab->ungrab_input(); + output->deactivate_plugin(&activation); + } + grabs.clear(); + wf::get_core().set_cursor("default"); + } + + void cancel_pick(const std::string& reason, bool send = true) + { + auto client = picker; + picker = nullptr; + finish_idle.disconnect(); + pick_timeout.disconnect(); + completing = false; + if (!grabs.empty()) release_grabs(); + if (client && send) client->send_json(wf::ipc::json_error(reason)); + } + + void finish_pick() + { + if (!picker || completing) return; + completing = true; + // Consume both halves of the selection click; release the scene grab + // outside its event callback before finding the underlying surface. + auto position = wf::get_core().get_cursor_position(); + finish_idle.run_once([this, position] + { + auto client = picker; + picker = nullptr; + pick_timeout.disconnect(); + release_grabs(); + completing = false; + auto hit = wf::get_core().scene()->find_node_at(position); + auto view = hit ? wf::node_to_view(hit->node.get()) : nullptr; + wf::json_t result = wf::ipc::json_error("No window at the selected position"); + if (view) + { + result = wf::ipc::json_ok(); + result["info"] = wf::ipc_rules::view_to_json(view); + } + if (client) client->send_json(result); + }); + } + + wf::signal::connection_t disconnected = + [this] (auto *event) + { + if (event->client == picker) cancel_pick("Selection client disconnected", false); + }; + wf::signal::connection_t output_removed = + [this] (auto*) { if (picker) cancel_pick("Output removed during selection"); }; + + wf::json_t begin_pick(wf::json_t data, wf::ipc::client_interface_t *client) + { + if (!client) return wf::ipc::json_error("Selection requires an IPC connection"); + if (picker) return wf::ipc::json_error("Another window selection is active"); + auto timeout = wf::ipc::json_get_optional_uint64(data, "timeout-ms").value_or(60000); + if (!timeout || timeout > 300000) return wf::ipc::json_error("Invalid selection timeout"); + for (auto output : wf::get_core().output_layout->get_outputs()) + { + if (!output->activate_plugin(&activation)) + { + release_grabs(); + return wf::ipc::json_error("Output is busy; cannot select a window"); + } + auto grab = std::make_unique(activation.name, output, this, this); + grab->grab_input(wf::scene::layer::OVERLAY); + grabs[output] = std::move(grab); + } + if (grabs.empty()) return wf::ipc::json_error("No outputs available"); + picker = client; + wf::get_core().set_cursor("crosshair"); + pick_timeout.set_timeout(timeout, [this] { cancel_pick("Window selection timed out"); }); + auto result = wf::ipc::json_ok(); + result["pending"] = true; + return result; + } + + public: + void handle_pointer_button(const wlr_pointer_button_event& event) override + { + if (event.button == BTN_LEFT && event.state == WL_POINTER_BUTTON_STATE_RELEASED) finish_pick(); + else if (event.button == BTN_RIGHT && event.state == WL_POINTER_BUTTON_STATE_RELEASED) + finish_idle.run_once([this] { cancel_pick("Window selection cancelled"); }); + } + + void handle_keyboard_key(wf::seat_t*, wlr_keyboard_key_event event) override + { + if (event.keycode == KEY_ESC && event.state == WL_KEYBOARD_KEY_STATE_RELEASED) + finish_idle.run_once([this] { cancel_pick("Window selection cancelled"); }); + } + + void init(wf::ipc::method_repository_t *repository) + { + repo = repository; + repo->connect(&disconnected); + wf::get_core().output_layout->connect(&output_removed); + repo->register_method("window-rules/pick-view", + [this] (auto data, auto *client) { return begin_pick(data, client); }); + } + + void fini() + { + cancel_pick("ipc-rules plugin unloaded"); + disconnected.disconnect(); + output_removed.disconnect(); + repo->unregister_method("window-rules/pick-view"); + } +}; +} --- a/plugins/ipc-rules/meson.build +++ b/plugins/ipc-rules/meson.build @@ -4,6 +4,8 @@ shared_module('ipc-rules', ['ipc-rules.cpp'], include_directories: all_include_dirs, dependencies: all_deps, + link_args: ['-Wl,--version-script=' + meson.project_source_root() / 'plugins/common/plugin-exports.map'], + link_depends: '../../plugins/common/plugin-exports.map', install: true, install_dir: conf_data.get('PLUGIN_PATH')) --- a/plugins/wm-actions/meson.build +++ b/plugins/wm-actions/meson.build @@ -2,6 +2,8 @@ ['wm-actions.cpp'], include_directories: [wayfire_api_inc, wayfire_conf_inc, plugins_common_inc, ipc_include_dirs], dependencies: [wlroots, pixman, wfconfig, json, plugin_pch_dep], + link_args: ['-Wl,--version-script=' + meson.project_source_root() / 'plugins/common/plugin-exports.map'], + link_depends: '../../plugins/common/plugin-exports.map', install: true, install_dir: join_paths(get_option('libdir'), 'wayfire')) --- /dev/null +++ b/plugins/common/plugin-exports.map @@ -0,0 +1,4 @@ +{ + global: newInstance; getWayfireVersion; + local: *; +};