...

Source file src/codeberg.org/tslocum/etk/filepicker.go

Documentation: codeberg.org/tslocum/etk

     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  // FilePickerMode represents a FilePicker selection mode.
    16  type FilePickerMode int
    17  
    18  // FilePicker modes.
    19  const (
    20  	ModeCreateDir  FilePickerMode = 0
    21  	ModeCreateFile FilePickerMode = 1
    22  	ModeSelectDir  FilePickerMode = 2
    23  	ModeSelectFile FilePickerMode = 3
    24  )
    25  
    26  // FilePicker is a file and directory creation and selection dialog.
    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  // NewFilePicker returns a new FilePicker.
    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  // SetMode sets the FilePicker mode.
   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  // SetExtensions sets the desired file extensions, if any. When set, only files
   292  // with matching extensions are shown. When creating a file and only one
   293  // extension is set, the file will be created with the specified extension.
   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  // SetResultFunc sets the FilePicker result handler. When a file or directory is
   306  // selected, depending on the FilePicker mode, the path to the file or directory
   307  // is provided. When the FilePicker is canceled, a blank path is provided.
   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  // SetButtonLabels sets the FilePicker cancel and confirm button labels.
   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  // Draw draws the FilePicker on the screen.
   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