(load "Lib.kiss") (method :Void _collectAndValidateArg [:CommandArg arg :Dynamic->Void continuation] (case arg.type (SelectedEntry (if (= 1 selectedEntries.length) (continuation (first selectedEntries)) (ui.reportError "The requested command expects 1 entry to be selected. You have selected: $selectedEntries.length"))) ((SelectedEntries min max) (unless min (set min 0)) // TODO might want to optimize this O(n) count operation by pre-calculating it (unless max (set max (count archive.entries))) (if !(<= min selectedEntries.length max) (ui.reportError "The requested command expects between $min and $max entries to be selected. You have selected: $selectedEntries.length") (continuation selectedEntries))) ((Text maxLength) (unless maxLength (set maxLength Math.POSITIVE_INFINITY)) (ui.enterText "${arg.name} (up to ${maxLength} characters):" (lambda :Void [text] (if !(<= text.length maxLength) (ui.reportError "The requested command expected a string up to $maxLength characters long. You entered: $text.length characters") (continuation text))) maxLength)) ((VarText maxLength) (unless maxLength (set maxLength Math.POSITIVE_INFINITY)) (let [collectedText [] &mut :Void->Void enterTextAgain null _enterTextAgain ->:Void (ui.enterText "${arg.name} (up to ${maxLength} characters):" (lambda :Void [text] (if !text (continuation collectedText) (if !(<= text.length maxLength) (ui.reportError "The requested command expected a list of strings up to $maxLength characters long. You entered: $text.length characters") {(collectedText.push text) (enterTextAgain)}))) maxLength)] (set enterTextAgain _enterTextAgain) (enterTextAgain))) ((Number min max inStepsOf) (unless min (set min Math.NEGATIVE_INFINITY)) (unless max (set max Math.POSITIVE_INFINITY)) (let [&mut prompt "${arg.name} (${min}-${max}"] (when inStepsOf (+= prompt " in steps of ${inStepsOf}")) (+= prompt "):") (ui.enterNumber prompt (lambda :Void [number] (let [minMaxError "The requested command expected a number between $min and $max" stepError "$minMaxError in steps of $inStepsOf" youEntered ". You entered: $number"] (if (or !(<= min number max) (and inStepsOf !(= 0 (% (- number min) inStepsOf)))) (if inStepsOf (ui.reportError "${stepError}$youEntered") (ui.reportError "${minMaxError}$youEntered")) (continuation number)))) min max inStepsOf))) (OneEntry (ui.chooseEntry "${arg.name}:" archive continuation)) ((Entries min max) (unless min (set min 1)) // TODO might want to optimize this O(n) count operation by pre-calculating it (unless max (set max (count archive.entries))) (ui.chooseEntries "${arg.name}:" archive (lambda :Void [:Array entries] (if (or (> min entries.length) (< max entries.length)) (ui.reportError "The requested command expects between $min and $max entries. You chose: $entries.length") (continuation entries))) min max)))) (method :Void->Void _composeArgCollector [:Array collectedArgs :CommandArg arg :Void->Void lastCollector] (lambda :Void [] (_collectAndValidateArg arg ->:Void [:Dynamic argValue] {(collectedArgs.push argValue) (lastCollector)}))) (method :Void runCommand [:Command command] (let [collectedArgs [] &mut lastCollector (lambda [] (set lastChangeSet (the ChangeSet (Reflect.callMethod null command.handler collectedArgs))) (ui.handleChanges archive lastChangeSet))] // To facilitate asynchronous arg input via UI, we need to construct an insanely complicated nested callback to give the UI (doFor arg (reversed command.args) (set lastCollector (_composeArgCollector collectedArgs arg lastCollector))) (lastCollector))) (defMacro defCommand [name args &body body] (let [argPairs (groups (expList args) 2) methodArgs (for [name type] argPairs // TODO write an exprCase macro that simplifies this terrible mess, // and maybe adds back precise pattern matching instead of relying // on partial string matching (exprCase type ((exprOr SelectedEntry OneEntry) `:nat.Entry ,name) ((exprOr (SelectedEntries _ _) (Entries _ _)) `:Array ,name) ((Text _) `:String ,name) ((VarText _) `:Array ,name) ((Number _ _ _) `:Float ,name))) commandArgs (for [name type] argPairs `(object name ,(symbolName name) type ,type))] `{ (method ,name [,@methodArgs] ,@body) (dictSet commands ,(symbolName name) (object args [,@commandArgs] handler (the Function ,name)))})) (defNew [&prop :Archive archive &prop :ArchiveUI ui] [&mut :Array selectedEntries [] &mut :ChangeSet lastChangeSet [] :Map commands (new Map)] (defCommand selectEntry [e OneEntry] (set selectedEntries [e]) []) (defCommand selectEntries [entries (Entries null null)] (set selectedEntries entries) []) (defCommand selectAllEntries [] (set selectedEntries (for =>id e archive.entries e)) []) (defCommand selectLastChangeSet [] (set selectedEntries lastChangeSet) []) (defCommand printSelectedEntries [entries (SelectedEntries null null)] (doFor e entries (ui.displayMessage (archive.fullString e))) []) (defCommand printComponent [entries (SelectedEntries null null) componentType (Text null)] (doFor e entries (ui.displayMessage (archive.componentData e componentType))) []) (defCommand createEntry [name (Text null)] [(archive.createEntry ->e (addComponent archive e Name name))]) (defCommand createEntries [names (VarText null)] (flatten (for name names (createEntry name)))) (defCommand addTags [entries (SelectedEntries 1 null) tagsToAdd (VarText null)] (doFor e entries (withWritableEntry archive e (if (hasComponent e Tags) (withWritableComponents archive e [tags Tags] (doFor tag tagsToAdd (dictSet tags tag 1))) (addComponent archive e Tags (for tag tagsToAdd =>tag 1))))) entries) // TODO this includes entries that already had the tag in the changeset (defCommand removeTags [entries (SelectedEntries 1 null) tagsToRemove (VarText null)] (doFor e entries (withWritableEntry archive e (when (hasComponent e Tags) (withWritableComponents archive e [tags Tags] (doFor tag tagsToRemove (tags.remove tag)))))) entries) // TODO this includes entries that didn't have the tag in the changeset (defCommand selectByTags [tagsBoolExp (Text null)] (selectEntries (filter archive.entries ->e (tagsMatch archive e tagsBoolExp)))) (defCommand selectByComponents [componentsBoolExp (Text null)] (selectEntries (filter archive.entries ->e (componentsMatch e componentsBoolExp)))))