sourcecamembert::BasicPlugin.fan

// History:
//  Jan 31 13 tcolar Creation
//

using netColarUtils
using concurrent
using fwt
using gfx

**
** BasicPlugin
** Base support for simple plugins
**
abstract const class BasicPlugin : Plugin
{
  //abstract static const Str _name

  ** Icon for this plugin projects
  abstract Image icon()

  ** Method that decides whether a given dir is a project or not
  abstract Bool isProject(File dir)

  ** Returns a name for the project
  virtual Str prjName(File prjDir) {prjDir.name}

  virtual Type optionsType() {BasicOptions#}

  ** Called upon this plugin env switch
  virtual Void envSwitched(BasicConfig newConf) {}

  virtual File? findProject(File curFile)
  {
    File? f := curFile
    while(f != null && ! isProject(f))
      f = f.parent
    return f
  }

  override PluginConfig? readConfig(Sys sys)
  {
    echo("Reading config of $name $envType")
    return BasicConfig(sys, name, optionsType, envType)
  }

  override Void onFrameReady(Frame frame, Bool initial := true)
  {
    // remove if alreday in
    plugins := (frame.menuBar as MenuBar).plugins
    plugins.remove(plugins.children.find{it->text == name})

    plugins.add(BasicMenu(frame, name, this.typeof))
  }

  override const |Uri -> Project?| projectFinder:= |Uri uri -> Project?|
  {
    f := uri.toFile
    if( ! f.exists || ! f.isDir) return null
    try
    {
      if(isProject(f))
        return projectItem(f)
    }
    catch(Err e){}
    return null
  }

  virtual Project projectItem(File f)
  {
    return Project
    {
      it.dis = prjName(f)
      it.dir = f.uri
      it.icon = this.icon
      it.plugin = this.typeof.pod.name
    }
  }

  override Space createSpace(Project prj)
  {
    return BasicSpace(Sys.cur.frame, prj.dir.toFile,
      this.typeof.pod.name, prj.icon.file.uri)
  }

  override Int spacePriority(Project prj)
  {
    if(prj.plugin != this.typeof.pod.name)
      return 0
    return 50
  }

  static BasicConfig config(Str podName)
  {
    return (BasicConfig) PluginManager.cur.conf(podName)
  }
}

@Serializable
const class BasicConfig : PluginConfig
{
  const BasicOptions options
  const BasicEnv[] envs := [,]

  const AtomicInt curEnvIndex := AtomicInt()

  new make(Sys sys, Str name, Type optionsType, Type envType)
  {
    // load options
    cfgFolder := sys.optionsFile.parent
    optsFile := cfgFolder + `$name/options.props`
    options = (BasicOptions) JsonSettings.load(optsFile, optionsType)

    // load envs
    tmp:= BasicEnv[,]
    folder := cfgFolder + `$name/`
    folder.listFiles.each
    {
      if(it.name.startsWith("env_"))
      {
        try
        {
          env := (BasicEnv) JsonSettings.load(it, envType)
          tmp.add(env)
        }
        catch(Err e) Sys.log.err("Failed to load $it.osPath !", e)
      }
    }
    if(tmp.isEmpty)
    {
      env := envType.make()
      JsonSettings{}.save(env, (cfgFolder + `$name/env_default.props`).out)
      tmp.add(env)
    }
    envs = tmp.sort |a, b| {a.order <=> b.order}
  }

  BasicEnv? envByName (Str name) {envs.find {it.name == name} }

  Void selectEnv(Str name)
  {
    Int? index := envs.eachWhile |env, index -> Int?| {if(env.name == name) return index; return null}
    if(index != null)
      curEnvIndex.val = index
  }

  BasicEnv curEnv() {envs[curEnvIndex.val]}
}

// Subclasses need to be const, serializable and ususally have a |this| constructor as well
@Serializable
const class BasicEnv
{
  @Setting{help = ["Env name"]}
  const Str envName := "default"

  @Setting{help = ["Env order"]}
  const Int envOrder := 10

  virtual Int order() {envOrder}
  virtual Str name() {envName}
  virtual Uri? envHome() {return null}

  new make(|This|? f := null)
  {
    if (f != null) f(this)
  }
}

class BasicMenu : Menu
{
  Menu envs

  new make(Frame frame, Str name, Type pluginType)
  {
    text = name

    envs = Menu
    {
      it.text = "Switch env"
    }

    first := true
    nm := pluginType.field("_name",false)?.get ?: pluginType.pod.name
    config := (BasicConfig) pluginType.method("config").call(nm)
    config.envs.each |env|
    {
      envs.add(MenuItem{
        it.command = BasicSwitchEnvCmd(env.name, pluginType).asCommand;
        it.mode = MenuItemMode.radio
        it.selected = first
      })
      first = false
    }

    add(envs)
  }
}

const class BasicSwitchEnvCmd : Cmd
{
  override const Str name
  const Type pluginType

  override Void invoke(Event event)
  {
    MenuItem mi := event.widget
    if(mi.selected)
    {
      Desktop.callAsync |->|
      {
        frame.curEnv = name
        nm := pluginType.field("_name",false)?.get ?: pluginType.pod.name
        config := (BasicConfig) pluginType.method("config").call(nm)
        config.selectEnv(name)

        // notify plugin of env switch
        plugin := PluginManager.cur.plugins[pluginType.pod.name] as BasicPlugin
        plugin.envSwitched(config)
      }
    }
  }

  new make(Str envName, Type pluginType)
  {
    this.name = envName
    this.pluginType = pluginType
  }
}

@Serializable
const class BasicOptions
{
  ** Default constructor with it-block
  new make(|This|? f := null)
  {
    if (f != null) f(this)
  }
}

class BasicSpace : FileSpaceBase
{
  override Str? plugin

  new make(Frame frame, File dir, Str plugin, Uri iconUri)
    : super(frame, dir, 220, Image(iconUri))
  {
    this.plugin = plugin
  }

  override Int match(FileItem item)
  {
    if (!FileUtil.contains(this.dir, item.file)) return 0
    // if project we don't want to open them here but in a proper space
    if (item.isProject) return 0
    return 1000 + this.dir.path.size
  }

  static Space loadSession(Frame frame, Str:Str props)
  {
    make(frame, File(props.getOrThrow("dir").toUri), props.getOrThrow("pluginName"),
         props.getOrThrow("icon").toUri)
  }

  override Str:Str saveSession()
  {
    props := ["dir": dir.uri.toStr, "icon" : icon.file.uri.toStr,
    "pluginName" : plugin]
    return props
  }
}