Skip to content

Commit 7d5662c

Browse files
committed
File browser: Select multiple files to open. Home/end keys work in the file list.
1 parent d51be12 commit 7d5662c

File tree

3 files changed

+160
-45
lines changed

3 files changed

+160
-45
lines changed

src/gui.gloa

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
onMousePressed, onMouseMoved, onMouseReleased, onMouseWheel
3737
pressButton
3838
refresh, refreshRecursively, refreshAll
39+
scrollTo, scrollIntoView
3940
setActive
4041
setFocus, blurFocus
4142
showMenu, showContextMenu, showContextMenuWithInput, hideContextMenu
@@ -197,6 +198,7 @@ export State :: struct {
197198
refresh :: _refresh,
198199
refreshAll :: _refreshAll,
199200
refreshRecursively :: _refreshRecursively,
201+
scrollIntoView :: _scrollIntoView,
200202
scrollTo :: _scrollTo,
201203
setActive :: _setActive,
202204
setFocus :: _setFocus,
@@ -234,6 +236,7 @@ local _pressButton :: pressButton
234236
local _refresh :: refresh
235237
local _refreshAll :: refreshAll
236238
local _refreshRecursively :: refreshRecursively
239+
local _scrollIntoView :: scrollIntoView
237240
local _scrollTo :: scrollTo
238241
local _setActive :: setActive
239242
local _setFocus :: setFocus
@@ -424,6 +427,7 @@ export Buttons :: struct {
424427
align = Alignment.CENTER,
425428
vertical = false,
426429
allowFocus = false,
430+
selectMultiple = false, -- Used if Buttons.allowFocus is set.
427431
buttonMinHeight = 0,
428432

429433
buttonWidth: float, -- Round when necessary!
@@ -588,20 +592,35 @@ export onKeyPressed :: (state:State, key:LK.KeyConstant, scancode:LK.Scancode, i
588592
if found triggerDoublePressEvent(state, buttons, buttons.buttons[i].name, i) -- @Cleanup: Rename double-press events to something better, or use the submit event.
589593

590594
} elseif key == "down" or key == "up" {
591-
if buttons.buttons {
592-
local dir = (key == "down") ? 1 : -1
593-
local found, i = indexWith(buttons.buttons, "selected", true)
595+
if not buttons.buttons return true
594596

595-
i = found
596-
? ((i-1+dir) % #buttons.buttons + 1)
597-
: (dir < 0 ? #buttons.buttons : 1)
597+
local dir = (key == "down") ? 1 : -1
598+
local found, i = indexWith(buttons.buttons, "selected", true)
598599

599-
for buttons.buttons it.selected = (itIndex == i)
600+
i = found
601+
? ((i-1+dir) % #buttons.buttons + 1)
602+
: (dir < 0 ? #buttons.buttons : 1)
600603

601-
-- @Incomplete: Scroll button into view if we're in a scrollable.
602-
triggerBeginEvent (state, buttons, buttons.buttons[i].name, i)
603-
triggerActionEvent(state, buttons, buttons.buttons[i].name, i, .KEYBOARD, alsoEnd=true)
604-
}
604+
for buttons.buttons it.selected = (itIndex == i)
605+
state.scrollIntoView!(buttons, i)
606+
607+
triggerBeginEvent (state, buttons, buttons.buttons[i].name, i)
608+
triggerActionEvent(state, buttons, buttons.buttons[i].name, i, .KEYBOARD, alsoEnd=true)
609+
610+
} elseif key == "pageup" or key == "pagedown" {
611+
-- @Incomplete
612+
613+
} elseif key == "end" or key == "home" {
614+
local iTarget = (key == "end") ? #buttons.buttons : 1
615+
local found, i = indexWith(buttons.buttons, "selected", true)
616+
617+
if found and i == iTarget return true
618+
619+
for buttons.buttons it.selected = (itIndex == iTarget)
620+
state.scrollIntoView!(buttons, iTarget)
621+
622+
triggerBeginEvent (state, buttons, buttons.buttons[i].name, i)
623+
triggerActionEvent(state, buttons, buttons.buttons[i].name, i, .KEYBOARD, alsoEnd=true)
605624
}
606625
}
607626

@@ -761,14 +780,25 @@ export onMousePressed :: (state:State, mx,my:int, mbutton:int, presses:int) -> (
761780
local buttons = cast(Buttons) el
762781
local slider = cast(Slider) el
763782

783+
local mayDoublePress = (presses % 2 == 0)
784+
764785
if el.type == Buttons and buttons.allowFocus {
765-
for buttons.buttons it.selected = (itIndex == state.hoveredSubid)
786+
if buttons.selectMultiple and LK.isDown(lctrl,rctrl) {
787+
local button = buttons.buttons[state.hoveredSubid]
788+
button.selected = not button.selected
789+
mayDoublePress = false
790+
-- @Incomplete: Maybe don't trigger action event like normal when deselecting?
791+
} else {
792+
for buttons.buttons {
793+
it.selected = (itIndex == state.hoveredSubid)
794+
}
795+
}
766796
}
767797

768798
-- Buttons which allow double press.
769799
if el.type == Buttons and (buttons.style == .CONTEXT_MENU or buttons.style == .LIST) {
770800
local handled = false
771-
if presses % 2 == 0 {
801+
if mayDoublePress {
772802
handled = triggerDoublePressEvent(state, buttons, buttons.buttons[state.hoveredSubid].name, state.hoveredSubid)
773803
}
774804
if not handled {
@@ -777,7 +807,7 @@ export onMousePressed :: (state:State, mx,my:int, mbutton:int, presses:int) -> (
777807
}
778808

779809
-- Double press on sliders.
780-
} elseif el.type == Slider and presses % 2 == 0 and triggerDoublePressEvent(state, slider, "", 0) {
810+
} elseif el.type == Slider and mayDoublePress and triggerDoublePressEvent(state, slider, "", 0) {
781811
-- void
782812

783813
-- Press that activates.
@@ -3272,7 +3302,8 @@ export setFocus :: (state:State, name:string) {
32723302
state.setFocus!(input)
32733303
}
32743304
export setFocus :: (state:State, widget:Widget) {
3275-
if state.focusId == widget.id return
3305+
if not state.isElementVisible!(widget) return
3306+
if state.focusId == widget.id return
32763307

32773308
state.blurFocus!(abort=false)
32783309
state.focusId = widget.id
@@ -3753,6 +3784,12 @@ export scrollTo :: (state:State, scrollable:Scrollable, scroll:int, limit=false)
37533784

37543785

37553786

3787+
export scrollIntoView :: (state:State, el:Element, subid:int) {
3788+
-- @Incomplete
3789+
}
3790+
3791+
3792+
37563793
export getButtonLayout :: (state:State, buttons:Buttons, i:int) -> (x,y,w,h:int) {
37573794
updateLayoutIfNeeded(state)
37583795

src/guiSetup.gloa

Lines changed: 95 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,7 @@ export showFileDialog :: (type:FileDialogType,
661661
onHighlight: type_of(fileDialog_onHighlight) = NULL,
662662
onChoose: type_of(fileDialog_onChoose) = NULL,
663663
onClose: type_of(fileDialog_onClose) = NULL,
664-
filenamePattern="", filenameHidePattern="", floating=false, chooseFolder=false, relativeTo=""
664+
filenamePattern="", filenameHidePattern="", floating=false, chooseFolder=false, relativeTo="", multiple=false
665665
) {
666666
fileDialog_type = type
667667
fileDialog_dir = dir
@@ -676,7 +676,8 @@ export showFileDialog :: (type:FileDialogType,
676676

677677
fileDialog_lastDirectory = dir ?: fileDialog_lastDirectory
678678

679-
guiState.getElement!("fileDialog_title", gui.Text).text = title
679+
guiState.getElement!("fileDialog_title", gui.Text).text = title
680+
guiState.getElement!("fileDialog_items", gui.Buttons).selectMultiple = multiple
680681

681682
local frame = guiState.getElement!("fileDialog", gui.Frame)
682683
if floating {
@@ -2843,8 +2844,22 @@ export setupGuiCallbacks :: () {
28432844

28442845
if item.isDir and (item.name == ".." or not fileDialog_chooseFolder) return
28452846

2846-
local input = guiState.getElement!("fileDialog_filename", gui.InputText)
2847-
input.value = item.name
2847+
do {
2848+
local itemNames: []string
2849+
2850+
for buttons.buttons if it.selected {
2851+
local item = cast(Item) it.value !shadow
2852+
if item.isDir and (item.name == ".." or not fileDialog_chooseFolder) continue
2853+
insert(itemNames, item.name)
2854+
}
2855+
2856+
local input = guiState.getElement!("fileDialog_filename", gui.InputText)
2857+
input.value = (
2858+
#itemNames == 0 ? "" :
2859+
#itemNames == 1 ? itemNames[1] :
2860+
concatinate(itemNames, '"', '" "', '"')
2861+
)
2862+
}
28482863

28492864
if fileDialog_onHighlight ~= NULL {
28502865
local pathObj = Path(dirCurrent)
@@ -2947,24 +2962,74 @@ export setupGuiCallbacks :: () {
29472962
local input = guiState.getElement!("fileDialog_filename", gui.InputText)
29482963
input.value = trim(input.value)
29492964

2950-
if input.value == "." and fileDialog_chooseFolder {
2951-
if fileDialog_onChoose ~= NULL {
2952-
local pathObj = Path(dirCurrent)
2953-
fileDialog_onChoose(makeRelativeOrClone(pathObj), pathObj)
2965+
local paths: []string
2966+
2967+
if find(input.value, '"') {
2968+
local ptr = 1
2969+
2970+
while ptr <= #input.value {
2971+
if input.value[ptr] == !char '"' {
2972+
local found, i1, i2 = findPattern(input.value, '"%s*', ptr+1)
2973+
if not found {
2974+
guiState.setFocus!(input)
2975+
input.field.selectAll!()
2976+
return -- @UX: Show error message?
2977+
}
2978+
insert(paths, getSubstring(input.value, ptr+1, i1-1))
2979+
ptr = i2 + 1
2980+
} else {
2981+
local found, i1, i2, filename = findPattern(input.value, '^([^%s"]+)%s*', ptr)
2982+
if not found {
2983+
guiState.setFocus!(input)
2984+
input.field.selectAll!()
2985+
return -- @UX: Show error message?
2986+
}
2987+
insert(paths, cast(string)filename)
2988+
ptr = i2 + 1
2989+
}
29542990
}
2955-
return
2991+
2992+
} else {
2993+
insert(paths, input.value)
29562994
}
29572995

2958-
local dotdot = (input.value == ".." or findPattern(input.value, "[/\\]%.%.$"))
2959-
local pathObj = Path(input.value)
2960-
input.value = pathObj.toString!()
2996+
if #paths > 1 and not guiState.getElement!("fileDialog_items", gui.Buttons).selectMultiple {
2997+
guiState.setFocus!(input)
2998+
input.field.selectAll!()
2999+
return -- @UX: Show error message?
3000+
}
29613001

2962-
if pathObj.isEmpty!() {
2963-
guiState.setFocus!("fileDialog_filename")
2964-
return
3002+
local gotDotdot = false
3003+
local pathObjs: []Path
3004+
3005+
for paths {
3006+
if it == "." {
3007+
if not fileDialog_chooseFolder {
3008+
guiState.setFocus!(input)
3009+
input.field.selectAll!()
3010+
return -- @UX: Show error message?
3011+
}
3012+
insert(pathObjs, Path(dirCurrent))
3013+
3014+
} else {
3015+
if it == ".." or findPattern(it, "[/\\]%.%.$") {
3016+
gotDotdot = true
3017+
}
3018+
3019+
local pathObj = Path(it)
3020+
3021+
if pathObj.isEmpty!() {
3022+
guiState.setFocus!(input)
3023+
input.field.selectAll!()
3024+
return -- @UX: Show error message?
3025+
}
3026+
3027+
pathObj.prepend!(dirCurrent) -- Fails if pathObj is absolute.
3028+
insert(pathObjs, pathObj)
3029+
}
29653030
}
29663031

2967-
pathObj.prepend!(dirCurrent) -- Fails if pathObj is absolute.
3032+
if not pathObjs return
29683033

29693034
local tryNavigate :: (pathObj:Path) -> (success:bool) {
29703035
local path = pathObj.toString!()
@@ -2986,21 +3051,27 @@ export setupGuiCallbacks :: () {
29863051
return true
29873052
}
29883053

2989-
if not fileDialog_chooseFolder or dotdot {
2990-
if tryNavigate(pathObj) return
2991-
if dotdot return -- Never trigger onChoose with ".."!
3054+
if #pathObjs == 1 and (not fileDialog_chooseFolder or gotDotdot) and tryNavigate(pathObjs[1]) {
3055+
return
3056+
}
3057+
3058+
if gotDotdot {
3059+
-- Never trigger onChoose with ".."!
3060+
guiState.setFocus!(input)
3061+
input.field.selectAll!()
3062+
return -- @UX: Show error message?
29923063
}
29933064

2994-
if fileDialog_type ~= .OPEN {
2995-
local ok, filename = pathObj.getFilename!()
3065+
if fileDialog_type ~= .OPEN for pathObjs {
3066+
local ok, filename = it.getFilename!()
29963067
if not ok {
2997-
guiState.setFocus!("fileDialog_filename")
3068+
guiState.setFocus!(input)
29983069
return
29993070
}
30003071
}
30013072

3002-
if fileDialog_onChoose ~= NULL {
3003-
fileDialog_onChoose(makeRelativeOrClone(pathObj), pathObj)
3073+
if fileDialog_onChoose ~= NULL for pathObjs {
3074+
fileDialog_onChoose(makeRelativeOrClone(it), it)
30043075
}
30053076
}
30063077
}

src/main.gloa

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2383,7 +2383,8 @@ local onDraw :: () {
23832383
local dist = fontLarge.getHeight!() + 5
23842384

23852385
if project.preview {
2386-
drawGuiText("PREVIEW", 0, math.HUGE, 10)
2386+
local _, filename = Path(project.path).getFilename!()
2387+
drawGuiText("Preview of "..filename, 0, math.HUGE, 10)
23872388
}
23882389
if anyFiniteEmitter and anyInfiniteEmitter {
23892390
drawGuiText("Warning: Mixing infinite and finite emitters", 0, math.HUGE, 10+1*dist)
@@ -3303,21 +3304,28 @@ export showOpenProjectDialog :: () {
33033304
local ok, dir = fullPathObj.getDirectoryAndFilename!()
33043305
if not ok dir = fileDialog_lastDirectory ?: getSaveDirectory().."/projects"
33053306

3306-
showFileDialog(.OPEN, "Open project", dir, "", filenamePattern="%.hotparticles$",
3307+
local popped = false
3308+
3309+
showFileDialog(.OPEN, "Open project", dir, "", filenamePattern="%.hotparticles$", multiple=true,
33073310
onHighlight = (pathObj:Path, fullPathObj:Path) {
33083311
loadPreview(fullPathObj.toString!())
33093312
},
3310-
onChoose = (pathObj:Path, fullPathObj:Path) {
3313+
onChoose = [popped] (pathObj:Path, fullPathObj:Path) {
33113314
local fullPath = fullPathObj.toString!()
33123315
local ok, project = openProject(fullPath)
3316+
33133317
if not ok {
33143318
local input = guiState.getElement!("fileDialog_filename", gui.InputText)
33153319
guiState.setFocus!(input)
33163320
return
33173321
}
33183322

3319-
project.preview = false -- In case the project was already opened as a preview (which it probably always is).
3320-
popPanel()
3323+
project.preview = false -- In case the project was already opened as a preview (which it probably always is, at least if only one project was chosen).
3324+
3325+
if not popped {
3326+
popped = true
3327+
popPanel() -- @Robustness: Preferably we'd like to process all paths, if multiple were chosen, before popping the panel.
3328+
}
33213329

33223330
local _, dir = fullPathObj.getDirectory!()
33233331
addRecent(app.recentFolders, dir, app.maxRecentFiles)
@@ -3333,7 +3341,6 @@ export showOpenProjectDialog :: () {
33333341
},
33343342
onClose = () {
33353343
unloadPreview()
3336-
projectBeingSaved = NULL
33373344
}
33383345
)
33393346
}

0 commit comments

Comments
 (0)