//
// 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
**
** Top-level frame
**
class Frame : Window
{
//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////
** Construct for given system
new make() : super(null)
{
// initialize
this.icon = Image(`fan://icons/x32/blueprints.png`)
Actor.locals["frame"] = this
history.pushListeners.add(
|history| {recentPane.update(history)}
)
// menu
menuBar = MenuBar()
// eventing
onClose.add |Event e| { e.consume; Sys.cur.commands.exit.invoke(e) }
onKeyDown.add |e| { trapKeyDown(e) }
onDrop = |data| { doDrop(data) }
// build UI
this.spaceBar = SpaceBar(this)
this.spacePane = ContentPane()
this.statusBar = StatusBar(this)
this.console = Console(this)
this.helpPane = HelpPane(this)
this.recentPane = RecentPane(this)
this.content = BgEdgePane
{
it.top = spaceBar
it.center = SashPane
{
orientation = Orientation.vertical
weights = [80, 20]
SashPane
{
orientation = Orientation.horizontal
weights = [80, 20] // Not shown by default, will be shown if any slots
spacePane,
SashPane
{
orientation = Orientation.vertical
weights = [30, 70]
recentPane,
helpPane,
}
},
console,
}
it.bottom = statusBar
}
// load session and home space
loadSession
switchSpace(spaces.first)
curSpace = spaces.first
onFrameReady
}
//////////////////////////////////////////////////////////////////////////
// Access
//////////////////////////////////////////////////////////////////////////
** Last license template used
Str? lastLicense
** Current space index
Space curSpace
** Current space file
File? curFile() { curSpace.curFile }
** If current space has loaded a view
View? curView() { curSpace.view }
** Currently open spaces
Space[] spaces := [,] { private set }
** Console
Console console { private set }
HelpPane helpPane { private set }
RecentPane recentPane { private set }
** Navigation history
History history := History() { private set }
ProcessUtil process := ProcessUtil() { private set }
PaneState paneState := PaneState{}
Void onFrameReady()
{
Desktop.callAsync |->| {
PluginManager.cur.onFrameReady(this)
ProjectRegistry.scan
}
}
//////////////////////////////////////////////////////////////////////////
// Space Lifecycle
//////////////////////////////////////////////////////////////////////////
** Select given space (upon being picked in spacebar)
Void select(Space space)
{
switchSpace(space)
}
** Route to best open space or open new one for given item.
Void goto(Item? item)
{
if(item == null)
return
// if this item is one of our marks, let console know
markIndex := marks.indexSame(item)
if (markIndex != null)
{
&curMark = markIndex
console.highlight(item)
}
// check if current view is on current item, if so nothing to do
if ( !(item is FileItem) || curView?.file == (item as FileItem).file)
{
curView.onGoto(item)
return
}
// confirm if we should close
if (!confirmClose) return
// save current file line number
if (curView != null)
filePosHis[curView.file] = curView.curPos
// unload current view
try
curView?.onUnload
catch (Err e)
Sys.log.err("View.onUnload", e)
// Push into history
if (item != null)
history.push(curSpace, item)
// find best open space to handle item
best:= matchSpace(item)
if (best == null)
{
// create new space
best = create(item)
if (best == null)
{
Sys.log.info("WARN: Cannot create space $item.dis")
return
}
else
{
spaces.add(best)
}
}
best.goto(item)
switchSpace(best)
// now check if we have view to handle line/col
if (curView != null) Desktop.callAsync |->|
{
if (item == null || item.loc.line <= 0)
{
pos := filePosHis[curView.file]
if (pos != null) item = Item.makeLoc(pos.line, pos.col, null).setDis(pos.toStr)
}
if (item != null) curView.onGoto(item)
}
}
Void switchSpace(Space? space)
{
if(space == null)
space = spaces.first
if(space <=> curSpace != 0)
{
curSpace = space
// update space ui
spacePane.content = space.ui
deepRelayout(spacePane)
// update spacebar
spaceBar.onLoad
spaceBar.relayout
// update status bar
updateStatus
// update helpPane (select matching docs for this space, in combo)
if(space.plugin!= null)
{
plugin := Sys.cur.plugin(space.plugin)
if(plugin.docProvider != null)
helpPane.provider.selected = plugin.docProvider.dis
}
}
}
** Recursively relayout the whole widget and children
** I had some issues with the space not relayout-ing properly
** some subcomponent would disapear etc ...
** So while this is not optimal this works
static Void deepRelayout(Widget w)
{
w.children.each { deepRelayout(it) }
w.relayout
}
Int? spaceIndex(Space space)
{
return spaces.eachWhile |Space sp, Int i -> Int?|
{
return sp <=> space == 0 ? i : null
}
}
Void closeSpace(Space space)
{
i := spaceIndex(space)
if (i == null) return // home space
spaces.removeAt(i)
if (curSpace <=> space == 0)
{
space = spaces.getSafe(i) ?: spaces.last
switchSpace(space)
}
else
spaceBar.onLoad
}
Void closeOtherSpaces(Space space)
{
spaces = [spaces.first, space]
if (curSpace <=> space != 0)
{
switchSpace(space)
}
else
spaceBar.onLoad
}
Void closeSpaces()
{
spaces = [spaces.first]
switchSpace(spaces.first)
}
private Space? matchSpace(Item item)
{
// find best match
Space? bestSpace := null
Int bestPriority := 0
this.spaces.each |s|
{
priority := s.match(item)
if (priority == 0) return
if (priority >= bestPriority)
{
bestSpace = s; bestPriority = priority
}
}
return bestSpace
}
private Space? create(FileItem item)
{
if (item.space != null && ! (item.space is IndexSpace))
return item.space
file := item.file
pSpace := createPluginSpace(file, 11)
if(pSpace != null)
return pSpace
// if we found no spaces with prio over 10, use filespace
dir := file.isDir ? file : file.parent
return FileSpace(this, dir)
}
** Find and create the space with the highest prio for given file
** If prio > minPrio return thge space istance, otherwise null
private Space? createPluginSpace(File file, Int minPrio)
{
Plugin? plugin
Project? prj
ProjectRegistry.projects.each|project, uri|
{
if(FileUtil.contains(uri.toFile, file))
{
p := Sys.cur.plugins[project.plugin]
if(p != null && p.spacePriority(project) >= minPrio)
{
if(plugin == null || p.spacePriority(project) >= plugin.spacePriority(project))
{
// Of all the matching plugins with same prio, use the one with the "narrowest" path
// ie: "best" subproject
if(prj == null || project.dir.pathStr.size > prj.dir.pathStr.size)
{
plugin = p
prj = project
}
}
}
}
}
return plugin?.createSpace(prj)
}
//////////////////////////////////////////////////////////////////////////
// Marks (build errors/finds)
//////////////////////////////////////////////////////////////////////////
Item[] marks := Item[,]
{
set { &marks = it; &curMark = -1; curView?.onMarks(it) }
}
internal Int curMark
{
set
{
if (it >= marks.size) it = marks.size - 1
if (it < 0) it = 0
&curMark = it
if (!marks.isEmpty) goto(marks[it])
}
}
//////////////////////////////////////////////////////////////////////////
// View Lifecycle
//////////////////////////////////////////////////////////////////////////
private Bool confirmClose()
{
if (curView == null || !curView.dirty) return true
r := Dialog.openQuestion(this, "Save changes to $curView.file.name?",
[Dialog.yes, Dialog.no, Dialog.cancel])
if (r == Dialog.cancel) return false
if (r == Dialog.yes) save
return true
}
Void save()
{
if (curView == null) return
curView.onSave
if(curFile != null)
PluginManager.cur.onFileSaved(curFile)
curView.dirty = false
updateStatus
}
Void updateStatus()
{
title := "Camembert"
if (curView != null)
{
title += " $curView.file.name"
if (curView.dirty)
title += "*"
}
this.title = title
this.statusBar.update
}
//////////////////////////////////////////////////////////////////////////
// Eventing
//////////////////////////////////////////////////////////////////////////
internal Void trapKeyDown(Event event)
{
cmd := Sys.cur.commands.findByKey(event.key)
if (cmd != null)
{
cmd.invoke(event)
}
if(event.keyChar >= '1'
&& event.keyChar<='9'
&& event.key.modifiers.toStr == Sys.cur.shortcuts.recentModifier)
{
Sys.cur.commands.recent.invoke(event)
}
}
private Void doDrop(Obj data)
{
files := data as File[]
if (files == null || files.isEmpty) return
file := files.first
goto(FileItem.makeFile(file))
}
Void toggleTextOnly()
{
if(paneState.textOnly)
{
// restore the state
if(paneState.helpOn)
helpPane.show
if(paneState.recentOn)
recentPane.show
if(paneState.consoleOn)
console.open
curSpace.showNav(true)
paneState.textOnly = false
}
else
{
// Save the state
paneState.textOnly = true
paneState.helpOn = helpPane.visible
paneState.recentOn = recentPane.visible
paneState.consoleOn = console.isOpen
// hide panes
helpPane.hide
recentPane.hide
console.close
curSpace.showNav(false)
}
}
//////////////////////////////////////////////////////////////////////////
// Session State
//////////////////////////////////////////////////////////////////////////
internal Void loadSession()
{
// read props
props := Str:Str[:]
try
if (sessionFile.exists) props = sessionFile.readProps
catch (Err e)
Sys.log.err("Cannot load session: $sessionFile", e)
// read bounds
this.bounds = Rect(props["frame.bounds"] ?: "100,100,600,500")
this.lastLicense = props["last.license"]
// spaces
spaces := Space[,]
for (i:=0; true; ++i)
{
// check for "space.nn.type"
prefix := "space.${i}."
typeKey := "${prefix}type"
type := props[typeKey]
if (type == null) break
// get all "space.nn.xxxx" props
spaceProps := Str:Str[:]
props.each |val, key|
{
if (key.startsWith(prefix))
spaceProps[key[prefix.size..-1]] = val
}
// load the space from session
try
{
loader := Type.find(type).method("loadSession")
Space space := loader.callList([this, spaceProps])
spaces.add(space)
}
catch (Err e) Sys.log.err("ERROR: Cannot load space $type", e)
}
// always insert IndexSpace
if (spaces.first isnot IndexSpace)
spaces.insert(0, IndexSpace(this))
// save spaces
this.spaces = spaces
}
internal Void saveSession()
{
props := Str:Str[:]
props.ordered = true
// frame state
props["saved"] = DateTime.now.toStr
if(lastLicense != null)
props["last.license"] = lastLicense
props["frame.bounds"] = this.bounds.toStr
// spaces
spaces.each |space, i|
{
props["space.${i}.type"] = space.typeof.qname
spaceProps := space.saveSession
spaceProps.keys.sort.each |key|
{
props["space.${i}.${key}"] = spaceProps.get(key)
}
}
// write
try
sessionFile.writeProps(props)
catch (Err e)
Sys.log.err("Cannot save $sessionFile", e)
}
//////////////////////////////////////////////////////////////////////////
// Private Fields
//////////////////////////////////////////////////////////////////////////
private File sessionFile := Sys.cur.optionsFile.parent + `state/session.props`
private SpaceBar spaceBar
private ContentPane spacePane
private StatusBar statusBar
private File:Pos filePosHis := [:]
Str? curEnv
}
class PaneState
{
new make(|This| f){}
Bool textOnly
Bool consoleOn
Bool helpOn
Bool recentOn
}