//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 21 Jul 08 Br ian Frank Creation
//
using concurrent
using gfx
using fwt
using compiler
** Manages all the main window's commands
internal class Commands
{
//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////
new make(Frame frame)
{
this.frame = frame
this.byId = Str:FluxCommand[:]
this.viewManaged = ViewManagedCommand[,]
Type.of(this).fields.each |Field f|
{
cmd := f.get(this) as FluxCommand
if (cmd != null)
{
cmd.frame = frame
byId.add(cmd.id, cmd)
if (cmd is ViewManagedCommand)
viewManaged.add(cmd)
}
}
// build tools search path (homeDir and maybe also workDir)
this.toolsDirs = [Env.cur.homeDir +`etc/flux/tools/`]
if (Env.cur.homeDir != Env.cur.workDir)
this.toolsDirs.add(Env.cur.workDir +`etc/flux/tools/`)
}
//////////////////////////////////////////////////////////////////////////
// Menu Bar
//////////////////////////////////////////////////////////////////////////
internal Menu buildMenuBar()
{
return Menu
{
buildFileMenu,
buildEditMenu,
buildViewMenu,
buildHistoryMenu,
buildToolsMenu,
buildHelpMenu,
}
}
private Menu buildFileMenu()
{
return Menu
{
text = Flux.locale("file.name")
addCommand(newTab)
addCommand(openLocation)
addCommand(closeTab)
addSep
addCommand(save)
addCommand(saveAll)
addSep
addCommand(exit)
}
}
private Menu buildEditMenu()
{
return Menu
{
text = Flux.locale("edit.name")
onOpen.add { onEditMenuOpen(it) }
addCommand(undo)
addCommand(redo)
addSep
addCommand(cut)
addCommand(copy)
addCommand(paste)
addSep
addCommand(find)
addCommand(findNext)
addCommand(findPrev)
addCommand(findInFiles)
addSep
addCommand(replace)
addCommand(replaceInFiles)
addSep
addCommand(goto)
addCommand(gotoFile)
addSep
addCommand(jumpNext)
addCommand(jumpPrev)
addSep
addCommand(selectAll)
}
}
private Menu buildViewMenu()
{
menu := Menu
{
text = Flux.locale("view.name")
onOpen.add { onViewMenuOpen(it) }
addCommand(reload)
addSep
}
types := Flux.qnamesToTypes(Env.cur.index("flux.sideBar"))
types = types.dup.sort |Type a, Type b->Int| { return a.name <=> b.name }
types.each |Type t|
{
cmd := SideBarCommand(frame, t)
byId.add(cmd.id, cmd)
menu.addCommand(cmd)
}
return menu
}
private Menu buildHistoryMenu()
{
menu := Menu
{
text = Flux.locale("history.name")
onOpen.add { onHistoryMenuOpen(it) }
addCommand(back)
addCommand(forward)
addCommand(up)
addCommand(home)
addCommand(recent)
addSep
}
historyMenuSize = menu.children.size
return menu
}
private Menu buildToolsMenu()
{
menu := Menu
{
text = Flux.locale("tools.name")
addCommand(options)
addCommand(refreshTools)
addSep
}
toolsMenuSize = menu.children.size
toolsMenu = menu
refreshToolsMenu
return menu
}
private Menu buildHelpMenu()
{
return Menu
{
text = Flux.locale("help.name")
addCommand(about)
}
}
//////////////////////////////////////////////////////////////////////////
// Tool Bar
//////////////////////////////////////////////////////////////////////////
internal ToolBar buildToolBar()
{
return ToolBar
{
addCommand(back)
addCommand(forward)
addCommand(up)
addCommand(reload)
addCommand(recent)
}
}
//////////////////////////////////////////////////////////////////////////
// Update
//////////////////////////////////////////////////////////////////////////
Void update()
{
tab := frame.view.tab
back.enabled = tab.backEnabled
forward.enabled = tab.forwardEnabled
up.enabled = tab.upEnabled
closeTab.enabled = frame.views.size > 1
updateEdit
updateSave
}
Void updateEdit()
{
tab := frame.view.tab
undo.enabled = tab.undoEnabled
redo.enabled = tab.redoEnabled
}
Void updateSave()
{
tab := frame.view.tab
save.enabled = tab.dirty
saveAll.enabled = frame.views.any |View v->Bool| { return v.dirty }
}
Void disableViewManaged()
{
viewManaged.each |Command c| { c.enabled = false }
}
//////////////////////////////////////////////////////////////////////////
// Eventing
//////////////////////////////////////////////////////////////////////////
Void onEditMenuOpen(Event event)
{
undo := Flux.locale("undo.name")
redo := Flux.locale("redo.name")
stack := frame.view.commandStack
if (stack.listUndo.size > 0) undo = "$undo $stack.listUndo.last.name"
if (stack.listRedo.size > 0) redo = "$redo $stack.listRedo.last.name"
kids := event.widget.children
kids[0]->text = undo
kids[1]->text = redo
}
Void onViewMenuOpen(Event event)
{
event.widget.each |Widget w|
{
if (w is MenuItem && w->command is SideBarCommand)
{
cmd := w->command as SideBarCommand
cmd.update
}
}
}
Void onHistoryMenuOpen(Event event)
{
// remove any old items on the history menu
menu := event.widget
children := menu.children
(historyMenuSize..<children.size).each |Int i|
{
menu.remove(children[i])
}
// add most 10 most recent
recent := History.load.items
if (recent.size > 10) recent = recent[0..9]
recent.each |HistoryItem item|
{
menu.add(toHistoryMenuItem(item))
}
}
MenuItem toHistoryMenuItem(HistoryItem item)
{
name := item.uri.name
if (item.uri.isDir) name += "/"
return MenuItem
{
text = name
onAction.add |->| { frame.load(item.uri) }
}
}
Void refreshToolsMenu()
{
// remove any old items on the tools menu
children := toolsMenu.children
(toolsMenuSize..<children.size).each |Int i|
{
toolsMenu.remove(children[i])
}
// add new menu items
toolsDirs.each |dir| { addToolScripts(toolsMenu, dir, true) }
}
Void addToolScripts(Menu menu, File f, Bool top := false)
{
if (f.isDir)
{
if (!top) { sub := Menu { text=f.name }; menu.add(sub); menu = sub }
FileResource.sortFiles(f.list).each |File k| { addToolScripts(menu, k) }
}
else
{
if (f.ext == "fan") menu.addCommand(ToolScriptCommand(frame, f))
}
}
//////////////////////////////////////////////////////////////////////////
// Commands
//////////////////////////////////////////////////////////////////////////
internal Frame frame
// File
internal FluxCommand newTab := NewTabCommand()
internal FluxCommand openLocation := OpenLocationCommand()
internal FluxCommand closeTab := CloseTabCommand()
internal FluxCommand save := SaveCommand()
internal FluxCommand saveAll := SaveAllCommand()
internal FluxCommand exit := ExitCommand()
// Edit
internal FluxCommand undo := UndoCommand()
internal FluxCommand redo := RedoCommand()
internal FluxCommand cut := CutCommand()
internal FluxCommand copy := CopyCommand()
internal FluxCommand paste := PasteCommand()
internal FluxCommand find := ViewManagedCommand(CommandId.find)
internal FluxCommand findNext := ViewManagedCommand(CommandId.findNext)
internal FluxCommand findPrev := ViewManagedCommand(CommandId.findPrev)
internal FluxCommand findInFiles := FindInFilesCommand()
internal FluxCommand replace := ViewManagedCommand(CommandId.replace)
internal FluxCommand replaceInFiles := ReplaceInFilesCommand()
internal FluxCommand goto := ViewManagedCommand(CommandId.goto)
internal FluxCommand gotoFile := GotoFileCommand()
internal FluxCommand jumpNext := JumpNextCommand()
internal FluxCommand jumpPrev := JumpPrevCommand()
internal FluxCommand selectAll := SelectAllCommand()
// View
internal FluxCommand reload := ReloadCommand()
// History
internal FluxCommand back := BackCommand()
internal FluxCommand forward := ForwardCommand()
internal FluxCommand up := UpCommand()
internal FluxCommand home := HomeCommand()
internal FluxCommand recent := RecentCommand()
// Tools
internal FluxCommand options := OptionsCommand()
internal FluxCommand refreshTools := RefreshToolsCommand()
// Help
internal FluxCommand about := AboutCommand()
// misc fields
internal ViewManagedCommand[] viewManaged
internal Str:FluxCommand byId
internal Int historyMenuSize
internal Menu? toolsMenu
internal Int toolsMenuSize
internal File[] toolsDirs
}
//////////////////////////////////////////////////////////////////////////
// Util Commands
//////////////////////////////////////////////////////////////////////////
** ViewManagedCommands are managed by the current view
internal class ViewManagedCommand : FluxCommand
{
new make(Str id) : super(id) { enabled=false }
override Void invoked(Event? event)
{
frame.view.onCommand(id, event)
}
}
//////////////////////////////////////////////////////////////////////////
// File
//////////////////////////////////////////////////////////////////////////
** Open a new view tab.
internal class NewTabCommand : FluxCommand
{
new make() : super(CommandId.newTab) {}
override Void invoked(Event? event)
{
frame.load(`flux:start`, LoadMode { newTab=true })
}
}
** Focus the uri location field
internal class OpenLocationCommand : FluxCommand
{
new make() : super(CommandId.openLocation) {}
override Void invoked(Event? event) { frame.locator.onLocation(event) }
}
** Close current view tab.
internal class CloseTabCommand : FluxCommand
{
new make() : super(CommandId.closeTab) {}
override Void invoked(Event? event)
{
if (frame.views.size > 1)
frame.tabPane.close(frame.view.tab)
}
}
** Save current view
internal class SaveCommand : FluxCommand
{
new make() : super(CommandId.save) {}
override Void invoked(Event? event) { frame.view.tab.save }
}
** Save every dirty view
internal class SaveAllCommand : FluxCommand
{
new make() : super(CommandId.saveAll) {}
override Void invoked(Event? event)
{
frame.views.each |View view| { view.tab.save }
}
}
** Exit the application
internal class ExitCommand : FluxCommand
{
new make() : super(CommandId.exit) {}
override Void invoked(Event? event)
{
dirty := frame.views.findAll |View v->Bool| { return v.dirty }
if (dirty.size > 0)
{
grid := GridPane { Label { text=Flux.locale("saveChanges"); font=Desktop.sysFont.toBold },}
dirty.each |View v|
{
grid.add(InsetPane(0,0,0,8) {
Button { it.mode=ButtonMode.check; it.text=v.resource.uri.toStr; it.selected=true },
})
}
saveSel := ExitSaveCommand(Pod.of(this), "saveSelected")
saveNone := ExitSaveCommand(Pod.of(this), "saveNone")
cancel := ExitSaveCommand(Command#.pod, "cancel")
pane := ConstraintPane
{
minw = 400
add(InsetPane(0,0,12,0).add(grid))
}
d := Dialog(frame) { title="Save"; body=pane; commands=[saveSel,saveNone,cancel] }
r := d.open
if (r == cancel) return
if (r == saveSel)
{
grid.children.each |Widget w, Int i|
{
if (w isnot InsetPane) return
c := w.children.first as Button
v := dirty[i-1]
if (c.selected) v.tab.save
}
}
}
flux::Main.exit(frame)
}
}
internal class ExitSaveCommand : Command
{
new make(Pod pod, Str keyBase) : super.makeLocale(pod, keyBase) {}
override Void invoked(Event? e) { window?.close(this) }
}
//////////////////////////////////////////////////////////////////////////
// Edit
//////////////////////////////////////////////////////////////////////////
** Undo last command
internal class UndoCommand : FluxCommand
{
new make() : super(CommandId.undo) {}
override Void invoked(Event? event) { frame.view.tab.undo }
}
** Redo last undone command
internal class RedoCommand : FluxCommand
{
new make() : super(CommandId.redo) {}
override Void invoked(Event? event) { frame.view.tab.redo }
}
** Cut command routes to 'focus?.cut'
internal class CutCommand : FluxCommand
{
new make() : super(CommandId.cut) {}
override Void invoked(Event? event)
{
try { Desktop.focus?->cut } catch (UnknownSlotErr e) {}
}
}
** Copy command routes to 'focus?.copy'
internal class CopyCommand : FluxCommand
{
new make() : super(CommandId.copy) {}
override Void invoked(Event? event)
{
try { Desktop.focus?->copy } catch (UnknownSlotErr e) {}
}
}
** Paste command routes to 'focus?.paste'
internal class PasteCommand : FluxCommand
{
new make() : super(CommandId.paste) {}
override Void invoked(Event? event)
{
try { Desktop.focus?->paste } catch (UnknownSlotErr e) {}
}
}
** Copy command routes to 'focus?.selectAll'
internal class SelectAllCommand : FluxCommand
{
new make() : super(CommandId.selectAll) {}
override Void invoked(Event? event)
{
try { Desktop.focus?->selectAll } catch (UnknownSlotErr e) {}
}
}
//////////////////////////////////////////////////////////////////////////
// Search
//////////////////////////////////////////////////////////////////////////
** Find in files
internal class FindInFilesCommand : FluxCommand
{
new make() : super(CommandId.findInFiles) {}
override Void invoked(Event? event) { FindInFiles.dialog(frame) }
}
** Replace in files
internal class ReplaceInFilesCommand : FluxCommand
{
new make() : super(CommandId.replaceInFiles) {}
override Void invoked(Event? event) { Dialog.openInfo(frame, "TODO: Replace in Files") }
}
** Goto file
internal class GotoFileCommand : FluxCommand
{
new make() : super(CommandId.gotoFile) {}
override Void invoked(Event? event)
{
if (!FileIndex.instance.ready)
{
Dialog.openInfo(frame, "Still indexing file system...")
return
}
// build dialog
Str last := Actor.locals.get("fluxText.gotoFileLast", "")
field := Text { it.text = last; it.prefCols = 20}
pane := GridPane
{
numCols = 4
expandCol = 1
halignCells=Halign.fill
Label { text="Goto File:" },
field,
Button { image=Flux.icon(`/x16/refresh.png`); onAction.add { rebuild(it.widget) } },
Button { image=Flux.icon(`/x16/question.png`); onAction.add { showHelp } },
}
field.onAction.add |e| { e.widget.window.close(Dialog.ok) }
// prompt user
r := Dialog(frame) { title="Goto File"; body=pane; commands=[Dialog.ok, Dialog.cancel] }.open
if (r != Dialog.ok) return
target := field.text
Actor.locals.set("fluxText.gotoFileLast", target)
// lookup target in our index
files := FileIndex.instance.find(target)
if (files.size == 0)
{
Dialog.openErr(frame, "File not found: $target")
return
}
// if exactly one match, go straight there
if (files.size == 1)
{
frame.load(files[0])
return
}
// prompt user with list of files (use same dialog as Recent Files)
items := HistoryItem[,]
files.each |uri| { items.add(HistoryItem { it.uri = uri; it.iconUri = FileResource.fileToIcon(uri.toFile).uri }) }
Dialog? dlg
picker := HistoryPicker(items, true) |HistoryItem item, Event e|
{
frame.load(item.uri, LoadMode(e))
dlg.close
}
pickerPane := ConstraintPane { minw = 500; maxh = 300; add(picker) }
dlg = Dialog(frame) { title="Goto File"; body=pickerPane ; commands=[Dialog.ok, Dialog.cancel] }
dlg.open
}
Void rebuild(Widget widget)
{
FileIndex.instance.rebuild
widget.window.close(Dialog.cancel)
}
Void showHelp()
{
msg :=
"""Goto File Cheat Sheet:\n
- Glob any file name such as "SideBar.fan" or "SideBar*.fan"\n
- Glob any file base name (without extension) such as "SideBar" or "SideBar*"\n
- Match camel case abbreviation such as "SB" for "SideBar"\n
Indexing is configured with GeneralOption.indexDirs (very primitive right
now). Use refresh button to manually rebuild index. See Regex.glob for
definition of glob syntax."""
Dialog.openInfo(frame, msg)
}
}
** Jump to next error/search position
internal class JumpNextCommand : FluxCommand
{
new make() : super(CommandId.jumpNext) {}
override Void invoked(Event? event)
{
if (frame.marks.isEmpty) return
if (frame.curMark == null)
frame.curMark = 0
else
frame.curMark++
if (frame.curMark >= frame.marks.size)
{
frame.curMark = frame.marks.size-1
return
}
frame.loadMark(frame.marks[frame.curMark])
}
}
** Jump to previous error/search position
internal class JumpPrevCommand : FluxCommand
{
new make() : super(CommandId.jumpPrev) {}
override Void invoked(Event? event)
{
if (frame.marks.isEmpty) return
if (frame.curMark == null)
frame.curMark = frame.marks.size-1
else
frame.curMark--
if (frame.curMark < 0)
{
frame.curMark = 0
return
}
frame.loadMark(frame.marks[frame.curMark])
}
}
//////////////////////////////////////////////////////////////////////////
// View
//////////////////////////////////////////////////////////////////////////
** Reload the current view
internal class ReloadCommand : FluxCommand
{
new make() : super(CommandId.reload) {}
override Void invoked(Event? event) { frame.view.tab.reload }
}
** Toggle sidebar shown/hidden
internal class SideBarCommand : FluxCommand
{
new make(Frame f, Type sbType) : super(sbType.name, sbType.pod)
{
this.mode = CommandMode.toggle
this.frame = f
this.sbType = sbType
this.name = sbType.name
}
override Void invoked(Event? event)
{
sb := frame.sideBar(sbType)
if (sb.showing) sb.hide; else sb.show
}
Void update()
{
sb := frame.sideBar(sbType, false)
selected = sb != null && sb.showing
}
const Type sbType
}
//////////////////////////////////////////////////////////////////////////
// History
//////////////////////////////////////////////////////////////////////////
** Hyperlink back in history
internal class BackCommand : FluxCommand
{
new make() : super(CommandId.back) {}
override Void invoked(Event? event) { frame.view.tab.back }
}
** Hyperlink forward in history
internal class ForwardCommand : FluxCommand
{
new make() : super(CommandId.forward) {}
override Void invoked(Event? event) { frame.view.tab.forward }
}
** Hyperlink up a level in the hierarchy
internal class UpCommand : FluxCommand
{
new make() : super(CommandId.up) {}
override Void invoked(Event? event) { frame.view.tab.up }
}
** Hyperlink to the home page
internal class HomeCommand : FluxCommand
{
new make() : super(CommandId.home) {}
override Void invoked(Event? event) { frame.load(GeneralOptions.load.homePage) }
}
** Open recent history dialog
internal class RecentCommand : FluxCommand
{
new make() : super(CommandId.recent) {}
override Void invoked(Event? event)
{
Dialog? dlg
picker := HistoryPicker(History.load.items, false) |HistoryItem item, Event e|
{
frame.load(item.uri, LoadMode(e))
dlg.close
}
pane := ConstraintPane { minw = 300; maxh = 300; add(picker) }
dlg = Dialog(frame) { title="Recent"; body=pane; commands=[Dialog.ok, Dialog.cancel]; defCommand = null }
dlg.open
}
}
//////////////////////////////////////////////////////////////////////////
// Tools
//////////////////////////////////////////////////////////////////////////
** Hyperlink to the options directory
internal class OptionsCommand : FluxCommand
{
new make() : super(CommandId.options) {}
override Void invoked(Event? event) { frame.load((Env.cur.workDir+`etc/flux/`).uri) }
}
** Refresh the tools menu
internal class RefreshToolsCommand : FluxCommand
{
new make() : super("refreshTools") {}
override Void invoked(Event? event) { frame.commands.refreshToolsMenu }
}
** Invoke a tool script
internal class ToolScriptCommand : FluxCommand
{
new make(Frame frame, File file) : super("tools.${file.basename}")
{
this.frame = frame
this.file = file
this.name = file.basename
}
override Void invoked(Event? event)
{
try
{
FluxCommand cmd := Env.cur.compileScript(file).make([id])
cmd.frame = frame
cmd.invoke(event)
}
catch (CompilerErr e)
{
Dialog.openErr(frame, "Cannot compile tool: $file", e)
}
catch (Err e)
{
e.trace
Dialog.openErr(frame, "Cannot invoke tool: $file", e)
}
}
const File file
}
//////////////////////////////////////////////////////////////////////////
// Help
//////////////////////////////////////////////////////////////////////////
** Hyperlink to the flux:about
internal class AboutCommand : FluxCommand
{
new make() : super(CommandId.about) {}
override Void invoked(Event? event)
{
icon := Pod.find("icons").file(`/x48/flux.png`)
big := Font { it.name=Desktop.sysFont.name; it.size=Desktop.sysFont.size+(Desktop.isMac ? 2 : 3); it.bold=true }
small := Font { it.name=Desktop.sysFont.name; it.size=Desktop.sysFont.size-(Desktop.isMac ? 3 : 1) }
versionInfo := GridPane
{
halignCells = Halign.center
vgap = 0
Label
{
text = "Version: ${this.typeof.pod.version}
Home Dir: ${Env.cur.homeDir}
Work Dir: ${Env.cur.workDir}
Env: ${Env.cur}"
font = small
},
}
content := GridPane
{
halignCells = Halign.center
Label { image = Image.makeFile(icon) },
Label { text = "Flux"; font = big },
versionInfo,
Label { font = small; text =
" Copyright (c) 2008-2011, Brian Frank and Andy Frank
Licensed under the Academic Free License version 3.0"
},
}
d := Dialog(frame) { title="About Flux"; body=content; commands=[Dialog.ok] }
d.open
}
}