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
|
plain = true
|
||||||
|
|
||||||
-- Input files
|
-- Input files
|
||||||
topics = "../README.md"
|
topics = {"../README.md", "filepicker.md"}
|
||||||
file = "../source/"
|
file = "../source/"
|
||||||
examples = "../sdcard/3ds/ctruLua/examples/"
|
examples = "../sdcard/3ds/ctruLua/examples/"
|
||||||
manual_url = "file://../libs/lua-5.3.1/doc/manual.html"
|
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
|
local ctr = require('ctr')
|
||||||
-- ctrµLua official shell
|
local keyboard = require('keyboard')
|
||||||
|
|
||||||
local ctr = require("ctr")
|
local gfx = ctr.gfx
|
||||||
local gfx = require("ctr.gfx")
|
|
||||||
|
|
||||||
local function saveGraphicsState()
|
local externalConfig
|
||||||
|
|
||||||
|
local function gfxPrepare()
|
||||||
local old = {gfx.get3D(), gfx.color.getDefault(), gfx.color.getBackground(),
|
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")
|
local mono = gfx.font.load(ctr.root .. "resources/VeraMono.ttf")
|
||||||
|
|
||||||
|
|
@ -14,186 +15,304 @@ local function saveGraphicsState()
|
||||||
gfx.color.setDefault(0xFFFDFDFD)
|
gfx.color.setDefault(0xFFFDFDFD)
|
||||||
gfx.color.setBackground(0xFF333333)
|
gfx.color.setBackground(0xFF333333)
|
||||||
gfx.font.setDefault(mono)
|
gfx.font.setDefault(mono)
|
||||||
|
gfx.setTextSize(12)
|
||||||
|
|
||||||
return old
|
return old
|
||||||
end
|
end
|
||||||
|
|
||||||
local function restoreGraphicsState(state)
|
local function gfxRestore(state)
|
||||||
gfx.set3D(state[1])
|
gfx.set3D(state[1])
|
||||||
gfx.color.setDefault(state[2])
|
gfx.color.setDefault(state[2])
|
||||||
gfx.color.setBackground(state[3])
|
gfx.color.setBackground(state[3])
|
||||||
gfx.font.setDefault(state[4])
|
gfx.font.setDefault(state[4])
|
||||||
|
gfx.setTextSize(state[5])
|
||||||
end
|
end
|
||||||
|
|
||||||
local function getExtension(sel, bindings)
|
local function systemBindings(bindings)
|
||||||
for _, ext in ipairs(bindings) do
|
bindings.__default.up = {
|
||||||
if ext.ext == sel:match("%..+$") then
|
function(_, selected, ...)
|
||||||
return ext
|
if selected.inList > 1 then
|
||||||
|
selected.inList = selected.inList - 1
|
||||||
|
if selected.inList == selected.offset then
|
||||||
|
selected.offset = selected.offset - 1
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
}
|
||||||
end
|
|
||||||
|
|
||||||
local function getFilelist(cur)
|
bindings.__default.down = {
|
||||||
local files = ctr.fs.list(cur)
|
function(externalConfig, selected, ...)
|
||||||
|
if selected.inList < #externalConfig.fileList then
|
||||||
if cur ~= "/" and cur ~= "sdmc:/" then
|
selected.inList = selected.inList + 1
|
||||||
table.insert(files, {name = "..", isDirectory = true})
|
if selected.inList - selected.offset >= 16 then
|
||||||
end
|
selected.offset = selected.offset + 1
|
||||||
|
end
|
||||||
-- Stealy stealing code from original openfile.lua
|
end
|
||||||
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
|
||||||
end)
|
}
|
||||||
|
|
||||||
return files
|
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
|
end
|
||||||
|
|
||||||
local function drawBottom(cur, selFile, bindings)
|
local function getFileList(workingDirectory)
|
||||||
local ext = getExtension(selFile.name, bindings)
|
local fileList = ctr.fs.list(workingDirectory)
|
||||||
|
|
||||||
|
if workingDirectory ~= "/" and workingDirectory ~= "sdmc:/" then
|
||||||
|
table.insert(fileList, {name = "..", isDirectory = true})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Stealy stealing code from original openfile.lua
|
||||||
|
table.sort(fileList, 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 fileList
|
||||||
|
end
|
||||||
|
|
||||||
|
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.start(gfx.BOTTOM)
|
||||||
gfx.rectangle(0, 0, gfx.BOTTOM_WIDTH, 16, 0, 0xFF0000B3)
|
gfx.rectangle(0, 0, gfx.BOTTOM_WIDTH, 16, 0, 0xFF0000B3)
|
||||||
gfx.text(1, 0, cur, 12)
|
gfx.text(1 - workingDirectoryScroll.value, 0, workingDirectory)
|
||||||
gfx.text(1, 15, selFile.name, 12)
|
if gfx.font.getDefault():width(workingDirectory) > gfx.BOTTOM_WIDTH - 2 then
|
||||||
if not selFile.isDirectory then
|
workingDirectoryScroll.value = workingDirectoryScroll.value + workingDirectoryScroll.phase
|
||||||
gfx.text(1, 45, selFile.fileSize, 12)
|
if workingDirectoryScroll.value == (gfx.BOTTOM_WIDTH - 2) - gfx.font.getDefault():width(workingDirectory) or
|
||||||
|
workingDirectoryScroll.value == 0 then
|
||||||
|
workingDirectoryScroll.phase = - workingDirectoryScroll.phase
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local keys = {"X: Quit/Cancel"}
|
gfx.text(1, 15, selectedFile.name, 12)
|
||||||
if selFile.isDirectory then
|
if not selectedFile.isDirectory then
|
||||||
gfx.text(1, 30, "Directory", 12, 0xFF727272)
|
gfx.text(1, 45, tostring(selectedFile.fileSize) .. "B", 12, 0xFF727272)
|
||||||
gfx.text(1, gfx.BOTTOM_HEIGHT - 30, "A: Open", 12)
|
end
|
||||||
gfx.text(1, gfx.BOTTOM_HEIGHT - 15, keys[1], 12)
|
|
||||||
elseif ext then
|
|
||||||
local lines = 1
|
|
||||||
|
|
||||||
-- Keys
|
local binding, pattern = getBinding(selectedFile, bindings)
|
||||||
if ext.y then
|
if selectedFile.isDirectory then
|
||||||
lines = lines + 1
|
gfx.text(1, 30, bindings.__directory.__name, 12, 0xFF727272)
|
||||||
table.insert(keys, "Y: " .. ext.y)
|
|
||||||
end
|
|
||||||
if ext.a then
|
|
||||||
lines = lines + 1
|
|
||||||
table.insert(keys, "A: " .. ext.a)
|
|
||||||
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)
|
|
||||||
else
|
else
|
||||||
gfx.text(1, 30, "File", 12, 0xFF727272)
|
gfx.text(1, 30, binding.__name, 12, 0xFF727272)
|
||||||
gfx.text(1, 45, tostring(selFile.fileSize) .. "B", 12, 0xFF727272)
|
|
||||||
gfx.text(1, gfx.BOTTOM_HEIGHT - 15, keys[1], 12)
|
|
||||||
end
|
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()
|
gfx.stop()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function drawTop(files, sel, scr)
|
local function drawTop(externalConfig, selected)
|
||||||
gfx.start(gfx.TOP)
|
gfx.start(gfx.TOP)
|
||||||
gfx.rectangle(0, (sel-scr-1)*15, gfx.TOP_WIDTH, 16, 0, 0xFF0000B3)
|
gfx.rectangle(0, (selected.inList-selected.offset-1)*15, gfx.TOP_WIDTH, 16, 0, 0xFF0000B9)
|
||||||
local over = #files - scr >= 16 and 16 or #files - scr
|
local over = #externalConfig.fileList - selected.offset >= 16 and 16 or #externalConfig.fileList - selected.offset
|
||||||
for i=scr+1, scr+over do
|
for i=selected.offset+1, selected.offset+over do
|
||||||
local color = files[i].isDirectory and 0xFF727272 or 0xFFFDFDFD
|
local color = externalConfig.fileList[i].isDirectory and 0xFF727272 or 0xFFFDFDFD
|
||||||
gfx.text(1, (i-scr-1)*15+1, files[i].name or "", 12, color)
|
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 eventHandler(externalConfig, selected)
|
||||||
|
externalConfig.callbacks.eventHandler(externalConfig, selected)
|
||||||
|
ctr.hid.read()
|
||||||
|
local state = ctr.hid.keys()
|
||||||
|
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
|
||||||
|
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
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
||||||
local function runY(cur, selFile, bindings)
|
local function nothing(externalConfig, selected, bindingName, bindingKey)
|
||||||
if not selFile.isDirectory then
|
-- externalConfig = {workingDirectory=string, bindings=table, callbacks=table, additionalArguments=table, fileList=table}
|
||||||
local ext = getExtension(selFile.name, bindings)
|
-- selected = {file=string, inList=number, offset=number}
|
||||||
if not ext then return end
|
-- bindings = {__default/__directory/[regex] = {__name, [keyName] = {(handlingFunction), (name)}}}
|
||||||
if ext.y then return cur .. selFile.name, ext.ext end
|
-- 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
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Open a file browser to allow the user to select a file.
|
local function newFile(externalConfig, selected, bindingName, bindingKey)
|
||||||
-- 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
|
local name = ""
|
||||||
-- @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
|
while ctr.run() do
|
||||||
drawBottom(cur, files[sel], bindings)
|
gfx.start(gfx.BOTTOM)
|
||||||
drawTop(files, sel, scr)
|
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()
|
gfx.render()
|
||||||
|
|
||||||
|
local char = (keyboard.read() or ""):gsub("[\t%/%?%<%>%\\%:%*%|%”%^]", "")
|
||||||
ctr.hid.read()
|
ctr.hid.read()
|
||||||
local state = ctr.hid.keys()
|
local keys = 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
|
if char ~= "" and char ~= "\b" and char ~= "\n" then
|
||||||
local selFile = files[sel]
|
name = name .. char
|
||||||
if selFile.isDirectory then
|
elseif char ~= "" and char == "\b" then
|
||||||
if selFile.name == ".." then
|
name = name:sub(1, (utf8.offset(name, -1) or 0)-1)
|
||||||
cur = cur:gsub("[^/]+/$", "")
|
elseif (char ~= "" and char == "\n" or keys.down.a) and name ~= "" then
|
||||||
else
|
local b, p = getBinding({name=name}, externalConfig.bindings)
|
||||||
cur = cur .. selFile.name .. "/"
|
return externalConfig.workingDirectory .. name, p, "new", b
|
||||||
end
|
elseif keys.down.b then
|
||||||
files, sel, scr = getFilelist(cur), 1, 0
|
break
|
||||||
else
|
|
||||||
local file, ext = runA(cur, selFile, bindings)
|
|
||||||
if file then
|
|
||||||
restoreGraphicsState(old)
|
|
||||||
return file, ext, "A"
|
|
||||||
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"
|
|
||||||
end
|
end
|
||||||
end
|
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