Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FileDialog: Added list view #2251

Merged
merged 13 commits into from
Jun 15, 2021
3 changes: 3 additions & 0 deletions cmd/fyne_demo/tutorials/icons.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,8 @@ func loadIcons() []iconInfo {
{"AccountIcon", theme.AccountIcon()},
{"LoginIcon", theme.LoginIcon()},
{"LogoutIcon", theme.LogoutIcon()},

{"ListIcon", theme.ListIcon()},
{"GridIcon", theme.GridIcon()},
}
}
182 changes: 133 additions & 49 deletions dialog/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,44 @@ import (

"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/storage/repository"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)

type viewLayout int

const (
gridView viewLayout = iota
listView
)

type textWidget interface {
fyne.Widget
SetText(string)
}

type favoriteItem struct {
locName string
locIcon fyne.Resource
loc fyne.URI
}

type fileDialog struct {
file *FileDialog
fileName textWidget
dismiss *widget.Button
open *widget.Button
breadcrumb *fyne.Container
files *fyne.Container
fileScroll *container.Scroll
showHidden bool
file *FileDialog
fileName textWidget
dismiss *widget.Button
open *widget.Button
breadcrumb *fyne.Container
breadcrumbScroll *container.Scroll
files *fyne.Container
filesScroll *container.Scroll
favorites []favoriteItem
favoritesList *widget.List
showHidden bool

view viewLayout

win *widget.PopUp
selected *fileDialogItem
Expand Down Expand Up @@ -164,75 +181,113 @@ func (f *fileDialog) makeUI() fyne.CanvasObject {
})
buttons := container.NewHBox(f.dismiss, f.open)

footer := fyne.NewContainerWithLayout(layout.NewBorderLayout(nil, nil, nil, buttons),
buttons, container.NewHScroll(f.fileName))

f.files = fyne.NewContainerWithLayout(layout.NewGridWrapLayout(fyne.NewSize(fileIconCellWidth,
fileIconSize+theme.Padding()+fileTextSize)),
)
f.fileScroll = container.NewScroll(f.files)
f.filesScroll = container.NewScroll(nil) // filesScroll's content will be set by setView function.
verticalExtra := float32(float64(fileIconSize) * 0.25)
f.fileScroll.SetMinSize(fyne.NewSize(fileIconCellWidth*2+theme.Padding(),
f.filesScroll.SetMinSize(fyne.NewSize(fileIconCellWidth*2+theme.Padding(),
(fileIconSize+fileTextSize)+theme.Padding()*2+verticalExtra))

f.breadcrumb = container.NewHBox()
scrollBread := container.NewHScroll(f.breadcrumb)
body := fyne.NewContainerWithLayout(layout.NewBorderLayout(scrollBread, nil, nil, nil),
scrollBread, f.fileScroll)
f.breadcrumbScroll = container.NewHScroll(container.NewPadded(f.breadcrumb))
title := label + " File"
if f.file.isDirectory() {
title = label + " Folder"
}
header := widget.NewLabelWithStyle(title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})

favorites := f.loadFavorites()
f.setView(gridView)
f.loadFavorites()

f.favoritesList = widget.NewList(
func() int {
return len(f.favorites)
},
func() fyne.CanvasObject {
return container.NewHBox(widget.NewIcon(theme.DocumentIcon()), widget.NewLabel("Template Object"))
},
func(id widget.ListItemID, item fyne.CanvasObject) {
item.(*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)
}

favoritesGroup := container.NewVScroll(widget.NewCard("Favorites", "",
container.NewVBox(favorites...)))
var optionsButton *widget.Button
optionsButton = widget.NewButtonWithIcon("Options", theme.SettingsIcon(), func() {
optionsButton = widget.NewButtonWithIcon("", theme.SettingsIcon(), func() {
f.optionsMenu(fyne.CurrentApp().Driver().AbsolutePositionForObject(optionsButton), optionsButton.Size())
})

left := container.NewBorder(nil, optionsButton, nil, nil, favoritesGroup)
var toggleViewButton *widget.Button
toggleViewButton = widget.NewButtonWithIcon("", theme.ListIcon(), func() {
if f.view == gridView {
f.setView(listView)
toggleViewButton.SetIcon(theme.GridIcon())
} else {
f.setView(gridView)
toggleViewButton.SetIcon(theme.ListIcon())
}
})

optionsbuttons := container.NewHBox(
toggleViewButton,
optionsButton,
)

header := container.NewBorder(nil, nil, nil, optionsbuttons,
optionsbuttons, widget.NewLabelWithStyle(title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
)

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 it's minimal width

return container.NewBorder(header, footer, left, nil, body)
return container.NewBorder(header, footer, nil, nil, body)
}

func (f *fileDialog) optionsMenu(position fyne.Position, buttonSize fyne.Size) {
hiddenFiles := widget.NewCheck("Show Hidden Files", func(changed bool) {
f.showHidden = changed
f.refreshDir(f.dir)
})
hiddenFiles.SetChecked(f.showHidden)
hiddenFiles.Checked = f.showHidden
hiddenFiles.Refresh()
content := container.NewVBox(hiddenFiles)

p := position.Add(buttonSize)
pos := fyne.NewPos(p.X, p.Y-content.MinSize().Height-theme.Padding()*2)
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() []fyne.CanvasObject {
func (f *fileDialog) loadFavorites() {
favoriteLocations, err := getFavoriteLocations()
if err != nil {
fyne.LogError("Getting favorite locations", err)
}
favoriteIcons := getFavoriteIcons()
favoriteOrder := getFavoriteOrder()

var places []fyne.CanvasObject
f.favorites = []favoriteItem{}
for _, locName := range favoriteOrder {
loc, ok := favoriteLocations[locName]
if !ok {
continue
}
icon := favoriteIcons[locName]
places = append(places, makeFavoriteButton(locName, icon, func() {
f.setLocation(loc)
}))
}
places = append(places, f.loadPlaces()...)
return places
locIcon := favoriteIcons[locName]
f.favorites = append(f.favorites, favoriteItem{
locName,
locIcon,
loc,
})
}
f.favorites = append(f.favorites, f.getPlaces()...)
}

func (f *fileDialog) refreshDir(dir fyne.ListableURI) {
Expand Down Expand Up @@ -273,8 +328,8 @@ func (f *fileDialog) refreshDir(dir fyne.ListableURI) {

f.files.Objects = icons
f.files.Refresh()
f.fileScroll.Offset = fyne.NewPos(0, 0)
f.fileScroll.Refresh()
f.filesScroll.Offset = fyne.NewPos(0, 0)
f.filesScroll.Refresh()
}

func (f *fileDialog) setLocation(dir fyne.URI) error {
Expand All @@ -286,6 +341,21 @@ func (f *fileDialog) setLocation(dir fyne.URI) error {
return err
}

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)
f.dir = list

Expand Down Expand Up @@ -327,6 +397,10 @@ func (f *fileDialog) setLocation(dir fyne.URI) error {
)
}

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()
Expand Down Expand Up @@ -365,6 +439,23 @@ func (f *fileDialog) setSelected(file *fileDialogItem) {
}
}

func (f *fileDialog) setView(view viewLayout) {
f.view = view
if f.view == gridView {
padding := fyne.NewSize(fileIconCellWidth-fileIconSize, theme.Padding())
f.files = container.NewGridWrap(
fyne.NewSize(fileIconSize, fileIconSize+fileTextSize).Add(padding),
)
} else {
f.files = container.NewVBox()
}
if f.dir != nil {
f.refreshDir(f.dir)
}
f.filesScroll.Content = container.NewPadded(f.files)
f.filesScroll.Refresh()
}

// 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.
Expand Down Expand Up @@ -433,8 +524,8 @@ func showFile(file *FileDialog) *fileDialog {

d.setLocation(file.effectiveStartingDir())

size := ui.MinSize().Add(fyne.NewSize(fileIconCellWidth*2+theme.Padding()*4,
(fileIconSize+fileTextSize)+theme.Padding()*4))
size := ui.MinSize().Add(fyne.NewSize(fileIconCellWidth*2+theme.Padding()*6+theme.Padding(),
(fileIconSize+fileTextSize)+theme.Padding()*6))

d.win = widget.NewModalPopUp(ui, file.parent.Canvas())
d.win.Resize(size)
Expand Down Expand Up @@ -594,13 +685,6 @@ func ShowFileSave(callback func(fyne.URIWriteCloser, error), parent fyne.Window)
dialog.Show()
}

func makeFavoriteButton(title string, icon fyne.Resource, f func()) *widget.Button {
b := widget.NewButtonWithIcon(title, icon, f)

b.Alignment = widget.ButtonAlignLeading
return b
}

func getFavoriteIcons() map[string]fyne.Resource {
return map[string]fyne.Resource{
"Home": theme.HomeIcon(),
Expand Down
4 changes: 2 additions & 2 deletions dialog/file_mobile.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"fyne.io/fyne/v2/storage"
)

func (f *fileDialog) loadPlaces() []fyne.CanvasObject {
return nil
func (f *fileDialog) getPlaces() []favoriteItem {
return []favoriteItem{}
}

func isHidden(file fyne.URI) bool {
Expand Down
5 changes: 2 additions & 3 deletions dialog/file_other_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"testing"

"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/widget"
"github.com/stretchr/testify/assert"
)

Expand All @@ -17,8 +16,8 @@ func TestIsHidden(t *testing.T) {

func TestFileDialog_LoadPlaces(t *testing.T) {
f := &fileDialog{}
places := f.loadPlaces()
places := f.getPlaces()

assert.Equal(t, 1, len(places))
assert.Equal(t, "Computer", places[0].(*widget.Button).Text)
assert.Equal(t, "Computer", places[0].locName)
}
Loading