From d8581c9b3f52c91ac7067d885939463fc51d5375 Mon Sep 17 00:00:00 2001 From: Guldoman Date: Sat, 20 May 2023 19:54:58 +0200 Subject: [PATCH] Add mouse grab (#1501) * Add mouse grab We now also send mouse movement events only to the interested view. * Add deprecation messages handler * Make various `View`s respect `on_mouse_left` * `StatusView` * `TitleView` * `TreeView` * `ToolbarView` * Fix scrollbar in `TreeView` not updating We were in some cases sending outdated mouse positions to the scrollbar, which made it think that the mouse was hovering it. This also updates the hovered item more responsively during scroll. --- data/core/commands/root.lua | 6 +-- data/core/init.lua | 11 ++++ data/core/node.lua | 14 ++++-- data/core/rootview.lua | 98 ++++++++++++++++++++++++++++-------- data/core/statusview.lua | 6 +++ data/core/titleview.lua | 6 +++ data/plugins/toolbarview.lua | 10 ++++ data/plugins/treeview.lua | 21 +++++--- 8 files changed, 137 insertions(+), 35 deletions(-) diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua index 0ae4c68e..03f22c25 100644 --- a/data/core/commands/root.lua +++ b/data/core/commands/root.lua @@ -104,7 +104,7 @@ end, t) command.add(nil, { ["root:scroll"] = function(delta) - local view = (core.root_view.overlapping_node and core.root_view.overlapping_node.active_view) or core.active_view + local view = core.root_view.overlapping_view or core.active_view if view and view.scrollable then view.scroll.to.y = view.scroll.to.y + delta * -config.mouse_wheel_scroll return true @@ -112,7 +112,7 @@ command.add(nil, { return false end, ["root:horizontal-scroll"] = function(delta) - local view = (core.root_view.overlapping_node and core.root_view.overlapping_node.active_view) or core.active_view + local view = core.root_view.overlapping_view or core.active_view if view and view.scrollable then view.scroll.to.x = view.scroll.to.x + delta * -config.mouse_wheel_scroll return true @@ -154,7 +154,7 @@ command.add(function(node) ) command.add(function() - local node = core.root_view.overlapping_node + local node = core.root_view.root_node:get_child_overlapping_point(core.root_view.mouse.x, core.root_view.mouse.y) if not node then return false end return (node.hovered_tab or node.hovered_scroll_button > 0) and true, node end, diff --git a/data/core/init.lua b/data/core/init.lua index 050a8ee2..2b9be2a1 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -1490,4 +1490,15 @@ function core.on_error(err) end +local alerted_deprecations = {} +---Show deprecation notice once per `kind`. +--- +---@param kind string +function core.deprecation_log(kind) + if alerted_deprecations[kind] then return end + alerted_deprecations[kind] = true + core.warn("Used deprecated functionality [%s]. Check if your plugins are up to date.", kind) +end + + return core diff --git a/data/core/node.lua b/data/core/node.lua index e282882c..0ea2c07e 100644 --- a/data/core/node.lua +++ b/data/core/node.lua @@ -18,7 +18,6 @@ function Node:new(type) if self.type == "leaf" then self:add_view(EmptyView()) end - self.hovered = {x = -1, y = -1 } self.hovered_close = 0 self.tab_shift = 0 self.tab_offset = 1 @@ -33,9 +32,10 @@ function Node:propagate(fn, ...) end +---@deprecated function Node:on_mouse_moved(x, y, ...) + core.deprecation_log("Node:on_mouse_moved") if self.type == "leaf" then - self.hovered.x, self.hovered.y = x, y self.active_view:on_mouse_moved(x, y, ...) else self:propagate("on_mouse_moved", x, y, ...) @@ -43,7 +43,9 @@ function Node:on_mouse_moved(x, y, ...) end +---@deprecated function Node:on_mouse_released(...) + core.deprecation_log("Node:on_mouse_released") if self.type == "leaf" then self.active_view:on_mouse_released(...) else @@ -52,7 +54,9 @@ function Node:on_mouse_released(...) end +---@deprecated function Node:on_mouse_left() + core.deprecation_log("Node:on_mouse_left") if self.type == "leaf" then self.active_view:on_mouse_left() else @@ -60,7 +64,10 @@ function Node:on_mouse_left() end end + +---@deprecated function Node:on_touch_moved(...) + core.deprecation_log("Node:on_touch_moved") if self.type == "leaf" then self.active_view:on_touch_moved(...) else @@ -68,6 +75,7 @@ function Node:on_touch_moved(...) end end + function Node:consume(node) for k, _ in pairs(self) do self[k] = nil end for k, v in pairs(node) do self[k] = v end @@ -489,7 +497,7 @@ function Node:update() for _, view in ipairs(self.views) do view:update() end - self:tab_hovered_update(self.hovered.x, self.hovered.y) + self:tab_hovered_update(core.root_view.mouse.x, core.root_view.mouse.y) local tab_width = self:target_tab_width() self:move_towards("tab_shift", tab_width * (self.tab_offset - 1), nil, "tabs") self:move_towards("tab_width", tab_width, nil, "tabs") diff --git a/data/core/rootview.lua b/data/core/rootview.lua index c6b18c47..18ae40ab 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -24,6 +24,9 @@ function RootView:new() base_color = style.drag_overlay_tab, color = { table.unpack(style.drag_overlay_tab) } } self.drag_overlay_tab.to = { x = 0, y = 0, w = 0, h = 0 } + self.grab = nil -- = {view = nil, button = nil} + self.overlapping_view = nil + self.touched_view = nil end @@ -116,6 +119,31 @@ function RootView:close_all_docviews(keep_active) end +---Obtain mouse grab. +--- +---This means that mouse movements will be sent to the specified view, even when +---those occur outside of it. +---There can't be multiple mouse grabs, even for different buttons. +---@see RootView:ungrab_mouse +---@param button core.view.mousebutton +---@param view core.view +function RootView:grab_mouse(button, view) + assert(self.grab == nil) + self.grab = {view = view, button = button} +end + + +---Release mouse grab. +--- +---The specified button *must* be the last button that grabbed the mouse. +---@see RootView:grab_mouse +---@param button core.view.mousebutton +function RootView:ungrab_mouse(button) + assert(self.grab and self.grab.button == button) + self.grab = nil +end + + ---Function to intercept mouse pressed events on the active view. ---Do nothing by default. ---@param button core.view.mousebutton @@ -132,6 +160,10 @@ end ---@param clicks integer ---@return boolean function RootView:on_mouse_pressed(button, x, y, clicks) + -- If there is a grab, release it first + if self.grab then + self:on_mouse_released(self.grab.button, x, y) + end local div = self.root_node:get_divider_overlapping_point(x, y) local node = self.root_node:get_child_overlapping_point(x, y) if div and (node and not node.active_view:scrollbar_overlaps_point(x, y)) then @@ -156,6 +188,7 @@ function RootView:on_mouse_pressed(button, x, y, clicks) end elseif not self.dragged_node then -- avoid sending on_mouse_pressed events when dragging tabs core.set_active_view(node.active_view) + self:grab_mouse(button, node.active_view) return self.on_view_mouse_pressed(button, x, y, clicks) or node.active_view:on_mouse_pressed(button, x, y, clicks) end end @@ -188,6 +221,21 @@ end ---@param x number ---@param y number function RootView:on_mouse_released(button, x, y, ...) + if self.grab then + if self.grab.button == button then + local grabbed_view = self.grab.view + grabbed_view:on_mouse_released(button, x, y, ...) + self:ungrab_mouse(button) + + -- If the mouse was released over a different view, send it the mouse position + local hovered_view = self.root_node:get_child_overlapping_point(x, y) + if grabbed_view ~= hovered_view then + self:on_mouse_moved(x, y, 0, 0) + end + end + return + end + if self.dragged_divider then self.dragged_divider = nil end @@ -228,8 +276,6 @@ function RootView:on_mouse_released(button, x, y, ...) end self.dragged_node = nil end - else -- avoid sending on_mouse_released events when dragging tabs - self.root_node:on_mouse_released(button, x, y, ...) end end @@ -250,6 +296,14 @@ end ---@param dx number ---@param dy number function RootView:on_mouse_moved(x, y, dx, dy) + self.mouse.x, self.mouse.y = x, y + + if self.grab then + self.grab.view:on_mouse_moved(x, y, dx, dy) + core.request_cursor(self.grab.view.cursor) + return + end + if core.active_view == core.nag_view then core.request_cursor("arrow") core.active_view:on_mouse_moved(x, y, dx, dy) @@ -269,8 +323,6 @@ function RootView:on_mouse_moved(x, y, dx, dy) return end - self.mouse.x, self.mouse.y = x, y - local dn = self.dragged_node if dn and not dn.dragging then -- start dragging only after enough movement @@ -283,30 +335,33 @@ function RootView:on_mouse_moved(x, y, dx, dy) -- avoid sending on_mouse_moved events when dragging tabs if dn then return end - self.root_node:on_mouse_moved(x, y, dx, dy) + local last_overlapping_view = self.overlapping_view + local overlapping_node = self.root_node:get_child_overlapping_point(x, y) + self.overlapping_view = overlapping_node and overlapping_node.active_view - local last_overlapping_node = self.overlapping_node - self.overlapping_node = self.root_node:get_child_overlapping_point(x, y) - - if last_overlapping_node and last_overlapping_node ~= self.overlapping_node then - last_overlapping_node:on_mouse_left() + if last_overlapping_view and last_overlapping_view ~= self.overlapping_view then + last_overlapping_view:on_mouse_left() end - if not self.overlapping_node then return end + + if not self.overlapping_view then return end + + self.overlapping_view:on_mouse_moved(x, y, dx, dy) + core.request_cursor(self.overlapping_view.cursor) + + if not overlapping_node then return end local div = self.root_node:get_divider_overlapping_point(x, y) - if self.overlapping_node:get_scroll_button_index(x, y) or self.overlapping_node:is_in_tab_area(x, y) then + if overlapping_node:get_scroll_button_index(x, y) or overlapping_node:is_in_tab_area(x, y) then core.request_cursor("arrow") - elseif div and not self.overlapping_node.active_view:scrollbar_overlaps_point(x, y) then + elseif div and not self.overlapping_view:scrollbar_overlaps_point(x, y) then core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev") - else - core.request_cursor(self.overlapping_node.active_view.cursor) end end function RootView:on_mouse_left() - if self.overlapping_node then - self.overlapping_node:on_mouse_left() + if self.overlapping_view then + self.overlapping_view:on_mouse_left() end end @@ -333,15 +388,16 @@ function RootView:on_text_input(...) end function RootView:on_touch_pressed(x, y, ...) - self.touched_node = self.root_node:get_child_overlapping_point(x, y) + local touched_node = self.root_node:get_child_overlapping_point(x, y) + self.touched_view = touched_node and touched_node.active_view end function RootView:on_touch_released(x, y, ...) - self.touched_node = nil + self.touched_view = nil end function RootView:on_touch_moved(x, y, dx, dy, ...) - if not self.touched_node then return end + if not self.touched_view then return end if core.active_view == core.nag_view then core.active_view:on_touch_moved(x, y, dx, dy, ...) return @@ -372,7 +428,7 @@ function RootView:on_touch_moved(x, y, dx, dy, ...) -- avoid sending on_touch_moved events when dragging tabs if dn then return end - self.touched_node:on_touch_moved(x, y, dx, dy, ...) + self.touched_view:on_touch_moved(x, y, dx, dy, ...) end function RootView:on_ime_text_editing(...) diff --git a/data/core/statusview.lua b/data/core/statusview.lua index 930c7aef..731c0552 100644 --- a/data/core/statusview.lua +++ b/data/core/statusview.lua @@ -989,6 +989,12 @@ function StatusView:on_mouse_pressed(button, x, y, clicks) end +function StatusView:on_mouse_left() + StatusView.super.on_mouse_left(self) + self.hovered_item = {} +end + + function StatusView:on_mouse_moved(x, y, dx, dy) if not self.visible then return end StatusView.super.on_mouse_moved(self, x, y, dx, dy) diff --git a/data/core/titleview.lua b/data/core/titleview.lua index 69315be1..482d6c8e 100644 --- a/data/core/titleview.lua +++ b/data/core/titleview.lua @@ -112,6 +112,12 @@ function TitleView:on_mouse_pressed(button, x, y, clicks) end +function TitleView:on_mouse_left() + TitleView.super.on_mouse_left(self) + self.hovered_item = nil +end + + function TitleView:on_mouse_moved(px, py, ...) if self.size.y == 0 then return end TitleView.super.on_mouse_moved(self, px, py, ...) diff --git a/data/plugins/toolbarview.lua b/data/plugins/toolbarview.lua index caf86d71..ddc0f39d 100644 --- a/data/plugins/toolbarview.lua +++ b/data/plugins/toolbarview.lua @@ -100,6 +100,16 @@ function ToolbarView:on_mouse_pressed(button, x, y, clicks) end +function ToolbarView:on_mouse_left() + ToolbarView.super.on_mouse_left(self) + if self.tooltip then + core.status_view:remove_tooltip() + self.tooltip = false + end + self.hovered_item = nil +end + + function ToolbarView:on_mouse_moved(px, py, ...) if not self.visible then return end ToolbarView.super.on_mouse_moved(self, px, py, ...) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index cab3d410..b27ba554 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -51,7 +51,7 @@ function TreeView:new() self.target_size = config.plugins.treeview.size self.cache = {} self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 } - self.cursor_pos = { x = 0, y = 0 } + self.last_scroll_y = 0 self.item_icon_width = 0 self.item_text_spacing = 0 @@ -251,10 +251,9 @@ function TreeView:get_text_bounding_box(item, x, y, w, h) end + function TreeView:on_mouse_moved(px, py, ...) if not self.visible then return end - self.cursor_pos.x = px - self.cursor_pos.y = py if TreeView.super.on_mouse_moved(self, px, py, ...) then -- mouse movement handled by the View (scrollbar) self.hovered_item = nil @@ -281,6 +280,12 @@ function TreeView:on_mouse_moved(px, py, ...) end +function TreeView:on_mouse_left() + TreeView.super.on_mouse_left(self) + self.hovered_item = nil +end + + function TreeView:update() -- update width local dest = self.visible and self.target_size or 0 @@ -304,10 +309,10 @@ function TreeView:update() self.item_text_spacing = style.icon_font:get_width("f") / 2 -- this will make sure hovered_item is updated - -- we don't want events when the thing is scrolling fast - local dy = math.abs(self.scroll.to.y - self.scroll.y) - if self.scroll.to.y ~= 0 and dy < self:get_item_height() then - self:on_mouse_moved(self.cursor_pos.x, self.cursor_pos.y, 0, 0) + local dy = math.abs(self.last_scroll_y - self.scroll.y) + if dy > 0 then + self:on_mouse_moved(core.root_view.mouse.x, core.root_view.mouse.y, 0, 0) + self.last_scroll_y = self.scroll.y end local config = config.plugins.treeview @@ -751,7 +756,7 @@ command.add( ["treeview-context:show"] = function() if view.hovered_item then - menu:show(view.cursor_pos.x, view.cursor_pos.y) + menu:show(core.root_view.mouse.x, core.root_view.mouse.y) return end