sourcecamAxonPlugin::AxonSpace.fan

// History:
//   12 5 12 - Thibaut Colar Creation

using camembert
using fwt
using gfx
using concurrent
//using haystack

**
** AxonSpace
**
@Serializable
class AxonSpace : BaseSpace
{
  const AxonSyncActor syncActor

  override View? view
  override Nav? nav
  override Image icon() { funcIcon }

  static const Image funcIcon := Image(`fan://icons/x16/func.png`)
  static const Image syncIcon := Image(`fan://icons/x16/sync.png`)
  static const Image helpIcon := Image(`fan://icons/x16/question.png`)
  static const Image errorIcon := Image(`fan://icons/x16/err.png`)

  new make(Frame frame, File dir, File? file := null) :
      super(frame, dir.normalize, file)
  {
    AxonActors acts := Sys.cur.plugins[Pod.of(this).name]->actors->val
    syncActor = acts.forProject(dir)

    view = View.makeBest(frame, this.file)
    nav = AxonNav(frame, dir, AxonItemBuilder(this), AxonItem.makeFile(this.file))

    navParent.content = navPane(nav)

    evalText := Text
    {
      data := AxonActorData {action=AxonActorAction.evalLast}
      it.text = (Str) syncActor.send(data).get
    }
    evalText.onKeyUp.add |Event e| {evalKeyUp(e, evalText)}
    evalText.onAction.add |e| {eval(evalText.text)}
    viewParent.content = EdgePane
    {
      center = file == null ? null : View.makeBest(frame, file)
      bottom = EdgePane{left = Label{it.text="Eval:"}; center = evalText}
    }
  }

  override Str:Str saveSession()
  {
    ["dir":dir.uri.toStr, "file":file.uri.toStr]
  }

  static Space loadSession(Frame frame, Str:Str props)
  {
  // sys.plugin......
    make(frame, props.getOrThrow("dir").toUri.toFile,
      props.get("file")?.toUri?.toFile)
  }

  override Int match(FileItem item)
  {
    // add 1000 so always preferred over filespace
    if (!FileUtil.contains(this.dir, item.file))
      return 0
    return 1000
  }

  Pane navPane(Nav nav)
  {
    return BgEdgePane
    {
      top = BgEdgePane
      {
        left = GridPane
        {
          numCols = 1
          Button
          {
            it.image = syncIcon
            it.onAction.add |e| {sync(syncActor, dir)}
          },
        }
        right = /*GridPane
        {
          numCols = 2*/
          Button
          {
            // TODO: set selected according to Cur Status of actor
            it.selected = autoStatus()
            it.text = "AutoSync"
            it.mode = ButtonMode.toggle
            it.onAction.add |e| {autoSync()}
          }/*,
          Label{image = syncIcon},
        }*/
      }
      center = nav.list
    }
  }

  override Void updateView(View newView)
  {
    dest := (viewParent.content as EdgePane)
    dest.center = newView
    view = newView
    dest.relayout
  }

  ** Enable / disable autosync
  Void autoSync()
  {
    on := autoStatus()
    if( ! on)
    {
      data := AxonActorData {action=AxonActorAction.autoOn}
      result := syncActor.send(data).get
      log("Auto sync -> on")
      sync(syncActor, dir) // kick off sync
    }
    else
    {
      data := AxonActorData {action=AxonActorAction.autoOff}
      log("Auto sync -> off")
      result := syncActor.send(data).get
    }
  }

  ** log to console
  ** NEEDS TO BE IMMUTABLE - SED IN CALLBACK
  static Void log(Str msg)
  {
    Sys.cur.frame.console.append([Item(msg)])
  }

  ** Sync the local project with the server
  ** NEEDS TO BE IMMUTABLE
  static Void sync(AxonSyncActor syncActor, File dir)
  {
    pass := getPass(syncActor, dir)
    if(pass == null)
      return // cancelled

    log("Sync")
    data := AxonActorData
    {
      action = AxonActorAction.sync
      password = pass
      callback = |Obj? obj|
      {
        if(obj != null && obj is AxonSyncInfo)
        {
          info := obj as AxonSyncInfo
          if( ! info.createdFiles.isEmpty)
          {
            Sys.cur.frame.curSpace.nav.refresh(dir)
          }
        }
        else
          showActorResults(obj)
      }
    }
    syncActor.send(data)
  }

  ** chck current autosync status
  Bool autoStatus()
  {
    data := AxonActorData {action=AxonActorAction.autoStatus}
    return (Bool) syncActor.send(data).get
  }

  ** Run an eval on the server and show the results
  ** TODO: run async ... but UI refresh are tricky (Not on UI thread business)
  Void eval(Str toEval)
  {
    pass := getPass(syncActor, dir)
    if(pass == null)
      return // cancelled

    data := AxonActorData
    {
      callback = |Obj? obj| {showActorResults(obj)}
      action = AxonActorAction.eval
      password = pass
      it.eval = toEval
    }
    syncActor.send(data)
  }

  Void remoteDelete(Str funcName)
  {
    pass := getPass(syncActor, dir)
    if(pass == null)
      return // cancelled

    data := AxonActorData
    {
      callback = |Obj? obj| {showActorResults(obj, true)}
      action = AxonActorAction.deleteFunc
      deleteFunc = funcName
      password = pass
    }
    syncActor.send(data)

  }

  ** Get the connection password. Ask user for it if we don't have it yet
  ** Returns null if cancel was pressed on dialog
  ** NEEDS TO BE IMMUTABLE - SED IN CALLBACK
  static Str? getPass(AxonSyncActor syncActor, File dir)
  {
    Str? pass := ""
    data := AxonActorData {action=AxonActorAction.needsPassword; password=pass}
    result := syncActor.send(data).get
    showActorResults(result, true)
    if(result == true)
      pass = Dialog.openPromptStr(Sys.cur.frame, "Password for project $dir.name:")
    return pass
  }

  ** Display call results to user
  ** Can display error messages and sync thead infos as well
  ** NEEDS TO BE IMMUTABLE - SED IN CALLBACK
  static Void showActorResults(Obj? result, Bool errorOnly := false)
  {
    if(result == null) return
    if(errorOnly && ! (result is Err)) return

    if(result is Unsafe)
      showActorResults((result as Unsafe).val)
    else if(result is Err)
    {
      e := result as Err
      items := [,]
      e.traceToStr.splitLines.each |line|
      {
        items.add(Item.makeStr(line).setIcon(errorIcon))
      }
      Sys.cur.frame.console.append(items)
    }
    else if(result is Grid)
    {
      g := result as Grid
      meta := g.meta
      if(meta.has("errTrace"))
      {
        items := [,]
        (meta["errTrace"] as Str)?.splitLines?.each |line|
        {
          items.add(Item.makeStr(line).setIcon(errorIcon))
        }
        Sys.cur.frame.console.append(items)
      }
      else
        Sys.cur.frame.console.append([AxonGridItem.makeGrid(g)])
    }
    else if(result is Str)
    {
      s := (Str) result
      log(s)
    }
  }

  ** Provides eval history navigation
  Void evalKeyUp(Event event, Text eval)
  {
    if(event.key == Key.up)
    {
      data := AxonActorData {action=AxonActorAction.evalUp}
      eval.text = (Str) syncActor.send(data).get
    }
    if(event.key == Key.down)
    {
      data := AxonActorData {action=AxonActorAction.evalDown}
      eval.text = (Str) syncActor.send(data).get
    }
  }
}