diff --git a/src/core/seat/input-method-relay.cpp b/src/core/seat/input-method-relay.cpp index a4561403d..a341ed376 100644 --- a/src/core/seat/input-method-relay.cpp +++ b/src/core/seat/input-method-relay.cpp @@ -1,9 +1,12 @@ #include #include "input-method-relay.hpp" #include "../core-impl.hpp" -#include "../../output/output-impl.hpp" +#include "../../view/view-impl.hpp" +#include "core/seat/seat-impl.hpp" +#include "wayfire/scene-operations.hpp" #include +#include wf::input_method_relay::input_method_relay() { @@ -17,7 +20,6 @@ wf::input_method_relay::input_method_relay() on_input_method_new.set_callback([&] (void *data) { auto new_input_method = static_cast(data); - if (input_method != nullptr) { LOGI("Attempted to connect second input method"); @@ -26,9 +28,12 @@ wf::input_method_relay::input_method_relay() return; } + LOGD("new input method connected"); input_method = new_input_method; on_input_method_commit.connect(&input_method->events.commit); on_input_method_destroy.connect(&input_method->events.destroy); + on_grab_keyboard.connect(&input_method->events.grab_keyboard); + on_new_popup_surface.connect(&input_method->events.new_popup_surface); auto *text_input = find_focusable_text_input(); if (text_input) @@ -45,6 +50,23 @@ wf::input_method_relay::input_method_relay() auto evt_input_method = static_cast(data); assert(evt_input_method == input_method); + // FIXME: workaround focus change while preediting + // + // With input method v2, we have no way to notify the input method that + // input focus has changed. The input method maintains its state, and + // will bring it to the new window, i.e. a half-finished preedit string + // from the old window will be brought to the new one. This is undesired. + // + // We ignore such commit requests so it doesn't have any affect on the + // new window. Even when the previous window isn't preediting when + // switching focus, it doesn't have any bad effect to the new window anyway. + if (focus_just_changed) + { + LOGI("focus_just_changed, ignore input method commit"); + focus_just_changed = false; + return; + } + auto *text_input = find_focused_text_input(); if (text_input == nullptr) { @@ -83,7 +105,11 @@ wf::input_method_relay::input_method_relay() on_input_method_commit.disconnect(); on_input_method_destroy.disconnect(); - input_method = nullptr; + on_grab_keyboard.disconnect(); + on_grab_keyboard_destroy.disconnect(); + on_new_popup_surface.disconnect(); + input_method = nullptr; + keyboard_grab = nullptr; auto *text_input = find_focused_text_input(); if (text_input != nullptr) @@ -96,6 +122,30 @@ wf::input_method_relay::input_method_relay() } }); + on_grab_keyboard.set_callback([&] (void *data) + { + if (keyboard_grab != nullptr) + { + LOGW("Attempted to grab input method keyboard twice"); + return; + } + + keyboard_grab = static_cast(data); + on_grab_keyboard_destroy.connect(&keyboard_grab->events.destroy); + }); + + on_grab_keyboard_destroy.set_callback([&] (void *data) + { + on_grab_keyboard_destroy.disconnect(); + keyboard_grab = nullptr; + }); + + on_new_popup_surface.set_callback([&] (void *data) + { + auto popup = static_cast(data); + popup_surfaces.push_back(wf::popup_surface::create(this, popup)); + }); + on_text_input_new.connect(&wf::get_core().protocols.text_input->events.text_input); on_input_method_new.connect(&wf::get_core().protocols.input_method->events.input_method); wf::get_core().connect(&keyboard_focus_changed); @@ -126,6 +176,15 @@ void wf::input_method_relay::disable_text_input(wlr_text_input_v3 *input) return; } + // Don't deactivate input method if the text input isn't in focus. + // We may get several and posibly interwined enable/disable calls while + // switching focus / closing windows; don't deactivate for the wrong one. + auto focused_input = find_focused_text_input(); + if (!focused_input || (input != focused_input->input)) + { + return; + } + wlr_input_method_v2_send_deactivate(input_method); send_im_state(input); } @@ -141,6 +200,84 @@ void wf::input_method_relay::remove_text_input(wlr_text_input_v3 *input) text_inputs.erase(it, text_inputs.end()); } +void wf::input_method_relay::remove_popup_surface(wf::popup_surface *popup) +{ + auto it = std::remove_if(popup_surfaces.begin(), + popup_surfaces.end(), + [&] (const auto & suf) + { + return suf.get() == popup; + }); + popup_surfaces.erase(it, popup_surfaces.end()); +} + +bool wf::input_method_relay::should_grab(wlr_keyboard *kbd) +{ + if ((keyboard_grab == nullptr) || !find_focused_text_input()) + { + return false; + } + + return !is_im_sent(kbd); +} + +bool wf::input_method_relay::is_im_sent(wlr_keyboard *kbd) +{ + struct wlr_virtual_keyboard_v1 *virtual_keyboard = wlr_input_device_get_virtual_keyboard(&kbd->base); + if (!virtual_keyboard) + { + return false; + } + + // We have already identified the device as IM-based device + auto device_impl = (wf::input_device_impl_t*)kbd->base.data; + if (device_impl->is_im_keyboard) + { + return true; + } + + if (this->input_method) + { + // This is a workaround because we do not have sufficient information to know which virtual keyboards + // are connected to IMs + auto im_client = wl_resource_get_client(input_method->resource); + auto vkbd_client = wl_resource_get_client(virtual_keyboard->resource); + + if (im_client == vkbd_client) + { + device_impl->is_im_keyboard = true; + return true; + } + } + + return false; +} + +bool wf::input_method_relay::handle_key(struct wlr_keyboard *kbd, uint32_t time, uint32_t key, + uint32_t state) +{ + if (!should_grab(kbd)) + { + return false; + } + + wlr_input_method_keyboard_grab_v2_set_keyboard(keyboard_grab, kbd); + wlr_input_method_keyboard_grab_v2_send_key(keyboard_grab, time, key, state); + return true; +} + +bool wf::input_method_relay::handle_modifier(struct wlr_keyboard *kbd) +{ + if (!should_grab(kbd)) + { + return false; + } + + wlr_input_method_keyboard_grab_v2_set_keyboard(keyboard_grab, kbd); + wlr_input_method_keyboard_grab_v2_send_modifiers(keyboard_grab, &kbd->modifiers); + return true; +} + wf::text_input*wf::input_method_relay::find_focusable_text_input() { auto it = std::find_if(text_inputs.begin(), text_inputs.end(), @@ -251,6 +388,11 @@ wf::text_input::text_input(wf::input_method_relay *rel, wlr_text_input_v3 *in) : return; } + for (auto popup : relay->popup_surfaces) + { + popup->update_geometry(); + } + relay->send_im_state(input); }); @@ -311,3 +453,179 @@ void wf::text_input::set_pending_focused_surface(wlr_surface *surface) wf::text_input::~text_input() {} + +wf::popup_surface::popup_surface(wf::input_method_relay *rel, wlr_input_popup_surface_v2 *in) : + relay(rel), surface(in) +{ + main_surface = std::make_shared(in->surface, true); + + on_destroy.set_callback([&] (void*) + { + on_map.disconnect(); + on_unmap.disconnect(); + on_destroy.disconnect(); + + relay->remove_popup_surface(this); + }); + + on_map.set_callback([&] (void*) { map(); }); + on_unmap.set_callback([&] (void*) { unmap(); }); + on_commit.set_callback([&] (void*) { update_geometry(); }); + + on_map.connect(&surface->surface->events.map); + on_unmap.connect(&surface->surface->events.unmap); + on_destroy.connect(&surface->events.destroy); +} + +std::shared_ptr wf::popup_surface::create( + wf::input_method_relay *rel, wlr_input_popup_surface_v2 *in) +{ + auto self = view_interface_t::create(rel, in); + auto translation_node = std::make_shared(); + translation_node->set_children_list({std::make_unique(in->surface, + false)}); + self->surface_root_node = translation_node; + self->set_surface_root_node(translation_node); + self->set_role(VIEW_ROLE_DESKTOP_ENVIRONMENT); + return self; +} + +void wf::popup_surface::map() +{ + auto text_input = this->relay->find_focused_text_input(); + if (!text_input) + { + LOGE("trying to map IM popup surface without text input."); + return; + } + + auto view = wf::wl_surface_to_wayfire_view(text_input->input->focused_surface->resource); + auto output = view->get_output(); + if (!output) + { + LOGD("trying to map input method popup with a view not on an output."); + return; + } + + set_output(output); + + auto target_layer = wf::scene::layer::UNMANAGED; + wf::scene::readd_front(get_output()->node_for_layer(target_layer), get_root_node()); + + priv->set_mapped_surface_contents(main_surface); + priv->set_mapped(true); + _is_mapped = true; + on_commit.connect(&surface->surface->events.commit); + + update_geometry(); + + damage(); + emit_view_map(); +} + +void wf::popup_surface::unmap() +{ + if (!is_mapped()) + { + return; + } + + damage(); + + priv->unset_mapped_surface_contents(); + + emit_view_unmap(); + priv->set_mapped(false); + _is_mapped = false; + on_commit.disconnect(); +} + +std::string wf::popup_surface::get_app_id() +{ + return "input-method-popup"; +} + +std::string wf::popup_surface::get_title() +{ + return "input-method-popup"; +} + +void wf::popup_surface::update_geometry() +{ + auto text_input = this->relay->find_focused_text_input(); + if (!text_input) + { + LOGI("no focused text input"); + return; + } + + if (!is_mapped()) + { + LOGI("input method window not mapped"); + return; + } + + bool cursor_rect = text_input->input->current.features & WLR_TEXT_INPUT_V3_FEATURE_CURSOR_RECTANGLE; + auto cursor = text_input->input->current.cursor_rectangle; + int x = 0, y = 0; + if (cursor_rect) + { + x = cursor.x; + y = cursor.y + cursor.height; + } + + auto wlr_surface = text_input->input->focused_surface; + auto view = wf::wl_surface_to_wayfire_view(wlr_surface->resource); + if (!view) + { + return; + } + + damage(); + + wf::pointf_t popup_offset = wf::place_popup_at(wlr_surface, surface->surface, {x* 1.0, y * 1.0}); + x = popup_offset.x; + y = popup_offset.y; + + auto width = surface->surface->current.width; + auto height = surface->surface->current.height; + + auto output = view->get_output(); + auto g_output = output->get_layout_geometry(); + // make sure right edge is on screen, sliding to the left when needed, + // but keep left edge on screen nonetheless. + x = std::max(0, std::min(x, g_output.width - width)); + // down edge is going to be out of screen; flip upwards + if (y + height > g_output.height) + { + y -= height; + if (cursor_rect) + { + y -= cursor.height; + } + } + + // make sure top edge is on screen, sliding down and sacrificing down edge if unavoidable + y = std::max(0, y); + + surface_root_node->set_offset({x, y}); + geometry.x = x; + geometry.y = y; + geometry.width = width; + geometry.height = height; + damage(); + wf::scene::update(get_surface_root_node(), wf::scene::update_flag::GEOMETRY); +} + +bool wf::popup_surface::is_mapped() const +{ + return priv->wsurface != nullptr && _is_mapped; +} + +wf::geometry_t wf::popup_surface::get_geometry() +{ + return geometry; +} + +wf::popup_surface::~popup_surface() +{} diff --git a/src/core/seat/input-method-relay.hpp b/src/core/seat/input-method-relay.hpp index 07d948185..1d6864755 100644 --- a/src/core/seat/input-method-relay.hpp +++ b/src/core/seat/input-method-relay.hpp @@ -1,9 +1,10 @@ #pragma once -#include "seat-impl.hpp" #include "wayfire/util.hpp" #include "wayfire/signal-definitions.hpp" #include "wayfire/view.hpp" #include +#include +#include #include #include @@ -11,15 +12,18 @@ namespace wf { struct text_input; +struct popup_surface; class input_method_relay { private: wf::wl_listener_wrapper on_text_input_new, - on_input_method_new, on_input_method_commit, on_input_method_destroy; + on_input_method_new, on_input_method_commit, on_input_method_destroy, + on_grab_keyboard, on_grab_keyboard_destroy, on_new_popup_surface; + wlr_input_method_keyboard_grab_v2 *keyboard_grab = nullptr; + bool focus_just_changed = false; text_input *find_focusable_text_input(); - text_input *find_focused_text_input(); void set_focus(wlr_surface*); wf::signal::connection_t keyboard_focus_changed = @@ -32,17 +36,27 @@ class input_method_relay { set_focus(nullptr); } + + focus_just_changed = true; }; + bool should_grab(wlr_keyboard*); + public: wlr_input_method_v2 *input_method = nullptr; std::vector> text_inputs; + std::vector> popup_surfaces; input_method_relay(); void send_im_state(wlr_text_input_v3*); + text_input *find_focused_text_input(); void disable_text_input(wlr_text_input_v3*); void remove_text_input(wlr_text_input_v3*); + void remove_popup_surface(popup_surface*); + bool handle_key(struct wlr_keyboard*, uint32_t time, uint32_t key, uint32_t state); + bool handle_modifier(struct wlr_keyboard*); + bool is_im_sent(struct wlr_keyboard*); ~input_method_relay(); }; @@ -61,4 +75,33 @@ struct text_input void set_pending_focused_surface(wlr_surface*); ~text_input(); }; + +struct popup_surface : public wf::view_interface_t +{ + input_method_relay *relay = nullptr; + wlr_input_popup_surface_v2 *surface = nullptr; + wf::wl_listener_wrapper on_destroy, on_map, on_unmap, on_commit; + + popup_surface(input_method_relay*, wlr_input_popup_surface_v2*); + static std::shared_ptr create(input_method_relay*, wlr_input_popup_surface_v2*); + bool is_mapped() const override; + std::string get_app_id() override; + std::string get_title() override; + wf::geometry_t get_geometry(); + void map(); + void unmap(); + void update_geometry(); + ~popup_surface(); + + private: + wf::geometry_t geometry{0, 0, 0, 0}; + std::shared_ptr main_surface; + std::shared_ptr surface_root_node; + bool _is_mapped = false; + + virtual wlr_surface *get_keyboard_focus_surface() override + { + return nullptr; + } +}; } diff --git a/src/core/seat/keyboard.cpp b/src/core/seat/keyboard.cpp index 5f91ea3bd..449082c79 100644 --- a/src/core/seat/keyboard.cpp +++ b/src/core/seat/keyboard.cpp @@ -12,6 +12,7 @@ #include "cursor.hpp" #include "touch.hpp" #include "input-manager.hpp" +#include "input-method-relay.hpp" #include "wayfire/compositor-view.hpp" #include "wayfire/signal-definitions.hpp" @@ -37,7 +38,8 @@ void wf::keyboard_t::setup_listeners() } seat->priv->set_keyboard(this); - if (!handle_keyboard_key(ev->keycode, ev->state) && (mode == input_event_processing_mode_t::FULL)) + if (!handle_keyboard_key(ev->time_msec, ev->keycode, + ev->state) && (mode == input_event_processing_mode_t::FULL)) { if (ev->state == WL_KEYBOARD_KEY_STATE_PRESSED) { @@ -71,8 +73,12 @@ void wf::keyboard_t::setup_listeners() auto kbd = static_cast(data); auto seat = wf::get_core().get_current_seat(); - wlr_seat_set_keyboard(seat, kbd); - wlr_seat_keyboard_send_modifiers(seat, &kbd->modifiers); + if (!wf::get_core_impl().im_relay->handle_modifier(kbd)) + { + wlr_seat_set_keyboard(seat, kbd); + wlr_seat_keyboard_send_modifiers(seat, &kbd->modifiers); + } + wf::get_core().seat->notify_activity(); }); @@ -281,13 +287,19 @@ bool wf::keyboard_t::has_only_modifiers() return true; } -bool wf::keyboard_t::handle_keyboard_key(uint32_t key, uint32_t state) +bool wf::keyboard_t::handle_keyboard_key(uint32_t time, uint32_t key, uint32_t state) { using namespace std::chrono; auto& input = wf::get_core_impl().input; auto& seat = wf::get_core_impl().seat; + if (wf::get_core_impl().im_relay->is_im_sent(handle)) + { + mod_binding_key = 0; + return false; + } + bool handled_in_plugin = false; auto mod = mod_from_key(key); input->locked_mods = this->get_locked_mods(); @@ -316,6 +328,11 @@ bool wf::keyboard_t::handle_keyboard_key(uint32_t key, uint32_t state) handled_in_plugin |= wf::get_core().bindings->handle_key( wf::keybinding_t{get_modifiers(), key}, mod_binding_key); + + if (!handled_in_plugin) + { + handled_in_plugin |= wf::get_core_impl().im_relay->handle_key(handle, time, key, state); + } } else { if (mod_binding_key != 0) @@ -332,6 +349,11 @@ bool wf::keyboard_t::handle_keyboard_key(uint32_t key, uint32_t state) } } + if (!handled_in_plugin) + { + handled_in_plugin |= wf::get_core_impl().im_relay->handle_key(handle, time, key, state); + } + mod_binding_key = 0; } diff --git a/src/core/seat/keyboard.hpp b/src/core/seat/keyboard.hpp index 9e4c4fda1..e982148b0 100644 --- a/src/core/seat/keyboard.hpp +++ b/src/core/seat/keyboard.hpp @@ -50,7 +50,7 @@ class keyboard_t std::chrono::steady_clock::time_point mod_binding_start; - bool handle_keyboard_key(uint32_t key, uint32_t state); + bool handle_keyboard_key(uint32_t time, uint32_t key, uint32_t state); /** Get the current locked mods */ uint32_t get_locked_mods(); diff --git a/src/core/seat/seat-impl.hpp b/src/core/seat/seat-impl.hpp index ad19ba1f3..2c00c250a 100644 --- a/src/core/seat/seat-impl.hpp +++ b/src/core/seat/seat-impl.hpp @@ -5,7 +5,6 @@ #include #include -#include "../../view/surface-impl.hpp" #include "wayfire/output.hpp" #include "wayfire/input-device.hpp" #include "wayfire/scene-input.hpp" @@ -23,11 +22,13 @@ class input_device_impl_t : public wf::input_device_t { public: input_device_impl_t(wlr_input_device *dev); - virtual ~input_device_impl_t() = default; + virtual ~input_device_impl_t(); wf::wl_listener_wrapper on_destroy; virtual void update_options() {} + + bool is_im_keyboard = false; }; class pointer_t; diff --git a/src/core/seat/seat.cpp b/src/core/seat/seat.cpp index c04f448d6..40ee3ad64 100644 --- a/src/core/seat/seat.cpp +++ b/src/core/seat/seat.cpp @@ -1,15 +1,12 @@ #include "seat-impl.hpp" #include "cursor.hpp" -#include "wayfire/compositor-view.hpp" #include "wayfire/geometry.hpp" -#include "wayfire/opengl.hpp" #include "../core-impl.hpp" #include "../view/view-impl.hpp" #include "keyboard.hpp" #include "pointer.hpp" #include "touch.hpp" #include "input-manager.hpp" -#include "wayfire/render-manager.hpp" #include "wayfire/output-layout.hpp" #include #include "wayfire/scene-input.hpp" @@ -593,6 +590,12 @@ wf::input_device_impl_t::input_device_impl_t(wlr_input_device *dev) : wf::get_core_impl().input->handle_input_destroyed(this->get_wlr_handle()); }); on_destroy.connect(&dev->events.destroy); + this->handle->data = this; +} + +wf::input_device_impl_t::~input_device_impl_t() +{ + this->handle->data = NULL; } static wf::pointf_t to_local_recursive(wf::scene::node_t *node, wf::pointf_t point) diff --git a/src/view/view-impl.cpp b/src/view/view-impl.cpp index 42fa35a30..71d3114db 100644 --- a/src/view/view-impl.cpp +++ b/src/view/view-impl.cpp @@ -375,3 +375,39 @@ wayfire_toplevel_view wf::find_topmost_parent(wayfire_toplevel_view v) return v; } + +// Offset relative to the parent surface +wf::pointf_t wf::place_popup_at(wlr_surface *parent, wlr_surface *popup, wf::pointf_t relative) +{ + auto popup_parent = wf::wl_surface_to_wayfire_view(parent->resource).get(); + + wf::pointf_t popup_offset = relative; + if (wlr_xdg_surface *xdg_surface = wlr_xdg_surface_try_from_wlr_surface(parent)) + { + wlr_box box; + wlr_xdg_surface_get_geometry(xdg_surface, &box); + popup_offset.x += box.x; + popup_offset.y += box.y; + } + + // Get the {0, 0} of the parent view in output coordinates + popup_offset += popup_parent->get_surface_root_node()->to_global({0, 0}); + + auto xdg_surface = wlr_xdg_surface_try_from_wlr_surface(parent); + if (xdg_surface) + { + // substract shadows etc; test app: d-feet + popup_offset.x -= xdg_surface->current.geometry.x; + popup_offset.y -= xdg_surface->current.geometry.y; + } + + // Apply transformers to the popup position + auto node = popup_parent->get_surface_root_node()->parent(); + while (node != popup_parent->get_transformed_node().get()) + { + popup_offset = node->to_global(popup_offset); + node = node->parent(); + } + + return popup_offset; +} diff --git a/src/view/view-impl.hpp b/src/view/view-impl.hpp index b9bf84eca..fa6574042 100644 --- a/src/view/view-impl.hpp +++ b/src/view/view-impl.hpp @@ -73,6 +73,8 @@ int xwayland_get_pid(); void init_desktop_apis(); void init_xdg_decoration_handlers(); + +pointf_t place_popup_at(wlr_surface *parent, wlr_surface *popup, wf::pointf_t relative); } #endif /* end of include guard: VIEW_IMPL_HPP */ diff --git a/src/view/xdg-shell.cpp b/src/view/xdg-shell.cpp index f7a658a1d..a8fc39210 100644 --- a/src/view/xdg-shell.cpp +++ b/src/view/xdg-shell.cpp @@ -165,29 +165,10 @@ void wayfire_xdg_popup::update_position() } // Offset relative to the parent surface - wf::pointf_t popup_offset = {1.0 * popup->current.geometry.x, 1.0 * popup->current.geometry.y}; - if (wlr_xdg_surface *xdg_surface = wlr_xdg_surface_try_from_wlr_surface(popup->parent)) - { - wlr_box box; - wlr_xdg_surface_get_geometry(xdg_surface, &box); - popup_offset.x += box.x; - popup_offset.y += box.y; - } - - // Get the {0, 0} of the parent view in output coordinates - popup_offset += popup_parent->get_surface_root_node()->to_global({0, 0}); - - // Subtract shadows, etc. - popup_offset.x -= popup->base->current.geometry.x; - popup_offset.y -= popup->base->current.geometry.y; - - // Apply transformers to the popup position - auto node = popup_parent->get_surface_root_node()->parent(); - while (node != popup_parent->get_transformed_node().get()) - { - popup_offset = node->to_global(popup_offset); - node = node->parent(); - } + wf::pointf_t popup_offset = wf::place_popup_at(popup->parent, popup->base->surface, { + popup->current.geometry.x * 1.0, + popup->current.geometry.y * 1.0, + }); this->move(popup_offset.x, popup_offset.y); }