Skip to content

Commit

Permalink
Optimize file tree events handling;
Browse files Browse the repository at this point in the history
Move files/folders to trash bin instead of delete them directly.
  • Loading branch information
oligo committed Oct 23, 2024
1 parent be143d1 commit afe3a6e
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 29 deletions.
5 changes: 3 additions & 2 deletions explorer/menu_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

// Default operation for file tree nodes.
// Support file/folder copy, cut, paste, rename, delete and new file/folder creation.
// This should better be used as an example. Feel free to copy and build your own.
func DefaultFileMenuOptions(vm view.ViewManager) MenuOptionFunc {
return func(gtx C, item *EntryNavItem) [][]menu.MenuOption {

Expand Down Expand Up @@ -79,7 +80,7 @@ func DefaultFileMenuOptions(vm view.ViewManager) MenuOptionFunc {
// create new file in current folder
{
OnClicked: func() error {
return item.CreateChild(gtx, FileNode)
return item.CreateChild(gtx, FileNode, nil)
},

Layout: func(gtx C, th *theme.Theme) D {
Expand All @@ -90,7 +91,7 @@ func DefaultFileMenuOptions(vm view.ViewManager) MenuOptionFunc {
// create subfolder
{
OnClicked: func() error {
return item.CreateChild(gtx, FolderNode)
return item.CreateChild(gtx, FolderNode, nil)
},

Layout: func(gtx C, th *theme.Theme) D {
Expand Down
106 changes: 106 additions & 0 deletions explorer/trash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//go:build !windows
// +build !windows

package explorer

import (
"errors"
"os"
"path/filepath"
"runtime"
)

func throwToTrash(path string) error {
trashDir, err := getTrashFolder()
if err != nil {
return err
}

absPath, err := filepath.Abs(path)
if err != nil {
return err
}

_, err = os.Stat(absPath)
if err != nil {
return err
}

trashPath := filepath.Join(trashDir, filepath.Base(path))
err = os.Rename(absPath, trashPath)
if err != nil {
return err
}

return nil
}

func getTrashFolder() (string, error) {
switch runtime.GOOS {
case "darwin", "ios":
return appleTrashDir()
default:
return unixTrashDir()
}
}

// According to Freedesktop.org specifications, the "home trash" directory
// is at $XDG_DATA_HOME/Trash, and $XDG_DATA_HOME in turn defaults to $HOME/.local/share.
// Refs: https://specifications.freedesktop.org/basedir-spec/latest/
func unixTrashDir() (string, error) {
if xdgHome := os.Getenv("XDG_DATA_HOME"); xdgHome != "" {
trashDir := filepath.Join(xdgHome, "Trash")
err := checkDirExists(trashDir)
if err != nil {
return "", err
}
return trashDir, nil
}

home, err := os.UserHomeDir()
if err != nil {
return "", err
}

trashDir := filepath.Join(home, ".local/share/Trash")
err = checkDirExists(trashDir)
if err != nil {
return "", err
}

return trashDir, nil
}

func appleTrashDir() (string, error) {
dir, err := os.UserHomeDir()
if err != nil {
return "", err
}

trashDir := filepath.Join(dir, ".Trash")
err = checkDirExists(trashDir)
if err != nil {
return "", err
}

return trashDir, nil
}

func checkDirExists(dir string) error {
stat, err := os.Stat(dir)
if errors.Is(err, os.ErrNotExist) {
err := os.MkdirAll(dir, os.ModeDir)
if err != nil {
return err
}
} else {
return err
}

if !stat.IsDir() {
return errors.New("Trash dir is not a folder")
}

return nil

}
90 changes: 90 additions & 0 deletions explorer/trash_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//go:build windows
// +build windows

package explorer

import (
"fmt"
"path/filepath"
"syscall"
"unsafe"

"golang.org/x/sys/windows"
)

// The code in this file are mostly credited to the authors of
// https://github.com/Kei-K23/trashbox/blob/main/trashbox_windows.go.

const (
FO_DELETE = 3 // File operation: delete
FOF_ALLOWUNDO = 0x40 // Allow to move to Recycle Bin
FOF_NOCONFIRMATION = 0x10 // No confirmation dialog
)

// SHFILEOPSTRUCT represents the structure used in SHFileOperationW.
type SHFILEOPSTRUCT struct {
WFunc uint32
PFrom *uint16
PTo *uint16
FFlags uint16
AnyOps bool
HNameMap uintptr
LpszProgressTitle *uint16
}

var (
shell32 = syscall.NewLazyDLL("shell32.dll")
procSHFileOperationW = shell32.NewProc("SHFileOperationW")
)

func shFileOperation(op *SHFILEOPSTRUCT) error {
ret, _, _ := procSHFileOperationW.Call(uintptr(unsafe.Pointer(op)))
if ret != 0 {
return fmt.Errorf("failed to move file to Recycle Bin, error code: %d", ret)
}
return nil
}

// throwToTrash moves the specified file or directory to the Windows Recycle Bin.
//
// This function takes the path of a file or directory as an argument,
// converts it to an absolute path, and then moves it to the Windows
// Recycle Bin using the Shell API. If the provided path does not
// exist or cannot be accessed, an error will be returned.
//
// The function uses the SHFileOperationW function from the Windows
// Shell API to perform the move operation. It sets the appropriate
// flags to allow undo and suppress confirmation dialogs. If the
// operation is successful, the file or directory will no longer exist
// at the original path and will be relocated to the Recycle Bin for
// potential recovery.
//
// Parameters:
// - path: The path of the file or directory to be moved to the
// Recycle Bin.
//
// Returns:
// - error: Returns nil on success. If an error occurs during the
// process (e.g., if the file does not exist or the move fails),
// an error will be returned explaining the reason for failure,
// including any relevant error codes from the Windows API.
func throwToTrash(path string) error {
// Get the absolute file path of delete file
absPath, err := filepath.Abs(path)
if err != nil {
return err
}

wPathPtr, err := windows.UTF16PtrFromString(absPath + "\x00")
if err != nil {
return err
}

op := &SHFILEOPSTRUCT{
WFunc: FO_DELETE,
PFrom: wPathPtr,
FFlags: FOF_ALLOWUNDO | FOF_NOCONFIRMATION,
}

return shFileOperation(op)
}
15 changes: 3 additions & 12 deletions explorer/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,22 +223,13 @@ func (n *EntryNode) UpdateName(newName string) error {
return os.Rename(n.Path, newPath)
}

// Delete removes the current file/folders. If onlyEmptyDir is set,
// Delete stops removing non-empty dir if n is a folder node and returns an error.
// TODO: only empty dir is allowed to be removed for now. May add support for
// removing to recyle bin.
func (n *EntryNode) Delete(onlyEmptyDir bool) error {
// Delete removes the current file/folders to the system Trash bin.
func (n *EntryNode) Delete() error {
if n.Parent == nil {
return errors.New("cannot update name of root dir")
}

// if onlyEmptyDir {
// return os.Remove(n.Path)
// } else {
// return os.RemoveAll(n.Path)
// }

err := os.Remove(n.Path)
err := throwToTrash(n.Path)
if err != nil {
return err
}
Expand Down
34 changes: 21 additions & 13 deletions explorer/tree_style.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,13 @@ type EntryNavItem struct {

parent navi.NavItem
children []navi.NavItem
label *gv.Editable
expaned bool
needSync bool

label *gv.Editable
expaned bool
needSync bool
}

type MenuOptionFunc func(gtx C, item *EntryNavItem) [][]menu.MenuOption
type OnSelectFunc func(gtx C, item *EntryNode) view.Intent
type OnSelectFunc func(item *EntryNode) view.Intent

// Construct a FileTreeNav object that loads files and folders from rootDir. The skipFolders
// parameter allows you to specify folder name prefixes to exclude from the navigation.
Expand Down Expand Up @@ -117,7 +116,7 @@ func (eitem *EntryNavItem) OnSelect(gtx C) view.Intent {
}

if eitem.state.Kind() == FileNode && eitem.onSelectFunc != nil {
return eitem.onSelectFunc(gtx, eitem.state)
return eitem.onSelectFunc(eitem.state)
}

return view.Intent{}
Expand All @@ -127,7 +126,7 @@ func (eitem *EntryNavItem) OnSelect(gtx C) view.Intent {
func (eitem *EntryNavItem) Layout(gtx layout.Context, th *theme.Theme, textColor color.NRGBA) D {

if eitem.label == nil {
eitem.label = gv.EditableLabel(th.TextSize, eitem.state.Name(), func(text string) {
eitem.label = gv.EditableLabel(eitem.state.Name(), func(text string) {
err := eitem.state.UpdateName(text)
if err != nil {
log.Println("err: ", err)
Expand All @@ -136,6 +135,7 @@ func (eitem *EntryNavItem) Layout(gtx layout.Context, th *theme.Theme, textColor
}

eitem.label.Color = textColor
eitem.label.TextSize = th.TextSize
return eitem.label.Layout(gtx, th)
}

Expand Down Expand Up @@ -204,7 +204,7 @@ func (eitem *EntryNavItem) StartEditing(gtx C) {

// Create file or subfolder under the current folder.
// File or subfolder is inserted at the beginning of the children.
func (eitem *EntryNavItem) CreateChild(gtx C, kind NodeKind) error {
func (eitem *EntryNavItem) CreateChild(gtx C, kind NodeKind, postAction func(node *EntryNode)) error {
if eitem.state.Kind() == FileNode {
return nil
}
Expand All @@ -217,22 +217,30 @@ func (eitem *EntryNavItem) CreateChild(gtx C, kind NodeKind) error {
}

if err != nil {
// TODO: use modal to show the error if user provided one.
log.Println(err)
return err
}

//eitem.StartEditing(gtx)
childNode := eitem.state.Children()[0]

child := &EntryNavItem{
parent: eitem,
state: eitem.state.Children()[0],
state: childNode,
menuOptionFunc: eitem.menuOptionFunc,
onSelectFunc: eitem.onSelectFunc,
expaned: false,
needSync: false,
}

child.label = gv.EditableLabel(childNode.Name(), func(text string) {
err := childNode.UpdateName(text)
if err != nil {
log.Println("update name err: ", err)
}
if postAction != nil {
postAction(childNode)
}
})

eitem.children = slices.Insert[[]navi.NavItem, navi.NavItem](eitem.children, 0, child)
// focus the child input
child.StartEditing(gtx)
Expand All @@ -245,7 +253,7 @@ func (eitem *EntryNavItem) Remove() error {
return errors.New("cannot remove root dir/file")
}

err := eitem.state.Delete(true)
err := eitem.state.Delete()
if err != nil {
return err
}
Expand Down
3 changes: 1 addition & 2 deletions widget/editable.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ type Editable struct {
editing bool
}

func EditableLabel(textSize unit.Sp, text string, onChanged func(text string)) *Editable {
func EditableLabel(text string, onChanged func(text string)) *Editable {
return &Editable{
Text: text,
TextSize: textSize,
OnChanged: onChanged,
}
}
Expand Down

0 comments on commit afe3a6e

Please sign in to comment.