1 package etk
2
3 import (
4 "image/color"
5 "io/fs"
6 "log"
7 "path/filepath"
8 "sort"
9 "strings"
10 "sync"
11
12 "github.com/hajimehoshi/ebiten/v2"
13 )
14
15
16 type FilePickerMode int
17
18
19 const (
20 ModeCreateDir FilePickerMode = 0
21 ModeCreateFile FilePickerMode = 1
22 ModeSelectDir FilePickerMode = 2
23 ModeSelectFile FilePickerMode = 3
24 )
25
26
27 type FilePicker struct {
28 *Grid
29 List *List
30 mode FilePickerMode
31 dir string
32 entries []string
33 extensions []string
34 dirLabel *Text
35 inputField *Input
36 cancelLabel string
37 selectLabel string
38 onResult func(path string) error
39 needRebuild bool
40 sync.Mutex
41 }
42
43
44 func NewFilePicker(mode FilePickerMode, dir string, extensions []string, onResult func(path string) error) *FilePicker {
45 itemHeight := int(float64(Style.TextSize) * 1.5)
46
47 f := &FilePicker{
48 Grid: NewGrid(),
49 mode: mode,
50 dir: dir,
51 extensions: extensions,
52 dirLabel: NewText(dir),
53 cancelLabel: "Cancel",
54 selectLabel: "Select",
55 onResult: onResult,
56 needRebuild: true,
57 }
58
59 f.dirLabel.SetVertical(AlignCenter)
60 f.dirLabel.SetAutoResize(true)
61
62 f.List = NewList(itemHeight, f.onListSelected, f.onListConfirmed)
63
64 f.inputField = NewInput("", nil, f.onInputConfirmed)
65 f.inputField.SetVertical(AlignCenter)
66 return f
67 }
68
69 func (f *FilePicker) SetFocus(focus bool) (accept bool) {
70 if focus {
71 SetFocus(f.inputField)
72 }
73 return false
74 }
75
76 func (f *FilePicker) handleResult(index int) {
77 var path string
78 modeCreate := f.mode == ModeCreateDir || f.mode == ModeCreateFile
79 if modeCreate {
80 path = f.dir
81 name := f.inputField.Text()
82 if name == "" {
83 name = f.entries[index]
84 } else if len(f.extensions) == 1 && !strings.HasSuffix(strings.ToLower(name), f.extensions[0]) {
85 name += f.extensions[0]
86 }
87 path = filepath.Join(path, name)
88 } else {
89 var err error
90 path, err = filepath.Abs(filepath.Join(f.dir, f.entries[index]))
91 if err != nil {
92 log.Fatal(err)
93 }
94 }
95 err := f.onResult(path)
96 if err != nil {
97 log.Fatal(err)
98 }
99 }
100
101 func (f *FilePicker) handleSelected(index int) {
102 path, err := filepath.Abs(filepath.Join(f.dir, f.entries[index]))
103 if err != nil {
104 log.Fatal(err)
105 }
106 modeCreate := f.mode == ModeCreateDir || f.mode == ModeCreateFile
107 if modeCreate {
108 if strings.TrimSpace(f.inputField.Text()) == "" {
109 isDir := strings.HasSuffix(f.entries[index], "/")
110 if isDir != (f.mode == ModeCreateDir) {
111 if isDir {
112 f.dir = path
113 f.needRebuild = true
114 }
115 return
116 }
117 }
118 f.handleResult(index)
119 return
120 }
121 selectFile := f.mode == ModeCreateFile || f.mode == ModeSelectFile
122 if selectFile && strings.HasSuffix(f.entries[index], "/") {
123 f.dir = path
124 f.needRebuild = true
125 return
126 }
127 f.handleResult(index)
128 }
129
130 func (f *FilePicker) onListSelected(index int) (accept bool) {
131 return true
132 }
133
134 func (f *FilePicker) onListConfirmed(index int) {
135 entry := f.entries[index]
136 _, selected := f.List.SelectedItem()
137 if index == selected && strings.HasSuffix(entry, "/") {
138 abs, err := filepath.Abs(filepath.Join(f.dir, entry))
139 if err == nil {
140 f.dir = abs
141 f.needRebuild = true
142 }
143 return
144 }
145 f.handleResult(index)
146 }
147
148 func (f *FilePicker) onInputConfirmed(text string) (handled bool) {
149 _, index := f.List.SelectedItem()
150 f.handleSelected(index)
151 return true
152 }
153
154 func (f *FilePicker) onButtonSelected() error {
155 _, index := f.List.SelectedItem()
156 f.handleSelected(index)
157 return nil
158 }
159
160 func (f *FilePicker) onCancel() error {
161 return f.onResult("")
162 }
163
164 func (f *FilePicker) walkDir(path string, d fs.DirEntry, err error) error {
165 if err != nil {
166 return err
167 }
168 label := strings.TrimPrefix(path, f.dir)
169 if len(label) > 1 && label[0] == '/' {
170 label = label[1:]
171 }
172
173 if d.IsDir() {
174 if path == f.dir {
175 return nil
176 }
177 label += "/"
178 f.entries = append(f.entries, label)
179 return filepath.SkipDir
180 }
181
182 if len(f.extensions) > 0 {
183 var found bool
184 for i := range f.extensions {
185 if strings.HasSuffix(strings.ToLower(d.Name()), f.extensions[i]) {
186 found = true
187 break
188 }
189 }
190 if !found {
191 return nil
192 }
193 }
194 f.entries = append(f.entries, label)
195 return nil
196 }
197
198 func (f *FilePicker) rebuild() {
199 f.Grid.Clear()
200 f.List.Clear()
201
202 path := f.dir
203 if !strings.HasSuffix(path, "/") {
204 path += "/"
205 }
206 f.dirLabel.SetText(path)
207
208 f.entries = f.entries[:0]
209 filepath.WalkDir(f.dir, f.walkDir)
210 sort.Slice(f.entries, func(i, j int) bool {
211 if strings.HasSuffix(f.entries[i], "/") != strings.HasSuffix(f.entries[j], "/") {
212 return strings.HasSuffix(f.entries[i], "/")
213 }
214 return strings.ToLower(f.entries[i]) < strings.ToLower(f.entries[j])
215 })
216
217 selectDir := f.mode == ModeCreateDir || f.mode == ModeSelectDir
218 if selectDir {
219 f.entries = append([]string{"./"}, f.entries...)
220 }
221 if f.dir != "/" {
222 f.entries = append([]string{"../"}, f.entries...)
223 }
224
225 var y int
226 for _, entry := range f.entries {
227 t := NewText(entry)
228 t.SetPadding(0)
229 t.SetAutoResize(true)
230 t.SetVertical(AlignCenter)
231
232 g := NewGrid()
233 g.SetColumnSizes(5, -1, 5)
234 g.AddChildAt(&WithoutMouse{t}, 1, 0, 1, 1)
235
236 f.List.AddChildAt(&WithoutMouse{g}, 0, y)
237 y++
238 }
239 f.List.SetSelectedItem(0, 0)
240
241 dividerA := NewBox()
242 dividerA.SetBackground(color.RGBA{255, 255, 255, 255})
243 dividerB := NewBox()
244 dividerB.SetBackground(color.RGBA{255, 255, 255, 255})
245
246 showInput := f.mode == ModeCreateDir || f.mode == ModeCreateFile
247
248 rowSizes := []int{Style.TextSize * 2, 2, -1, 2}
249 if showInput {
250 rowSizes = append(rowSizes, Style.TextSize*2)
251 }
252 rowSizes = append(rowSizes, Style.TextSize*2)
253
254 f.Grid.SetRowSizes(rowSizes...)
255 f.Grid.AddChildAt(f.dirLabel, 0, 0, 2, 1)
256 f.Grid.AddChildAt(dividerA, 0, 1, 2, 1)
257 f.Grid.AddChildAt(&WithoutFocus{f.List}, 0, 2, 2, 1)
258 f.Grid.AddChildAt(dividerB, 0, 3, 2, 1)
259 y = 4
260 if showInput {
261 nameText := "Name"
262 nameSize := 150
263 if len(f.extensions) == 1 {
264 nameText += " (" + f.extensions[0] + ")"
265 nameSize = 300
266 }
267 nameLabel := NewText(nameText)
268 nameLabel.SetVertical(AlignCenter)
269 nameLabel.SetAutoResize(true)
270 g := NewGrid()
271 g.SetColumnPadding(5)
272 g.SetColumnSizes(nameSize, -1)
273 g.AddChildAt(nameLabel, 0, 0, 1, 1)
274 g.AddChildAt(f.inputField, 1, 0, 1, 1)
275 f.Grid.AddChildAt(g, 0, y, 2, 1)
276 y++
277 }
278 f.Grid.AddChildAt(NewButton(f.cancelLabel, f.onCancel), 0, y, 1, 1)
279 f.Grid.AddChildAt(NewButton(f.selectLabel, f.onButtonSelected), 1, y, 1, 1)
280 }
281
282
283 func (f *FilePicker) SetMode(mode FilePickerMode) {
284 f.Lock()
285 defer f.Unlock()
286
287 f.mode = mode
288 f.needRebuild = true
289 }
290
291
292
293
294 func (f *FilePicker) SetExtensions(extensions []string) {
295 f.Lock()
296 defer f.Unlock()
297
298 f.extensions = make([]string, len(extensions))
299 for i := range extensions {
300 f.extensions[i] = strings.ToLower(extensions[i])
301 }
302 f.needRebuild = true
303 }
304
305
306
307
308 func (f *FilePicker) SetResultFunc(onResult func(path string) error) {
309 f.Lock()
310 defer f.Unlock()
311
312 f.onResult = onResult
313 f.needRebuild = true
314 }
315
316
317 func (f *FilePicker) SetButtonLabels(cancel string, confirm string) {
318 f.Lock()
319 defer f.Unlock()
320
321 f.cancelLabel = cancel
322 f.selectLabel = confirm
323 f.needRebuild = true
324 }
325
326
327 func (f *FilePicker) Draw(screen *ebiten.Image) error {
328 f.Lock()
329 defer f.Unlock()
330
331 if f.needRebuild {
332 f.rebuild()
333 f.needRebuild = false
334 }
335
336 return f.Grid.Draw(screen)
337 }
338
View as plain text