271 lines
6.5 KiB
Lua
271 lines
6.5 KiB
Lua
|
local core = require "core"
|
||
|
local common = require "core.common"
|
||
|
local config = require "core.config"
|
||
|
local command = require "core.command"
|
||
|
local keymap = require "core.keymap"
|
||
|
local style = require "core.style"
|
||
|
local Object = require "core.object"
|
||
|
local RootView = require "core.rootview"
|
||
|
|
||
|
local border_width = 1
|
||
|
local divider_width = 1
|
||
|
local DIVIDER = {}
|
||
|
|
||
|
local ContextMenu = Object:extend()
|
||
|
|
||
|
ContextMenu.DIVIDER = DIVIDER
|
||
|
|
||
|
function ContextMenu:new()
|
||
|
self.itemset = {}
|
||
|
self.show_context_menu = false
|
||
|
self.selected = -1
|
||
|
self.height = 0
|
||
|
self.position = { x = 0, y = 0 }
|
||
|
end
|
||
|
|
||
|
local function get_item_size(item)
|
||
|
local lw, lh
|
||
|
if item == DIVIDER then
|
||
|
lw = 0
|
||
|
lh = divider_width
|
||
|
else
|
||
|
lw = style.font:get_width(item.text)
|
||
|
if item.info then
|
||
|
lw = lw + style.padding.x + style.font:get_width(item.info)
|
||
|
end
|
||
|
lh = style.font:get_height() + style.padding.y
|
||
|
end
|
||
|
return lw, lh
|
||
|
end
|
||
|
|
||
|
function ContextMenu:register(predicate, items)
|
||
|
if type(predicate) == "string" then
|
||
|
predicate = require(predicate)
|
||
|
end
|
||
|
if type(predicate) == "table" then
|
||
|
local class = predicate
|
||
|
predicate = function() return core.active_view:is(class) end
|
||
|
end
|
||
|
|
||
|
local width, height = 0, 0 --precalculate the size of context menu
|
||
|
for i, item in ipairs(items) do
|
||
|
if item ~= DIVIDER then
|
||
|
item.info = keymap.reverse_map[item.command]
|
||
|
end
|
||
|
local lw, lh = get_item_size(item)
|
||
|
width = math.max(width, lw)
|
||
|
height = height + lh
|
||
|
end
|
||
|
width = width + style.padding.x * 2
|
||
|
items.width, items.height = width, height
|
||
|
table.insert(self.itemset, { predicate = predicate, items = items })
|
||
|
end
|
||
|
|
||
|
function ContextMenu:show(x, y)
|
||
|
self.items = nil
|
||
|
for _, items in ipairs(self.itemset) do
|
||
|
if items.predicate(x, y) then
|
||
|
self.items = items.items
|
||
|
break
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if self.items then
|
||
|
local w, h = self.items.width, self.items.height
|
||
|
|
||
|
-- by default the box is opened on the right and below
|
||
|
if x + w >= core.root_view.size.x then
|
||
|
x = x - w
|
||
|
end
|
||
|
if y + h >= core.root_view.size.y then
|
||
|
y = y - h
|
||
|
end
|
||
|
|
||
|
self.position.x, self.position.y = x, y
|
||
|
self.show_context_menu = true
|
||
|
return true
|
||
|
end
|
||
|
return false
|
||
|
end
|
||
|
|
||
|
function ContextMenu:hide()
|
||
|
self.show_context_menu = false
|
||
|
self.items = nil
|
||
|
self.selected = -1
|
||
|
self.height = 0
|
||
|
end
|
||
|
|
||
|
function ContextMenu:each_item()
|
||
|
local x, y, w = self.position.x, self.position.y, self.items.width
|
||
|
local oy = y
|
||
|
return coroutine.wrap(function()
|
||
|
for i, item in ipairs(self.items) do
|
||
|
local _, lh = get_item_size(item)
|
||
|
if y - oy > self.height then break end
|
||
|
coroutine.yield(i, item, x, y, w, lh)
|
||
|
y = y + lh
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
function ContextMenu:on_mouse_moved(px, py)
|
||
|
if not self.show_context_menu then return end
|
||
|
|
||
|
for i, item, x, y, w, h in self:each_item() do
|
||
|
if px > x and px <= x + w and py > y and py <= y + h then
|
||
|
system.set_cursor("arrow")
|
||
|
self.selected = i
|
||
|
return true
|
||
|
end
|
||
|
end
|
||
|
self.selected = -1
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
function ContextMenu:on_selected(item)
|
||
|
if type(item.command) == "string" then
|
||
|
command.perform(item.command)
|
||
|
else
|
||
|
item.command()
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function ContextMenu:on_mouse_pressed(button, x, y, clicks)
|
||
|
local selected = (self.items or {})[self.selected]
|
||
|
local caught = false
|
||
|
|
||
|
self:hide()
|
||
|
if button == "left" then
|
||
|
if selected then
|
||
|
self:on_selected(selected)
|
||
|
caught = true
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if button == "right" then
|
||
|
caught = self:show(x, y)
|
||
|
end
|
||
|
return caught
|
||
|
end
|
||
|
|
||
|
-- copied from core.docview
|
||
|
function ContextMenu:move_towards(t, k, dest, rate)
|
||
|
if type(t) ~= "table" then
|
||
|
return self:move_towards(self, t, k, dest, rate)
|
||
|
end
|
||
|
local val = t[k]
|
||
|
if math.abs(val - dest) < 0.5 then
|
||
|
t[k] = dest
|
||
|
else
|
||
|
t[k] = common.lerp(val, dest, rate or 0.5)
|
||
|
end
|
||
|
if val ~= dest then
|
||
|
core.redraw = true
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function ContextMenu:update()
|
||
|
if self.show_context_menu then
|
||
|
self:move_towards("height", self.items.height)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function ContextMenu:draw()
|
||
|
if not self.show_context_menu then return end
|
||
|
core.root_view:defer_draw(self.draw_context_menu, self)
|
||
|
end
|
||
|
|
||
|
function ContextMenu:draw_context_menu()
|
||
|
if not self.items then return end
|
||
|
local bx, by, bw, bh = self.position.x, self.position.y, self.items.width, self.height
|
||
|
|
||
|
renderer.draw_rect(
|
||
|
bx - border_width,
|
||
|
by - border_width,
|
||
|
bw + (border_width * 2),
|
||
|
bh + (border_width * 2),
|
||
|
style.divider
|
||
|
)
|
||
|
renderer.draw_rect(bx, by, bw, bh, style.background3)
|
||
|
|
||
|
for i, item, x, y, w, h in self:each_item() do
|
||
|
if item == DIVIDER then
|
||
|
renderer.draw_rect(x, y, w, h, style.caret)
|
||
|
else
|
||
|
if i == self.selected then
|
||
|
renderer.draw_rect(x, y, w, h, style.selection)
|
||
|
end
|
||
|
|
||
|
common.draw_text(style.font, style.text, item.text, "left", x + style.padding.x, y, w, h)
|
||
|
if item.info then
|
||
|
common.draw_text(style.font, style.dim, item.info, "right", x, y, w - style.padding.x, h)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
|
||
|
local menu = ContextMenu()
|
||
|
local root_view_on_mouse_pressed = RootView.on_mouse_pressed
|
||
|
local root_view_on_mouse_moved = RootView.on_mouse_moved
|
||
|
local root_view_update = RootView.update
|
||
|
local root_view_draw = RootView.draw
|
||
|
|
||
|
function RootView:on_mouse_moved(...)
|
||
|
if menu:on_mouse_moved(...) then return end
|
||
|
root_view_on_mouse_moved(self, ...)
|
||
|
end
|
||
|
|
||
|
-- copied from core.rootview
|
||
|
function RootView:on_mouse_pressed(button, x,y, clicks)
|
||
|
local div = self.root_node:get_divider_overlapping_point(x, y)
|
||
|
if div then
|
||
|
self.dragged_divider = div
|
||
|
return
|
||
|
end
|
||
|
local node = self.root_node:get_child_overlapping_point(x, y)
|
||
|
local idx = node:get_tab_overlapping_point(x, y)
|
||
|
if idx then
|
||
|
node:set_active_view(node.views[idx])
|
||
|
if button == "right" then --< @r-lyeh middle>right
|
||
|
node:close_active_view(self.root_node)
|
||
|
end
|
||
|
else
|
||
|
core.set_active_view(node.active_view)
|
||
|
-- send to context menu first
|
||
|
if not menu:on_mouse_pressed(button, x, y, clicks) then
|
||
|
node.active_view:on_mouse_pressed(button, x, y, clicks)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function RootView:update(...)
|
||
|
root_view_update(self, ...)
|
||
|
menu:update()
|
||
|
end
|
||
|
|
||
|
function RootView:draw(...)
|
||
|
root_view_draw(self, ...)
|
||
|
menu:draw()
|
||
|
end
|
||
|
|
||
|
command.add(nil, {
|
||
|
["context:show"] = function()
|
||
|
menu:show(core.active_view.position.x, core.active_view.position.y)
|
||
|
end
|
||
|
})
|
||
|
|
||
|
keymap.add {
|
||
|
["menu"] = "context:show"
|
||
|
}
|
||
|
|
||
|
-- register some sensible defaults
|
||
|
menu:register("core.docview", {
|
||
|
{ text = "Cut", command = "doc:cut" },
|
||
|
{ text = "Copy", command = "doc:copy" },
|
||
|
{ text = "Paste", command = "doc:paste" },
|
||
|
DIVIDER,
|
||
|
{ text = "Command Palette...", command = "core:find-command" }
|
||
|
})
|
||
|
|
||
|
return menu
|