Files

907 lines
21 KiB
Go

package assets
import (
"aego/editor/core/code/highlight"
favs "aego/editor/core/favorites"
"aego/editor/core/refs"
"os/exec"
"path"
"runtime"
"strconv"
"strings"
"aego/asset"
"aego/asset/pipeline"
"aego/core/ids"
"aego/core/mathx"
"aego/editor/core/assetfiles"
"aego/editor/core/assetops"
"aego/editor/core/command"
"aego/editor/core/context"
"aego/editor/core/preview"
"aego/editor/core/services"
"aego/editor/core/workspace"
"aego/platform/input"
"aego/project"
"aego/schema"
"aego/ui/core"
"aego/ui/dnd"
"aego/ui/widgets"
)
const (
CommandRename = "assets.rename"
CommandDelete = "assets.delete"
CommandDuplicate = "assets.duplicate"
CommandUndo = "assets.undo"
CommandRedo = "assets.redo"
CommandCopy = "assets.copy"
CommandCut = "assets.cut"
CommandPaste = "assets.paste"
CommandFindReferences = "assets.find_references"
)
type dirRow struct {
path string
depth int
has bool
}
type Deps struct {
Files *assetfiles.Files
Pipeline *pipeline.Pipeline
Thumbs *preview.Service
Docs services.Documents
Selection services.Selection
Scenes services.SceneOps
Prefabs services.Prefabs
Commands *command.Registry
Notices services.Notices
Nav services.Navigation
Project *project.Project
Schema *schema.Registry
Inspect func(id ids.AssetID)
OpenTable func(id ids.AssetID)
DataType func(id ids.AssetID) (string, bool)
Favorites *favs.Store
Refs *refs.Service
ShowReferences func(id ids.AssetID, deleting []ids.AssetID)
BatchRename func(list []ids.AssetID)
Compare func(rel string)
}
type Panel struct {
Instantiate func(id ids.AssetID, name string)
d Deps
f *services.Frame
now float64
folder string
expanded map[string]bool
dirs []string
rows []dirRow
items []pipeline.AssetInfo
view []int
subdirs []string
rev uint64
dirsAt float64
sel map[ids.AssetID]bool
active ids.AssetID
anchor ids.AssetID
pending ids.AssetID
selDir string
search string
split float32
treeScroll float32
gridScroll float32
overDir bool
renaming ids.AssetID
renameStart bool
renameText string
renamingDir string
renameDirStart bool
renameDirText string
menuDir string
assetMenuID core.ID
dirMenuID core.ID
cut cutSet
scrollTo ids.AssetID
tile float32
stacks map[string][]int
stackRev uint64
spans []highlight.Span
}
func New(d Deps) *Panel {
return &Panel{
d: d,
folder: "assets",
expanded: map[string]bool{"assets": true, "scenes": true, "scripts": true, "prefabs": true},
sel: map[ids.AssetID]bool{},
split: 0.28,
rev: ^uint64(0),
}
}
func (p *Panel) report(err error) {
if err != nil {
p.d.Notices.Notify(context.LevelError, err.Error())
}
}
func (p *Panel) CanUndo() bool {
return p.d.Files.CanUndo()
}
func (p *Panel) CanRedo() bool {
return p.d.Files.CanRedo()
}
func (p *Panel) ID() string {
return workspace.Assets
}
func (p *Panel) Title() string {
return "Assets"
}
func (p *Panel) Scope() string {
return command.ScopeAssets
}
func (p *Panel) Selected() ids.AssetID {
return p.active
}
func (p *Panel) HasSelection() bool {
return len(p.sel) > 0
}
func (p *Panel) Single() bool {
return len(p.sel) == 1
}
func (p *Panel) Selection() []ids.AssetID {
out := make([]ids.AssetID, 0, len(p.sel))
for i := range p.items {
if p.sel[p.items[i].ID] {
out = append(out, p.items[i].ID)
}
}
return out
}
func (p *Panel) Undo() error {
err := p.d.Files.Undo()
p.invalidate()
return err
}
func (p *Panel) Redo() error {
err := p.d.Files.Redo()
p.invalidate()
return err
}
func (p *Panel) find(id ids.AssetID) *pipeline.AssetInfo {
for i := range p.items {
if p.items[i].ID == id {
return &p.items[i]
}
}
return nil
}
func (p *Panel) invalidate() {
p.rev = ^uint64(0)
}
func (p *Panel) refresh() {
pl := p.d.Pipeline
if rev := pl.Revision(); rev != p.rev {
p.rev = rev
p.items = pl.Assets()
p.dirs = pl.Dirs()
p.dirsAt = p.now
p.prune()
} else if p.now-p.dirsAt > 2 {
p.dirs = pl.Dirs()
p.dirsAt = p.now
}
found := false
for _, d := range p.dirs {
if d == p.folder {
found = true
break
}
}
if !found && len(p.dirs) > 0 {
p.folder = p.dirs[0]
}
}
func (p *Panel) prune() {
alive := make(map[ids.AssetID]bool, len(p.items))
for i := range p.items {
alive[p.items[i].ID] = true
}
for id := range p.sel {
if !alive[id] {
delete(p.sel, id)
}
}
if !alive[p.active] {
p.active = ids.AssetID{}
}
}
func (p *Panel) selectOnly(id ids.AssetID) {
clear(p.sel)
if !id.IsZero() {
p.sel[id] = true
}
p.active, p.anchor = id, id
}
func (p *Panel) toggle(id ids.AssetID) {
if p.sel[id] {
delete(p.sel, id)
if p.active == id {
p.active = ids.AssetID{}
}
} else {
p.sel[id] = true
p.active = id
}
p.anchor = id
}
func (p *Panel) viewIndex(id ids.AssetID) int {
for k, i := range p.view {
if p.items[i].ID == id {
return k
}
}
return -1
}
func (p *Panel) selectRange(id ids.AssetID) {
a, b := p.viewIndex(p.anchor), p.viewIndex(id)
if a < 0 || b < 0 {
p.selectOnly(id)
return
}
clear(p.sel)
for k := min(a, b); k <= max(a, b); k++ {
p.sel[p.items[p.view[k]].ID] = true
}
p.active = id
}
func (p *Panel) press(id ids.AssetID) {
mods := p.f.UI.Mods()
p.selDir = ""
switch {
case mods.Primary():
p.toggle(id)
case mods.Has(input.ModShift):
p.selectRange(id)
case p.sel[id] && len(p.sel) > 1:
p.pending, p.active = id, id
default:
p.selectOnly(id)
}
p.inspectActive()
}
func (p *Panel) inspectActive() {
if p.d.Inspect == nil {
return
}
var target ids.AssetID
if a := p.find(p.active); a != nil && len(p.sel) == 1 && a.Kind == asset.ArtData {
target = a.ID
}
p.d.Inspect(target)
}
func (p *Panel) settle() {
if p.pending.IsZero() {
return
}
switch {
case p.f.DnD.Active():
p.pending = ids.AssetID{}
case !p.f.UI.MouseDown(input.MouseLeft):
p.selectOnly(p.pending)
p.pending = ids.AssetID{}
p.inspectActive()
}
}
func (p *Panel) Draw(f *services.Frame, r mathx.Rect) {
p.f, p.now = f, f.Now
ui := f.UI
th := &ui.Theme
if p.d.Pipeline == nil {
widgets.LabelDim(ui, "Asset pipeline is not available")
return
}
p.assetMenuID = ui.ID("amenu")
p.dirMenuID = ui.ID("dmenu")
p.refresh()
p.settle()
bar := ui.Cursor().Next(0)
bw, label := float32(100), "New Folder"
if bar.W < 300 {
bw, label = bar.H, "+"
}
sw := float32(0)
if bar.W >= 260 {
sw = 3 * (bar.H + 4)
}
newID := ui.ID("newdir")
widgets.SearchField(ui, ui.ID("search"), mathx.Rect{X: bar.X, Y: bar.Y, W: max(0, bar.W-bw-sw-8), H: bar.H}, &p.search)
if sw > 0 {
p.sizeButtons(mathx.Rect{X: bar.X + bar.W - bw - sw - 4, Y: bar.Y, W: sw, H: bar.H})
}
if widgets.ButtonRect(ui, newID, mathx.Rect{X: bar.X + bar.W - bw, Y: bar.Y, W: bw, H: bar.H}, label) {
p.newFolder(p.folder)
}
widgets.Tooltip(ui, newID, "New Folder")
top := bar.Y + bar.H + th.Spacing
body := mathx.Rect{X: r.X, Y: top, W: r.W, H: max(0, r.Y+r.H-top)}
if body.W < 260 {
p.drawGrid(body)
} else {
left, right := widgets.Splitter(ui, ui.ID("split"), body, false, &p.split)
p.drawTree(left)
p.drawGrid(right)
}
p.menus()
}
func (p *Panel) buildRows() {
p.rows = p.rows[:0]
for i, d := range p.dirs {
visible := true
for parent := path.Dir(d); parent != "." && parent != "/"; parent = path.Dir(parent) {
if !p.expanded[parent] {
visible = false
break
}
}
if !visible {
continue
}
has := false
for _, other := range p.dirs[i+1:] {
if strings.HasPrefix(other, d+"/") {
has = true
break
}
}
p.rows = append(p.rows, dirRow{path: d, depth: strings.Count(d, "/"), has: has})
}
}
func dirPayload(dir string) dnd.Payload {
return dnd.Payload{Kind: context.KindAssets, Label: path.Base(dir), Data: context.AssetsPayload{Dirs: []string{dir}}}
}
func (p *Panel) drawTree(r mathx.Rect) {
ui := p.f.UI
th := &ui.Theme
p.buildRows()
widgets.VirtualList(ui, ui.ID("tree"), r, len(p.rows), th.RowHeight, &p.treeScroll, func(i int, rr mathx.Rect) {
row := p.rows[i]
if p.renamingDir == row.path {
p.drawDirRename(row, rr)
return
}
id := core.IDOf("assets.dir/" + row.path)
ev := widgets.TreeRowRect(ui, id, rr, widgets.TreeRow{
Depth: row.depth,
Label: path.Base(row.path),
HasChildren: row.has,
Open: p.expanded[row.path],
Selected: p.folder == row.path && p.search == "",
Focused: p.f.Focused,
})
if ev.Toggled || (ev.DoubleClicked && row.has) {
p.expanded[row.path] = !p.expanded[row.path]
}
if ev.Pressed {
p.folder = row.path
p.gridScroll = 0
p.search = ""
}
if ev.RightClicked {
p.menuDir = row.path
widgets.OpenContextMenu(ui, p.dirMenuID)
}
if ui.Active() == id && path.Dir(row.path) != "." {
p.f.DnD.Source(ui, id, dirPayload(row.path))
}
p.dropOnDir(row.path, rr)
})
}
func (p *Panel) filter() {
p.view = p.view[:0]
p.subdirs = p.subdirs[:0]
q := strings.ToLower(strings.TrimSpace(p.search))
if q == "" {
for _, d := range p.dirs {
if path.Dir(d) == p.folder {
p.subdirs = append(p.subdirs, d)
}
}
}
for i := range p.items {
a := &p.items[i]
if q != "" {
if strings.Contains(strings.ToLower(a.Path), q) {
p.view = append(p.view, i)
}
continue
}
if path.Dir(a.Path) == p.folder {
p.view = append(p.view, i)
}
}
}
func (p *Panel) drawGrid(r mathx.Rect) {
ui := p.f.UI
th := &ui.Theme
p.filter()
count := len(p.subdirs) + len(p.view)
tw := p.tileW()
cols := max(1, int(r.W/tw))
tileH := p.tileSize() + th.RowHeight + 12
rows := (count + cols - 1) / cols
p.scrollToTile(cols, tileH, r.H)
p.overDir = false
widgets.VirtualList(ui, ui.ID("grid"), r, rows, tileH, &p.gridScroll, func(row int, rr mathx.Rect) {
for c := 0; c < cols; c++ {
i := row*cols + c
if i >= count {
break
}
tr := mathx.Rect{X: rr.X + float32(c)*tw + 2, Y: rr.Y + 2, W: tw - 4, H: tileH - 4}
if i < len(p.subdirs) {
p.dirTile(p.subdirs[i], tr)
continue
}
p.assetTile(&p.items[p.view[i-len(p.subdirs)]], tr)
}
})
if count == 0 {
ui.TextIn(r, "Drop files here to import", th.TextDisabled, core.AlignCenter)
}
used := mathx.Clamp(float32(rows)*tileH-p.gridScroll, 0, r.H)
empty := mathx.Rect{X: r.X, Y: r.Y + used, W: r.W, H: r.H - used}
id := ui.ID("gridbg")
if empty.H > 0 {
if ui.Pressed(id, empty) && !ui.Primary() && !ui.Mods().Has(input.ModShift) {
p.selectOnly(ids.AssetID{})
p.selDir = ""
}
if ui.RightClicked(id, empty) {
p.menuDir = p.folder
widgets.OpenContextMenu(ui, p.dirMenuID)
}
}
if !p.overDir {
p.dropOnDir(p.folder, r)
}
}
func fitRect(box mathx.Rect, w, h uint16) mathx.Rect {
if w == 0 || h == 0 {
return box
}
s := min(box.W/float32(w), box.H/float32(h))
fw, fh := float32(w)*s, float32(h)*s
return mathx.Rect{X: box.X + (box.W-fw)/2, Y: box.Y + (box.H-fh)/2, W: fw, H: fh}
}
func assetTileID(id ids.AssetID) core.ID {
return core.IDSeq(core.ID(id.Hi), int(uint32(id.Lo)))
}
func (p *Panel) payload(a *pipeline.AssetInfo) dnd.Payload {
if !p.sel[a.ID] || len(p.sel) < 2 {
return dnd.Payload{
Kind: context.KindAsset,
Label: path.Base(a.Path),
Data: context.AssetPayload{ID: a.ID, Kind: a.Kind, Name: a.Name},
}
}
items := make([]context.AssetPayload, 0, len(p.sel))
for i := range p.items {
if it := &p.items[i]; p.sel[it.ID] {
items = append(items, context.AssetPayload{ID: it.ID, Kind: it.Kind, Name: it.Name})
}
}
return dnd.Payload{Kind: context.KindAssets, Label: strconv.Itoa(len(items)) + " assets", Data: context.AssetsPayload{Items: items}}
}
func (p *Panel) assetTile(a *pipeline.AssetInfo, r mathx.Rect) {
ui := p.f.UI
th := &ui.Theme
id := assetTileID(a.ID)
if ui.Pressed(id, r) {
p.press(a.ID)
if ui.DoubleClicked(id) {
p.selectOnly(a.ID)
p.open(a)
}
}
if ui.RightClicked(id, r) {
if !p.sel[a.ID] {
p.selectOnly(a.ID)
}
p.active = a.ID
widgets.OpenContextMenu(ui, p.assetMenuID)
}
if ui.Active() == id {
p.f.DnD.Source(ui, id, p.payload(a))
}
switch {
case p.sel[a.ID]:
ui.Rect(r, th.Selection)
case ui.Hot() == id:
ui.Rect(r, th.WidgetHot)
}
ts := p.tileSize()
img := mathx.Rect{X: r.X + (r.W-ts)/2, Y: r.Y + 4, W: ts, H: ts}
if reg, ok := p.d.Thumbs.Get(*a); ok {
if preview.Image(a.Kind) || a.Kind == asset.ArtPrefab {
p.checker(img)
}
ui.List().Texture(reg, fitRect(img, reg.W, reg.H), mathx.Color{R: 1, G: 1, B: 1, A: 1})
} else {
p.schematic(a, img)
}
startup := p.d.Project != nil && a.ID == p.d.Project.MainScene
if startup {
widgets.IconAt(ui, mathx.Rect{X: img.X + img.W - 16, Y: img.Y, W: 16, H: 16}, "play.start", th.Accent)
}
nameR := mathx.Rect{X: r.X + 2, Y: img.Y + img.H + 2, W: r.W - 4, H: th.RowHeight}
if p.renaming == a.ID {
p.drawAssetRename(a, nameR)
} else {
ui.PushClip(nameR)
ui.TextIn(nameR, widgets.Elide(ui, assetops.Stem(a.Path), nameR.W), th.Text, core.AlignCenter)
ui.PopClip()
}
tip := a.Path
if startup {
tip += " · startup scene"
}
if a.Error != "" {
ui.List().RectOutline(r, th.Error, 1)
tip = a.Error
}
widgets.Tooltip(ui, id, tip)
}
func (p *Panel) enter(dir string) {
p.folder = dir
p.expanded[path.Dir(dir)] = true
p.gridScroll = 0
p.selDir = ""
}
func (p *Panel) dirTile(dir string, r mathx.Rect) {
ui := p.f.UI
th := &ui.Theme
id := core.IDOf("assets.tile/" + dir)
if ui.Pressed(id, r) {
p.selDir = dir
if ui.DoubleClicked(id) {
p.enter(dir)
}
}
if ui.RightClicked(id, r) {
p.menuDir = dir
widgets.OpenContextMenu(ui, p.dirMenuID)
}
if ui.Active() == id && path.Dir(dir) != "." {
p.f.DnD.Source(ui, id, dirPayload(dir))
}
switch {
case p.selDir == dir:
ui.Rect(r, th.Selection)
case ui.Hot() == id:
ui.Rect(r, th.WidgetHot)
}
ts := p.tileSize()
img := mathx.Rect{X: r.X + (r.W-ts)/2, Y: r.Y + 4, W: ts, H: ts}
p.folderStack(dir, img)
nameR := mathx.Rect{X: r.X + 2, Y: img.Y + img.H + 2, W: r.W - 4, H: th.RowHeight}
ui.PushClip(nameR)
ui.TextIn(nameR, widgets.Elide(ui, path.Base(dir), nameR.W), th.Text, core.AlignCenter)
ui.PopClip()
if p.f.DnD.Hovering(ui, r) {
p.overDir = true
}
p.dropOnDir(dir, r)
}
func (p *Panel) accepts(pl dnd.Payload, dir string) bool {
switch pl.Kind {
case context.KindFiles, context.KindAsset:
return true
case context.KindNodes:
return p.d.Prefabs != nil
case context.KindAssets:
ap, ok := pl.Data.(context.AssetsPayload)
if !ok {
return false
}
for _, d := range ap.Dirs {
if d == dir || assetops.Under(dir, d) || path.Dir(d) == dir {
return false
}
}
return true
}
return false
}
func (p *Panel) dropOnDir(dir string, r mathx.Rect) {
ui := p.f.UI
dn := p.f.DnD
if !dn.Hovering(ui, r) {
return
}
pl, _ := dn.Payload()
if !p.accepts(pl, dir) {
dn.Target(ui, r, "reject")
return
}
got, ok := dn.Target(ui, r, context.KindAsset, context.KindAssets, context.KindFiles, context.KindNodes)
if !ok {
return
}
switch got.Kind {
case context.KindFiles:
files, _ := got.Data.([]string)
p.importFiles(files, dir)
case context.KindAsset:
ap, _ := got.Data.(context.AssetPayload)
p.moveInto([]context.AssetPayload{ap}, nil, dir)
case context.KindAssets:
as, _ := got.Data.(context.AssetsPayload)
p.moveInto(as.Items, as.Dirs, dir)
case context.KindNodes:
np, _ := got.Data.(context.NodesPayload)
p.report(p.d.Prefabs.CreateFrom(p.d.Docs.Documents().Get(np.Doc), np.Nodes, dir))
p.invalidate()
}
}
func movedWith(src string, dirs []string) bool {
for _, d := range dirs {
if assetops.Under(src, d) {
return true
}
}
return false
}
func (p *Panel) moveInto(items []context.AssetPayload, dirs []string, target string) {
var ops []assetops.FileOp
for _, d := range dirs {
dst := path.Join(target, path.Base(d))
if dst == d || d == target || assetops.Under(target, d) {
continue
}
ops = append(ops, assetops.FileOp{Dir: true, From: d, To: dst})
}
for _, it := range items {
src, ok := p.d.Pipeline.PathOf(it.ID)
if !ok || movedWith(src, dirs) {
continue
}
if dst := path.Join(target, path.Base(src)); dst != src {
ops = append(ops, assetops.FileOp{ID: it.ID, From: src, To: dst})
}
}
if len(ops) == 0 {
return
}
err := p.d.Files.Apply(ops)
for _, op := range ops {
if op.Dir && !p.d.Files.Exists(op.From) && (p.folder == op.From || assetops.Under(p.folder, op.From)) {
p.folder = op.To + strings.TrimPrefix(p.folder, op.From)
}
}
if err != nil {
p.d.Notices.Notify(context.LevelError, err.Error())
}
p.invalidate()
}
func (p *Panel) importFiles(files []string, dir string) {
created, err := p.d.Files.ImportExternal(files, dir)
if err != nil {
p.d.Notices.Notify(context.LevelError, err.Error())
return
}
p.d.Notices.Notify(context.LevelInfo, "Imported "+strconv.Itoa(len(created))+" file(s) into "+dir)
if len(created) > 0 {
p.selectOnly(created[0])
p.folder = dir
}
p.invalidate()
}
func openExternal(abs string) error {
switch runtime.GOOS {
case "darwin":
return exec.Command("open", abs).Start()
case "windows":
return exec.Command("cmd", "/c", "start", "", abs).Start()
}
return exec.Command("xdg-open", abs).Start()
}
func openable(k asset.ArtifactKind) bool {
switch k {
case asset.ArtScene, asset.ArtScript, asset.ArtPrefab, asset.ArtData, asset.ArtTable:
return true
}
return false
}
func (p *Panel) open(a *pipeline.AssetInfo) {
switch a.Kind {
case asset.ArtScene, asset.ArtPrefab:
if _, err := p.d.Docs.Documents().Open(a.Path); err != nil {
p.d.Notices.Notify(context.LevelError, err.Error())
}
case asset.ArtScript:
if p.d.Files.ScriptEditor() {
p.d.Nav.Open(context.At(a.Path, 0, 1))
return
}
if err := openExternal(p.d.Pipeline.Abs(a.Path)); err != nil {
p.d.Notices.Notify(context.LevelError, err.Error())
}
case asset.ArtData:
if p.d.Inspect != nil {
p.d.Inspect(a.ID)
}
case asset.ArtTable:
if p.d.OpenTable != nil {
p.d.OpenTable(a.ID)
}
}
}
func (p *Panel) Reveal(rel string) {
if p.d.Pipeline == nil {
return
}
p.refresh()
p.search = ""
p.gridScroll = 0
folder := path.Dir(rel)
for i := range p.items {
if p.items[i].Path == rel {
p.selectOnly(p.items[i].ID)
break
}
}
for _, d := range p.dirs {
if d == rel {
folder = rel
break
}
}
p.folder = folder
for d := folder; d != "." && d != "/"; d = path.Dir(d) {
p.expanded[d] = true
}
}
func (p *Panel) StartRename() {
if len(p.sel) == 1 && p.sel[p.active] {
p.renaming, p.renameStart = p.active, true
}
}
func (p *Panel) drawAssetRename(a *pipeline.AssetInfo, r mathx.Rect) {
ui := p.f.UI
fid := ui.ID("arename")
ext := path.Ext(a.Path)
if p.renameStart {
p.renameStart = false
p.renameText = assetops.Stem(a.Path)
ui.SetFocus(fid)
ui.EditBuffer(fid, p.renameText).SelectAll()
}
if _, committed := widgets.TextFieldRect(ui, fid, r, &p.renameText, false); committed {
name := strings.TrimSpace(p.renameText)
if assetops.ValidName(name) {
if !strings.HasSuffix(name, ext) {
name += ext
}
if dst := path.Join(path.Dir(a.Path), name); dst != a.Path {
if err := p.d.Files.Apply([]assetops.FileOp{{ID: a.ID, From: a.Path, To: dst}}); err != nil {
p.d.Notices.Notify(context.LevelError, err.Error())
}
p.invalidate()
}
}
}
if ui.Focus() != fid {
p.renaming = ids.AssetID{}
}
}
func (p *Panel) drawDirRename(row dirRow, rr mathx.Rect) {
ui := p.f.UI
th := &ui.Theme
fid := ui.ID("drename")
x := rr.X + float32(row.depth)*th.Indent + th.Indent + 2
fr := mathx.Rect{X: x, Y: rr.Y + 1, W: max(40, rr.X+rr.W-x-2), H: rr.H - 2}
if p.renameDirStart {
p.renameDirStart = false
p.renameDirText = path.Base(row.path)
ui.SetFocus(fid)
ui.EditBuffer(fid, p.renameDirText).SelectAll()
}
if _, committed := widgets.TextFieldRect(ui, fid, fr, &p.renameDirText, false); committed {
name := strings.TrimSpace(p.renameDirText)
dst := path.Join(path.Dir(row.path), name)
if assetops.ValidName(name) && dst != row.path {
if err := p.d.Files.Apply([]assetops.FileOp{{Dir: true, From: row.path, To: dst}}); err != nil {
p.d.Notices.Notify(context.LevelError, err.Error())
} else {
if p.folder == row.path || assetops.Under(p.folder, row.path) {
p.folder = dst + strings.TrimPrefix(p.folder, row.path)
}
p.expanded[dst] = p.expanded[row.path]
}
p.invalidate()
}
}
if ui.Focus() != fid {
p.renamingDir = ""
}
}
func (p *Panel) newFolder(parent string) {
dir := p.d.Pipeline.UniqueDir(path.Join(parent, "New Folder"))
if err := p.d.Pipeline.Mkdir(dir); err != nil {
p.d.Notices.Notify(context.LevelError, err.Error())
return
}
p.expanded[parent] = true
p.renamingDir, p.renameDirStart = dir, true
p.invalidate()
}
func reveal(abs string, isDir bool) error {
switch runtime.GOOS {
case "darwin":
if isDir {
return exec.Command("open", abs).Start()
}
return exec.Command("open", "-R", abs).Start()
case "windows":
if isDir {
return exec.Command("explorer", abs).Start()
}
return exec.Command("explorer", "/select,", abs).Start()
}
if !isDir {
abs = path.Dir(abs)
}
return exec.Command("xdg-open", abs).Start()
}