Fantom

 

//
// Copyright (c) 2011, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   3 Feb 2011  Andy Frank  Creation
//

using fwt
using gfx

**
** ColTree visualizes a TreeModel as a series of columns.
**
@NoDoc
@Js
class ColTree : ContentPane
{
  ** Constructor.
  new make(|This|? f := null)
  {
    if (f != null) f(this)
    this.lists    = [,]
    this.listPane = ColListPane(this)
    this.scroll   = WebScrollPane
    {
      bg = Color("#eee")
      hpolicy = WebScrollPane.auto
      vpolicy = WebScrollPane.off
      listPane,
    }
    this.content = scroll
    refreshAll
  }

  ** Backing data model of tree.
  TreeModel model := TreeModel()

  **
  ** Show column headers.  To specify column headers, implement
  ** a 'header' method on your TreeModel, where the return type
  ** can be a Str# or Widget#:
  **
  **   Obj header(Obj node)
  **
  Bool headerVisible := false

  ** Width of each column in pixels.
  Int colw := 200

  ** Update the entire tree's contents from the model.
  Void refreshAll()
  {
    lists.clear
    lists.add(ColList(this, model.roots))

    listPane.removeAll
    listPane.add(lists.first)
    relayout
  }

  ** Update the specified node from the model.
  Void refreshNode(Obj node)
  {
    // TODO FIXIT
    setExpanded(node, true)
  }

  ** Get list of selected path in widget.
  Obj[] path()
  {
    if (lists.size <= 1) return [,]
    return lists[0..-2].map |x| { x.selected }
  }

  ** Set the expanded state for this node.
  Void setExpanded(Obj node, Bool expanded)
  {
    if (!expanded) return
    if (!model.hasChildren(node)) return

    // check if we need to rollback
    index := lists.findIndex |l| { l.items.contains(node) }
    diff  := lists.size-1 - index
    while (diff > 0)
    {
      listPane.remove(lists.removeAt(-1))
      diff--
    }

    // make sure item is selected
    lists[index].select(node)

    // add new list
    kids := model.children(node)
    list := ColList(this, kids)
    lists.add(list)
    listPane.add(list)
    scroll.relayout
    scroll.scrollToRight
    onExpanded.fire(Event { data=node; widget=this })
  }

  **
  ** Callback when a node's expanded state changes.
  **
  ** Event fields:
  **   - Event.data: node object that was expanded.
  **
  once EventListeners onExpanded() { EventListeners() }

  **
  ** Callback when selected nodes change.
  **
  ** Event id fired:
  **   - EventId.select
  **
  ** Event fields:
  **   - Event.data: the primary selection node object.
  **
  once EventListeners onSelect() { EventListeners() }

  ** Fire onSelect event.
  internal Void fireSelect(Obj node)
  {
    onSelect.fire(Event { id=EventId.select; data=node })
  }

  private ColList[] lists
  private ColListPane? listPane
  private WebScrollPane scroll
}

**************************************************************************
** ColListPane
**************************************************************************
@Js
internal class ColListPane : Pane
{
  new make(ColTree tree) { this.tree = tree }
  override Size prefSize(Hints hints := Hints.defVal)
  {
    sz := children.size
    pw := sz * tree.colw - (sz-1) - 2
    return Size(pw, 200)
  }
  override Void onLayout()
  {
    x := -1
    y := -1
    h := size.h+2
    children.each |kid|
    {
      kid.bounds = Rect(x, y, tree.colw, h)
      x += tree.colw-1
    }
  }
  private ColTree tree
}

**************************************************************************
** ColList
**************************************************************************
@Js
internal class ColList : EdgePane
{
  new make(ColTree tree, Obj[] items)
  {
    this.tree    = tree
    this.items   = items
    this.webList = ColWebList(tree, items)
    if (tree.headerVisible) this.top = makeHeader
    this.center = webList
  }

  Obj[] items
  Obj? selected() { webList.selected.first }

  Void select(Obj node) { webList.selected = [node] }

  private Widget makeHeader()
  {
    try
    {
      h := items.isEmpty ? "" : tree.model->header(items.first)
      if (h is Widget) return h
      else if (h is Str)
      {
        return BorderPane
        {
          bg = labelBg
          insets = labelPad
          WebLabel
          {
            text = ((Str)h).upper
            fg = labelFg
            font = Desktop.sysFontSmall.toBold
            style = ["text-shadow":"#fff 0px 1px 1px"]
          },
        }
      }
    }
    catch (Err err) { err.trace }
    return BorderPane
    {
      bg = labelBg
      insets = labelPad
      Label { font=Desktop.sysFontSmall.toBold },
    }
  }

  private static const Insets labelPad := Insets(6)
  private static const Color labelBg := Color("#eee")
  private static const Color labelFg := Color("#666")

  private ColTree tree
  private ColWebList webList
}

**************************************************************************
** ColWebList
**************************************************************************
@Js
internal class ColWebList : WebList
{
  new make(ColTree tree, Obj[] items)
  {
    this.tree   = tree
    this.items  = items
    this.onSelect.add |e|
    {
      tree.setExpanded(e.data, true)
      tree.fireSelect(e.data)
    }
  }

  // force native peer
  private native Void dummy()

  private ColTree tree
}