394 lines
10 KiB
Lua
394 lines
10 KiB
Lua
local core = require "core"
|
|
local common = require "core.common"
|
|
local style = require "core.style"
|
|
local Widget = require "libraries.widget"
|
|
local Button = require "libraries.widget.button"
|
|
local Label = require "libraries.widget.label"
|
|
|
|
---@class widget.filepicker : widget
|
|
---@field public pick_mode integer
|
|
---@field public filters table<integer,string>
|
|
---@field private path string
|
|
---@field private file widget.label
|
|
---@field private textbox widget.textbox
|
|
---@field private button widget.button
|
|
local FilePicker = Widget:extend()
|
|
|
|
---Operation modes for the file picker.
|
|
---@type table<string,integer>
|
|
FilePicker.mode = {
|
|
---Opens file browser the selected file does not has to exist.
|
|
FILE = 1,
|
|
---Opens file browser the selected file has to exist.
|
|
FILE_EXISTS = 2,
|
|
---Opens directory browser the selected directory does not has to exist.
|
|
DIRECTORY = 4,
|
|
---Opens directory browser the selected directory has to exist.
|
|
DIRECTORY_EXISTS = 8
|
|
}
|
|
|
|
---@param text string
|
|
local function suggest_directory(text)
|
|
text = common.home_expand(text)
|
|
return common.home_encode_list(common.dir_path_suggest(text))
|
|
end
|
|
|
|
---@param path string
|
|
local function check_directory_path(path)
|
|
local abs_path = system.absolute_path(path)
|
|
local info = abs_path and system.get_file_info(abs_path)
|
|
if not info or info.type ~= 'dir' then return nil end
|
|
return abs_path
|
|
end
|
|
|
|
---@param str string
|
|
---@param find string
|
|
---@param replace string
|
|
local function str_replace(str, find, replace)
|
|
local start, ending = str:find(find, 1, true)
|
|
if start == 1 then
|
|
return replace .. str:sub(ending + 1)
|
|
else
|
|
return str:sub(1, start - 1) .. replace .. str:sub(ending + 1)
|
|
end
|
|
end
|
|
|
|
---@alias widget.filepicker.modes
|
|
---| `FilePicker.mode.FILE`
|
|
---| `FilePicker.mode.FILE_EXISTS`
|
|
---| `FilePicker.mode.DIRECTORY`
|
|
---| `FilePicker.mode.DIRECTORY_EXISTS`
|
|
|
|
---Constructor
|
|
---@param parent widget
|
|
---@param path? string
|
|
function FilePicker:new(parent, path)
|
|
FilePicker.super.new(self, parent)
|
|
|
|
local this = self
|
|
|
|
self.type_name = "widget.filepicker"
|
|
|
|
self.filters = {}
|
|
self.border.width = 0
|
|
self.pick_mode = FilePicker.mode.FILE
|
|
|
|
self.file = Label(self, "")
|
|
self.file.clickable = true
|
|
self.file:set_border_width(1)
|
|
function self.file:on_click(button)
|
|
if button == "left" then
|
|
this:show_picker()
|
|
end
|
|
end
|
|
function self.file:on_mouse_enter(...)
|
|
Label.super.on_mouse_enter(self, ...)
|
|
self.border.color = style.caret
|
|
end
|
|
function self.file:on_mouse_leave(...)
|
|
Label.super.on_mouse_leave(self, ...)
|
|
self.border.color = style.text
|
|
end
|
|
|
|
self.button = Button(self, "")
|
|
self.button:set_icon("D")
|
|
self.button:set_tooltip("open file browser")
|
|
function self.button:on_click(button)
|
|
if button == "left" then
|
|
this:show_picker()
|
|
end
|
|
end
|
|
|
|
local label_width = self.file:get_width()
|
|
if label_width <= 10 then
|
|
label_width = 200 + (self.file.border.width * 2)
|
|
self.file:set_size(200, self.button:get_height() - self.button.border.width * 2)
|
|
end
|
|
|
|
self:set_size(
|
|
label_width + self.button:get_width(),
|
|
math.max(self.file:get_height(), self.button:get_height())
|
|
)
|
|
|
|
self:set_path(path)
|
|
end
|
|
|
|
---Set the filepicker size
|
|
---@param width? number
|
|
---@param height? number
|
|
function FilePicker:set_size(width, height)
|
|
FilePicker.super.set_size(self, width, height)
|
|
|
|
self.file:set_position(0, 0)
|
|
self.file:set_size(
|
|
self:get_width() - self.button:get_width(),
|
|
self.button:get_height()
|
|
)
|
|
|
|
self.button:set_position(self.file:get_right(), 0)
|
|
|
|
self.size.y = math.max(
|
|
self.file:get_height(),
|
|
self.button:get_height()
|
|
-- something is off on calculation since adding border width should not
|
|
-- be needed to display whole rendered control at all...
|
|
) + self.button.border.width
|
|
end
|
|
|
|
---Add a lua pattern to the filters list
|
|
---@param pattern string
|
|
function FilePicker:add_filter(pattern)
|
|
table.insert(self.filters, pattern)
|
|
end
|
|
|
|
---Clear the filters list
|
|
function FilePicker:clear_filters()
|
|
self.filters = {}
|
|
end
|
|
|
|
---Set the operation mode for the file picker.
|
|
---@param mode widget.filepicker.modes | string | integer
|
|
function FilePicker:set_mode(mode)
|
|
if type(mode) == "string" then
|
|
---@type integer
|
|
local intmode = FilePicker.mode[mode:upper()]
|
|
self.pick_mode = intmode
|
|
else
|
|
self.pick_mode = mode
|
|
end
|
|
end
|
|
|
|
---Set the full path including directory and filename.
|
|
---@param path? string
|
|
function FilePicker:set_path(path)
|
|
if path then
|
|
self.path = path or ""
|
|
if common.path_belongs_to(path, core.project_dir) then
|
|
self.file.label = path ~= "" and
|
|
common.relative_path(core.project_dir, path)
|
|
or
|
|
""
|
|
else
|
|
self.file.label = path
|
|
end
|
|
else
|
|
self.path = ""
|
|
self.file.label = ""
|
|
end
|
|
end
|
|
|
|
---Get the full path including directory and filename.
|
|
---@return string | nil
|
|
function FilePicker:get_path()
|
|
if self.path ~= "" then
|
|
return self.path
|
|
end
|
|
return nil
|
|
end
|
|
|
|
---Get the full path relative to current project dir or absolute if it doesn't
|
|
---belongs to the current project directory.
|
|
---@return string
|
|
function FilePicker:get_relative_path()
|
|
if
|
|
self.path ~= ""
|
|
and
|
|
common.path_belongs_to(self.path, core.project_dir)
|
|
then
|
|
return common.relative_path(core.project_dir, self.path)
|
|
end
|
|
return self.path or ""
|
|
end
|
|
|
|
---Set the filename part only.
|
|
---@param name string
|
|
function FilePicker:set_filename(name)
|
|
local dir_part = common.dirname(self.path)
|
|
if dir_part then
|
|
self:set_path(dir_part .. "/" .. name)
|
|
else
|
|
self:set_path(name)
|
|
end
|
|
end
|
|
|
|
---Get the filename part only.
|
|
---@return string | nil
|
|
function FilePicker:get_filename()
|
|
local dir_part = common.dirname(self.path)
|
|
if dir_part then
|
|
local filename = str_replace(self.path, dir_part .. "/", "")
|
|
return filename
|
|
elseif self.path ~= "" then
|
|
return self.path
|
|
end
|
|
return nil
|
|
end
|
|
|
|
---Set the directory part only.
|
|
---@param dir string
|
|
function FilePicker:set_directory(dir)
|
|
local filename = self:get_filename()
|
|
if filename then
|
|
self:set_path(dir:gsub("[\\/]$", "") .. "/" .. filename)
|
|
else
|
|
self:set_path(dir:gsub("[\\/]$", ""))
|
|
end
|
|
end
|
|
|
|
---Get the directory part only.
|
|
---@return string | nil
|
|
function FilePicker:get_directory()
|
|
if self.path ~= "" then
|
|
local dir_part = common.dirname(self.path)
|
|
if dir_part then return dir_part end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
---Filter a list of directories by applying currently set filters.
|
|
---@param self widget.filepicker
|
|
---@param list table<integer, string>
|
|
---@return table<integer,string>
|
|
local function filter(self, list)
|
|
if #self.filters > 0 then
|
|
local new_list = {}
|
|
for _, value in ipairs(list) do
|
|
if common.match_pattern(value, self.filters) then
|
|
table.insert(new_list, value)
|
|
elseif
|
|
self.pick_mode == FilePicker.mode.FILE
|
|
or
|
|
self.pick_mode == FilePicker.mode.FILE_EXISTS
|
|
then
|
|
local path = common.home_expand(value)
|
|
local abs_path = check_directory_path(path)
|
|
if abs_path then
|
|
table.insert(new_list, value)
|
|
end
|
|
end
|
|
end
|
|
return new_list
|
|
end
|
|
return list
|
|
end
|
|
|
|
---@param self widget.filepicker
|
|
local function show_file_picker(self)
|
|
core.command_view:enter("Choose File", {
|
|
text = self:get_relative_path(),
|
|
submit = function(text)
|
|
---@type string
|
|
local filename = text
|
|
local dirname = common.dirname(common.home_expand(text))
|
|
if dirname then
|
|
filename = common.home_expand(text)
|
|
filename = system.absolute_path(dirname)
|
|
.. "/"
|
|
.. str_replace(filename, dirname .. "/", "")
|
|
elseif filename ~= "" then
|
|
filename = core.project_dir .. "/" .. filename
|
|
end
|
|
self:set_path(filename)
|
|
self:on_change(filename ~= "" and filename or nil)
|
|
end,
|
|
suggest = function (text)
|
|
return filter(
|
|
self,
|
|
common.home_encode_list(common.path_suggest(common.home_expand(text)))
|
|
)
|
|
end,
|
|
validate = function(text)
|
|
if #self.filters > 0 and text ~= "" and not common.match_pattern(text, self.filters) then
|
|
core.error(
|
|
"File does not match the filters: %s",
|
|
table.concat(self.filters, ", ")
|
|
)
|
|
return false
|
|
end
|
|
local filename = common.home_expand(text)
|
|
local path_stat, err = system.get_file_info(filename)
|
|
if path_stat and path_stat.type == 'dir' then
|
|
core.error("Cannot open %s, is a folder", text)
|
|
return false
|
|
end
|
|
if self.pick_mode == FilePicker.mode.FILE_EXISTS then
|
|
if not path_stat then
|
|
core.error("Cannot open file %s: %s", text, err)
|
|
return false
|
|
end
|
|
else
|
|
local dirname = common.dirname(filename)
|
|
local dir_stat = dirname and system.get_file_info(dirname)
|
|
if dirname and not dir_stat then
|
|
core.error("Directory does not exists: %s", dirname)
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end,
|
|
})
|
|
end
|
|
|
|
---@param self widget.filepicker
|
|
local function show_dir_picker(self)
|
|
core.command_view:enter("Choose Directory", {
|
|
text = self:get_relative_path(),
|
|
submit = function(text)
|
|
local path = common.home_expand(text)
|
|
local abs_path = check_directory_path(path)
|
|
self:set_path(abs_path or text)
|
|
self:on_change(abs_path or (text ~= "" and text or nil))
|
|
end,
|
|
suggest = function(text)
|
|
return filter(self, suggest_directory(text))
|
|
end,
|
|
validate = function(text)
|
|
if #self.filters > 0 and text ~= "" and not common.match_pattern(text, self.filters) then
|
|
core.error(
|
|
"Directory does not match the filters: %s",
|
|
table.concat(self.filters, ", ")
|
|
)
|
|
return false
|
|
end
|
|
if self.pick_mode == FilePicker.mode.DIRECTORY_EXISTS then
|
|
local path = common.home_expand(text)
|
|
local abs_path = check_directory_path(path)
|
|
if not abs_path then
|
|
core.error("Cannot open directory %q", path)
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
})
|
|
end
|
|
|
|
---Show the command view file or directory browser depending on the
|
|
---current file picker mode.
|
|
function FilePicker:show_picker()
|
|
if
|
|
self.pick_mode == FilePicker.mode.FILE
|
|
or
|
|
self.pick_mode == FilePicker.mode.FILE_EXISTS
|
|
then
|
|
show_file_picker(self)
|
|
else
|
|
show_dir_picker(self)
|
|
end
|
|
end
|
|
|
|
function FilePicker:update()
|
|
if not FilePicker.super.update(self) then return false end
|
|
|
|
if self:get_width() ~= (self.file:get_width() + self.button:get_width()) then
|
|
self:set_size(
|
|
self.file:get_width() + self.button:get_width(),
|
|
self.button:get_height()
|
|
)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
|
|
return FilePicker
|