Fantom

 

//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   21 Jul 08  Brian Frank  Creation
//

using gfx
using fwt

**
** NavBar is the primary top level tree based navigation side bar.
**
internal class NavBar : SideBar
{

//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////

  new make()
  {
    content = EdgePane
    {
      top = InsetPane(5,4,5,4) { it.add(combo) }
      center = BorderPane
      {
        it.content = treePane
        it.border  = Border("1,1,0,0 $Desktop.sysNormShadow")
      }
    }

    // always add root as first tree
    addTree(null)
    select(0)
  }

//////////////////////////////////////////////////////////////////////////
// State
//////////////////////////////////////////////////////////////////////////

  override Void onLoad()
  {
    state := NavBarState.load
    state.roots.each |uri| { addTree(Resource.resolve(uri)) }
    if (state.selected != null) select(state.selected)
  }

  override Void onUnload()
  {
    state := NavBarState()
    trees.each |Tree t, Int i|
    {
      // never store root
      if (i > 0) state.roots.add(t.model.roots.first->uri)
    }
    state.selected = combo.selectedIndex
    state.save
  }

//////////////////////////////////////////////////////////////////////////
// Methods
//////////////////////////////////////////////////////////////////////////

  **
  ** Add a new tree rooted at the given resource. If null
  ** is passed, use the root of the file system for the
  ** tree root.
  **
  Void addTree(Resource? r)
  {
    // create new tree for r
    tree := Tree
    {
      it.model = NavTreeModel.make(r == null ? Resource.roots : [r])
      it.border = false
      it.onAction.add |e| { this.onAction(e) }
      it.onPopup.add  |e| { this.onPopup(e) }
    }

    // add tree
    trees.add(tree)
    treePane.add(tree)

    // ignore onModify events while we update combo
    ignore = true
    old   := combo.selectedIndex
    name  := r == null ? Flux.locale("navBar.root") : r.name
    items := combo.items.size == 0 ? combo.items.dup : combo.items[0..<-1]
    combo.items = items.add(name).add(Flux.locale("navBar.editList"))
    if (old >= 0) combo.selectedIndex = old
    ignore = false
  }

  **
  ** Select the tree with the given index.
  **
  Void select(Int index)
  {
    if (index < 0 || index >= trees.size)
      throw ArgErr("Index out of bounds: $index")

    // bail if already selected
    tree := trees[index]
    if (active === tree) return

    // update tree pane
    active = tree
    if (treePane.active != null)
      treePane.active.visible = false
    treePane.active = tree
    treePane.active.visible = true
    treePane.relayout

    // update combo
    ignore = true
    combo.selectedIndex = index
    ignore = false
  }

//////////////////////////////////////////////////////////////////////////
// Events
//////////////////////////////////////////////////////////////////////////

  internal Void onModify(Event event)
  {
    if (ignore) return
    index := event.widget->selectedIndex
    if (index >= 0)
    {
      if (index == trees.size) editList
      else select(index)
    }
  }

  internal Void onAction(Event event)
  {
    if (event.data != null)
      frame.load(event.data->uri, LoadMode(event))
  }

  internal Void onPopup(Event event)
  {
    Menu? menu
    n := event.data as NavNode
    if (n != null)
    {
      r := n?.resource
      menu = r?.popup(frame, event) ?: Menu()
      if (r is FileResource && r->file->isDir)
      {
        menu.add(MenuItem { mode = MenuItemMode.sep })
        menu.add(MenuItem { command = Command.makeLocale(Pod.of(this), "navBar.refresh") {onRefresh(n)} })
        menu.add(MenuItem { command = Command.makeLocale(Pod.of(this), "navBar.goInto") {onGoInto(n)} })
      }
    }
    else
    {
      menu = Menu()
      menu.add(MenuItem { command = Command.makeLocale(Pod.of(this), "navBar.sync") {onSync} })
    }
    event.popup = menu
  }

  internal Void onRefresh(NavNode n)
  {
    n.refresh
    active.refreshNode(n)
  }

  internal Void onGoInto(NavNode n)
  {
    addTree(n.resource)
    select(trees.size-1)
  }

  internal Void onSync()
  {
    r := frame.view.resource
    if (r isnot FileResource)
    {
      Dialog.openErr(frame, "Resource not found in tree")
      return
    }

    Obj? node := null
    nodes := active.model.roots
    path  := r.uri.path
    path.eachWhile |Str s->Obj|
    {
      found := nodes.eachWhile |Obj n->Obj?|
      {
        if (n->name == s)
        {
          node = n
          nodes = active.model.children(n)
          active.setExpanded(n, true)
          return true
        }
        return null
      }
      return found ? null : false
    }

    if (node != null)
    {
      active.select(node)
      active.show(node)
    }
    else Dialog.openErr(frame, "Resource not found in tree")
  }

  internal Void editList()
  {
    // reset index back to active
    ignore = true
    combo.selectedIndex = trees.findIndex |Tree t->Bool| { t === active }
    ignore = false

    // now show dialog
    list := EditList(combo.items[0..<-1])
    dlg  := Dialog(frame)
    {
      title    = Flux.locale("navBar.edit")
      body     = list
      commands = [Dialog.ok, Dialog.cancel]
    }
    if (dlg.open == Dialog.ok)
    {
      items    := list.getItems
      newTrees := Tree[,]

      // copy new tree order
      items.each |Str item|
      {
        i := combo.items.findIndex |Str x->Bool| { return item == x }
        newTrees.add(trees[i])
      }

      // remove deleted trees
      trees.each |Tree t|
      {
        r := newTrees.find |Tree n->Bool| { return t === n }
        if (r == null) treePane.remove(t)
      }

      // try to select the same tree, or if it was removed
      // fallback to selecting the root
      index := newTrees.findIndex |Tree t->Bool| { return t === active }
      if (index == null) index = 0

      // update widget
      trees = newTrees
      ignore = true
      combo.items = items.add(Flux.locale("navBar.editList"))
      combo.selectedIndex = index
      ignore = false
      select(index)
    }
  }

//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////

  Tree? active
  Tree[] trees := Tree[,]
  Bool ignore  := true
  Combo combo  := Combo() { it.onModify.add |e| { this.onModify(e) } }
  NavTreePane treePane := NavTreePane()
}

**************************************************************************
** NavBarState
**************************************************************************

@Serializable
internal class NavBarState
{
  static NavBarState load() { return Flux.loadOptions(Flux.pod, "session/navBar", NavBarState#) }
  Void save() { Flux.saveOptions(Flux.pod, "session/navBar", this) }
  Uri[] roots := Uri[,]
  Int? selected := null
}

**************************************************************************
** NavTreeModel
**************************************************************************

internal class NavTreeModel : TreeModel
{
  new make(Resource[] roots) { this.myRoots = NavNode.map(roots) }
  override Obj[] roots() { return myRoots }
  override Str text(Obj node) { return ((NavNode)node).name }
  override Image? image(Obj node) { return ((NavNode)node).icon }
  override Bool hasChildren(Obj node) { return ((NavNode)node).hasChildren }
  override Obj[] children(Obj node) { return ((NavNode)node).children }
  private NavNode[] myRoots
}

**************************************************************************
** NavNode
**************************************************************************

@Serializable
internal class NavNode
{
  static NavNode[] map(Resource[] r)
  {
    return r.map |Resource x->NavNode| { NavNode(x) }
  }
  new make(Resource r) { resource = r }
  Resource resource
  override Str toStr() { return resource.toStr }
  Uri uri() { return resource.uri }
  Str name() { return resource.name }
  Image? icon() { return resource.icon }
  Bool hasChildren() { return resource.hasChildren }
  NavNode[]? children
  {
    get
    {
      if (&children == null) &children = map(resource.children)
      return &children
    }
  }
  Void refresh()
  {
    resource = Resource.resolve(resource.uri)
    children = null
  }
}

**************************************************************************
** NavTreePane
**************************************************************************

internal class NavTreePane : Pane
{
  override Size prefSize(Hints hints := Hints.defVal) { return Size(100,100) }
  override Void onLayout()
  {
    if (active != null)
    {
      active.bounds = Rect(0, 0, size.w, size.h)
      active.relayout
    }
  }
  Widget? active
}

**************************************************************************
** EditListWidget
**************************************************************************

internal class EditList : Canvas
{
  new make(Obj[] items)
  {
    this.items = items.map |Obj obj->Str|  { obj.toStr }
    this.keep  = items.map |Obj obj->Bool| { true }
    onMouseDown.add { onPressed(it) }
  }

  Str[] getItems()
  {
    return items.findAll |Str s, Int i->Bool| { return keep[i] }
  }

  override Size prefSize(Hints hints := Hints.defVal)
  {
    pw := 0
    ph := rowh * items.size
    items.each |Str item| { pw = pw.max(font.width(item)) }
    pw += 64 + 16
    return Size(pw, ph)
  }

  override Void onPaint(Graphics g)
  {
    g.brush = Desktop.sysBg
    g.fillRect(0, 0, size.w, size.h)
    g.brush = Desktop.sysFg
    g.font = font
    items.each |Str item, Int i|
    {
      dy := i * rowh
      iy := dy + (rowh - 16) / 2
      ty := dy + (rowh - font.height) / 2
      g.push
      try
      {
        if (!keep[i]) g.alpha = 95;
        if (i>0) g.drawImage(delete, 0, iy)
        g.drawImage(folder, 20, iy)
        g.drawText(item, 40, ty)
        if (i>1) g.drawImage(up, size.w-32, iy)
        if (i>0 && i<items.size-1) g.drawImage(down, size.w-16, iy)
      }
      finally g.pop
    }
  }

  Void onPressed(Event e)
  {
    row := e.pos.y / rowh
    if (row < 0) row = 0
    if (row > items.size-1) row = items.size-1
    if (row == 0) return  // can't modify row zero, so just bail
    if (e.pos.x <= 16)    // delete
    {
      keep[row] = !keep[row]
      repaint
      return
    }
    if (e.pos.x > size.w-32) {
      if (e.pos.x < size.w-16) {
        if (row > 1)
        {
          // move up
          items.swap(row, row-1)
          keep.swap(row, row-1)
        }
      }
      else
      {
        if (row < items.size-1)
        {
          // move down
          items.swap(row, row+1)
          keep.swap(row, row+1)
        }
      }
      repaint
    }
  }

  private Str[] items
  private Bool[] keep
  private Font font    := Desktop.sysFont
  private Image folder := Flux.icon(`/x16/folder.png`)
  private Image delete := Flux.icon(`/x16/circleDelete.png`)
  private Image up     := Flux.icon(`/x16/circleArrowUp.png`)
  private Image down   := Flux.icon(`/x16/circleArrowDown.png`)
  private Int rowh     := 16.max(font.height) + 4
}