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