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

Add support for opening folders. #1449

Merged
merged 5 commits into from Oct 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Expand Up @@ -8,7 +8,7 @@ More detailed release notes can be found on the [releases page](https://github.c
### Added (highlights)

* List (#156), Table (#157) and Tree collection Widgets
* Card, FileItem widgets
* Card, FileItem, Separator widgets
* ColorPicker dialog
* User selection of primary colour
* Container API package to ease using layouts and container widgets
Expand All @@ -20,6 +20,7 @@ More detailed release notes can be found on the [releases page](https://github.c
* Canvas.InteractiveArea() to indicate where widgets should avoid
* TextFormatter for ProgressBar
* FileDialog.SetLocation() (#821)
* Added dialog.ShowFolderOpen (#941)
* Support to install on iOS and android with 'fyne install'
* Support asset bundling with go:generate
* Add fyne release command for preparing signed apps
Expand Down
354 changes: 179 additions & 175 deletions cmd/fyne/internal/mobile/dex.go

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions cmd/fyne_demo/screens/window.go
Expand Up @@ -172,6 +172,18 @@ func loadDialogGroup(win fyne.Window) *widget.Card {
fileSaved(writer)
}, win)
}),
widget.NewButton("Folder Open", func() {
dialog.ShowFolderOpen(func(list fyne.ListableURI, err error) {
if err != nil {
dialog.ShowError(err, win)
return
}
if list == nil {
return
}
dialog.ShowInformation("Folder Open", list.String(), win)
}, win)
}),
widget.NewButton("Color Picker", func() {
picker := dialog.NewColorPicker("Pick a Color", "What is your favorite color?", func(c color.Color) {
colorPicked(c, win)
Expand Down
26 changes: 25 additions & 1 deletion dialog/file.go
Expand Up @@ -120,6 +120,13 @@ func (f *fileDialog) makeUI() fyne.CanvasObject {
f.file.onClosedCallback(true)
}
callback(storage.OpenFileFromURI(f.selected.location))
} 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)
}
})
f.open.Style = widget.PrimaryButton
Expand All @@ -136,6 +143,8 @@ func (f *fileDialog) makeUI() fyne.CanvasObject {
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)
}
Expand All @@ -157,7 +166,11 @@ func (f *fileDialog) makeUI() fyne.CanvasObject {
scrollBread := widget.NewHScrollContainer(f.breadcrumb)
body := fyne.NewContainerWithLayout(layout.NewBorderLayout(scrollBread, nil, nil, nil),
scrollBread, f.fileScroll)
header := widget.NewLabelWithStyle(label+" File", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
title := label + " File"
if f.file.isDirectory() {
title = label + " Folder"
}
header := widget.NewLabelWithStyle(title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})

favorites := f.loadFavorites()

Expand Down Expand Up @@ -215,6 +228,9 @@ func (f *fileDialog) refreshDir(dir fyne.ListableURI) {
if isHidden(file) {
continue
}
if f.file.isDirectory() && !isListable(file) {
continue
}

_, err := storage.ListerForURI(file)
if err == nil {
Expand Down Expand Up @@ -273,6 +289,10 @@ func (f *fileDialog) setLocation(dir fyne.ListableURI) error {
)
}

if f.file.isDirectory() {
f.fileName.SetText(dir.Name())
f.open.Enable()
}
f.refreshDir(dir)

return nil
Expand Down Expand Up @@ -479,6 +499,10 @@ func (f *FileDialog) SetOnClosed(closed func()) {

// 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)
Expand Down
6 changes: 5 additions & 1 deletion dialog/file_mobile.go
Expand Up @@ -23,7 +23,11 @@ func isHidden(file fyne.URI) bool {
}

func fileOpenOSOverride(f *FileDialog) bool {
gomobile.ShowFileOpenPicker(f.callback.(func(fyne.URIReadCloser, error)), f.filter)
if f.isDirectory() {
gomobile.ShowFolderOpenPicker(f.callback.(func(fyne.ListableURI, error)))
} else {
gomobile.ShowFileOpenPicker(f.callback.(func(fyne.URIReadCloser, error)), f.filter)
}
return true
}

Expand Down
29 changes: 11 additions & 18 deletions dialog/file_test.go
Expand Up @@ -4,7 +4,6 @@ import (
"log"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

Expand Down Expand Up @@ -164,10 +163,18 @@ func TestShowFileOpen(t *testing.T) {
var chosen fyne.URIReadCloser
var openErr error
win := test.NewWindow(widget.NewLabel("Content"))
ShowFileOpen(func(file fyne.URIReadCloser, err error) {
d := NewFileOpen(func(file fyne.URIReadCloser, err error) {
chosen = file
openErr = err
}, win)
testDataPath, _ := filepath.Abs("testdata")
testData := storage.NewFileURI(testDataPath)
dir, err := storage.ListerForURI(testData)
if err != nil {
t.Error("Failed to open testdata dir", err)
}
d.SetLocation(dir)
d.Show()

popup := win.Canvas().Overlays().Top().(*widget.PopUp)
defer win.Canvas().Overlays().Remove(popup)
Expand All @@ -184,21 +191,14 @@ func TestShowFileOpen(t *testing.T) {
breadcrumb := ui.Objects[3].(*fyne.Container).Objects[0].(*widget.ScrollContainer).Content.(*widget.Box)
assert.Greater(t, len(breadcrumb.Children), 0)

home, err := os.UserHomeDir()
if runtime.GOOS == "windows" {
// on windows os.Gethome() returns '\'
home = strings.ReplaceAll(home, "\\", "/")
}
t.Logf("home='%s'", home)
assert.Nil(t, err)
components := strings.Split(home, "/")
components := strings.Split(testData.String()[7:], "/")
if components[0] == "" {
// Splitting a unix path will give a "" at the beginning, but we actually want the path bar to show "/".
components[0] = "/"
}
if assert.Equal(t, len(components), len(breadcrumb.Children)) {
for i := range components {
t.Logf("i=%d components[i]='%s' breadcrumb...Text[i]='%s'", i, components[i], breadcrumb.Children[i].(*widget.Button).Text)
assert.Equal(t, components[i], breadcrumb.Children[i].(*widget.Button).Text)
}
}
Expand All @@ -216,14 +216,7 @@ func TestShowFileOpen(t *testing.T) {
target = icon.(*fileDialogItem)
}
}

if target == nil {
log.Println("Could not find a file in the default directory to tap :(")
return
}

// This will only execute if we have a file in the home path.
// Until we have a way to set the directory of an open file dialog.
assert.NotNil(t, target, "Failed to find file in testdata")
test.Tap(target)
assert.Equal(t, target.location.Name(), nameLabel.Text)
assert.False(t, open.Disabled())
Expand Down
41 changes: 41 additions & 0 deletions dialog/folder.go
@@ -0,0 +1,41 @@
package dialog

import (
"fyne.io/fyne"
"fyne.io/fyne/storage"
)

var folderFilter = storage.NewMimeTypeFileFilter([]string{"application/x-directory"})

// NewFolderOpen creates a file dialog allowing the user to choose a folder to open.
// The dialog will appear over the window specified when Show() is called.
func NewFolderOpen(callback func(fyne.ListableURI, error), parent fyne.Window) *FileDialog {
dialog := &FileDialog{}
dialog.callback = callback
dialog.parent = parent
dialog.filter = folderFilter
return dialog
}

// ShowFolderOpen creates and shows a file dialog allowing the user to choose a folder to open.
// The dialog will appear over the window specified.
func ShowFolderOpen(callback func(fyne.ListableURI, error), parent fyne.Window) {
dialog := NewFolderOpen(callback, parent)
if fileOpenOSOverride(dialog) {
return
}
dialog.Show()
}

func (f *FileDialog) isDirectory() bool {
return f.filter == folderFilter
}

func isListable(u fyne.URI) bool {
if _, ok := u.(fyne.ListableURI); ok {
return true
}

_, err := storage.ListerForURI(u)
return err == nil
}
69 changes: 69 additions & 0 deletions dialog/folder_test.go
@@ -0,0 +1,69 @@
package dialog

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"

"fyne.io/fyne"
"fyne.io/fyne/storage"
"fyne.io/fyne/test"
"fyne.io/fyne/widget"
)

func TestShowFolderOpen(t *testing.T) {
var chosen fyne.ListableURI
var openErr error
win := test.NewWindow(widget.NewLabel("OpenDir"))
d := NewFolderOpen(func(file fyne.ListableURI, err error) {
chosen = file
openErr = err
}, win)
testData, _ := filepath.Abs("testdata")
dir, err := storage.ListerForURI(storage.NewFileURI(testData))
if err != nil {
t.Error("Failed to open testdata dir", err)
}
d.SetLocation(dir)
d.Show()

popup := win.Canvas().Overlays().Top().(*widget.PopUp)
defer win.Canvas().Overlays().Remove(popup)
assert.NotNil(t, popup)

ui := popup.Content.(*fyne.Container)
title := ui.Objects[1].(*widget.Label)
assert.Equal(t, "Open Folder", title.Text)

nameLabel := ui.Objects[2].(*fyne.Container).Objects[1].(*widget.ScrollContainer).Content.(*widget.Label)
buttons := ui.Objects[2].(*fyne.Container).Objects[0].(*widget.Box)
open := buttons.Children[1].(*widget.Button)

files := ui.Objects[3].(*fyne.Container).Objects[1].(*widget.ScrollContainer).Content.(*fyne.Container)
assert.Greater(t, len(files.Objects), 0)

fileName := files.Objects[0].(*fileDialogItem).name
assert.Equal(t, "(Parent)", fileName)
assert.False(t, open.Disabled())

var target *fileDialogItem
for _, icon := range files.Objects {
if icon.(*fileDialogItem).dir {
target = icon.(*fileDialogItem)
} else {
t.Error("Folder dialog should not list files")
}
}

assert.NotNil(t, target, "Failed to find folder in testdata")
test.Tap(target)
assert.Equal(t, target.location.Name(), nameLabel.Text)
assert.False(t, open.Disabled())

test.Tap(open)
assert.Nil(t, win.Canvas().Overlays().Top())
assert.Nil(t, openErr)

assert.Equal(t, target.location.String(), chosen.String())
}
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -7,7 +7,7 @@ require (
github.com/akavel/rsrc v0.8.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.4.9
github.com/fyne-io/mobile v0.0.3-0.20201019162131-a1e87190904e
github.com/fyne-io/mobile v0.0.3-0.20201023100309-6e4995148130
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3
github.com/godbus/dbus/v5 v5.0.3
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Expand Up @@ -8,8 +8,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fyne-io/mobile v0.0.3-0.20201019162131-a1e87190904e h1:8uXNy4NvCpLlfg1R01TJk0+n5trhDc0vofqbA/S1I48=
github.com/fyne-io/mobile v0.0.3-0.20201019162131-a1e87190904e/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/fyne-io/mobile v0.0.3-0.20201023100309-6e4995148130 h1:l+E5KcwiFEOR/0y8I+fIlUOpvSskhyNQ/8e2geqRJ6Y=
github.com/fyne-io/mobile v0.0.3-0.20201023100309-6e4995148130/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3 h1:q521PfSp5/z6/sD9FZZOWj4d1MLmfQW8PkRnI9M6PCE=
Expand Down
12 changes: 12 additions & 0 deletions internal/driver/gomobile/file.go
Expand Up @@ -74,3 +74,15 @@ func ShowFileOpenPicker(callback func(fyne.URIReadCloser, error), filter storage
}, mobileFilter(filter))
}
}

// ShowFolderOpenPicker loads the native folder open dialog and calls back the chosen directory path as a ListableURI.
func ShowFolderOpenPicker(callback func(fyne.ListableURI, error)) {
filter := storage.NewMimeTypeFileFilter([]string{"application/x-directory"})
drv := fyne.CurrentApp().Driver().(*mobileDriver)
if a, ok := drv.app.(hasPicker); ok {
a.ShowFileOpenPicker(func(uri string, _ func()) {
f, err := drv.ListerForURI(storage.NewURI(uri))
callback(f, err)
}, mobileFilter(filter))
}
}
8 changes: 6 additions & 2 deletions vendor/github.com/fyne-io/mobile/app/GoNativeActivity.java

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 9 additions & 4 deletions vendor/github.com/fyne-io/mobile/app/darwin_ios.m

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vendor/modules.txt
Expand Up @@ -8,7 +8,7 @@ github.com/akavel/rsrc/ico
github.com/davecgh/go-spew/spew
# github.com/fsnotify/fsnotify v1.4.9
github.com/fsnotify/fsnotify
# github.com/fyne-io/mobile v0.0.3-0.20201019162131-a1e87190904e
# github.com/fyne-io/mobile v0.0.3-0.20201023100309-6e4995148130
github.com/fyne-io/mobile/app
github.com/fyne-io/mobile/app/internal/callfn
github.com/fyne-io/mobile/event/key
Expand Down