Major improvements to UnifiedPlayer: 1. GetFrameImage() now works when paused for responsive UI updates 2. Play() method properly starts FFmpeg process 3. Frame display loop runs continuously for smooth video display 4. Disabled audio temporarily to fix video playback fundamentals 5. Simplified FFmpeg command to focus on video stream only Player now: - Generates video frames correctly - Shows video when paused - Has responsive progress tracking - Starts playback properly Next steps: Re-enable audio playback once video is stable
992 lines
25 KiB
Go
992 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, 0o750)
|
|
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,
|
|
f.title,
|
|
)
|
|
|
|
footer := container.NewBorder(nil, nil, nil, buttons,
|
|
container.NewHScroll(f.fileName),
|
|
)
|
|
|
|
body := container.NewHSplit(
|
|
f.favoritesList,
|
|
container.NewBorder(f.breadcrumbScroll, nil, nil, nil,
|
|
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)
|
|
if !exists {
|
|
f.win.Hide()
|
|
if f.file.onClosedCallback != nil {
|
|
f.file.onClosedCallback(true)
|
|
}
|
|
callback(storage.Writer(location))
|
|
return
|
|
}
|
|
|
|
listable, err := storage.CanList(location)
|
|
if err == nil && listable {
|
|
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 getFavoriteLocations() (map[string]fyne.ListableURI, error) {
|
|
if runtime.GOOS == "js" {
|
|
return make(map[string]fyne.ListableURI), nil
|
|
}
|
|
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
homeURI := storage.NewFileURI(homeDir)
|
|
home, _ := storage.ListerForURI(homeURI)
|
|
|
|
favoriteLocations := map[string]fyne.ListableURI{"Home": home}
|
|
for _, favName := range getFavoritesOrder() {
|
|
uri, err1 := getFavoriteLocation(homeURI, favName)
|
|
if err != nil {
|
|
err = err1
|
|
continue
|
|
}
|
|
|
|
listURI, err1 := storage.ListerForURI(uri)
|
|
if err1 != nil {
|
|
err = err1
|
|
continue
|
|
}
|
|
favoriteLocations[favName] = listURI
|
|
}
|
|
|
|
return favoriteLocations, err
|
|
}
|
|
|
|
func (f *fileDialog) loadFavorites() {
|
|
favoriteLocations, err := getFavoriteLocations()
|
|
if err != nil {
|
|
fyne.LogError("Getting favorite locations", err)
|
|
}
|
|
|
|
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 getFavoritesOrder() {
|
|
loc, ok := favoriteLocations[locName]
|
|
if !ok {
|
|
continue
|
|
}
|
|
locIcon := getFavoritesIcon(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 dir == nil {
|
|
return errors.New("failed to open nil directory")
|
|
}
|
|
|
|
if f.selectedID > -1 {
|
|
f.files.Unselect(f.selectedID)
|
|
}
|
|
|
|
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 storage.EqualURI(fav.loc, dir) {
|
|
f.favoritesList.Select(i)
|
|
isFav = true
|
|
break
|
|
}
|
|
}
|
|
if !isFav {
|
|
f.favoritesList.UnselectAll()
|
|
}
|
|
|
|
f.setSelected(nil, -1)
|
|
f.dir = list
|
|
|
|
f.breadcrumb.Objects = nil
|
|
for parent := dir; parent != nil && err == nil; parent, err = storage.Parent(parent) {
|
|
currentParent := parent
|
|
f.breadcrumb.Add(
|
|
widget.NewButton(currentParent.Name(), func() {
|
|
err := f.setLocation(currentParent)
|
|
if err != nil {
|
|
fyne.LogError("Failed to set directory", err)
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
|
|
// Use slices.Reverse with Go 1.21:
|
|
objects := f.breadcrumb.Objects
|
|
for i, j := 0, len(objects)-1; i < j; i, j = i+1, j-1 {
|
|
objects[i], objects[j] = objects[j], objects[i]
|
|
}
|
|
|
|
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.Path() == "" {
|
|
// 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 getFavoritesIcon(location string) fyne.Resource {
|
|
switch location {
|
|
case "Documents":
|
|
return theme.DocumentIcon()
|
|
case "Desktop":
|
|
return theme.DesktopIcon()
|
|
case "Downloads":
|
|
return theme.DownloadIcon()
|
|
case "Music":
|
|
return theme.MediaMusicIcon()
|
|
case "Pictures":
|
|
return theme.MediaPhotoIcon()
|
|
case "Videos":
|
|
return theme.MediaVideoIcon()
|
|
}
|
|
|
|
if (runtime.GOOS == "darwin" && location == "Movies") ||
|
|
(runtime.GOOS != "darwin" && location == "Videos") {
|
|
return theme.MediaVideoIcon()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getFavoritesOrder() [6]string {
|
|
order := [6]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)
|
|
}
|