From 68459a9199f8fffec316bcd28d1c74d86d25269a Mon Sep 17 00:00:00 2001 From: jgmdev Date: Thu, 24 Jun 2021 14:07:50 -0400 Subject: [PATCH 1/3] Added context menu to treeview. --- data/core/contextmenu.lua | 218 ++++++++++++++++++++++++++++++++++ data/plugins/contextmenu.lua | 219 +---------------------------------- data/plugins/treeview.lua | 154 ++++++++++++++++++++++-- 3 files changed, 368 insertions(+), 223 deletions(-) create mode 100644 data/core/contextmenu.lua diff --git a/data/core/contextmenu.lua b/data/core/contextmenu.lua new file mode 100644 index 00000000..36247597 --- /dev/null +++ b/data/core/contextmenu.lua @@ -0,0 +1,218 @@ +local core = require "core" +local common = require "core.common" +local command = require "core.command" +local config = require "core.config" +local keymap = require "core.keymap" +local style = require "core.style" +local Object = require "core.object" + +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 + local items_list = { width = 0, height = 0 } + for _, items in ipairs(self.itemset) do + if items.predicate(x, y) then + items_list.width = math.max(items_list.width, items.items.width) + items_list.height = items_list.height + items.items.height + for _, subitems in ipairs(items.items) do + table.insert(items_list, subitems) + end + end + end + + if #items_list > 0 then + self.items = items_list + 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 + + self.selected = -1 + 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 + self.selected = i + break + end + end + if self.selected >= 0 then + core.request_cursor("arrow") + end + 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 not config.transitions or math.abs(val - dest) < 0.5 then + t[k] = dest + else + rate = rate or 0.5 + if config.fps ~= 60 or config.animation_rate ~= 1 then + local dt = 60 / config.fps + rate = 1 - common.clamp(1 - rate, 1e-8, 1 - 1e-8)^(config.animation_rate * dt) + end + t[k] = common.lerp(val, dest, rate) + 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 + +return ContextMenu diff --git a/data/plugins/contextmenu.lua b/data/plugins/contextmenu.lua index 4de46080..c0fe49fd 100644 --- a/data/plugins/contextmenu.lua +++ b/data/plugins/contextmenu.lua @@ -1,223 +1,10 @@ -- mod-version:1 -- lite-xl 1.16 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 ContextMenu = require "core.contextmenu" 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 - local items_list = { width = 0, height = 0 } - for _, items in ipairs(self.itemset) do - if items.predicate(x, y) then - items_list.width = math.max(items_list.width, items.items.width) - items_list.height = items_list.height + items.items.height - for _, subitems in ipairs(items.items) do - table.insert(items_list, subitems) - end - end - end - - if #items_list > 0 then - self.items = items_list - 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 - - self.selected = -1 - 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 - self.selected = i - break - end - end - if self.selected >= 0 then - core.request_cursor("arrow") - end - 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 not config.transitions or math.abs(val - dest) < 0.5 then - t[k] = dest - else - rate = rate or 0.5 - if config.fps ~= 60 or config.animation_rate ~= 1 then - local dt = 60 / config.fps - rate = 1 - common.clamp(1 - rate, 1e-8, 1 - 1e-8)^(config.animation_rate * dt) - end - t[k] = common.lerp(val, dest, rate) - 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 on_view_mouse_pressed = RootView.on_view_mouse_pressed local on_mouse_moved = RootView.on_mouse_moved @@ -260,10 +47,10 @@ if require("plugins.scale") then { text = "Font +", command = "scale:increase" }, { text = "Font -", command = "scale:decrease" }, { text = "Font Reset", command = "scale:reset" }, - DIVIDER, + ContextMenu.DIVIDER, { text = "Find", command = "find-replace:find" }, { text = "Replace", command = "find-replace:replace" }, - DIVIDER, + ContextMenu.DIVIDER, { text = "Find Pattern", command = "find-replace:find-pattern" }, { text = "Replace Pattern", command = "find-replace:replace-pattern" }, }) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 8214bda4..90145260 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -6,9 +6,12 @@ local config = require "core.config" local keymap = require "core.keymap" local style = require "core.style" local View = require "core.view" +local ContextMenu = require "core.contextmenu" +local RootView = require "core.rootview" + local default_treeview_size = 200 * SCALE -local tooltip_offset = style.font:get_height("A") +local tooltip_offset = style.font:get_height() local tooltip_border = 1 local tooltip_delay = 0.5 local tooltip_alpha = 255 @@ -173,13 +176,13 @@ end function TreeView:on_mouse_moved(px, py, ...) TreeView.super.on_mouse_moved(self, px, py, ...) if self.dragging_scrollbar then return end - + local item_changed, tooltip_changed for item, x,y,w,h in self:each_item() do if px > x and py > y and px <= x + w and py <= y + h then item_changed = true self.hovered_item = item - + x,y,w,h = self:get_text_bounding_box(item, x,y,w,h) if px > x and py > y and px <= x + w and py <= y + h then tooltip_changed = true @@ -210,7 +213,7 @@ end function TreeView:on_mouse_pressed(button, x, y, clicks) local caught = TreeView.super.on_mouse_pressed(self, button, x, y, clicks) - if caught then + if caught or button ~= "left" then return end local hovered_item = self.hovered_item @@ -256,7 +259,7 @@ function TreeView:update() else self:move_towards(self.size, "x", dest) end - + local duration = system.get_time() - self.tooltip.begin if self.hovered_item and self.tooltip.x and duration > tooltip_delay then self:move_towards(self.tooltip, "alpha", tooltip_alpha, tooltip_alpha_rate) @@ -353,9 +356,10 @@ local treeview_node = node:split("left", view, {x = true}, true) -- plugin to be independent of each other. In addition it is not the -- plugin module that plug itself in the active node but it is plugged here -- in the treeview node. +local toolbar_view = nil local toolbar_plugin, ToolbarView = core.try(require, "plugins.toolbarview") if config.toolbarview ~= false and toolbar_plugin then - local toolbar_view = ToolbarView() + toolbar_view = ToolbarView() treeview_node:split("down", toolbar_view, {y = true}) local min_toolbar_width = toolbar_view:get_min_width() view:set_target_size("x", math.max(default_treeview_size, min_toolbar_width)) @@ -366,12 +370,148 @@ if config.toolbarview ~= false and toolbar_plugin then }) end +-- Add a context menu to the treeview +local menu = ContextMenu() --- register commands and keymap +local on_view_mouse_pressed = RootView.on_view_mouse_pressed +local 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 + on_mouse_moved(self, ...) +end + +function RootView.on_view_mouse_pressed(button, x, y, clicks) + -- We give the priority to the menu to process mouse pressed events. + if button == "right" then + view.tooltip.alpha = 0 + view.tooltip.x, view.tooltip.y = nil, nil + end + local handled = menu:on_mouse_pressed(button, x, y, clicks) + return handled or on_view_mouse_pressed(button, x, y, clicks) +end + +function RootView:update(...) + root_view_update(self, ...) + menu:update() +end + +function RootView:draw(...) + root_view_draw(self, ...) + menu:draw() +end + +local function is_project_folder(path) + return common.basename(core.project_dir) == path +end + +menu:register(function() return view.hovered_item end, { + { text = "Open in System", command = "treeview:open-in-system" }, + ContextMenu.DIVIDER +}) + +menu:register( + function() + return view.hovered_item + and not is_project_folder(view.hovered_item.filename) + end, + { + { text = "Rename", command = "treeview:rename" }, + { text = "Delete", command = "treeview:delete" }, + } +) + +menu:register( + function() + return view.hovered_item and view.hovered_item.type == "dir" + end, + { + { text = "New File", command = "treeview:new-file" }, + { text = "New Folder", command = "treeview:new-folder" }, + } +) + +-- Register the TreeView commands and keymap command.add(nil, { ["treeview:toggle"] = function() view.visible = not view.visible end, + + ["treeview:rename"] = function() + local old_filename = view.hovered_item.filename + core.command_view:set_text(old_filename) + core.command_view:enter("Rename", function(filename) + os.rename(old_filename, filename) + core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) + end, common.path_suggest) + end, + + ["treeview:new-file"] = function() + local dir_name = view.hovered_item.filename + if not is_project_folder(dir_name) then + core.command_view:set_text(dir_name .. "/") + end + core.command_view:enter("Filename", function(filename) + local doc_filename = core.project_dir .. PATHSEP .. filename + local file = io.open(doc_filename, "a+") + file:write("") + file:close() + core.root_view:open_doc(core.open_doc(doc_filename)) + core.log("Created %s", doc_filename) + end, common.path_suggest) + end, + + ["treeview:new-folder"] = function() + local dir_name = view.hovered_item.filename + if not is_project_folder(dir_name) then + core.command_view:set_text(dir_name .. "/") + end + core.command_view:enter("Folder Name", function(filename) + local dir_path = core.project_dir .. PATHSEP .. filename + common.mkdirp(dir_path) + core.log("Created %s", dir_path) + end, common.path_suggest) + end, + + ["treeview:delete"] = function() + local filename = view.hovered_item.abs_filename + local file_info = system.get_file_info(filename) + if file_info.type == "dir" then + local deleted, error, path = common.rm(filename, true) + if not deleted then + core.error("Error: %s - \"%s\" ", error, path) + return + end + else + local removed, error = os.remove(filename) + if not removed then + core.error("Error: %s - \"%s\"", error, filename) + return + end + end + core.log("Deleted \"%s\"", filename) + end, + + ["treeview:open-in-system"] = function() + local hovered_item = view.hovered_item + + if PLATFORM == "Windows" then + system.exec("start " .. hovered_item.abs_filename) + elseif string.find(PLATFORM, "Mac") then + system.exec(string.format("open %q", hovered_item.abs_filename)) + elseif PLATFORM == "Linux" then + system.exec(string.format("xdg-open %q", hovered_item.abs_filename)) + end + end, }) keymap.add { ["ctrl+\\"] = "treeview:toggle" } + +-- Return the treeview with toolbar and contextmenu to allow +-- user or plugin modifications +view.toolbar = toolbar_view +view.contextmenu = menu + +return view From a4d5622edae6c3689d3e2e62201b84751112fcd1 Mon Sep 17 00:00:00 2001 From: jgmdev Date: Sun, 11 Jul 2021 23:03:33 -0400 Subject: [PATCH 2/3] Make use of core.reschedule_project_scan() --- data/plugins/treeview.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 90145260..87a9db1c 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -444,6 +444,7 @@ command.add(nil, { core.command_view:set_text(old_filename) core.command_view:enter("Rename", function(filename) os.rename(old_filename, filename) + core.reschedule_project_scan() core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) end, common.path_suggest) end, @@ -459,6 +460,7 @@ command.add(nil, { file:write("") file:close() core.root_view:open_doc(core.open_doc(doc_filename)) + core.reschedule_project_scan() core.log("Created %s", doc_filename) end, common.path_suggest) end, @@ -471,6 +473,7 @@ command.add(nil, { core.command_view:enter("Folder Name", function(filename) local dir_path = core.project_dir .. PATHSEP .. filename common.mkdirp(dir_path) + core.reschedule_project_scan() core.log("Created %s", dir_path) end, common.path_suggest) end, @@ -491,6 +494,7 @@ command.add(nil, { return end end + core.reschedule_project_scan() core.log("Deleted \"%s\"", filename) end, From afa0c175e85ae183c6537fd23cd9e91cd45a1789 Mon Sep 17 00:00:00 2001 From: jgmdev Date: Mon, 12 Jul 2021 11:33:14 -0400 Subject: [PATCH 3/3] Added delete confirmation using NagView. --- data/plugins/treeview.lua | 47 +++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 87a9db1c..0b9304aa 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -480,22 +480,41 @@ command.add(nil, { ["treeview:delete"] = function() local filename = view.hovered_item.abs_filename + local relfilename = view.hovered_item.filename local file_info = system.get_file_info(filename) - if file_info.type == "dir" then - local deleted, error, path = common.rm(filename, true) - if not deleted then - core.error("Error: %s - \"%s\" ", error, path) - return + local file_type = file_info.type == "dir" and "Directory" or "File" + -- Ask before deleting + local opt = { + { font = style.font, text = "Yes", default_yes = true }, + { font = style.font, text = "No" , default_no = true } + } + core.nag_view:show( + string.format("Delete %s", file_type), + string.format( + "Are you sure you want to delete the %s?\n%s: %s", + file_type:lower(), file_type, relfilename + ), + opt, + function(item) + if item.text == "Yes" then + if file_info.type == "dir" then + local deleted, error, path = common.rm(filename, true) + if not deleted then + core.error("Error: %s - \"%s\" ", error, path) + return + end + else + local removed, error = os.remove(filename) + if not removed then + core.error("Error: %s - \"%s\"", error, filename) + return + end + end + core.reschedule_project_scan() + core.log("Deleted \"%s\"", filename) + end end - else - local removed, error = os.remove(filename) - if not removed then - core.error("Error: %s - \"%s\"", error, filename) - return - end - end - core.reschedule_project_scan() - core.log("Deleted \"%s\"", filename) + ) end, ["treeview:open-in-system"] = function()