mirror of
https://github.com/VladimirMarkelov/clui.git
synced 2025-04-24 13:48:53 +08:00
closes #98
This commit is contained in:
parent
1d1beb797c
commit
bca28ea40c
@ -7,7 +7,7 @@ Command Line User Interface (Console UI inspired by TurboVision) with built-in t
|
||||
|
||||
|
||||
## Current version
|
||||
The current version is 0.8.0. Please see details in [changelog](./changelog).
|
||||
The current version is 0.9.0 RC1. Please see details in [changelog](./changelog).
|
||||
|
||||
## Applications that uses the library
|
||||
* Terminal FB2 reader(termfb2): https://github.com/VladimirMarkelov/termfb2
|
||||
@ -38,6 +38,7 @@ The current version is 0.8.0. Please see details in [changelog](./changelog).
|
||||
* BarChart (Horizontal bar chart without scroll)
|
||||
* SparkChart (Show tabular data as a bar graph)
|
||||
* GridView (Table to show structured data - only virtual and readonly mode with scroll support)
|
||||
* 
|
||||
|
||||
## Screenshots
|
||||
The main demo (theme changing and radio group control)
|
||||
|
21
changelog
21
changelog
@ -1,3 +1,22 @@
|
||||
2018-08-04 - version 0.9.0 RC1
|
||||
[+] New control: File Picker (a dialog for file save/load operations)
|
||||
[+] New property for Label: TextDisplay. It defines which part of Label title
|
||||
is shown when the title is longer than the Label length:
|
||||
- AlignLeft - the head of the Title is displayed
|
||||
- AlignRight - the tail of the Title is displayed
|
||||
[+] New ListBox functions:
|
||||
- Item(index int) (title string, ok book) - returns a text of ListBox item
|
||||
by its index. If the item does not exist then ok is set to false. The
|
||||
function works similar Go 's, ok := map[key]'
|
||||
- PartialFind(text string, caseSensitive bool) int - return the index of
|
||||
the first item of ListBox that starts with the 'text'. If no item starts
|
||||
with the 'text' then '-1' is returned
|
||||
[*] Fix bug in EditField: when new Title was set the current display offset was
|
||||
not reset. It might result in an application crash when new Title was
|
||||
shorter than the old one
|
||||
[*] Fix bug with ListBox 'select item' event: the event was not emitted after
|
||||
pressing End, Home, PageUp, and PageDown
|
||||
|
||||
2018-07-17 - version 0.8.1
|
||||
[+] New API functions SetColorMap and GetColorMap to change the current color
|
||||
palette
|
||||
@ -108,7 +127,7 @@
|
||||
[+] Added constants for dragging events - new type DragType
|
||||
[+] Added new type of Event MouseClick: it is generated if a user does mouse
|
||||
button down and mouse release at the same coordinates. In this case a
|
||||
control recieves 3 events: mouse down, mouse release, and mouse click
|
||||
control receives 3 events: mouse down, mouse release, and mouse click
|
||||
[+] Main loop moved to separate source file
|
||||
[+] Added test for color parsing functions
|
||||
|
||||
|
99
demos/fileselect/fselect.go
Normal file
99
demos/fileselect/fselect.go
Normal file
@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
ui "github.com/VladimirMarkelov/clui"
|
||||
)
|
||||
|
||||
func createView() {
|
||||
view := ui.AddWindow(0, 0, 30, 7, "File select")
|
||||
view.SetPack(ui.Vertical)
|
||||
view.SetGaps(0, 1)
|
||||
view.SetPaddings(2, 2)
|
||||
|
||||
frmPath := ui.CreateFrame(view, 1, 1, ui.BorderNone, ui.Fixed)
|
||||
frmPath.SetPack(ui.Horizontal)
|
||||
ui.CreateLabel(frmPath, ui.AutoSize, ui.AutoSize, "Initial path", ui.Fixed)
|
||||
edPath := ui.CreateEditField(frmPath, 16, "", 1)
|
||||
|
||||
frmMask := ui.CreateFrame(view, 1, 1, ui.BorderNone, ui.Fixed)
|
||||
frmMask.SetPack(ui.Horizontal)
|
||||
ui.CreateLabel(frmMask, ui.AutoSize, ui.AutoSize, "File masks", ui.Fixed)
|
||||
edMasks := ui.CreateEditField(frmMask, 16, "*", 1)
|
||||
|
||||
frmOpts := ui.CreateFrame(view, 1, 1, ui.BorderNone, ui.Fixed)
|
||||
frmOpts.SetPack(ui.Horizontal)
|
||||
cbDir := ui.CreateCheckBox(frmOpts, ui.AutoSize, "Select directory", ui.Fixed)
|
||||
cbMust := ui.CreateCheckBox(frmOpts, ui.AutoSize, "Must exists", ui.Fixed)
|
||||
ui.CreateFrame(frmOpts, 1, 1, ui.BorderNone, 1)
|
||||
|
||||
lblSelected := ui.CreateLabel(view, 30, 5, "Selected:", ui.Fixed)
|
||||
lblSelected.SetMultiline(true)
|
||||
|
||||
frmBtns := ui.CreateFrame(view, 1, 1, ui.BorderNone, ui.Fixed)
|
||||
frmBtns.SetPack(ui.Horizontal)
|
||||
btnSet := ui.CreateButton(frmBtns, ui.AutoSize, 4, "Select", ui.Fixed)
|
||||
btnQuit := ui.CreateButton(frmBtns, ui.AutoSize, 4, "Quit", ui.Fixed)
|
||||
ui.CreateFrame(frmBtns, 1, 1, ui.BorderNone, 1)
|
||||
|
||||
ui.ActivateControl(view, edMasks)
|
||||
|
||||
btnSet.OnClick(func(ev ui.Event) {
|
||||
s := "Select "
|
||||
if cbDir.State() == 1 {
|
||||
s += "directory"
|
||||
} else {
|
||||
s += "file"
|
||||
}
|
||||
if cbMust.State() == 1 {
|
||||
s += "[X]"
|
||||
}
|
||||
dlg := ui.CreateFileSelectDialog(
|
||||
s,
|
||||
edMasks.Title(),
|
||||
edPath.Title(),
|
||||
cbDir.State() == 1,
|
||||
cbMust.State() == 1)
|
||||
dlg.OnClose(func() {
|
||||
if !dlg.Selected {
|
||||
lblSelected.SetTitle("Selected:\nNothing")
|
||||
return
|
||||
}
|
||||
|
||||
var lb string
|
||||
if dlg.Exists {
|
||||
lb = "Selected existing"
|
||||
} else {
|
||||
lb = "Create new"
|
||||
}
|
||||
|
||||
if cbDir.State() == 0 {
|
||||
lb += " file:\n"
|
||||
} else {
|
||||
lb += " directory:\n"
|
||||
}
|
||||
|
||||
lb += dlg.FilePath
|
||||
lblSelected.SetTitle(lb)
|
||||
})
|
||||
})
|
||||
|
||||
btnQuit.OnClick(func(ev ui.Event) {
|
||||
go ui.Stop()
|
||||
})
|
||||
}
|
||||
|
||||
func mainLoop() {
|
||||
// Every application must create a single Composer and
|
||||
// call its intialize method
|
||||
ui.InitLibrary()
|
||||
defer ui.DeinitLibrary()
|
||||
|
||||
createView()
|
||||
|
||||
// start event processing loop - the main core of the library
|
||||
ui.MainLoop()
|
||||
}
|
||||
|
||||
func main() {
|
||||
mainLoop()
|
||||
}
|
54
docs/fselect.md
Normal file
54
docs/fselect.md
Normal file
@ -0,0 +1,54 @@
|
||||
# File Picker
|
||||
|
||||
Use file picker if you need to select a single file or directory.
|
||||
|
||||
<img src="/docs/img/fselect.png" alt="File picker dialog">
|
||||
|
||||
The dialog includes:
|
||||
|
||||
- title that shows the custom text followed by file masks
|
||||
- current directory
|
||||
- list of directories and files inside the current directory
|
||||
- edit field to a) enter a name of a new file or directory b) quick search. When edit field has focus you can use arrows up and down to select previous or next object
|
||||
- button **Open** enters the selected directory. The button is useful in select directory mode
|
||||
- button **Select** closes the dialog and returns a path to the selected object. If edit field is empty the path is the path to the object selected in the file list box, otherwise the path is made from current directory and edit field text
|
||||
- button **Cancel** closes the dialog and does not return path to selected object
|
||||
|
||||
### Returned values
|
||||
|
||||
After the dialog is closed a few its properties contains information what object a user has selected:
|
||||
|
||||
- **Selected** contains information about how the dialog was closed: true - a user has selected an object and clicked **Select**, false - a user canceled the dialog without selecting any object
|
||||
- **Exists** is true if a user has selected existing file or directory, and it is false if a user entered name of a new object and clicked **Select**. The latter is possible only if option **mustExist** is set to false
|
||||
- **FilePath** is a full path to the selected object
|
||||
|
||||
### API
|
||||
|
||||
To show a dialog, call the function
|
||||
```
|
||||
func CreateFileSelectDialog(title, fileMasks, initPath string, selectDir, mustExist bool) *FileSelectDialog
|
||||
```
|
||||
|
||||
Function arguments:
|
||||
|
||||
- **title** is a custom dialog title. It should not contain file masks because the dialog always shows title and the file masks follows it
|
||||
- **fileMasks** is list of file masks separated with comma or OS path separator (';' - for Windows, ':' - for Linux). Empty, "*", and "*.*" mean *all files*
|
||||
- **initPath** sets the starting directory for the dialog. If it is empty then the dialog uses the current working directory. If the **intiPath** does not exist, then the dialog looks up for the first existing directory in the directory tree starting from **initPath**. In case it fails to find any existing directory, the dialog opens the current working directory
|
||||
- **selectDir** - set it to *true* if you want to select a directory instead of a file. In case of **selectDir** is *true*, the file list box does not display regular files
|
||||
- **mustExist** set it to *true* if you want a user to select only existing object. If it is *false* then the dialog allows a user to enter any name into edit field and click **Select**. The latter is useful for "File save" dialog.
|
||||
|
||||
Do not forget to set a callback that is called after the dialog is closed:
|
||||
```
|
||||
bookFile := ""
|
||||
dlg := CreateFileSelectDialog("Select a book to read", "*.fb2,*.epub,*.txt", "", false, true)
|
||||
dlg.OnClose(func() {
|
||||
if !dlg.Selected {
|
||||
// a user canceled the dialog
|
||||
return
|
||||
}
|
||||
bookFile = dlg.FilePath
|
||||
})
|
||||
```
|
||||
|
||||
Please, check the  for more details.
|
||||
|
BIN
docs/img/fselect.png
Normal file
BIN
docs/img/fselect.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
1
edit.go
1
edit.go
@ -22,6 +22,7 @@ func (e *EditField) OnKeyPress(fn func(term.Key, rune) bool) {
|
||||
// SetTitle changes the EditField content and emits OnChage eventif the new value does not equal to old one
|
||||
func (e *EditField) SetTitle(title string) {
|
||||
e.setTitleInternal(title)
|
||||
e.offset = 0
|
||||
e.end()
|
||||
}
|
||||
|
||||
|
419
fileselectdlg.go
Normal file
419
fileselectdlg.go
Normal file
@ -0,0 +1,419 @@
|
||||
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
|
||||
}
|
33
label.go
33
label.go
@ -14,8 +14,9 @@ is always left aligned
|
||||
*/
|
||||
type Label struct {
|
||||
BaseControl
|
||||
direction Direction
|
||||
multiline bool
|
||||
direction Direction
|
||||
multiline bool
|
||||
textDisplay Align
|
||||
}
|
||||
|
||||
/*
|
||||
@ -45,6 +46,7 @@ func CreateLabel(parent Control, w, h int, title string, scale int) *Label {
|
||||
c.SetConstraints(w, h)
|
||||
c.SetScale(scale)
|
||||
c.tabSkip = true
|
||||
c.textDisplay = AlignLeft
|
||||
|
||||
if parent != nil {
|
||||
parent.AddChild(c)
|
||||
@ -121,9 +123,15 @@ func (l *Label) Draw() {
|
||||
} else {
|
||||
if l.direction == Horizontal {
|
||||
shift, str := AlignColorizedText(l.title, l.width, l.align)
|
||||
if str != l.title && l.align != l.textDisplay {
|
||||
shift, str = AlignColorizedText(l.title, l.width, l.textDisplay)
|
||||
}
|
||||
DrawText(l.x+shift, l.y, str)
|
||||
} else {
|
||||
shift, str := AlignColorizedText(l.title, l.height, l.align)
|
||||
if str != l.title && l.align != l.textDisplay {
|
||||
shift, str = AlignColorizedText(l.title, l.width, l.textDisplay)
|
||||
}
|
||||
DrawTextVertical(l.x, l.y+shift, str)
|
||||
}
|
||||
}
|
||||
@ -141,3 +149,24 @@ func (l *Label) Multiline() bool {
|
||||
func (l *Label) SetMultiline(multi bool) {
|
||||
l.multiline = multi
|
||||
}
|
||||
|
||||
// TextDisplay returns which part of the lable title is displayed in case of
|
||||
// title is longer than the label:
|
||||
// - AlignLeft - the head of the title is shown
|
||||
// - AlignRight - the tail of the title is shown
|
||||
// The property is used only by single line Label
|
||||
func (l *Label) TextDisplay() Align {
|
||||
return l.textDisplay
|
||||
}
|
||||
|
||||
// SetTextDisplay sets which part of the title is displayed in case of the title
|
||||
// is longer than the lable. Only AlignLeft and AlignRigth are valid values
|
||||
// for the property. Any other value does is skipped and does not affect
|
||||
// displaying the title
|
||||
func (l *Label) SetTextDisplay(align Align) {
|
||||
if align != AlignLeft && align != AlignRight {
|
||||
return
|
||||
}
|
||||
|
||||
l.textDisplay = align
|
||||
}
|
||||
|
52
listbox.go
52
listbox.go
@ -134,16 +134,25 @@ func (l *ListBox) Draw() {
|
||||
}
|
||||
|
||||
func (l *ListBox) home() {
|
||||
if l.currSelection == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if len(l.items) > 0 {
|
||||
l.currSelection = 0
|
||||
}
|
||||
l.topLine = 0
|
||||
|
||||
if l.onSelectItem != nil {
|
||||
ev := Event{Y: l.currSelection, Msg: l.SelectedItemText()}
|
||||
l.onSelectItem(ev)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ListBox) end() {
|
||||
length := len(l.items)
|
||||
|
||||
if length == 0 {
|
||||
if length == 0 || l.currSelection == length-1 {
|
||||
return
|
||||
}
|
||||
|
||||
@ -151,6 +160,11 @@ func (l *ListBox) end() {
|
||||
if length > l.height {
|
||||
l.topLine = length - l.height
|
||||
}
|
||||
|
||||
if l.onSelectItem != nil {
|
||||
ev := Event{Y: l.currSelection, Msg: l.SelectedItemText()}
|
||||
l.onSelectItem(ev)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ListBox) moveUp(dy int) {
|
||||
@ -352,7 +366,7 @@ func (l *ListBox) AddItem(item string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SelectItem slects item which number in the list equals
|
||||
// SelectItem selects item which number in the list equals
|
||||
// id. If the item exists the ListBox scrolls the list to
|
||||
// make the item visible.
|
||||
// Returns true if the item is selected successfully
|
||||
@ -366,6 +380,16 @@ func (l *ListBox) SelectItem(id int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Item returns item text by its index.
|
||||
// If index is out of range an empty string and false are returned
|
||||
func (l *ListBox) Item(id int) (string, bool) {
|
||||
if len(l.items) <= id || id < 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return l.items[id], true
|
||||
}
|
||||
|
||||
// FindItem looks for an item in list which text equals
|
||||
// to text, by default the search is casesensitive.
|
||||
// Returns item number in item list or -1 if nothing is found.
|
||||
@ -379,6 +403,30 @@ func (l *ListBox) FindItem(text string, caseSensitive bool) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
// PartialFindItem looks for an item in list which text starts from
|
||||
// the given substring, by default the search is casesensitive.
|
||||
// Returns item number in item list or -1 if nothing is found.
|
||||
func (l *ListBox) PartialFindItem(text string, caseSensitive bool) int {
|
||||
if !caseSensitive {
|
||||
text = strings.ToLower(text)
|
||||
}
|
||||
|
||||
for idx, itm := range l.items {
|
||||
if caseSensitive {
|
||||
if strings.HasPrefix(itm, text) {
|
||||
return idx
|
||||
}
|
||||
} else {
|
||||
low := strings.ToLower(itm)
|
||||
if strings.HasPrefix(low, text) {
|
||||
return idx
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// SelectedItem returns currently selected item id
|
||||
func (l *ListBox) SelectedItem() int {
|
||||
return l.currSelection
|
||||
|
Loading…
x
Reference in New Issue
Block a user