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 35647067d8
commit 528e5641fb
8 changed files with 137 additions and 35 deletions

View File

@ -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,

View File

@ -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

View File

@ -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")

View File

@ -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(...)

View File

@ -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)

View File

@ -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, ...)

View File

@ -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, ...)

View File

@ -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