mirror of
https://github.com/ctruLua/ctruLua.git
synced 2025-10-27 16:39:29 +00:00
Updated filepicker.lua and adding documentation
Effectively rendering openfile.lua completely obsolete, which is why it was deleted. filepicker.lua is now way smarter, handles file creation and has a documentation file in LDoc.
This commit is contained in:
parent
c687efcfb4
commit
4669a68402
4 changed files with 361 additions and 305 deletions
|
|
@ -11,7 +11,7 @@ format = "markdown"
|
|||
plain = true
|
||||
|
||||
-- Input files
|
||||
topics = "../README.md"
|
||||
topics = {"../README.md", "filepicker.md"}
|
||||
file = "../source/"
|
||||
examples = "../sdcard/3ds/ctruLua/examples/"
|
||||
manual_url = "file://../libs/lua-5.3.1/doc/manual.html"
|
||||
|
|
|
|||
93
doc/filepicker.md
Normal file
93
doc/filepicker.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# filepicker
|
||||
## filePicker([workingDirectory[, bindings[, callbacks[, ...]]]])
|
||||
### Argument: workingDirectory
|
||||
The directory that shows up first in the file browser.
|
||||
If this is nil, ctr.fs.getDirectory() is used.
|
||||
The recommended form is sdmc:/path/ or romfs:/path/, but it can be a simple /path/ instead.
|
||||
#### Possible values
|
||||
- string
|
||||
- nil
|
||||
|
||||
### Argument: bindings
|
||||
A table, list of filetypes and key bindings related to these filetypes.
|
||||
#### Format
|
||||
```
|
||||
{
|
||||
__default, __directory, [Lua regexp] = {
|
||||
[keys from ctr.hid.keys()] = {
|
||||
function
|
||||
string
|
||||
}...
|
||||
__name = string
|
||||
}...
|
||||
}
|
||||
```
|
||||
|
||||
The Lua regexp is matched against the filename to determine if it is of this type.
|
||||
__directory is the "file type" for directories
|
||||
__default is the "file type" for files that cannot be matched with any other.
|
||||
Also, every other type inherits the values it doesn't define from __default.
|
||||
A file type contains the human-readable name (__name) of the type, displayed on the bottom screen,
|
||||
as well as an optional binding for each key.
|
||||
The optional binding is formed of an anonymous function, followed by the key's label to be displayed on the bottom screen,
|
||||
if the label is nil, the key isn't displayed but still bound.
|
||||
|
||||
The function is defined as-is:
|
||||
##### function(externalConfig, selected, bindingPattern, bindingKey)
|
||||
externalConfig.workingDirectory is the active directory for filePicker, doesn't necessarily match ctrµLua's.
|
||||
externalConfig.bindings, externalConfig.callbacks and externalConfig.additionalArguments all are the arguments passed to filePicker,
|
||||
starting from position 2.
|
||||
externalConfig.fileList is the list of files currently displayed by filePicker, in the same format as is returned by ctr.fs.listDirectory().
|
||||
selected.inList is the absolute position of the cursor in externalConfig.fileList
|
||||
selected.offset is the number of items skipped for display from fileList
|
||||
bindingPattern is the [Lua regexp] defined earlier, and bindingKey the [key] that triggered this event.
|
||||
This function may return the same thing as filePicker itself (Defined later here), or nothing. If it returns nothing,
|
||||
filePicker will keep running, if it returns the same returns as filePicker, filePicker will exit, returning these values.
|
||||
|
||||
#### Notes
|
||||
Sane defaults are set if you did not set them otherwise:
|
||||
__default.x quits filePicker, returning the current directory, "__directory", nil and "x". See the returns to understand what this means.
|
||||
__directory.a changes directories to that directory.
|
||||
|
||||
### Argument: callbacks
|
||||
A table defining the callbacks ran at the end of each of the equivalent phases.
|
||||
#### Format
|
||||
```
|
||||
{
|
||||
drawTop, drawBottom, eventHandler = function
|
||||
}
|
||||
```
|
||||
All of these take the following parameters: (externalConfig, selected)
|
||||
They have the meaning defined earlier.
|
||||
|
||||
#### Notes
|
||||
Although drawTop and drawBottom are ran at the end of their respective functions,
|
||||
eventHandler is not, as it cannot be without being run repeatedly, so it's run at the beginning of
|
||||
the ACTUAL eventHandler instead of its end.
|
||||
|
||||
### Argument: ...
|
||||
Additional parameters. All of these are aggregated orderly in externalConfig.additionalArguments and passed around as explained earlier.
|
||||
They have no specific meaning unless defined so by event handling functions.
|
||||
|
||||
### Return 1: selectedPath
|
||||
The path selected by the either, may or may not have the sdmc:/romfs: prefix, depending on your input.
|
||||
A string.
|
||||
|
||||
### Return 2: bindingPattern
|
||||
The pattern the file matched to. You can use this to know exactly which kind of file you're dealing with
|
||||
A string.
|
||||
|
||||
### Return 3: mode
|
||||
Included handlers may have it be "open", in case you're opening an existing file, "new" in case the user wants to create a new file,
|
||||
Or nil. A "nil" is assumed to mean that the user didn't pick anything.
|
||||
A string or nil.
|
||||
|
||||
### Return 4: key
|
||||
The key that triggered the event that made filePicker exit.
|
||||
A string.
|
||||
|
||||
## Included event handlers you have available are:
|
||||
- filepicker.changeDirectory - The name is on the tin, change workingDirectory to the active element and refresh the file list for that path.
|
||||
- filepicker.openFile - Quits and returns as described by this document based on the active element.
|
||||
- filepicker.newFile - Prompts the user to input a file name manually, relative to the current working directory, and with FAT-incompatible characters excluded. Quits and returns that.
|
||||
- filepicker.nothing - Do nothing and keep on running. Literally. This is used as a plug to enable an action for all (or a certain type) but files of a certain type (or more precise than the initial type).
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
-- LSH version 0.1
|
||||
-- ctrµLua official shell
|
||||
local ctr = require('ctr')
|
||||
local keyboard = require('keyboard')
|
||||
|
||||
local ctr = require("ctr")
|
||||
local gfx = require("ctr.gfx")
|
||||
local gfx = ctr.gfx
|
||||
|
||||
local function saveGraphicsState()
|
||||
local externalConfig
|
||||
|
||||
local function gfxPrepare()
|
||||
local old = {gfx.get3D(), gfx.color.getDefault(), gfx.color.getBackground(),
|
||||
gfx.font.getDefault()}
|
||||
gfx.font.getDefault(), gfx.getTextSize()}
|
||||
|
||||
local mono = gfx.font.load(ctr.root .. "resources/VeraMono.ttf")
|
||||
|
||||
|
|
@ -14,34 +15,67 @@ local function saveGraphicsState()
|
|||
gfx.color.setDefault(0xFFFDFDFD)
|
||||
gfx.color.setBackground(0xFF333333)
|
||||
gfx.font.setDefault(mono)
|
||||
gfx.setTextSize(12)
|
||||
|
||||
return old
|
||||
end
|
||||
|
||||
local function restoreGraphicsState(state)
|
||||
local function gfxRestore(state)
|
||||
gfx.set3D(state[1])
|
||||
gfx.color.setDefault(state[2])
|
||||
gfx.color.setBackground(state[3])
|
||||
gfx.font.setDefault(state[4])
|
||||
gfx.setTextSize(state[5])
|
||||
end
|
||||
|
||||
local function getExtension(sel, bindings)
|
||||
for _, ext in ipairs(bindings) do
|
||||
if ext.ext == sel:match("%..+$") then
|
||||
return ext
|
||||
local function systemBindings(bindings)
|
||||
bindings.__default.up = {
|
||||
function(_, selected, ...)
|
||||
if selected.inList > 1 then
|
||||
selected.inList = selected.inList - 1
|
||||
if selected.inList == selected.offset then
|
||||
selected.offset = selected.offset - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
bindings.__default.down = {
|
||||
function(externalConfig, selected, ...)
|
||||
if selected.inList < #externalConfig.fileList then
|
||||
selected.inList = selected.inList + 1
|
||||
if selected.inList - selected.offset >= 16 then
|
||||
selected.offset = selected.offset + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
bindings.__default.left = {
|
||||
function(_, selected, ...)
|
||||
selected.inList, selected.offset = 1, 0
|
||||
end
|
||||
}
|
||||
|
||||
bindings.__default.right = {
|
||||
function(externalConfig, selected, ...)
|
||||
selected.inList = #externalConfig.fileList
|
||||
if #externalConfig.fileList > 15 then
|
||||
selected.offset = #externalConfig.fileList - 16
|
||||
end
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
local function getFilelist(cur)
|
||||
local files = ctr.fs.list(cur)
|
||||
local function getFileList(workingDirectory)
|
||||
local fileList = ctr.fs.list(workingDirectory)
|
||||
|
||||
if cur ~= "/" and cur ~= "sdmc:/" then
|
||||
table.insert(files, {name = "..", isDirectory = true})
|
||||
if workingDirectory ~= "/" and workingDirectory ~= "sdmc:/" then
|
||||
table.insert(fileList, {name = "..", isDirectory = true})
|
||||
end
|
||||
|
||||
-- Stealy stealing code from original openfile.lua
|
||||
table.sort(files, function(i, j)
|
||||
table.sort(fileList, function(i, j)
|
||||
if i.isDirectory and not j.isDirectory then
|
||||
return true
|
||||
elseif i.isDirectory == j.isDirectory then
|
||||
|
|
@ -49,151 +83,236 @@ local function getFilelist(cur)
|
|||
end
|
||||
end)
|
||||
|
||||
return files
|
||||
return fileList
|
||||
end
|
||||
|
||||
local function drawBottom(cur, selFile, bindings)
|
||||
local ext = getExtension(selFile.name, bindings)
|
||||
local function getBinding(selectedFile, bindings)
|
||||
if selectedFile.isDirectory then
|
||||
return bindings.__directory, "__directory"
|
||||
end
|
||||
for pattern, values in pairs(bindings) do
|
||||
if selectedFile.name:match(pattern) then
|
||||
return values, pattern
|
||||
end
|
||||
end
|
||||
return bindings.__default, "__default"
|
||||
end
|
||||
|
||||
local function drawBottom(externalConfig, workingDirectoryScroll, selected)
|
||||
local workingDirectory = externalConfig.workingDirectory
|
||||
local bindings = externalConfig.bindings
|
||||
local selectedFile = externalConfig.fileList[selected.inList]
|
||||
gfx.start(gfx.BOTTOM)
|
||||
gfx.rectangle(0, 0, gfx.BOTTOM_WIDTH, 16, 0, 0xFF0000B3)
|
||||
gfx.text(1, 0, cur, 12)
|
||||
gfx.text(1, 15, selFile.name, 12)
|
||||
if not selFile.isDirectory then
|
||||
gfx.text(1, 45, selFile.fileSize, 12)
|
||||
gfx.text(1 - workingDirectoryScroll.value, 0, workingDirectory)
|
||||
if gfx.font.getDefault():width(workingDirectory) > gfx.BOTTOM_WIDTH - 2 then
|
||||
workingDirectoryScroll.value = workingDirectoryScroll.value + workingDirectoryScroll.phase
|
||||
if workingDirectoryScroll.value == (gfx.BOTTOM_WIDTH - 2) - gfx.font.getDefault():width(workingDirectory) or
|
||||
workingDirectoryScroll.value == 0 then
|
||||
workingDirectoryScroll.phase = - workingDirectoryScroll.phase
|
||||
end
|
||||
end
|
||||
|
||||
local keys = {"X: Quit/Cancel"}
|
||||
if selFile.isDirectory then
|
||||
gfx.text(1, 30, "Directory", 12, 0xFF727272)
|
||||
gfx.text(1, gfx.BOTTOM_HEIGHT - 30, "A: Open", 12)
|
||||
gfx.text(1, gfx.BOTTOM_HEIGHT - 15, keys[1], 12)
|
||||
elseif ext then
|
||||
local lines = 1
|
||||
|
||||
-- Keys
|
||||
if ext.y then
|
||||
lines = lines + 1
|
||||
table.insert(keys, "Y: " .. ext.y)
|
||||
end
|
||||
if ext.a then
|
||||
lines = lines + 1
|
||||
table.insert(keys, "A: " .. ext.a)
|
||||
gfx.text(1, 15, selectedFile.name, 12)
|
||||
if not selectedFile.isDirectory then
|
||||
gfx.text(1, 45, tostring(selectedFile.fileSize) .. "B", 12, 0xFF727272)
|
||||
end
|
||||
|
||||
-- Drawing
|
||||
for i=lines, 1, -1 do
|
||||
gfx.text(1, gfx.BOTTOM_HEIGHT - 15*i, keys[i], 12)
|
||||
end
|
||||
gfx.text(1, 30, ext.name, 12, 0xFF727272)
|
||||
gfx.text(1, 45, tostring(selFile.fileSize) .. "B", 12, 0xFF727272)
|
||||
local binding, pattern = getBinding(selectedFile, bindings)
|
||||
if selectedFile.isDirectory then
|
||||
gfx.text(1, 30, bindings.__directory.__name, 12, 0xFF727272)
|
||||
else
|
||||
gfx.text(1, 30, "File", 12, 0xFF727272)
|
||||
gfx.text(1, 45, tostring(selFile.fileSize) .. "B", 12, 0xFF727272)
|
||||
gfx.text(1, gfx.BOTTOM_HEIGHT - 15, keys[1], 12)
|
||||
gfx.text(1, 30, binding.__name, 12, 0xFF727272)
|
||||
end
|
||||
|
||||
local bindingNames = {
|
||||
{"start", "Start"}, {"select", "Select"},
|
||||
{"x", "X"}, {"y", "Y"},
|
||||
{"b", "B"}, {"a", "A"},
|
||||
{"r", "R"}, {"l", "L"},
|
||||
{"zr", "ZR"}, {"zl", "ZL"},
|
||||
{"cstickDown", "C Down"}, {"cstickUp", "C Up"},
|
||||
{"cstickRight", "C Right"}, {"cstickLeft", "C Left"}
|
||||
}
|
||||
|
||||
local j = 0
|
||||
|
||||
for i, v in ipairs(bindingNames) do
|
||||
if binding[v[1]] and binding[v[1]][2] then
|
||||
j = j + 1
|
||||
gfx.text(1, gfx.BOTTOM_HEIGHT - 15*j, v[2] .. ": " .. binding[v[1]][2])
|
||||
end
|
||||
end
|
||||
|
||||
externalConfig.callbacks.drawBottom(externalConfig, selected)
|
||||
gfx.stop()
|
||||
end
|
||||
|
||||
local function drawTop(files, sel, scr)
|
||||
local function drawTop(externalConfig, selected)
|
||||
gfx.start(gfx.TOP)
|
||||
gfx.rectangle(0, (sel-scr-1)*15, gfx.TOP_WIDTH, 16, 0, 0xFF0000B3)
|
||||
local over = #files - scr >= 16 and 16 or #files - scr
|
||||
for i=scr+1, scr+over do
|
||||
local color = files[i].isDirectory and 0xFF727272 or 0xFFFDFDFD
|
||||
gfx.text(1, (i-scr-1)*15+1, files[i].name or "", 12, color)
|
||||
gfx.rectangle(0, (selected.inList-selected.offset-1)*15, gfx.TOP_WIDTH, 16, 0, 0xFF0000B9)
|
||||
local over = #externalConfig.fileList - selected.offset >= 16 and 16 or #externalConfig.fileList - selected.offset
|
||||
for i=selected.offset+1, selected.offset+over do
|
||||
local color = externalConfig.fileList[i].isDirectory and 0xFF727272 or 0xFFFDFDFD
|
||||
gfx.text(1, (i-selected.offset-1)*15+1, externalConfig.fileList[i].name or "", 12, color)
|
||||
end
|
||||
externalConfig.callbacks.drawTop(externalConfig, selected)
|
||||
gfx.stop()
|
||||
end
|
||||
|
||||
local function runA(cur, selFile, bindings)
|
||||
if not selFile.isDirectory then
|
||||
local ext = getExtension(selFile.name, bindings)
|
||||
if not ext then return end
|
||||
if ext.a then return cur .. selFile.name, ext.ext end
|
||||
end
|
||||
end
|
||||
|
||||
local function runY(cur, selFile, bindings)
|
||||
if not selFile.isDirectory then
|
||||
local ext = getExtension(selFile.name, bindings)
|
||||
if not ext then return end
|
||||
if ext.y then return cur .. selFile.name, ext.ext end
|
||||
end
|
||||
end
|
||||
|
||||
--- Open a file browser to allow the user to select a file.
|
||||
-- It will save current graphical settings and set them back after ending. Press up or down to move one element at a time, and press left or right to go at the beginning or at the end of the list. This function is the return result of requiring filepicker.lua
|
||||
-- @name filePicker
|
||||
-- @param bindings A table of the extensions the user can select in the format {{name, ext, a, y},...}, name will show up instead of "File" or "Directory" on the bottom screen, a and y set the action names for those keys on the bottom screen (and also enable them, so if there's neither a or y, the file will have a custom type name but won't be effectively selectable), and ext is the extension to search for, dot included. Everything must be strings.
|
||||
-- @param workdir Optional, current working directory will be used if not specified, otherwise, sets the path at which the file browser first shows up, a string.
|
||||
-- @returns The absolute path to the file, nil in case no file was picked.
|
||||
-- @returns The extension of the file, this might be helpful in cases were multiple file types could be expected, nil in case no file was picked.
|
||||
-- @returns The "mode", which indicates which key was used to select the file, "A" or "Y". "X" in case no file was picked.
|
||||
return function(bindings, workdir)
|
||||
-- Initialization
|
||||
local old = saveGraphicsState()
|
||||
local cur = workdir or ctr.fs.getDirectory()
|
||||
if cur:sub(-1) ~= "/" then
|
||||
cur = cur .. "/"
|
||||
end
|
||||
local bindings = bindings or {}
|
||||
|
||||
local files = getFilelist(cur) or {{name = "- Empty -"}}
|
||||
local sel = 1
|
||||
local scr = 0
|
||||
|
||||
while ctr.run() do
|
||||
drawBottom(cur, files[sel], bindings)
|
||||
drawTop(files, sel, scr)
|
||||
gfx.render()
|
||||
|
||||
local function eventHandler(externalConfig, selected)
|
||||
externalConfig.callbacks.eventHandler(externalConfig, selected)
|
||||
ctr.hid.read()
|
||||
local state = ctr.hid.keys()
|
||||
if (state.down.dDown or state.down.cpadDown) and sel < #files then
|
||||
sel = sel + 1
|
||||
if sel - scr >= 16 then
|
||||
scr = scr + 1
|
||||
end
|
||||
elseif (state.down.dUp or state.down.cpadUp) and sel > 1 then
|
||||
sel = sel - 1
|
||||
if sel == scr then
|
||||
scr = scr - 1
|
||||
end
|
||||
elseif state.down.dLeft or state.down.cpadLeft then
|
||||
sel = 1
|
||||
scr = 0
|
||||
elseif state.down.dRight or state.down.cpadRight then
|
||||
sel = #files
|
||||
if #files > 15 then
|
||||
scr = #files - 16
|
||||
end
|
||||
|
||||
elseif state.down.a then
|
||||
local selFile = files[sel]
|
||||
if selFile.isDirectory then
|
||||
if selFile.name == ".." then
|
||||
cur = cur:gsub("[^/]+/$", "")
|
||||
else
|
||||
cur = cur .. selFile.name .. "/"
|
||||
end
|
||||
files, sel, scr = getFilelist(cur), 1, 0
|
||||
else
|
||||
local file, ext = runA(cur, selFile, bindings)
|
||||
if file then
|
||||
restoreGraphicsState(old)
|
||||
return file, ext, "A"
|
||||
local binding, pattern = getBinding(externalConfig.fileList[selected.inList], externalConfig.bindings)
|
||||
for k, v in pairs(binding) do
|
||||
if k ~= "__name" and state.down[k] then
|
||||
local a, b, c, key = v[1](externalConfig, selected, pattern, k)
|
||||
if key then return a, b, c, key
|
||||
else return end
|
||||
end
|
||||
end
|
||||
elseif state.down.y then
|
||||
local file, ext = runY(cur, files[sel], bindings)
|
||||
if file then
|
||||
restoreGraphicsState(old)
|
||||
return file, ext, "Y"
|
||||
end
|
||||
elseif state.down.x then
|
||||
restoreGraphicsState(old)
|
||||
return nil, nil, "X"
|
||||
for k, v in pairs(externalConfig.bindings.__default) do
|
||||
if k ~= "__name" and state.down[k] then
|
||||
local a, b, c, key = v[1](externalConfig, selected, pattern, k)
|
||||
if key then return a, b, c, key
|
||||
else break end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function nothing(externalConfig, selected, bindingName, bindingKey)
|
||||
-- externalConfig = {workingDirectory=string, bindings=table, callbacks=table, additionalArguments=table, fileList=table}
|
||||
-- selected = {file=string, inList=number, offset=number}
|
||||
-- bindings = {__default/__directory/[regex] = {__name, [keyName] = {(handlingFunction), (name)}}}
|
||||
-- callbacks = {drawTop, drawBottom, eventHandler}
|
||||
end
|
||||
|
||||
local function changeDirectory(externalConfig, selected, bindingName, bindingKey)
|
||||
if externalConfig.fileList[selected.inList].isDirectory then
|
||||
if externalConfig.fileList[selected.inList].name == ".." then
|
||||
externalConfig.workingDirectory = externalConfig.workingDirectory:gsub("[^/]+/$", "")
|
||||
else
|
||||
externalConfig.workingDirectory = externalConfig.workingDirectory .. externalConfig.fileList[selected.inList].name .. "/"
|
||||
end
|
||||
externalConfig.fileList = getFileList(externalConfig.workingDirectory)
|
||||
selected.inList, selected.offset = 1, 0
|
||||
end
|
||||
end
|
||||
|
||||
local function newFile(externalConfig, selected, bindingName, bindingKey)
|
||||
local name = ""
|
||||
while ctr.run() do
|
||||
gfx.start(gfx.BOTTOM)
|
||||
gfx.rectangle(0, 0, gfx.BOTTOM_WIDTH, 16, 0, 0xFF0000B3)
|
||||
gfx.text(1, 0, externalConfig.workingDirectory)
|
||||
keyboard.draw(4, 115)
|
||||
gfx.stop()
|
||||
|
||||
gfx.start(gfx.TOP)
|
||||
gfx.rectangle(0, 0, gfx.TOP_WIDTH, 16, 0, 0xFF0000B3)
|
||||
gfx.text(1, 0, "Creating new file")
|
||||
gfx.rectangle(4, gfx.TOP_HEIGHT // 2 - 15, gfx.TOP_WIDTH - 8, 30, 0, 0xFF727272)
|
||||
gfx.text(5, gfx.TOP_HEIGHT // 2 - 6, name, 12)
|
||||
gfx.stop()
|
||||
gfx.render()
|
||||
|
||||
local char = (keyboard.read() or ""):gsub("[\t%/%?%<%>%\\%:%*%|%”%^]", "")
|
||||
ctr.hid.read()
|
||||
local keys = ctr.hid.keys()
|
||||
|
||||
if char ~= "" and char ~= "\b" and char ~= "\n" then
|
||||
name = name .. char
|
||||
elseif char ~= "" and char == "\b" then
|
||||
name = name:sub(1, (utf8.offset(name, -1) or 0)-1)
|
||||
elseif (char ~= "" and char == "\n" or keys.down.a) and name ~= "" then
|
||||
local b, p = getBinding({name=name}, externalConfig.bindings)
|
||||
return externalConfig.workingDirectory .. name, p, "new", b
|
||||
elseif keys.down.b then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function openFile(externalConfig, selected, bindingName, bindingKey)
|
||||
return externalConfig.workingDirectory .. externalConfig.fileList[selected.inList].name,
|
||||
bindingName, "open", bindingKey
|
||||
end
|
||||
|
||||
local function filePicker(workingDirectory, bindings, callbacks, ...)
|
||||
-- Argument sanitization
|
||||
local additionalArguments = { ... }
|
||||
workingDirectory = workingDirectory or ctr.fs.getDirectory()
|
||||
bindings = bindings or {}
|
||||
callbacks = callbacks or {}
|
||||
for _, v in ipairs({"drawTop", "drawBottom", "eventHandler"}) do
|
||||
if not callbacks[v] then
|
||||
callbacks[v] = function(...) end
|
||||
end
|
||||
end
|
||||
|
||||
if workingDirectory:sub(utf8.offset(workingDirectory, -1) or -1) ~= "/" then
|
||||
workingDirectory = workingDirectory .. "/"
|
||||
end
|
||||
|
||||
-- Default Bindings
|
||||
bindings.__default = bindings.__default or {}
|
||||
bindings.__default.__name = bindings.__default.__name or "File"
|
||||
bindings.__default.x = bindings.__default.x or {
|
||||
function(externalConfig, ...)
|
||||
return externalConfig.workingDirectory, "__directory", nil, "x"
|
||||
end, "Quit"
|
||||
}
|
||||
|
||||
bindings.__directory = bindings.__directory or {}
|
||||
bindings.__directory.__name = bindings.__directory.__name or "Directory"
|
||||
bindings.__directory.a = bindings.__directory.a or {
|
||||
changeDirectory, "Open"
|
||||
}
|
||||
|
||||
local movementKeys = {
|
||||
"up" , "down" , "left" , "right" ,
|
||||
"cpadUp", "cpadDown", "cpadLeft", "cpadRight",
|
||||
"dUp" , "dDown" , "dLeft" , "dRight"
|
||||
}
|
||||
|
||||
for k, v in pairs(bindings) do
|
||||
if k ~= "__default" then
|
||||
setmetatable(bindings[k], {__index = bindings.__default})
|
||||
end
|
||||
|
||||
for _, w in ipairs(movementKeys) do
|
||||
if v[w] then bindings[k][w] = nil end
|
||||
end
|
||||
end
|
||||
|
||||
systemBindings(bindings)
|
||||
|
||||
-- Other Initialization
|
||||
local selected = {inList = 1, offset = 0}
|
||||
local workingDirectoryScroll = { value = 0, phase = -1 }
|
||||
local gfxState = gfxPrepare()
|
||||
|
||||
-- Main Loop
|
||||
externalConfig = {workingDirectory=workingDirectory, bindings=bindings,
|
||||
callbacks=callbacks, additionalArguments=additionalArguments,
|
||||
fileList=getFileList(workingDirectory)}
|
||||
while ctr.run() do
|
||||
drawBottom(externalConfig, workingDirectoryScroll, selected)
|
||||
drawTop(externalConfig, selected)
|
||||
gfx.render()
|
||||
|
||||
local file, binding, mode, key = eventHandler(externalConfig, selected)
|
||||
|
||||
if key then
|
||||
gfxRestore(gfxState)
|
||||
return file, binding, mode, key
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local returnTable = {filePicker = filePicker, openFile = openFile,
|
||||
newFile = newFile, changeDirectory = changeDirectory}
|
||||
setmetatable(returnTable, {__call = function(self, ...) return self.filePicker(...) end})
|
||||
|
||||
return returnTable
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
-- Sort ctr.fs.list returns (directories first and alphabetical sorting)
|
||||
local function sort(files)
|
||||
table.sort(files, function(i, j)
|
||||
if i.isDirectory and not j.isDirectory then
|
||||
return true
|
||||
elseif i.isDirectory == j.isDirectory then
|
||||
return string.lower(i.name) < string.lower(j.name)
|
||||
end
|
||||
end)
|
||||
return files
|
||||
end
|
||||
|
||||
--- Open a file explorer to select a file.
|
||||
-- string title: title of the file explorer.
|
||||
-- string curdir: the directory to initially open the file explorer in, or nil for the current directory.
|
||||
-- string exts: the file extensions the user can select, separated by ";". If nil, all extensions are accepted.
|
||||
-- string type: "exist" to select an existing file, "new" to select an non-existing file or "any" to select a existing
|
||||
-- or non-existing file name. If nil, defaults to "exist".
|
||||
-- returns string: the file the user has selected, or nil if the explorer was closed without selecting a file.
|
||||
-- string: "exist" if the file exist or "new" if it doesn't
|
||||
return function(title, curdir, exts, type)
|
||||
-- Open libs
|
||||
local ctr = require("ctr")
|
||||
local gfx = require("ctr.gfx")
|
||||
|
||||
local keyboard = require("keyboard")
|
||||
|
||||
-- Arguments
|
||||
local curdir = curdir or ctr.fs.getDirectory()
|
||||
local type = type or "exist"
|
||||
|
||||
-- Variables
|
||||
local sel = 1
|
||||
local scroll = 0
|
||||
local files = sort(ctr.fs.list(curdir))
|
||||
|
||||
if curdir ~= "/" then table.insert(files, 1, { name = "..", isDirectory = true }) end
|
||||
local newFileName = ""
|
||||
|
||||
local ret = nil
|
||||
|
||||
-- Remember and set defaults
|
||||
local was3D = gfx.get3D()
|
||||
local wasDefault = gfx.color.getDefault()
|
||||
local wasBackground = gfx.color.getBackground()
|
||||
local wasFont = gfx.font.getDefault()
|
||||
gfx.set3D(false)
|
||||
gfx.color.setDefault(0xFFFFFFFF)
|
||||
gfx.color.setBackground(0xFF000000)
|
||||
gfx.font.setDefault()
|
||||
|
||||
while ctr.run() do
|
||||
ctr.hid.read()
|
||||
local keys = ctr.hid.keys()
|
||||
if keys.down.start then break end
|
||||
|
||||
-- Keys input
|
||||
if keys.down.down and sel < #files then
|
||||
sel = sel + 1
|
||||
if sel > scroll + 14 then scroll = scroll + 1 end
|
||||
elseif keys.down.up and sel > 1 then
|
||||
sel = sel - 1
|
||||
if sel <= scroll then scroll = scroll - 1 end
|
||||
end
|
||||
|
||||
if keys.down.a then
|
||||
local f = files[sel]
|
||||
|
||||
if f.isDirectory then
|
||||
if f.name == ".." then curdir = curdir:gsub("[^/]+/$", "")
|
||||
else curdir = curdir..f.name.."/" end
|
||||
|
||||
sel = 1
|
||||
scroll = 0
|
||||
files = sort(ctr.fs.list(curdir))
|
||||
|
||||
if curdir ~= "/" then
|
||||
table.insert(files, 1, { name = "..", isDirectory = true })
|
||||
end
|
||||
elseif type == "exist" or type == "any" then
|
||||
if exts then
|
||||
for ext in (exts..";"):gmatch("[^;]+") do
|
||||
if f.name:match("%..+$") == ext then
|
||||
ret = { curdir..f.name, "exist" }
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
ret = { curdir..f.name, "exist" }
|
||||
end
|
||||
if ret then break end
|
||||
end
|
||||
end
|
||||
|
||||
-- Keyboard input
|
||||
if type == "new" or type == "any" then
|
||||
local input = keyboard.read()
|
||||
if input then
|
||||
if input == "BACK" then
|
||||
newFileName = newFileName:sub(1, (utf8.offset(newFileName, -1) or 0)-1)
|
||||
elseif input == "\n" then
|
||||
local fileStatus = "new"
|
||||
local f = io.open(curdir..newFileName)
|
||||
if f then fileStatus = "exist" f:close() end
|
||||
ret = { curdir..newFileName, fileStatus }
|
||||
break
|
||||
else
|
||||
newFileName = newFileName..input
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw
|
||||
gfx.start(gfx.TOP)
|
||||
|
||||
gfx.rectangle(0, 10+(sel-scroll)*15, gfx.TOP_WIDTH, 15, 0, gfx.color.RGBA8(0, 0, 200))
|
||||
|
||||
for i = scroll+1, scroll+14, 1 do
|
||||
local f = files[i]
|
||||
if not f then break end
|
||||
local name = f.isDirectory and "["..f.name.."]" or f.name.." ("..f.fileSize.."b)"
|
||||
if not f.isHidden then gfx.text(5, 12+(i-scroll)*15, name) end
|
||||
end
|
||||
|
||||
gfx.rectangle(0, 0, gfx.TOP_WIDTH, 25, 0, gfx.color.RGBA8(200, 200, 200))
|
||||
gfx.text(3, 3, curdir, 13, gfx.color.RGBA8(0, 0, 0))
|
||||
|
||||
gfx.stop()
|
||||
|
||||
gfx.start(gfx.BOTTOM)
|
||||
|
||||
gfx.text(5, 5, title)
|
||||
gfx.text(5, 20, "Accepted file extensions: "..(exts or "all"))
|
||||
|
||||
if type == "new" or type == "any" then
|
||||
gfx.text(5, 90, newFileName)
|
||||
keyboard.draw(5, 115)
|
||||
end
|
||||
|
||||
gfx.stop()
|
||||
|
||||
gfx.render()
|
||||
end
|
||||
|
||||
-- Reset defaults
|
||||
gfx.set3D(was3D)
|
||||
gfx.color.setDefault(wasDefault)
|
||||
gfx.color.setBackground(wasBackground)
|
||||
gfx.font.setDefault(wasFont)
|
||||
|
||||
if ret then
|
||||
return table.unpack(ret)
|
||||
else
|
||||
return ret
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue