implementation module DynamicEditor import StdMisc, Data.Tuple, Text, Data.Maybe from StdFunc import seq, flip from Data.Tuple import appFst import iTasks, iTasks.UI.Definition, iTasks.UI.Editor.Common, iTasks.UI.Editor.Modifiers import qualified Data.Map as Map from Data.Func import $ import Util from Data.List import zip3, intersperse import Data.Functor :: DynamicCons = { consId :: !DynamicConsId , label :: !String , builder :: !DynamicConsBuilder , showIfOnlyChoice :: !Bool } (<<@@@) infixl 2 :: !DynamicCons !DynamicConsOption -> DynamicCons (<<@@@) cons HideIfOnlyChoice = {cons & showIfOnlyChoice = False} (@@@>>) infixr 2 :: !DynamicConsOption !DynamicCons -> DynamicCons (@@@>>) opt cons = cons <<@@@ opt functionCons :: !String !String !a -> DynamicCons | TC a functionCons consId label func = functionConsDyn consId label (dynamic func) functionConsDyn :: !String !String !Dynamic -> DynamicCons functionConsDyn consId label func = { consId = consId , label = label , builder = FunctionCons func , showIfOnlyChoice = True } listCons :: !String !String !([a] -> b) -> DynamicCons | TC a & TC b listCons consId label func = listConsDyn consId label (dynamic func) listConsDyn :: !String !String !Dynamic -> DynamicCons listConsDyn consId label func = { consId = consId , label = label , builder = ListCons func , showIfOnlyChoice = True } customEditorCons :: !String !String !(Editor a) -> DynamicCons | TC, JSONEncode{|*|}, JSONDecode{|*|}, gText{|*|} a customEditorCons consId label editor = { consId = consId , label = label , builder = CustomEditorCons editor , showIfOnlyChoice = True } // TODO: don't use aborts here toValue :: !(DynamicEditor a) !(DynamicEditorValue a) -> a | TC a toValue (DynamicEditor dynEditor) (DynamicEditorValue cid val) = case toValue` (cid, val) of (v :: a^) = v _ = abort "corrupt dynamic editor value" where toValue` :: !(!DynamicConsId, !DEVal) -> Dynamic toValue` (cid, val) = case val of DEApplication args = case cons.builder of FunctionCons fbuilder = toValueFunc fbuilder args ListCons lbuilder = toValueList lbuilder args _ = abort "corrupt dynamic editor value" DEJSONValue json = case cons.builder of CustomEditorCons editor = toValueGen editor json _ = abort "corrupt dynamic editor value" where (cons, _) = consWithId cid dynEditor toValueFunc :: !Dynamic ![(!DynamicConsId, !DEVal)] -> Dynamic toValueFunc v [] = v toValueFunc f [x : xs] = case (f, toValue` x) of (f :: a -> b, x :: a) = toValueFunc (dynamic (f x)) xs _ = abort "corrupt dynamic editor value" toValueGen :: (Editor a) !JSONNode -> Dynamic | JSONDecode{|*|}, TC a toValueGen editor json = dynamic (fromJSON` editor json) where fromJSON` :: (Editor a) !JSONNode -> a | JSONDecode{|*|} a fromJSON` _ json = fromMaybe (abort "corrupt dynamic editor value") $ fromJSON json toValueList :: !Dynamic ![(!DynamicConsId, !DEVal)] -> Dynamic toValueList (f :: [a] -> b) [] = dynamic (f []) toValueList f args=:[fst : _] = case (f, toValue` fst) of (g :: [a] -> b, _ :: a) -> dynamic (g $ fromDynList [toValue` val \\ val <- args]) _ -> abort "corrupt dynamic editor value" toValueList _ _ = abort "corrupt dynamic editor value" fromDynList :: ![Dynamic] -> [a] | TC a fromDynList dyns = fromDynList` dyns [] where fromDynList` [] acc = reverse acc fromDynList` [(a :: a^) : dyns] acc = fromDynList` dyns [a:acc] fromDynList` _ _ = abort "corrupt dynamic editor value" dynEditorValToString :: !(DynamicEditor a) !(DynamicEditorValue a) -> String dynEditorValToString (DynamicEditor dynEditor) (DynamicEditorValue cid val) = concat $ reverse $ dynEditorValToString` (cid, val) [] where dynEditorValToString` :: !(!DynamicConsId, !DEVal) ![String] -> [String] dynEditorValToString` (cid, val) accum = case val of DEApplication args = case cons.builder of FunctionCons fbuilder = foldl (flip dynEditorValToString`) [" ", cons.DynamicCons.label : accum] args ListCons lbuilder # listElStrs = flatten $ intersperse [" ", cons.DynamicCons.label] $ (\arg -> dynEditorValToString` arg []) <$> reverse args = listElStrs ++ [" "] ++ accum _ = abort "corrupt dynamic editor value" DEJSONValue json = case cons.builder of CustomEditorCons editor = [ " ", toStringGen editor json , " ", cons.DynamicCons.label : accum ] _ = abort "corrupt dynamic editor value" where (cons, _) = consWithId cid dynEditor toStringGen :: (Editor a) !JSONNode -> String | gText{|*|}, JSONDecode{|*|} a toStringGen editor json = toSingleLineText $ fromJSON` editor json where fromJSON` :: (Editor a) !JSONNode -> a | JSONDecode{|*|} a fromJSON` _ json = fromMaybe (abort "corrupt dynamic editor value") $ fromJSON json derive class iTask DynamicEditorValue, DEVal :: E = E.a: E (Editor (DynamicEditorValue a)) :: ConsType = Function | List | CustomEditor derive JSONEncode ConsType derive JSONDecode ConsType dynamicEditor :: !(DynamicEditor a) -> Editor (DynamicEditorValue a) | TC a dynamicEditor dynEditor=:(DynamicEditor conses) | duplicateIds = abort "duplicate cons IDs in dynamic editor" = compoundEditorToEditor {CompoundEditor| genUI = genUI, onEdit = onEdit, onRefresh = onRefresh, valueFromState = valueFromState} where duplicateIds = hasDup $ (\b -> b.consId) <$> conses where // TODO: use hasDup from platform as soon as available hasDup :: ![a] -> Bool | Eq a hasDup [] = False hasDup [x:xs] = isMember x xs || hasDup xs genUI :: DataPath !(EditMode (DynamicEditorValue a)) !*VSt -> *(!MaybeErrorString (!UI, !Maybe (!DynamicConsId, !ConsType), ![EditState]), !*VSt) genUI dp mode vst=:{VSt|taskId} = case mode of Enter = case matchingConses of [onlyChoice] | hideCons # (mbUis, _, type, _, vst) = genChildEditors dp onlyChoice.consId Enter vst # mbUis = ( \(uis, childSts) -> (uiContainer uis, Just (onlyChoice.consId, type), [nullState: childSts]) ) <$> mbUis = (mbUis, vst) _ # (consChooseUI, chooseSt) = genConsChooseUI taskId dp Nothing = (Ok (uiContainer [consChooseUI], Nothing, [chooseSt]), vst) Update (DynamicEditorValue cid val) # (mbUis, idx, type, label, vst) = genChildEditors dp cid (Update val) vst = case mbUis of Ok (uis, childSts) | hideCons = (Ok (uiContainer uis, Just (cid, type), [nullState: childSts]), vst) | otherwise # (consChooseUI, chooseSt) = genConsChooseUI taskId dp (Just idx) = (Ok (uiContainer [consChooseUI: uis], Just (cid, type), [chooseSt: childSts]), vst) Error e = (Error e, vst) View (DynamicEditorValue cid val) # (mbUis, _, type, label, vst) = genChildEditors dp cid (View val) vst = case mbUis of Ok (uis, childSts) | hideCons = (Ok (uiContainer uis, Just (cid, type), [nullState: childSts]), vst) | otherwise # consChooseUI = uia UITextView $ valueAttr $ JSONString label = (Ok (uiContainer [consChooseUI: uis], Just (cid, type), [nullState: childSts]), vst) Error e = (Error e, vst) genConsChooseUI taskId dp mbSelectedCons = (consChooseUI, consChooseSt) where consOptions = [JSONObject [("id",JSONInt i),("text",JSONString cons.DynamicCons.label)] \\ cons <- matchingConses & i <- [0..]] consChooseUI = uia UIDropdown ( 'Map'.put "width" JSONNull $ choiceAttrs taskId (editorId dp) (maybe [] (\x -> [x]) mbSelectedCons) consOptions ) consChooseSt = LeafState {touched=False,state=maybe JSONNull (\x -> JSONInt x) mbSelectedCons} onEdit :: !DataPath !(!DataPath, !JSONNode) !(Maybe (!DynamicConsId, !ConsType)) ![EditState] !*VSt -> *( !MaybeErrorString (!UIChange, !Maybe (!DynamicConsId, !ConsType), ![EditState]) , !*VSt ) // new builder is selected: create a UI for the new builder onEdit dp ([], JSONArray [JSONInt builderIdx]) _ [_: childrenSts] vst | builderIdx < 0 || builderIdx >= length matchingConses = (Error "Dynamic editor selection out of bounds", vst) # cons = matchingConses !! builderIdx # (mbRes, _, type, _, vst) = genChildEditors dp cons.consId Enter vst = case mbRes of Ok (uis, childSts) // insert new UIs for arguments # inserts = [(i, InsertChild ui) \\ ui <- uis & i <- [1..]] # removals = removeNChildren $ length childrenSts # change = ChangeUI [] (removals ++ inserts) # builderChooseState = LeafState {touched = True, state = JSONInt $ length uis} = (Ok (change, Just (cons.consId, type), [builderChooseState: childSts]), vst) Error e = (Error e, vst) // other events targeted directly at this building cons onEdit dp ([],e) _ [_: childSts] vst | e =: JSONNull || e =: (JSONArray []) // A null or an empty array are accepted as a reset events //If necessary remove the fields of the previously selected cons # change = ChangeUI [] $ removeNChildren $ length childSts = (Ok (change, Nothing, [nullState: childSts]), vst) | otherwise = (Error $ concat ["Unknown dynamic editor select event: '", toString e, "'"], vst) // update is targeted somewhere inside this value onEdit dp ([argIdx: tp], e) (Just (cid, type)) childSts vst # (cons, _) = consWithId cid matchingConses # (res, vst) = case cons.builder of FunctionCons fbuilder # children = childrenEditors fbuilder | argIdx < 0 || argIdx >= length children = (Error "Edit event for dynamic editor has invalid path", vst) # (E editor) = children !! argIdx = editor.Editor.onEdit (dp ++ [argIdx]) (tp, e) (childSts !! (argIdx + 1)) vst ListCons lbuilder = (listBuilderEditor lbuilder).Editor.onEdit (dp ++ [0]) (tp, e) (childSts !! 1) vst CustomEditorCons editor = editor.Editor.onEdit (dp ++ [0]) (tp, e) (childSts !! 1) vst = case res of Ok (change, childSt) # change = ChangeUI [] [(argIdx + if hideCons 0 1, ChangeChild change)] // replace state for this child = (Ok (change, Just (cid, type), updateAt (argIdx + 1) childSt childSts), vst) Error e = (Error e, vst) onEdit _ _ _ _ vst = (Error "Invalid edit event for dynamic editor.", vst) removeNChildren :: !Int -> [(!Int, !UIChildChange)] removeNChildren nrArgs = repeatn nrArgs (1, RemoveChild) childrenEditors :: !Dynamic -> [E] childrenEditors (f :: a -> b) = [E $ dynamicEditorFstArg f : childrenEditors (dynamic (f undef))] where // first argument only used for type dynamicEditorFstArg :: (a -> b) -> Editor (DynamicEditorValue a) | TC a dynamicEditorFstArg _ = dynamicEditor $ DynamicEditor conses childrenEditors _ = [] onRefresh :: !DataPath !(DynamicEditorValue a) !(Maybe (!DynamicConsId, !ConsType)) ![EditState] !*VSt -> *( !MaybeErrorString ( !UIChange , !Maybe (!DynamicConsId, !ConsType) , ![EditState] ) , !*VSt ) onRefresh dp new mbCid childSts vst = (Error "dynamic editor: onRefresh not implemented!", vst) // TODO: accept ID or index genChildEditors :: !DataPath !DynamicConsId !(EditMode DEVal) !*VSt -> *(!MaybeErrorString (![UI], ![EditState]), Int, ConsType, String, !*VSt) genChildEditors dp cid mode vst= case cons.builder of FunctionCons fbuilder # (mbUis, vst) = genChildEditors` (reverse $ zip3 vals (childrenEditors fbuilder) [0..]) [] [] vst = (mbUis, idx, type, cons.DynamicCons.label, vst) where genChildEditors` [] accUi accSt vst = (Ok (accUi, accSt), vst) genChildEditors` [(mbVal, E editor, i): children] accUi accSt vst = case editor.Editor.genUI (dp ++ [i]) (maybe Enter (if viewMode View Update) mbVal) vst of (Ok (ui, st), vst) = genChildEditors` children [ui: accUi] [st: accSt] vst (Error e, vst) = (Error e, vst) vals :: [Maybe (DynamicEditorValue a)] vals = case editModeValue mode of // update or view mode Just (DEApplication children) = [Just $ DynamicEditorValue cid val \\ (cid, val) <- children] // enter mode _ = repeat Nothing ListCons lbuilder # listEditorMode = mapEditMode (\(DEApplication listElems) -> listElems) mode # (mbUi, vst) = (listBuilderEditor lbuilder).Editor.genUI (dp ++ [0]) listEditorMode vst = ((\(ui, st) -> ([ui], [st])) <$> mbUi, idx, type, cons.DynamicCons.label, vst) CustomEditorCons editor # editorMode = mapEditMode (\(DEJSONValue json) -> fromMaybe (abort "Invalid dynamic editor state") $ fromJSON json) mode # (mbUi, vst) = editor.Editor.genUI (dp ++ [0]) editorMode vst = ((\(ui, st) -> ([ui], [st])) <$> mbUi, idx, type, cons.DynamicCons.label, vst) where (cons, idx) = consWithId cid matchingConses type = case cons.builder of FunctionCons _ = Function ListCons _ = List CustomEditorCons _ = CustomEditor viewMode = mode =: View _ hideCons = case matchingConses of [onlyChoice] | not onlyChoice.showIfOnlyChoice = True _ = False matchingConses = catMaybes (matchingCons dynEditor <$> conses) // first arg only used for type // packs matching conses, with possibly updated (= more specific) type matchingCons :: !(DynamicEditor a) !DynamicCons -> Maybe DynamicCons | TC a matchingCons dynEd cons=:{builder} = (\b -> {cons & builder = b}) <$> mbBuilder` where mbBuilder` = case builder of FunctionCons fbuilder = matchf fbuilder CustomEditorCons editor = matchc editor ListCons lbuilder = matchl lbuilder // works for functions with upto 10 args // the type of the dynamic is updated by unifying the function result with the type produced by the editor matchf :: !Dynamic -> Maybe DynamicConsBuilder matchf b = case (b, dynamic dynEd) of (b :: a b c d e f g h i j -> z, _ :: DynamicEditor z) = Just $ FunctionCons (dynamic b) (b :: a b c d e f g h i -> z, _ :: DynamicEditor z) = Just $ FunctionCons (dynamic b) (b :: a b c d e f g h -> z, _ :: DynamicEditor z) = Just $ FunctionCons (dynamic b) (b :: a b c d e f g -> z, _ :: DynamicEditor z) = Just $ FunctionCons (dynamic b) (b :: a b c d e f -> z, _ :: DynamicEditor z) = Just $ FunctionCons (dynamic b) (b :: a b c d e -> z, _ :: DynamicEditor z) = Just $ FunctionCons (dynamic b) (b :: a b c d -> z, _ :: DynamicEditor z) = Just $ FunctionCons (dynamic b) (b :: a b c -> z, _ :: DynamicEditor z) = Just $ FunctionCons (dynamic b) (b :: a b -> z, _ :: DynamicEditor z) = Just $ FunctionCons (dynamic b) (b :: a -> z, _ :: DynamicEditor z) = Just $ FunctionCons (dynamic b) (b :: z, _ :: DynamicEditor z) = Just $ FunctionCons (dynamic b) _ = Nothing // custom editors do not allow for quantified variables, so no type update is required matchc e = case (dynamic e, dynamic dynEd) of (_ :: Editor a, _ :: DynamicEditor a) = Just $ CustomEditorCons e _ = Nothing matchl f = case (f, dynamic dynEd) of (f :: [a] -> b, _ :: DynamicEditor b) = Just $ ListCons (dynamic f) _ = Nothing listBuilderEditor :: !Dynamic -> Editor [(!DynamicConsId, !DEVal)] listBuilderEditor (lbuilder :: [a] -> b) = listEditor (Just $ const Nothing) True True Nothing childrenEd` where childrenEd = childrenEditorList lbuilder childrenEd` = bijectEditorValue (\(cid, val) -> DynamicEditorValue cid val) (\(DynamicEditorValue cid val) -> (cid, val)) childrenEd // first argument only used for type childrenEditorList :: ([a] -> b) -> Editor (DynamicEditorValue a) | TC a childrenEditorList _ = dynamicEditor $ DynamicEditor conses listBuilderEditor _ = abort "dynamic editors: invalid list builder value" uiContainer :: [UI] -> UI uiContainer uis = UI UIContainer ('Map'.fromList [("direction", JSONString "horizontal"), ("width", JSONString "wrap")]) uis valueFromState :: !(Maybe (!DynamicConsId, !ConsType)) ![EditState] -> *Maybe (DynamicEditorValue a) valueFromState (Just (cid, CustomEditor)) [_: [editorSt]] = mapMaybe (DynamicEditorValue cid o DEJSONValue o toJSON`) $ editor.Editor.valueFromState editorSt where ({builder}, _) = consWithId cid conses // toJSON` is used to solve overloading, JSONEncode{|*|} is attached to CustomEditorCons (editor, toJSON`) = case builder of CustomEditorCons editor = (editor, toJSON) _ = abort "corrupt dynamic editor state" valueFromState (Just (cid, type)) [_: childSts] = mapMaybe (\childVals -> DynamicEditorValue cid $ DEApplication childVals) $ childValuesFor childSts` [] where childSts` = case (type, childSts) of (List, [CompoundState _ childSts]) = childSts (_, childSts) = childSts childValuesFor :: ![EditState] ![(!DynamicConsId, !DEVal)] -> Maybe [(!DynamicConsId, !DEVal)] childValuesFor [] acc = Just $ reverse acc childValuesFor [childSt: childSts] acc = case (dynamicEditor dynEditor).Editor.valueFromState childSt of Just (DynamicEditorValue childCid childVal) = childValuesFor childSts [(childCid, childVal): acc] _ = Nothing valueFromState _ _ = Nothing consWithId :: !DynamicConsId ![DynamicCons] -> (!DynamicCons, !Int) consWithId cid conses = case filter (\({consId}, _) -> consId == cid) $ zip2 conses [0..] of [cons] = cons [] = abort $ "cons not found: " +++ cid _ = abort $ "duplicate conses: " +++ cid nullState :: EditState nullState = LeafState {touched = True, state = JSONNull}