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.
This commit is contained in:
Guldoman 2023-05-20 19:54:58 +02:00 committed by George Sokianos
parent 39182d49d6
commit 43f9b8accc
8 changed files with 137 additions and 35 deletions

View File

@ -104,7 +104,7 @@ end, t)
command.add(nil, { command.add(nil, {
["root:scroll"] = function(delta) ["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 if view and view.scrollable then
view.scroll.to.y = view.scroll.to.y + delta * -config.mouse_wheel_scroll view.scroll.to.y = view.scroll.to.y + delta * -config.mouse_wheel_scroll
return true return true
@ -112,7 +112,7 @@ command.add(nil, {
return false return false
end, end,
["root:horizontal-scroll"] = function(delta) ["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 if view and view.scrollable then
view.scroll.to.x = view.scroll.to.x + delta * -config.mouse_wheel_scroll view.scroll.to.x = view.scroll.to.x + delta * -config.mouse_wheel_scroll
return true return true
@ -154,7 +154,7 @@ command.add(function(node)
) )
command.add(function() 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 if not node then return false end
return (node.hovered_tab or node.hovered_scroll_button > 0) and true, node return (node.hovered_tab or node.hovered_scroll_button > 0) and true, node
end, end,

View File

@ -1490,5 +1490,16 @@ function core.on_error(err)
end 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 return core

View File

@ -18,7 +18,6 @@ function Node:new(type)
if self.type == "leaf" then if self.type == "leaf" then
self:add_view(EmptyView()) self:add_view(EmptyView())
end end
self.hovered = {x = -1, y = -1 }
self.hovered_close = 0 self.hovered_close = 0
self.tab_shift = 0 self.tab_shift = 0
self.tab_offset = 1 self.tab_offset = 1
@ -33,9 +32,10 @@ function Node:propagate(fn, ...)
end end
---@deprecated
function Node:on_mouse_moved(x, y, ...) function Node:on_mouse_moved(x, y, ...)
core.deprecation_log("Node:on_mouse_moved")
if self.type == "leaf" then if self.type == "leaf" then
self.hovered.x, self.hovered.y = x, y
self.active_view:on_mouse_moved(x, y, ...) self.active_view:on_mouse_moved(x, y, ...)
else else
self:propagate("on_mouse_moved", x, y, ...) self:propagate("on_mouse_moved", x, y, ...)
@ -43,7 +43,9 @@ function Node:on_mouse_moved(x, y, ...)
end end
---@deprecated
function Node:on_mouse_released(...) function Node:on_mouse_released(...)
core.deprecation_log("Node:on_mouse_released")
if self.type == "leaf" then if self.type == "leaf" then
self.active_view:on_mouse_released(...) self.active_view:on_mouse_released(...)
else else
@ -52,7 +54,9 @@ function Node:on_mouse_released(...)
end end
---@deprecated
function Node:on_mouse_left() function Node:on_mouse_left()
core.deprecation_log("Node:on_mouse_left")
if self.type == "leaf" then if self.type == "leaf" then
self.active_view:on_mouse_left() self.active_view:on_mouse_left()
else else
@ -60,7 +64,10 @@ function Node:on_mouse_left()
end end
end end
---@deprecated
function Node:on_touch_moved(...) function Node:on_touch_moved(...)
core.deprecation_log("Node:on_touch_moved")
if self.type == "leaf" then if self.type == "leaf" then
self.active_view:on_touch_moved(...) self.active_view:on_touch_moved(...)
else else
@ -68,6 +75,7 @@ function Node:on_touch_moved(...)
end end
end end
function Node:consume(node) function Node:consume(node)
for k, _ in pairs(self) do self[k] = nil end for k, _ in pairs(self) do self[k] = nil end
for k, v in pairs(node) do self[k] = v 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 for _, view in ipairs(self.views) do
view:update() view:update()
end 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() local tab_width = self:target_tab_width()
self:move_towards("tab_shift", tab_width * (self.tab_offset - 1), nil, "tabs") self:move_towards("tab_shift", tab_width * (self.tab_offset - 1), nil, "tabs")
self:move_towards("tab_width", tab_width, nil, "tabs") self:move_towards("tab_width", tab_width, nil, "tabs")

View File

@ -24,6 +24,9 @@ function RootView:new()
base_color = style.drag_overlay_tab, base_color = style.drag_overlay_tab,
color = { table.unpack(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.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 end
@ -116,6 +119,31 @@ function RootView:close_all_docviews(keep_active)
end 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. ---Function to intercept mouse pressed events on the active view.
---Do nothing by default. ---Do nothing by default.
---@param button core.view.mousebutton ---@param button core.view.mousebutton
@ -132,6 +160,10 @@ end
---@param clicks integer ---@param clicks integer
---@return boolean ---@return boolean
function RootView:on_mouse_pressed(button, x, y, clicks) 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 div = self.root_node:get_divider_overlapping_point(x, y)
local node = self.root_node:get_child_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 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 end
elseif not self.dragged_node then -- avoid sending on_mouse_pressed events when dragging tabs elseif not self.dragged_node then -- avoid sending on_mouse_pressed events when dragging tabs
core.set_active_view(node.active_view) 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) return self.on_view_mouse_pressed(button, x, y, clicks) or node.active_view:on_mouse_pressed(button, x, y, clicks)
end end
end end
@ -188,6 +221,21 @@ end
---@param x number ---@param x number
---@param y number ---@param y number
function RootView:on_mouse_released(button, x, y, ...) 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 if self.dragged_divider then
self.dragged_divider = nil self.dragged_divider = nil
end end
@ -228,8 +276,6 @@ function RootView:on_mouse_released(button, x, y, ...)
end end
self.dragged_node = nil self.dragged_node = nil
end end
else -- avoid sending on_mouse_released events when dragging tabs
self.root_node:on_mouse_released(button, x, y, ...)
end end
end end
@ -250,6 +296,14 @@ end
---@param dx number ---@param dx number
---@param dy number ---@param dy number
function RootView:on_mouse_moved(x, y, dx, dy) 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 if core.active_view == core.nag_view then
core.request_cursor("arrow") core.request_cursor("arrow")
core.active_view:on_mouse_moved(x, y, dx, dy) core.active_view:on_mouse_moved(x, y, dx, dy)
@ -269,8 +323,6 @@ function RootView:on_mouse_moved(x, y, dx, dy)
return return
end end
self.mouse.x, self.mouse.y = x, y
local dn = self.dragged_node local dn = self.dragged_node
if dn and not dn.dragging then if dn and not dn.dragging then
-- start dragging only after enough movement -- 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 -- avoid sending on_mouse_moved events when dragging tabs
if dn then return end 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 if last_overlapping_view and last_overlapping_view ~= self.overlapping_view then
self.overlapping_node = self.root_node:get_child_overlapping_point(x, y) last_overlapping_view:on_mouse_left()
if last_overlapping_node and last_overlapping_node ~= self.overlapping_node then
last_overlapping_node:on_mouse_left()
end 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) 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") 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") core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev")
else
core.request_cursor(self.overlapping_node.active_view.cursor)
end end
end end
function RootView:on_mouse_left() function RootView:on_mouse_left()
if self.overlapping_node then if self.overlapping_view then
self.overlapping_node:on_mouse_left() self.overlapping_view:on_mouse_left()
end end
end end
@ -333,15 +388,16 @@ function RootView:on_text_input(...)
end end
function RootView:on_touch_pressed(x, y, ...) 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 end
function RootView:on_touch_released(x, y, ...) function RootView:on_touch_released(x, y, ...)
self.touched_node = nil self.touched_view = nil
end end
function RootView:on_touch_moved(x, y, dx, dy, ...) 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 if core.active_view == core.nag_view then
core.active_view:on_touch_moved(x, y, dx, dy, ...) core.active_view:on_touch_moved(x, y, dx, dy, ...)
return return
@ -372,7 +428,7 @@ function RootView:on_touch_moved(x, y, dx, dy, ...)
-- avoid sending on_touch_moved events when dragging tabs -- avoid sending on_touch_moved events when dragging tabs
if dn then return end 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 end
function RootView:on_ime_text_editing(...) function RootView:on_ime_text_editing(...)

View File

@ -989,6 +989,12 @@ function StatusView:on_mouse_pressed(button, x, y, clicks)
end 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) function StatusView:on_mouse_moved(x, y, dx, dy)
if not self.visible then return end if not self.visible then return end
StatusView.super.on_mouse_moved(self, x, y, dx, dy) StatusView.super.on_mouse_moved(self, x, y, dx, dy)

View File

@ -112,6 +112,12 @@ function TitleView:on_mouse_pressed(button, x, y, clicks)
end end
function TitleView:on_mouse_left()
TitleView.super.on_mouse_left(self)
self.hovered_item = nil
end
function TitleView:on_mouse_moved(px, py, ...) function TitleView:on_mouse_moved(px, py, ...)
if self.size.y == 0 then return end if self.size.y == 0 then return end
TitleView.super.on_mouse_moved(self, px, py, ...) TitleView.super.on_mouse_moved(self, px, py, ...)

View File

@ -100,6 +100,16 @@ function ToolbarView:on_mouse_pressed(button, x, y, clicks)
end 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, ...) function ToolbarView:on_mouse_moved(px, py, ...)
if not self.visible then return end if not self.visible then return end
ToolbarView.super.on_mouse_moved(self, px, py, ...) ToolbarView.super.on_mouse_moved(self, px, py, ...)

View File

@ -51,7 +51,7 @@ function TreeView:new()
self.target_size = config.plugins.treeview.size self.target_size = config.plugins.treeview.size
self.cache = {} self.cache = {}
self.tooltip = { x = 0, y = 0, begin = 0, alpha = 0 } 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_icon_width = 0
self.item_text_spacing = 0 self.item_text_spacing = 0
@ -251,10 +251,9 @@ function TreeView:get_text_bounding_box(item, x, y, w, h)
end end
function TreeView:on_mouse_moved(px, py, ...) function TreeView:on_mouse_moved(px, py, ...)
if not self.visible then return end 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 if TreeView.super.on_mouse_moved(self, px, py, ...) then
-- mouse movement handled by the View (scrollbar) -- mouse movement handled by the View (scrollbar)
self.hovered_item = nil self.hovered_item = nil
@ -281,6 +280,12 @@ function TreeView:on_mouse_moved(px, py, ...)
end end
function TreeView:on_mouse_left()
TreeView.super.on_mouse_left(self)
self.hovered_item = nil
end
function TreeView:update() function TreeView:update()
-- update width -- update width
local dest = self.visible and self.target_size or 0 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 self.item_text_spacing = style.icon_font:get_width("f") / 2
-- this will make sure hovered_item is updated -- this will make sure hovered_item is updated
-- we don't want events when the thing is scrolling fast local dy = math.abs(self.last_scroll_y - self.scroll.y)
local dy = math.abs(self.scroll.to.y - self.scroll.y) if dy > 0 then
if self.scroll.to.y ~= 0 and dy < self:get_item_height() then self:on_mouse_moved(core.root_view.mouse.x, core.root_view.mouse.y, 0, 0)
self:on_mouse_moved(self.cursor_pos.x, self.cursor_pos.y, 0, 0) self.last_scroll_y = self.scroll.y
end end
local config = config.plugins.treeview local config = config.plugins.treeview
@ -751,7 +756,7 @@ command.add(
["treeview-context:show"] = function() ["treeview-context:show"] = function()
if view.hovered_item then 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 return
end end