clui/fileselectdlg.go
Vladimir Markelov bca28ea40c closes #98
2018-08-04 21:18:33 -07:00

420 lines
9.9 KiB
Go

package clui
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
term "github.com/nsf/termbox-go"
)
// FileSelectDialog is a dialog to select a file or directory.
// Public properties:
// * Selected - whether a user has selected an object, or the user canceled
// or closed the dialog. In latter case all other properties are not
// used and contain default values
// * FilePath - full path to the selected file or directory
// * Exists - if the selected object exists or a user entered manually a
// name of the object
type FileSelectDialog struct {
View *Window
FilePath string
Exists bool
Selected bool
fileMasks []string
currPath string
mustExist bool
selectDir bool
result int
onClose func()
listBox *ListBox
edFile *EditField
curDir *Label
}
// Set the cursor the first item in the file list
func (d *FileSelectDialog) selectFirst() {
d.listBox.SelectItem(0)
}
// Checks if the file name matches the mask. Empty mask, *, and *.* match any file
func (d *FileSelectDialog) fileFitsMask(finfo os.FileInfo) bool {
if finfo.IsDir() {
return true
}
if d.selectDir {
return false
}
if len(d.fileMasks) == 0 {
return true
}
for _, msk := range d.fileMasks {
if msk == "*" || msk == "*.*" {
return true
}
matched, err := filepath.Match(msk, finfo.Name())
if err == nil && matched {
return true
}
}
return false
}
// Fills the ListBox with the file names from current directory.
// Files which names do not match mask are filtered out.
// If select directory is set, then the ListBox contains only directories.
// Directory names ends with path separator
func (d *FileSelectDialog) populateFiles() error {
d.listBox.Clear()
isRoot := filepath.Dir(d.currPath) == d.currPath
if !isRoot {
d.listBox.AddItem("..")
}
f, err := os.Open(d.currPath)
if err != nil {
return err
}
finfos, err := f.Readdir(0)
f.Close()
if err != nil {
return err
}
fnLess := func(i, j int) bool {
if finfos[i].IsDir() && !finfos[j].IsDir() {
return true
} else if !finfos[i].IsDir() && finfos[j].IsDir() {
return false
}
return strings.ToLower(finfos[i].Name()) < strings.ToLower(finfos[j].Name())
}
sort.Slice(finfos, fnLess)
for _, finfo := range finfos {
if !d.fileFitsMask(finfo) {
continue
}
if finfo.IsDir() {
d.listBox.AddItem(finfo.Name() + string(os.PathSeparator))
} else {
d.listBox.AddItem(finfo.Name())
}
}
return nil
}
// Tries to find the best fit for the given path.
// It goes up until it gets into the existing directory.
// If all fails it returns working directory.
func (d *FileSelectDialog) detectPath() {
p := d.currPath
if p == "" {
d.currPath, _ = os.Getwd()
return
}
p = filepath.Clean(p)
for {
_, err := os.Stat(p)
if err == nil {
break
}
dirUp := filepath.Dir(p)
if dirUp == p {
p, _ = os.Getwd()
break
}
p = dirUp
}
d.currPath = p
}
// Goes up in the directory tree if it is possible
func (d *FileSelectDialog) pathUp() {
dirUp := filepath.Dir(d.currPath)
if dirUp == d.currPath {
return
}
d.currPath = dirUp
d.populateFiles()
d.selectFirst()
}
// Enters the directory
func (d *FileSelectDialog) pathDown(dir string) {
d.currPath = filepath.Join(d.currPath, dir)
d.populateFiles()
d.selectFirst()
}
// Sets the EditField value with the selected item in ListBox if:
// * a directory is selected and option 'select directory' is set
// * a file is selected and option 'select directory' is not set
func (d *FileSelectDialog) updateEditBox() {
s := d.listBox.SelectedItemText()
if s == "" || s == ".." ||
(strings.HasSuffix(s, string(os.PathSeparator)) && !d.selectDir) {
d.edFile.Clear()
return
}
d.edFile.SetTitle(strings.TrimSuffix(s, string(os.PathSeparator)))
}
// CreateFileSelectDialog creates a new file select dialog. It is useful if
// the application needs a way to select a file or directory for reading and
// writing data.
// * title - custom dialog title (the final dialog title includes this one
// and the file mask
// * fileMasks - a list of file masks separated with comma or path separator.
// If selectDir is true this option is not used. Setting fileMasks to
// '*', '*.*', or empty string means all files
// * selectDir - what kind of object to select: file (false) or directory
// (true). If selectDir is true then the dialog does not show files
// * mustExists - if it is true then the dialog allows a user to select
// only existing object ('open file' case). If it is false then the dialog
// makes possible to enter a name manually and click 'Select' (useful
// for file 'file save' case)
func CreateFileSelectDialog(title, fileMasks, initPath string, selectDir, mustExist bool) *FileSelectDialog {
dlg := new(FileSelectDialog)
cw, ch := term.Size()
dlg.selectDir = selectDir
dlg.mustExist = mustExist
if fileMasks != "" {
maskList := strings.FieldsFunc(fileMasks,
func(c rune) bool { return c == ',' || c == ':' })
dlg.fileMasks = make([]string, 0, len(maskList))
for _, m := range maskList {
if m != "" {
dlg.fileMasks = append(dlg.fileMasks, m)
}
}
}
dlg.View = AddWindow(10, 4, 20, 16, fmt.Sprintf("%s (%s)", title, fileMasks))
WindowManager().BeginUpdate()
defer WindowManager().EndUpdate()
dlg.View.SetModal(true)
dlg.View.SetPack(Vertical)
dlg.currPath = initPath
dlg.detectPath()
dlg.curDir = CreateLabel(dlg.View, AutoSize, AutoSize, "", Fixed)
dlg.curDir.SetTextDisplay(AlignRight)
flist := CreateFrame(dlg.View, 1, 1, BorderNone, 1)
flist.SetPaddings(1, 1)
flist.SetPack(Horizontal)
dlg.listBox = CreateListBox(flist, 16, ch-20, 1)
fselected := CreateFrame(dlg.View, 1, 1, BorderNone, Fixed)
// text + edit field to enter name manually
fselected.SetPack(Vertical)
fselected.SetPaddings(1, 0)
CreateLabel(fselected, AutoSize, AutoSize, "Selected object:", 1)
dlg.edFile = CreateEditField(fselected, cw-22, "", 1)
// buttons at the right
blist := CreateFrame(flist, 1, 1, BorderNone, Fixed)
blist.SetPack(Vertical)
blist.SetPaddings(1, 1)
btnOpen := CreateButton(blist, AutoSize, AutoSize, "Open", Fixed)
btnSelect := CreateButton(blist, AutoSize, AutoSize, "Select", Fixed)
btnCancel := CreateButton(blist, AutoSize, AutoSize, "Cancel", Fixed)
btnCancel.OnClick(func(ev Event) {
WindowManager().DestroyWindow(dlg.View)
WindowManager().BeginUpdate()
dlg.Selected = false
closeFunc := dlg.onClose
WindowManager().EndUpdate()
if closeFunc != nil {
closeFunc()
}
})
btnSelect.OnClick(func(ev Event) {
WindowManager().DestroyWindow(dlg.View)
WindowManager().BeginUpdate()
dlg.Selected = true
dlg.FilePath = filepath.Join(dlg.currPath, dlg.listBox.SelectedItemText())
if dlg.edFile.Title() != "" {
dlg.FilePath = filepath.Join(dlg.currPath, dlg.edFile.Title())
}
_, err := os.Stat(dlg.FilePath)
dlg.Exists = !os.IsNotExist(err)
closeFunc := dlg.onClose
WindowManager().EndUpdate()
if closeFunc != nil {
closeFunc()
}
})
dlg.View.OnClose(func(ev Event) bool {
if dlg.result == DialogAlive {
dlg.result = DialogClosed
if ev.X != 1 {
WindowManager().DestroyWindow(dlg.View)
}
if dlg.onClose != nil {
dlg.onClose()
}
}
return true
})
dlg.listBox.OnSelectItem(func(ev Event) {
item := ev.Msg
if item == ".." {
btnSelect.SetEnabled(false)
btnOpen.SetEnabled(true)
return
}
isDir := strings.HasSuffix(item, string(os.PathSeparator))
if isDir {
btnOpen.SetEnabled(true)
if !dlg.selectDir {
btnSelect.SetEnabled(false)
return
}
}
if !isDir {
btnOpen.SetEnabled(false)
}
btnSelect.SetEnabled(true)
if (isDir && dlg.selectDir) || !isDir {
dlg.edFile.SetTitle(item)
} else {
dlg.edFile.Clear()
}
})
btnOpen.OnClick(func(ev Event) {
s := dlg.listBox.SelectedItemText()
if s != ".." && (s == "" || !strings.HasSuffix(s, string(os.PathSeparator))) {
return
}
if s == ".." {
dlg.pathUp()
} else {
dlg.pathDown(s)
}
})
dlg.edFile.OnChange(func(ev Event) {
s := ""
lowCurrText := strings.ToLower(dlg.listBox.SelectedItemText())
lowEditText := strings.ToLower(dlg.edFile.Title())
if !strings.HasPrefix(lowCurrText, lowEditText) {
itemIdx := dlg.listBox.PartialFindItem(dlg.edFile.Title(), false)
if itemIdx == -1 {
if dlg.mustExist {
btnSelect.SetEnabled(false)
}
return
}
s, _ = dlg.listBox.Item(itemIdx)
dlg.listBox.SelectItem(itemIdx)
} else {
s = dlg.listBox.SelectedItemText()
}
isDir := strings.HasSuffix(s, string(os.PathSeparator))
equal := strings.EqualFold(s, dlg.edFile.Title()) ||
strings.EqualFold(s, dlg.edFile.Title()+string(os.PathSeparator))
btnOpen.SetEnabled(isDir || s == "..")
if isDir {
btnSelect.SetEnabled(dlg.selectDir && (equal || !dlg.mustExist))
return
}
btnSelect.SetEnabled(equal || !dlg.mustExist)
})
dlg.edFile.OnKeyPress(func(key term.Key, c rune) bool {
if key == term.KeyArrowUp {
idx := dlg.listBox.SelectedItem()
if idx > 0 {
dlg.listBox.SelectItem(idx - 1)
dlg.updateEditBox()
}
return true
}
if key == term.KeyArrowDown {
idx := dlg.listBox.SelectedItem()
if idx < dlg.listBox.ItemCount()-1 {
dlg.listBox.SelectItem(idx + 1)
dlg.updateEditBox()
}
return true
}
return false
})
dlg.listBox.OnKeyPress(func(key term.Key) bool {
if key == term.KeyBackspace || key == term.KeyBackspace2 {
dlg.pathUp()
return true
}
if key == term.KeyCtrlM {
s := dlg.listBox.SelectedItemText()
if s == ".." {
dlg.pathUp()
return true
}
if strings.HasSuffix(s, string(os.PathSeparator)) {
dlg.pathDown(s)
return true
}
return false
}
return false
})
dlg.curDir.SetTitle(dlg.currPath)
dlg.populateFiles()
dlg.selectFirst()
ActivateControl(dlg.View, dlg.listBox)
return dlg
}
// OnClose sets the callback that is called when the
// dialog is closed
func (d *FileSelectDialog) OnClose(fn func()) {
WindowManager().BeginUpdate()
defer WindowManager().EndUpdate()
d.onClose = fn
}