sourcecamembert::ItemList.fan

//
// Copyright (c) 2012, Brian Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   25 May 12  Brian Frank  Creation
//

using gfx
using fwt
using concurrent
using petanque

**
** ItemList
**
class ItemList : Panel, Themable
{
  //////////////////////////////////////////////////////////////////////////
  // Constructor
  //////////////////////////////////////////////////////////////////////////

  new make(Frame frame, Item[] items, Int width := 200)
  {
    this.frame = frame
    this.width = width
    this.items = items
    onMouseUp.add |e| { doMouseUp(e) }
    onMouseWheel.add |e| { mouseWheel(e) }
    updateTheme()
    colw = font.width("m")
    update
  }

  override Void updateTheme()
  {
    t := Sys.cur.theme
    wallpaperColor = t.bg
    viewportColor = t.bg
    font = t.font
    selectedItemColor = t.selectedItem
    fontColor = t.fontColor
    colw = font.width("m")
    gutterColor = t.scrollBg
    thumbColor = t.scrollFg
    repaint
  }

  //////////////////////////////////////////////////////////////////////////
  // Config
  //////////////////////////////////////////////////////////////////////////

  Frame? frame { private set }

  Item[] items := [,] {set{&items = it; update}}

  Font? font
  Color? selectedItemColor
  Color? fontColor

  Item? highlight { set { &highlight = it; repaint } }

//////////////////////////////////////////////////////////////////////////
// Panel
//////////////////////////////////////////////////////////////////////////

  override Int lineCount() { items.size }

  override Int lineh() { itemh }

  override Int colCount := 5 { private set }

  override Int colw

  Int itemh() { font.height.max(18) }

  Int width

//////////////////////////////////////////////////////////////////////////
// Items
//////////////////////////////////////////////////////////////////////////

  Void addItem(Item item)
  {
    items.add(item)
    update()
  }

  private Void update()
  {
    max := 5
    items.each |x| { max = x.dis.size.max(max) }
    this.colCount = max + 2 // leave 2 for icon
    relayout
    repaint
  }

  Void clear() { items = Item[,] }

//////////////////////////////////////////////////////////////////////////
// Layout
//////////////////////////////////////////////////////////////////////////

  override Size prefSize(Hints hints := Hints.defVal)
  {
    Size(width ,200)
  }

//////////////////////////////////////////////////////////////////////////
// Painting
//////////////////////////////////////////////////////////////////////////
  override Void onPaintLines(Graphics g, Range lines)
  {
    g.font = font

    x := 0
    y := 0
    itemh := this.itemh

    Str? collapsedBase := null
    items.eachRange(lines) |item, i|
    {
      paintItem(g, item, x, y)
      y += itemh
    }
  }

  virtual Void paintItem(Graphics g, Item item, Int x, Int y)
  {
    if (item === this.highlight)
    {
      g.brush = selectedItemColor
      g.fillRect(0, y, size.w, itemh)
    }
    x += item.indent*20
    g.brush = fontColor
    if (item.icon != null) g.drawImage(item.icon, x, y)
    g.drawText(item.dis, x+20, y)
  }

//////////////////////////////////////////////////////////////////////////
// Eventing
//////////////////////////////////////////////////////////////////////////

  private Item? yToItem(Int y) { itemAtLine(yToLine(y)) }

  private Item? itemAtLine(Int line)
  {
    items.getSafe(line)
  }

  virtual Void doMouseUp(Event event)
  {
    obj := itemAtLine(yToLine(event.pos.y))
    event.consume

    if(obj==null)
    {
      return
    }

    if (event.count == 1 && event.button == 1)
      obj.selected(frame)

    if(! (obj is FileItem))
      return

    item := obj as FileItem
    if(item.file == null)
      return

    if (event.count == 1 && event.button == 1)
    {
      if(item.file.isDir && ! item.isProject)
      {
        toggleCollapse(item)
      }
      return
    }


    if (event.id === EventId.mouseUp && event.button == 3 && event.count == 1)
    {
      menu := item.popup(frame)

      if(item.file.isDir)
      {
        if(menu == null)
          menu = Menu()
        else
          menu.addSep

        menu.add(MenuItem{it.text="Refresh tree"
          it.onAction.add |e| {frame.curSpace.nav?.refresh(item.file)} })
        menu.add(MenuItem{it.text="Expand tree"
          it.onAction.add |e| {collapse(item);expand(item, true)} })
        menu.add(MenuItem{it.text="Collapse"
          it.onAction.add |e| {collapse(item)} })

        menu.addSep
        menu.add(MenuItem{it.text="Mark As Project"
          it.onAction.add |e| {
            (Sys.cur.commands.markAsProject as MarkAsProjectCmd).markAsProject(item.file, frame)
          }
        })
      }
      if (menu != null)
      {
        menu.open(event.widget, event.pos)
      }
      return
    }
  }

  ** Toggle collapse / expand an item (1 level)
  Void toggleCollapse(FileItem item)
  {
    if(item.collapsed)
      expand(item)
    else
      collapse(item)
  }

  ** Collapse a folder and all subfolders
  Void collapse(FileItem base)
  {
    base.setCollapsed(true)
    Int? start := items.eachWhile |item, index -> Int?|
    {
      return (item as FileItem).file == base.file ? index : null
    }
    start += 1
    index := start
    // hide children
    while(index < items.size)
    {
      that := items[index] as FileItem
      if( ! that.file.pathStr.startsWith(base.file.pathStr))
        break
      index++
    }

    items.removeRange(start ..< index)

    update
  }

  ** Expand a folder
  ** if recurse is true then expand any subfolder as well
  Void expand(FileItem base, Bool recurse := false)
  {
    base.setCollapsed(false)
    Int? index := items.eachWhile |item, index -> Int?|
    {
      return (item as FileItem).file == base.file ? index : null
    }
    newItems := FileItem[,]
    frame.curSpace.nav?.findItems(base.file, newItems, false,
                     (index == 0 ? "" : base.dis), recurse ? 1000 : null)
    items.insertAll(index + 1, newItems)

    update
  }

  ** Refresh an item tree, typically a directory
  ** base is the item (base of tree) being refreshed
  Void refresh(File base, FileItem[] newItems)
  {
    Int? start := items.eachWhile |item, index -> Int?|
    {
      return (item as FileItem).file.normalize == base.normalize ? index : null
    }

    if(start == null)
      return

    // drop the current subtree
    start += 1
    removeEnd := start
    pathStr := base.pathStr
    while(removeEnd < items.size)
    {
      that := items[removeEnd] as FileItem
      if(that.file.pathStr.startsWith(pathStr))
        removeEnd++
      else
        break // out of the subtree
    }

    items = items[0 ..< start].addAll(newItems).addAll(items[removeEnd .. -1])

    update
  }

  FileItem? findForFile(File f)
  {
    return files.find {it.file.normalize == f.normalize}
  }

  FileItem[] files()
  {
    return (FileItem[]) items.findAll{it is FileItem}
  }
}