983 lines
25 KiB
Go
983 lines
25 KiB
Go
package dialog
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/lang"
|
|
"fyne.io/fyne/v2/storage"
|
|
"fyne.io/fyne/v2/storage/repository"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
// ViewLayout can be passed to SetView() to set the view of
|
|
// a FileDialog
|
|
//
|
|
// Since: 2.5
|
|
type ViewLayout int
|
|
|
|
const (
|
|
defaultView ViewLayout = iota
|
|
ListView
|
|
GridView
|
|
)
|
|
|
|
const (
|
|
viewLayoutKey = "fyne:fileDialogViewLayout"
|
|
lastFolderKey = "fyne:fileDialogLastFolder"
|
|
)
|
|
|
|
type textWidget interface {
|
|
fyne.Widget
|
|
SetText(string)
|
|
}
|
|
|
|
type favoriteItem struct {
|
|
locName string
|
|
locIcon fyne.Resource
|
|
loc fyne.URI
|
|
}
|
|
|
|
type fileDialogPanel interface {
|
|
fyne.Widget
|
|
|
|
Unselect(int)
|
|
}
|
|
|
|
type fileDialog struct {
|
|
file *FileDialog
|
|
fileName textWidget
|
|
title *widget.Label
|
|
dismiss *widget.Button
|
|
open *widget.Button
|
|
breadcrumb *fyne.Container
|
|
breadcrumbScroll *container.Scroll
|
|
files fileDialogPanel
|
|
filesScroll *container.Scroll
|
|
favorites []favoriteItem
|
|
favoritesList *widget.List
|
|
showHidden bool
|
|
|
|
view ViewLayout
|
|
|
|
data []fyne.URI
|
|
|
|
win *widget.PopUp
|
|
selected fyne.URI
|
|
selectedID int
|
|
dir fyne.ListableURI
|
|
// this will be the initial filename in a FileDialog in save mode
|
|
initialFileName string
|
|
|
|
toggleViewButton *widget.Button
|
|
}
|
|
|
|
// FileDialog is a dialog containing a file picker for use in opening or saving files.
|
|
type FileDialog struct {
|
|
callback any
|
|
onClosedCallback func(bool)
|
|
parent fyne.Window
|
|
dialog *fileDialog
|
|
|
|
titleText string
|
|
confirmText, dismissText string
|
|
desiredSize fyne.Size
|
|
filter storage.FileFilter
|
|
save bool
|
|
// this will be applied to dialog.dir when it's loaded
|
|
startingLocation fyne.ListableURI
|
|
// this will be the initial filename in a FileDialog in save mode
|
|
initialFileName string
|
|
// this will be the initial view in a FileDialog
|
|
initialView ViewLayout
|
|
}
|
|
|
|
// Declare conformity to Dialog interface
|
|
var _ Dialog = (*FileDialog)(nil)
|
|
|
|
func (f *fileDialog) makeUI() fyne.CanvasObject {
|
|
if f.file.save {
|
|
saveName := widget.NewEntry()
|
|
saveName.OnChanged = func(s string) {
|
|
if s == "" {
|
|
f.open.Disable()
|
|
} else {
|
|
f.open.Enable()
|
|
}
|
|
}
|
|
saveName.SetPlaceHolder(lang.L("Enter filename"))
|
|
saveName.OnSubmitted = func(s string) {
|
|
f.open.OnTapped()
|
|
}
|
|
f.fileName = saveName
|
|
} else {
|
|
f.fileName = widget.NewLabel("")
|
|
}
|
|
|
|
label := lang.L("Open")
|
|
if f.file.save {
|
|
label = lang.L("Save")
|
|
}
|
|
if f.file.confirmText != "" {
|
|
label = f.file.confirmText
|
|
}
|
|
f.open = f.makeOpenButton(label)
|
|
|
|
if f.file.save {
|
|
f.fileName.SetText(f.initialFileName)
|
|
}
|
|
|
|
dismissLabel := lang.L("Cancel")
|
|
if f.file.dismissText != "" {
|
|
dismissLabel = f.file.dismissText
|
|
}
|
|
f.dismiss = f.makeDismissButton(dismissLabel)
|
|
|
|
buttons := container.NewGridWithRows(1, f.dismiss, f.open)
|
|
|
|
f.filesScroll = container.NewScroll(nil) // filesScroll's content will be set by setView function.
|
|
verticalExtra := float32(float64(fileIconSize) * 0.25)
|
|
itemMin := f.newFileItem(storage.NewFileURI("filename.txt"), false, false).MinSize()
|
|
f.filesScroll.SetMinSize(itemMin.AddWidthHeight(itemMin.Width+theme.Padding()*3, verticalExtra))
|
|
|
|
f.breadcrumb = container.NewHBox()
|
|
f.breadcrumbScroll = container.NewHScroll(container.NewPadded(f.breadcrumb))
|
|
title := label + " " + lang.L("File")
|
|
if f.file.isDirectory() {
|
|
title = label + " " + lang.L("Folder")
|
|
}
|
|
if f.file.titleText != "" {
|
|
title = f.file.titleText
|
|
}
|
|
f.title = widget.NewLabelWithStyle(title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
|
|
|
view := ViewLayout(fyne.CurrentApp().Preferences().Int(viewLayoutKey))
|
|
|
|
// handle invalid values
|
|
if view != GridView && view != ListView {
|
|
view = defaultView
|
|
}
|
|
|
|
if view == defaultView {
|
|
// set GridView as default
|
|
view = GridView
|
|
|
|
if f.file.initialView != defaultView {
|
|
view = f.file.initialView
|
|
}
|
|
}
|
|
|
|
// icon of button is set in subsequent setView() call
|
|
f.toggleViewButton = widget.NewButtonWithIcon("", nil, func() {
|
|
if f.view == GridView {
|
|
f.setView(ListView)
|
|
} else {
|
|
f.setView(GridView)
|
|
}
|
|
})
|
|
f.setView(view)
|
|
|
|
f.loadFavorites()
|
|
|
|
f.favoritesList = widget.NewList(
|
|
func() int {
|
|
return len(f.favorites)
|
|
},
|
|
func() fyne.CanvasObject {
|
|
return container.NewHBox(container.New(&iconPaddingLayout{}, widget.NewIcon(theme.DocumentIcon())), widget.NewLabel("Template Object"))
|
|
},
|
|
func(id widget.ListItemID, item fyne.CanvasObject) {
|
|
item.(*fyne.Container).Objects[0].(*fyne.Container).Objects[0].(*widget.Icon).SetResource(f.favorites[id].locIcon)
|
|
item.(*fyne.Container).Objects[1].(*widget.Label).SetText(f.favorites[id].locName)
|
|
},
|
|
)
|
|
f.favoritesList.OnSelected = func(id widget.ListItemID) {
|
|
f.setLocation(f.favorites[id].loc)
|
|
}
|
|
|
|
var optionsButton *widget.Button
|
|
optionsButton = widget.NewButtonWithIcon("", theme.SettingsIcon(), func() {
|
|
f.optionsMenu(fyne.CurrentApp().Driver().AbsolutePositionForObject(optionsButton), optionsButton.Size())
|
|
})
|
|
|
|
newFolderButton := widget.NewButtonWithIcon("", theme.FolderNewIcon(), func() {
|
|
newFolderEntry := widget.NewEntry()
|
|
ShowForm(lang.L("New Folder"), lang.L("Create Folder"), lang.L("Cancel"), []*widget.FormItem{
|
|
{
|
|
Text: lang.X("file.name", "Name"),
|
|
Widget: newFolderEntry,
|
|
},
|
|
}, func(s bool) {
|
|
if !s || newFolderEntry.Text == "" {
|
|
return
|
|
}
|
|
|
|
newFolderPath := filepath.Join(f.dir.Path(), newFolderEntry.Text)
|
|
createFolderErr := os.MkdirAll(newFolderPath, 0750)
|
|
if createFolderErr != nil {
|
|
fyne.LogError(
|
|
fmt.Sprintf("Failed to create folder with path %s", newFolderPath),
|
|
createFolderErr,
|
|
)
|
|
ShowError(errors.New("folder cannot be created"), f.file.parent)
|
|
}
|
|
f.refreshDir(f.dir)
|
|
}, f.file.parent)
|
|
})
|
|
|
|
optionsbuttons := container.NewHBox(
|
|
newFolderButton,
|
|
f.toggleViewButton,
|
|
optionsButton,
|
|
)
|
|
|
|
header := container.NewBorder(nil, nil, nil, optionsbuttons,
|
|
optionsbuttons, f.title,
|
|
)
|
|
|
|
footer := container.NewBorder(nil, nil, nil, buttons,
|
|
buttons, container.NewHScroll(f.fileName),
|
|
)
|
|
|
|
body := container.NewHSplit(
|
|
f.favoritesList,
|
|
container.NewBorder(f.breadcrumbScroll, nil, nil, nil,
|
|
f.breadcrumbScroll, f.filesScroll,
|
|
),
|
|
)
|
|
body.SetOffset(0) // Set the minimum offset so that the favoritesList takes only its minimal width
|
|
|
|
return container.NewBorder(header, footer, nil, nil, body)
|
|
}
|
|
|
|
func (f *fileDialog) makeOpenButton(label string) *widget.Button {
|
|
btn := widget.NewButton(label, func() {
|
|
if f.file.callback == nil {
|
|
f.win.Hide()
|
|
if f.file.onClosedCallback != nil {
|
|
f.file.onClosedCallback(false)
|
|
}
|
|
return
|
|
}
|
|
|
|
if f.file.save {
|
|
callback := f.file.callback.(func(fyne.URIWriteCloser, error))
|
|
name := f.fileName.(*widget.Entry).Text
|
|
location, _ := storage.Child(f.dir, name)
|
|
|
|
exists, _ := storage.Exists(location)
|
|
|
|
// check if a directory is selected
|
|
listable, err := storage.CanList(location)
|
|
|
|
if !exists {
|
|
f.win.Hide()
|
|
if f.file.onClosedCallback != nil {
|
|
f.file.onClosedCallback(true)
|
|
}
|
|
callback(storage.Writer(location))
|
|
return
|
|
} else if err == nil && listable {
|
|
// a directory has been selected
|
|
ShowInformation("Cannot overwrite",
|
|
"Files cannot replace a directory,\ncheck the file name and try again", f.file.parent)
|
|
return
|
|
}
|
|
|
|
ShowConfirm("Overwrite?", "Are you sure you want to overwrite the file\n"+name+"?",
|
|
func(ok bool) {
|
|
if !ok {
|
|
return
|
|
}
|
|
f.win.Hide()
|
|
|
|
callback(storage.Writer(location))
|
|
if f.file.onClosedCallback != nil {
|
|
f.file.onClosedCallback(true)
|
|
}
|
|
}, f.file.parent)
|
|
} else if f.selected != nil {
|
|
callback := f.file.callback.(func(fyne.URIReadCloser, error))
|
|
f.win.Hide()
|
|
if f.file.onClosedCallback != nil {
|
|
f.file.onClosedCallback(true)
|
|
}
|
|
callback(storage.Reader(f.selected))
|
|
} else if f.file.isDirectory() {
|
|
callback := f.file.callback.(func(fyne.ListableURI, error))
|
|
f.win.Hide()
|
|
if f.file.onClosedCallback != nil {
|
|
f.file.onClosedCallback(true)
|
|
}
|
|
callback(f.dir, nil)
|
|
}
|
|
})
|
|
|
|
btn.Importance = widget.HighImportance
|
|
btn.Disable()
|
|
|
|
return btn
|
|
}
|
|
|
|
func (f *fileDialog) makeDismissButton(label string) *widget.Button {
|
|
btn := widget.NewButton(label, func() {
|
|
f.win.Hide()
|
|
if f.file.onClosedCallback != nil {
|
|
f.file.onClosedCallback(false)
|
|
}
|
|
if f.file.callback != nil {
|
|
if f.file.save {
|
|
f.file.callback.(func(fyne.URIWriteCloser, error))(nil, nil)
|
|
} else if f.file.isDirectory() {
|
|
f.file.callback.(func(fyne.ListableURI, error))(nil, nil)
|
|
} else {
|
|
f.file.callback.(func(fyne.URIReadCloser, error))(nil, nil)
|
|
}
|
|
}
|
|
})
|
|
|
|
return btn
|
|
}
|
|
|
|
func (f *fileDialog) optionsMenu(position fyne.Position, buttonSize fyne.Size) {
|
|
hiddenFiles := widget.NewCheck(lang.L("Show Hidden Files"), func(changed bool) {
|
|
f.showHidden = changed
|
|
f.refreshDir(f.dir)
|
|
})
|
|
hiddenFiles.Checked = f.showHidden
|
|
hiddenFiles.Refresh()
|
|
content := container.NewVBox(hiddenFiles)
|
|
|
|
p := position.Add(buttonSize)
|
|
pos := fyne.NewPos(p.X-content.MinSize().Width-theme.Padding()*2, p.Y+theme.Padding()*2)
|
|
widget.ShowPopUpAtPosition(content, f.win.Canvas, pos)
|
|
}
|
|
|
|
func (f *fileDialog) loadFavorites() {
|
|
favoriteLocations, err := getFavoriteLocations()
|
|
if err != nil {
|
|
fyne.LogError("Getting favorite locations", err)
|
|
}
|
|
favoriteIcons := getFavoriteIcons()
|
|
favoriteOrder := getFavoriteOrder()
|
|
|
|
f.favorites = []favoriteItem{
|
|
{locName: "Home", locIcon: theme.HomeIcon(), loc: favoriteLocations["Home"]}}
|
|
app := fyne.CurrentApp()
|
|
if hasAppFiles(app) {
|
|
f.favorites = append(f.favorites,
|
|
favoriteItem{locName: "App Files", locIcon: theme.FileIcon(), loc: storageURI(app)})
|
|
}
|
|
f.favorites = append(f.favorites, f.getPlaces()...)
|
|
|
|
for _, locName := range favoriteOrder {
|
|
loc, ok := favoriteLocations[locName]
|
|
if !ok {
|
|
continue
|
|
}
|
|
locIcon := favoriteIcons[locName]
|
|
f.favorites = append(f.favorites,
|
|
favoriteItem{locName: locName, locIcon: locIcon, loc: loc})
|
|
}
|
|
}
|
|
|
|
func (f *fileDialog) refreshDir(dir fyne.ListableURI) {
|
|
f.data = nil
|
|
|
|
files, err := dir.List()
|
|
if err != nil {
|
|
fyne.LogError("Unable to read ListableURI "+dir.String(), err)
|
|
return
|
|
}
|
|
|
|
var icons []fyne.URI
|
|
parent, err := storage.Parent(dir)
|
|
if err != nil && err != repository.ErrURIRoot {
|
|
fyne.LogError("Unable to get parent of "+dir.String(), err)
|
|
return
|
|
}
|
|
if parent != nil && parent.String() != dir.String() {
|
|
icons = append(icons, parent)
|
|
}
|
|
|
|
for _, file := range files {
|
|
if !f.showHidden && isHidden(file) {
|
|
continue
|
|
}
|
|
|
|
listable, err := storage.ListerForURI(file)
|
|
if f.file.isDirectory() && err != nil {
|
|
continue
|
|
} else if err == nil { // URI points to a directory
|
|
icons = append(icons, listable)
|
|
} else if f.file.filter == nil || f.file.filter.Matches(file) {
|
|
icons = append(icons, file)
|
|
}
|
|
}
|
|
|
|
toSort := icons
|
|
if parent != nil {
|
|
toSort = icons[1:]
|
|
}
|
|
sort.Slice(toSort, func(i, j int) bool {
|
|
if parent != nil { // avoiding the parent in [0]
|
|
i++
|
|
j++
|
|
}
|
|
|
|
return strings.ToLower(icons[i].Name()) < strings.ToLower(icons[j].Name())
|
|
})
|
|
f.data = icons
|
|
|
|
f.files.Refresh()
|
|
f.filesScroll.Offset = fyne.NewPos(0, 0)
|
|
f.filesScroll.Refresh()
|
|
}
|
|
|
|
func (f *fileDialog) setLocation(dir fyne.URI) error {
|
|
if f.selectedID > -1 {
|
|
f.files.Unselect(f.selectedID)
|
|
}
|
|
if dir == nil {
|
|
return errors.New("failed to open nil directory")
|
|
}
|
|
list, err := storage.ListerForURI(dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fyne.CurrentApp().Preferences().SetString(lastFolderKey, dir.String())
|
|
isFav := false
|
|
for i, fav := range f.favorites {
|
|
if fav.loc == nil {
|
|
continue
|
|
}
|
|
if fav.loc.Path() == dir.Path() {
|
|
f.favoritesList.Select(i)
|
|
isFav = true
|
|
break
|
|
}
|
|
}
|
|
if !isFav {
|
|
f.favoritesList.UnselectAll()
|
|
}
|
|
|
|
f.setSelected(nil, -1)
|
|
f.dir = list
|
|
|
|
f.breadcrumb.Objects = nil
|
|
|
|
localdir := dir.String()[len(dir.Scheme())+3:]
|
|
|
|
buildDir := filepath.VolumeName(localdir)
|
|
for i, d := range strings.Split(localdir, "/") {
|
|
if d == "" {
|
|
if i > 0 { // what we get if we split "/"
|
|
break
|
|
}
|
|
buildDir = "/"
|
|
d = "/"
|
|
} else if i > 0 {
|
|
buildDir = filepath.Join(buildDir, d)
|
|
} else {
|
|
d = buildDir
|
|
buildDir = d + string(os.PathSeparator)
|
|
}
|
|
|
|
newDir := storage.NewFileURI(buildDir)
|
|
isDir, err := storage.CanList(newDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !isDir {
|
|
return errors.New("location was not a listable URI")
|
|
}
|
|
f.breadcrumb.Add(
|
|
widget.NewButton(d, func() {
|
|
err := f.setLocation(newDir)
|
|
if err != nil {
|
|
fyne.LogError("Failed to set directory", err)
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
|
|
f.breadcrumbScroll.Refresh()
|
|
f.breadcrumbScroll.Offset.X = f.breadcrumbScroll.Content.Size().Width - f.breadcrumbScroll.Size().Width
|
|
f.breadcrumbScroll.Refresh()
|
|
|
|
if f.file.isDirectory() {
|
|
f.fileName.SetText(dir.Name())
|
|
f.open.Enable()
|
|
}
|
|
f.refreshDir(list)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (f *fileDialog) setSelected(file fyne.URI, id int) {
|
|
if file != nil {
|
|
if listable, err := storage.CanList(file); err == nil && listable {
|
|
f.setLocation(file)
|
|
return
|
|
}
|
|
}
|
|
f.selected = file
|
|
f.selectedID = id
|
|
|
|
if file == nil || file.String()[len(file.Scheme())+3:] == "" {
|
|
// keep user input while navigating
|
|
// in a FileSave dialog
|
|
if !f.file.save {
|
|
f.fileName.SetText("")
|
|
f.open.Disable()
|
|
}
|
|
} else {
|
|
f.fileName.SetText(file.Name())
|
|
f.open.Enable()
|
|
}
|
|
}
|
|
|
|
func (f *fileDialog) setView(view ViewLayout) {
|
|
f.view = view
|
|
fyne.CurrentApp().Preferences().SetInt(viewLayoutKey, int(view))
|
|
var selectF func(id int)
|
|
choose := func(id int) {
|
|
if file, ok := f.getDataItem(id); ok {
|
|
f.selectedID = id
|
|
f.setSelected(file, id)
|
|
}
|
|
}
|
|
count := func() int {
|
|
return len(f.data)
|
|
}
|
|
template := func() fyne.CanvasObject {
|
|
return f.newFileItem(storage.NewFileURI("./tempfile"), true, false)
|
|
}
|
|
update := func(id widget.GridWrapItemID, o fyne.CanvasObject) {
|
|
if dir, ok := f.getDataItem(id); ok {
|
|
parent := id == 0 && len(dir.Path()) < len(f.dir.Path())
|
|
_, isDir := dir.(fyne.ListableURI)
|
|
o.(*fileDialogItem).setLocation(dir, isDir || parent, parent)
|
|
o.(*fileDialogItem).choose = selectF
|
|
o.(*fileDialogItem).id = id
|
|
o.(*fileDialogItem).open = f.open.OnTapped
|
|
}
|
|
}
|
|
// Actually, during the real interaction, the OnSelected won't be called.
|
|
// It will be called only when we directly calls container.select(i)
|
|
if f.view == GridView {
|
|
grid := widget.NewGridWrap(count, template, update)
|
|
grid.OnSelected = choose
|
|
f.files = grid
|
|
f.toggleViewButton.SetIcon(theme.ListIcon())
|
|
selectF = grid.Select
|
|
} else {
|
|
list := widget.NewList(count, template, update)
|
|
list.OnSelected = choose
|
|
f.files = list
|
|
f.toggleViewButton.SetIcon(theme.GridIcon())
|
|
selectF = list.Select
|
|
}
|
|
|
|
if f.dir != nil {
|
|
f.refreshDir(f.dir)
|
|
}
|
|
f.filesScroll.Content = container.NewPadded(f.files)
|
|
f.filesScroll.Refresh()
|
|
}
|
|
|
|
func (f *fileDialog) getDataItem(id int) (fyne.URI, bool) {
|
|
if id >= len(f.data) {
|
|
return nil, false
|
|
}
|
|
|
|
return f.data[id], true
|
|
}
|
|
|
|
// effectiveStartingDir calculates the directory at which the file dialog should
|
|
// open, based on the values of startingDirectory, CWD, home, and any error
|
|
// conditions which occur.
|
|
//
|
|
// Order of precedence is:
|
|
//
|
|
// - file.startingDirectory if non-empty, os.Stat()-able, and uses the file://
|
|
// URI scheme
|
|
// - previously used file open/close folder within this app
|
|
// - the current app's document storage, if App.Storage() documents have been saved
|
|
// - os.UserHomeDir()
|
|
// - os.Getwd()
|
|
// - "/" (should be filesystem root on all supported platforms)
|
|
func (f *FileDialog) effectiveStartingDir() fyne.ListableURI {
|
|
if f.startingLocation != nil {
|
|
if f.startingLocation.Scheme() == "file" {
|
|
path := f.startingLocation.Path()
|
|
|
|
// the starting directory is set explicitly
|
|
if _, err := os.Stat(path); err != nil {
|
|
fyne.LogError("Error with StartingLocation", err)
|
|
} else {
|
|
return f.startingLocation
|
|
}
|
|
}
|
|
return f.startingLocation
|
|
}
|
|
|
|
// last used
|
|
lastPath := fyne.CurrentApp().Preferences().String(lastFolderKey)
|
|
if lastPath != "" {
|
|
parsed, err := storage.ParseURI(lastPath)
|
|
if err == nil {
|
|
dir, err := storage.ListerForURI(parsed)
|
|
if err == nil {
|
|
return dir
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try app storage
|
|
app := fyne.CurrentApp()
|
|
if hasAppFiles(app) {
|
|
list, _ := storage.ListerForURI(storageURI(app))
|
|
return list
|
|
}
|
|
|
|
// Try home dir
|
|
dir, err := os.UserHomeDir()
|
|
if err == nil {
|
|
lister, err := storage.ListerForURI(storage.NewFileURI(dir))
|
|
if err == nil {
|
|
return lister
|
|
}
|
|
fyne.LogError("Could not create lister for user home dir", err)
|
|
}
|
|
fyne.LogError("Could not load user home dir", err)
|
|
|
|
// Try to get ./
|
|
wd, err := os.Getwd()
|
|
if err == nil {
|
|
lister, err := storage.ListerForURI(storage.NewFileURI(wd))
|
|
if err == nil {
|
|
return lister
|
|
}
|
|
fyne.LogError("Could not create lister for working dir", err)
|
|
}
|
|
|
|
lister, err := storage.ListerForURI(storage.NewFileURI("/"))
|
|
if err != nil {
|
|
fyne.LogError("could not create lister for /", err)
|
|
return nil
|
|
}
|
|
return lister
|
|
}
|
|
|
|
func showFile(file *FileDialog) *fileDialog {
|
|
d := &fileDialog{file: file, initialFileName: file.initialFileName, view: GridView}
|
|
ui := d.makeUI()
|
|
pad := theme.Padding()
|
|
itemMin := d.newFileItem(storage.NewFileURI("filename.txt"), false, false).MinSize()
|
|
size := ui.MinSize().Add(itemMin.AddWidthHeight(itemMin.Width+pad*4, pad*2))
|
|
|
|
d.win = widget.NewModalPopUp(ui, file.parent.Canvas())
|
|
d.win.Resize(size)
|
|
|
|
d.setLocation(file.effectiveStartingDir())
|
|
d.win.Show()
|
|
if file.save {
|
|
d.win.Canvas.Focus(d.fileName.(*widget.Entry))
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Dismiss instructs the dialog to close without any affirmative action.
|
|
//
|
|
// Since: 2.6
|
|
func (f *FileDialog) Dismiss() {
|
|
f.dialog.dismiss.OnTapped()
|
|
}
|
|
|
|
// MinSize returns the size that this dialog should not shrink below
|
|
//
|
|
// Since: 2.1
|
|
func (f *FileDialog) MinSize() fyne.Size {
|
|
return f.dialog.win.MinSize()
|
|
}
|
|
|
|
// Show shows the file dialog.
|
|
func (f *FileDialog) Show() {
|
|
if f.save {
|
|
if fileSaveOSOverride(f) {
|
|
return
|
|
}
|
|
} else {
|
|
if fileOpenOSOverride(f) {
|
|
return
|
|
}
|
|
}
|
|
if f.dialog != nil {
|
|
f.dialog.win.Show()
|
|
return
|
|
}
|
|
f.dialog = showFile(f)
|
|
if !f.desiredSize.IsZero() {
|
|
f.Resize(f.desiredSize)
|
|
}
|
|
}
|
|
|
|
// Refresh causes this dialog to be updated
|
|
func (f *FileDialog) Refresh() {
|
|
f.dialog.win.Refresh()
|
|
}
|
|
|
|
// Resize dialog to the requested size, if there is sufficient space.
|
|
// If the parent window is not large enough then the size will be reduced to fit.
|
|
func (f *FileDialog) Resize(size fyne.Size) {
|
|
f.desiredSize = size
|
|
if f.dialog == nil {
|
|
return
|
|
}
|
|
f.dialog.win.Resize(size)
|
|
}
|
|
|
|
// Hide hides the file dialog.
|
|
func (f *FileDialog) Hide() {
|
|
if f.dialog == nil {
|
|
return
|
|
}
|
|
f.dialog.win.Hide()
|
|
if f.onClosedCallback != nil {
|
|
f.onClosedCallback(false)
|
|
}
|
|
}
|
|
|
|
// SetConfirmText allows custom text to be set in the confirmation button
|
|
//
|
|
// Since: 2.2
|
|
func (f *FileDialog) SetConfirmText(label string) {
|
|
f.confirmText = label
|
|
if f.dialog == nil {
|
|
return
|
|
}
|
|
f.dialog.open.SetText(label)
|
|
f.dialog.win.Refresh()
|
|
}
|
|
|
|
// SetDismissText allows custom text to be set in the dismiss button
|
|
func (f *FileDialog) SetDismissText(label string) {
|
|
f.dismissText = label
|
|
if f.dialog == nil {
|
|
return
|
|
}
|
|
f.dialog.dismiss.SetText(label)
|
|
f.dialog.win.Refresh()
|
|
}
|
|
|
|
// SetTitleText allows custom text to be set in the dialog title
|
|
//
|
|
// Since: 2.6
|
|
func (f *FileDialog) SetTitleText(label string) {
|
|
f.titleText = label
|
|
if f.dialog == nil {
|
|
return
|
|
}
|
|
f.dialog.title.SetText(label)
|
|
}
|
|
|
|
// SetLocation tells this FileDialog which location to display.
|
|
// This is normally called before the dialog is shown.
|
|
//
|
|
// Since: 1.4
|
|
func (f *FileDialog) SetLocation(u fyne.ListableURI) {
|
|
f.startingLocation = u
|
|
if f.dialog != nil {
|
|
f.dialog.setLocation(u)
|
|
}
|
|
}
|
|
|
|
// SetOnClosed sets a callback function that is called when
|
|
// the dialog is closed.
|
|
func (f *FileDialog) SetOnClosed(closed func()) {
|
|
// If there is already a callback set, remember it and call both.
|
|
originalCallback := f.onClosedCallback
|
|
|
|
f.onClosedCallback = func(response bool) {
|
|
if f.dialog == nil {
|
|
return
|
|
}
|
|
if originalCallback != nil {
|
|
originalCallback(response)
|
|
}
|
|
closed()
|
|
}
|
|
}
|
|
|
|
// SetFilter sets a filter for limiting files that can be chosen in the file dialog.
|
|
func (f *FileDialog) SetFilter(filter storage.FileFilter) {
|
|
if f.isDirectory() {
|
|
fyne.LogError("Cannot set a filter for a folder dialog", nil)
|
|
return
|
|
}
|
|
f.filter = filter
|
|
if f.dialog != nil {
|
|
f.dialog.refreshDir(f.dialog.dir)
|
|
}
|
|
}
|
|
|
|
// SetFileName sets the filename in a FileDialog in save mode.
|
|
// This is normally called before the dialog is shown.
|
|
func (f *FileDialog) SetFileName(fileName string) {
|
|
if f.save {
|
|
f.initialFileName = fileName
|
|
//Update entry if fileDialog has already been created
|
|
if f.dialog != nil {
|
|
f.dialog.fileName.SetText(fileName)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetView changes the default display view of the FileDialog
|
|
// This is normally called before the dialog is shown.
|
|
//
|
|
// Since: 2.5
|
|
func (f *FileDialog) SetView(v ViewLayout) {
|
|
f.initialView = v
|
|
if f.dialog != nil {
|
|
f.dialog.setView(v)
|
|
}
|
|
}
|
|
|
|
// NewFileOpen creates a file dialog allowing the user to choose a file to open.
|
|
//
|
|
// The callback function will run when the dialog closes and provide a reader for the chosen file.
|
|
// The reader will be nil when the user cancels or when nothing is selected.
|
|
// When the reader isn't nil it must be closed by the callback.
|
|
//
|
|
// The dialog will appear over the window specified when Show() is called.
|
|
func NewFileOpen(callback func(reader fyne.URIReadCloser, err error), parent fyne.Window) *FileDialog {
|
|
dialog := &FileDialog{callback: callback, parent: parent}
|
|
return dialog
|
|
}
|
|
|
|
// NewFileSave creates a file dialog allowing the user to choose a file to save
|
|
// to (new or overwrite). If the user chooses an existing file they will be
|
|
// asked if they are sure.
|
|
//
|
|
// The callback function will run when the dialog closes and provide a writer for the chosen file.
|
|
// The writer will be nil when the user cancels or when nothing is selected.
|
|
// When the writer isn't nil it must be closed by the callback.
|
|
//
|
|
// The dialog will appear over the window specified when Show() is called.
|
|
func NewFileSave(callback func(writer fyne.URIWriteCloser, err error), parent fyne.Window) *FileDialog {
|
|
dialog := &FileDialog{callback: callback, parent: parent, save: true}
|
|
return dialog
|
|
}
|
|
|
|
// ShowFileOpen creates and shows a file dialog allowing the user to choose a
|
|
// file to open.
|
|
//
|
|
// The callback function will run when the dialog closes and provide a reader for the chosen file.
|
|
// The reader will be nil when the user cancels or when nothing is selected.
|
|
// When the reader isn't nil it must be closed by the callback.
|
|
//
|
|
// The dialog will appear over the window specified.
|
|
func ShowFileOpen(callback func(reader fyne.URIReadCloser, err error), parent fyne.Window) {
|
|
dialog := NewFileOpen(callback, parent)
|
|
if fileOpenOSOverride(dialog) {
|
|
return
|
|
}
|
|
dialog.Show()
|
|
}
|
|
|
|
// ShowFileSave creates and shows a file dialog allowing the user to choose a
|
|
// file to save to (new or overwrite). If the user chooses an existing file they
|
|
// will be asked if they are sure.
|
|
//
|
|
// The callback function will run when the dialog closes and provide a writer for the chosen file.
|
|
// The writer will be nil when the user cancels or when nothing is selected.
|
|
// When the writer isn't nil it must be closed by the callback.
|
|
//
|
|
// The dialog will appear over the window specified.
|
|
func ShowFileSave(callback func(writer fyne.URIWriteCloser, err error), parent fyne.Window) {
|
|
dialog := NewFileSave(callback, parent)
|
|
if fileSaveOSOverride(dialog) {
|
|
return
|
|
}
|
|
dialog.Show()
|
|
}
|
|
|
|
func getFavoriteIcons() map[string]fyne.Resource {
|
|
if runtime.GOOS == "darwin" {
|
|
return map[string]fyne.Resource{
|
|
"Documents": theme.DocumentIcon(),
|
|
"Desktop": theme.DesktopIcon(),
|
|
"Downloads": theme.DownloadIcon(),
|
|
"Music": theme.MediaMusicIcon(),
|
|
"Pictures": theme.MediaPhotoIcon(),
|
|
"Movies": theme.MediaVideoIcon(),
|
|
}
|
|
}
|
|
|
|
return map[string]fyne.Resource{
|
|
"Documents": theme.DocumentIcon(),
|
|
"Desktop": theme.DesktopIcon(),
|
|
"Downloads": theme.DownloadIcon(),
|
|
"Music": theme.MediaMusicIcon(),
|
|
"Pictures": theme.MediaPhotoIcon(),
|
|
"Videos": theme.MediaVideoIcon(),
|
|
}
|
|
}
|
|
|
|
func getFavoriteOrder() []string {
|
|
order := []string{
|
|
"Desktop",
|
|
"Documents",
|
|
"Downloads",
|
|
"Music",
|
|
"Pictures",
|
|
"Videos",
|
|
}
|
|
|
|
if runtime.GOOS == "darwin" {
|
|
order[5] = "Movies"
|
|
}
|
|
|
|
return order
|
|
}
|
|
|
|
func hasAppFiles(a fyne.App) bool {
|
|
if a.UniqueID() == "testApp" {
|
|
return false
|
|
}
|
|
|
|
return len(a.Storage().List()) > 0
|
|
}
|
|
|
|
func storageURI(a fyne.App) fyne.URI {
|
|
dir, _ := storage.Child(a.Storage().RootURI(), "Documents")
|
|
return dir
|
|
}
|
|
|
|
// iconPaddingLayout adds padding to the left of a widget.Icon().
|
|
// NOTE: It assumes that the slice only contains one item.
|
|
type iconPaddingLayout struct {
|
|
}
|
|
|
|
func (i *iconPaddingLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
|
padding := theme.Padding() * 2
|
|
objects[0].Move(fyne.NewPos(padding, 0))
|
|
objects[0].Resize(size.SubtractWidthHeight(padding, 0))
|
|
}
|
|
|
|
func (i *iconPaddingLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
|
|
return objects[0].MinSize().AddWidthHeight(theme.Padding()*2, 0)
|
|
}
|