Generic component widgets. Built on top of the system specific widget set, and on top of abstract Components and Widgets. \begin{code} module ComponentWidgets where import AbstractInterface import WidgetSet import qualified FranPrim as F import StaticTypes import qualified BehaviorTypes as B import Maybe import IOExts import Utils import List import FM import FRPWrap import TreeIO import Component import Widget import DisplayWidget import ListB (fromList,toList,insertWith) import TclPrimImpl (tcl_) \end{code} Tick every v seconds firing a primitive action, until the root window is destroyed, or the remove IO action is run \begin{code} everyTick :: Double -> GUI (IO ()) everyTick v = do w <- getRootWindow withGUIDef $ \g -> do rm <- everyW v (runGUI g update) (guiInfo g) addFinaliserW w rm return rm \end{code} tick every v seconds until the root window is destroyed, telling an event about the tick events. Finish if we lose interest in the event. \begin{code} afterE :: Time -> GUI (Event ()) afterE v = do win <- getRootWindow withGUIDef $ \g -> do w <- newWire fixIO $ \rm -> do l <- fireListener' (input w) rm rm <- afterW v (primAction g (l ())) (guiInfo g) addFinaliserW win rm return rm return (event w) everyE :: Time -> GUI (Event ()) everyE v = do win <- getRootWindow withGUIDef $ \g -> do w <- newWire fixIO $ \rm -> do l <- fireListener' (input w) rm rm <- everyW v (primAction g (l ())) (guiInfo g) addFinaliserW win rm return rm return (event w) \end{code} Rendering window components with components inside \begin{code} withRootWindow :: [Conf Window] -> Component -> WComponent withRootWindow cs c = do w <- getRootWindow csetAllB windowNames w cs renderInWindow w c return $ wwidget w mkWindow :: [Conf Window] -> Component -> WComponent mkWindow cs c = do w <- window cs renderInWindow w c return $ wwidget w --render a component in a window, by setting the current window context, --displaying it and then resetting the window context renderInWindow :: Window -> Component -> GUI () renderInWindow win c = do cur <- getCurrentWindow setCurrentWindow ( win) w <- c withGUIDef $ displayWidget (WW win) w setCurrentWindow cur \end{code} Component definitions built on top of the WidgetSet widgets The really simple ones \begin{code} mkMessage :: [Conf Message] -> Component mkMessage = fmap widget . message mkLabel :: [Conf Label] -> Component mkLabel = fmap widget . label mkButton :: [Conf Button] -> Listener () -> Component mkButton cs l = fmap widget $ button (command l:cs) mkRadiobutton :: [Conf Radiobutton] -> Listener () -> Component mkRadiobutton cs l = fmap widget $ radiobutton (command l:cs) mkHScrollbar,mkVScrollbar :: [Conf Scrollbar] -> Component mkHScrollbar cs = fmap widget $ hscroll cs mkVScrollbar cs = fmap widget $ vscroll cs \end{code} components that sample their own value and tell that to their listener \begin{code} mkVScale,mkHScale :: [Conf Scale] -> Listener Int -> Component mkVScale cs l = fmap widget $ vscale (commandSelf (liftIO . getScale) l:cs) mkHScale cs l = fmap widget $ hscale (commandSelf (liftIO . getScale) l:cs) mkCheckbutton :: [Conf Checkbutton] -> Listener Bool -> Component mkCheckbutton cs l = fmap widget $ checkbutton (commandSelf (liftIO . getCheck) l:cs) \end{code} -- Canvas ------------------------------------------- When we display a canvas we create a canvas, set it as the current context, display the canvas component and then clear the canvas context \begin{code} mkCanvas :: [Conf Canvas] -> CComponent -> Component mkCanvas cs cc = do can <- canvas cs setInCanvas can cw <- cc wr <- mkWire die <- liftIO $ fireListener (input wr) liftIO $ addFinaliserW can (die ()) withGUIDef $ displayCanvasWidget (event wr) cw clearFromCanvas return $ widget can mkCOval :: B.Vector2B -> [Conf COval] -> CComponent mkCOval v c = fmap cwidget $ coval v c mkCLine :: [B.Point2B] -> [Conf CLine] -> CComponent mkCLine p c = fmap cwidget $ cline p c mkCArc :: B.Vector2B -> IntB -> IntB -> [Conf CArc] -> CComponent mkCArc v r1 r2 cs = fmap cwidget $ carc v r1 r2 cs mkCRectangle :: B.Vector2B -> [Conf CRectangle] -> CComponent mkCRectangle v cs = fmap cwidget $ crectangle v cs mkCPolygon :: [B.Point2B] -> [Conf CPolygon] -> CComponent mkCPolygon p cs = fmap cwidget $ cpolygon p cs mkCText :: [Conf CText] -> CComponent mkCText cs = fmap cwidget $ ctext cs mkCBitmap :: [Conf CBitmap] -> CComponent mkCBitmap cs = fmap cwidget $ cbitmap cs \end{code} To display a component in a canvas item, create a subpanel to display the component in and place this subpanel in the canvas item. Connect all user input listeners from the cwindow to the every widget in the component through a wire. Bind requests are send along the wire. Remove actions for the bind are placed in a ref cell. This can be used to unbind when no longer needed. \begin{code} mkCWindow :: [Conf CWindow] -> Component -> CComponent mkCWindow cs c = fmap cwidget $ do w <- getCurrentWindow p <- liftIO $ getWinPanel (widgetPrim w) >>= mkSubPanel wgt <- c wire <- mkWire tell <- liftIO $ fireListener (input wire) withGUIDef $ displayWidgetWith (Just $ Bind (\w -> F.addListener (event wire) (mkL $ \(a,l,ref) -> do rm <- bind w a l writeIORef ref rm))) p wgt w <- cwindow (\ a l -> do ref <- newIORef (return ()) tell (a,l,ref) return $ callRemoverRef ref) (raise p) (lower p) (uniqueId p) cs liftIO $ addFinaliserW w (destroy p) return w \end{code} Menus - now life starts to get a bit more complex, as in FranTk menu items are an abstract data type. make a menu button, and make a menu for its config option, either from a static or dynamic list of items \begin{code} mkMenubutton :: [Conf Menubutton] -> Component mkMenubutton = fmap widget . menubutton withMenu :: [Conf Menu] -> [MenuItem] -> Conf Menubutton withMenu cs mis = confGUI $ \w -> do m <- menu' w cs mapM_ (runMenuItem m PlaceBottom) mis csetB w [use_menu m] withMenuL :: [Conf Menu] -> ListB MenuItem -> Conf Menubutton withMenuL cs mis = confGUI $ \w -> do m <- menu' w cs runMenuItems m mis csetB w [use_menu m] \end{code} make a menu as a GUI action with a static or dynamic list of items \begin{code} mkMenu :: [Conf Menu] -> [MenuItem] -> GUI Menu mkMenu cs items = do m <- menu cs mapM_ (runMenuItem m PlaceBottom) items return m mkMenuL :: [Conf Menu] -> ListB MenuItem -> GUI Menu mkMenuL cs items = do m <- menu cs runMenuItems m items return m -- make a menu popup or popdown based on an event popupE :: Event (Maybe Point2) -> Conf Menu popupE e = confGUI $ \w -> do addListenerW e (mkL $ popup w) w -- make a menu popup or popdown based on an event popupME :: Event Bool -> Conf Menu popupME e = confGUI $ \w -> do addListenerW e (mkL $ popupM w) w -- make a window use a menu useMenu :: GUI Menu -> Conf Window useMenu mk = confGUI $ \w -> do m <- mk csetB w [use_menu m] \end{code} Display a dynamic list of menu items, using dispL \begin{code} runMenuItems :: Menu -> ListB MenuItem -> GUI () runMenuItems m items = do dispL lookupIdent (insert m) (reset m) delete (move m) m items return () where -- insert an item into the menu insert :: Menu -> MenuItem -> PlacePos MInfo -> GUI MInfo insert m mi pos = runMenuItem m (fmap fst pos) mi -- reset the whole list reset :: Menu -> [MInfo] -> [MenuItem] -> GUI [MInfo] reset m olds news = do liftIO $ mapM_ snd olds mapM (runMenuItem m PlaceBottom) news -- delete an item using the recorded remover delete :: MInfo -> GUI () delete (_,rm) = liftIO $ rm -- move an item above another move :: Menu -> MInfo -> PlacePos MInfo -> GUI () move m (id,_) pos = liftIO $ moveMItem (widgetPrim m) id (fmap fst pos) -- we store the identifier and remove action for each item type MInfo = (Ident,Remover) -- find data for all items with a given index lookupIdent :: Ident -> [(Ident,b)] -> [b] lookupIdent a = mapMaybe (\(x,y) -> if a == x then Just y else Nothing) \end{code} -- Menu items ----------------------------------------------- Menu items are an abstract data type in FranTk \begin{code} data MenuItem = MenuButton [Conf MButton] (Listener ()) | MenuCheckbutton [Conf MCheckbutton] (Listener Bool) | MenuRadiobutton [Conf MRadiobutton] (Listener ()) | MenuCascade [Conf MCascade] (Menu -> GUI Menu) | MenuSeparator mbutton :: [Conf MButton] -> Listener () -> MenuItem mcheckbutton :: [Conf MCheckbutton] -> Listener Bool -> MenuItem mradiobutton :: [Conf MRadiobutton] -> Listener () -> MenuItem mcascade :: [Conf MCascade] -> GUI Menu -> MenuItem mseparator :: MenuItem mbutton = MenuButton mcheckbutton = MenuCheckbutton mradiobutton = MenuRadiobutton mcascade cs mk = MenuCascade cs (const mk) mseparator = MenuSeparator \end{code} make a menu item given a menu, position, creation function and list of configs, returning the id of the menu and destroy action \begin{code} runItem :: WidgetItem w => Menu -> PlacePos Ident -> (Menu -> [Conf w] -> PlacePos Ident -> GUI w) -> [Conf w] -> GUI (Ident,IO ()) runItem menu pos mk cs = do w <- mk menu cs pos return (uniqueId w,destroy w) \end{code} display an abstract menu item \begin{code} runMenuItem :: Menu -> PlacePos Ident -> MenuItem -> GUI (Ident,Remover) runMenuItem menu pos (MenuButton cs l) = do runItem menu pos mbutton' $ -- the listener hears button presses (see PrimWidget.lhs for more on -- primActions command (wrapIOL (primAction $ guiDef menu) l):cs runMenuItem menu pos (MenuCheckbutton cs l) = runItem menu pos mcheckbutton' $ -- the checkbutton samples its own value on press and passes it to -- the listener commandSelf (liftIO . getMCheck) l:cs runMenuItem menu pos (MenuRadiobutton cs l) = runItem menu pos mradiobutton' $ -- the listener hears button presses (see PrimWidget.lhs for more on -- primActions command (wrapIOL (primAction $ guiDef menu) l):cs runMenuItem menu pos (MenuSeparator) = runItem menu pos (\m c p -> mseparator' m p) [] runMenuItem menu pos (MenuCascade cs mkm) = do mn <- mkm menu (id,rm) <- runItem menu pos (\m c p -> do c <- mcascade' m mn cs p return c ) cs return (id,rm) \end{code} Display a listbox, give it a list of items or a dynamic list of items \begin{code} mkListbox :: [Conf Listbox] -> Component mkListbox = fmap widget . listbox listItems :: [String] -> Conf Listbox listItems xs = confIO $ \w -> resetListbox w xs listItemsB :: Behavior [String] -> Conf Listbox listItemsB b = confIO $ actB resetListbox b -- display a dynamic list collection in a listbox -- note that all listbox ops expect access to the numerical -- index of the list item. When performing a lookup we -- therefore need to find the indices of each item -- we also maintain the String at each location, for use when -- moving item listItemsLB :: ListB String -> Conf Listbox listItemsLB items = confGUI $ \l -> do dispL lookupItem (insert l) (reset l) (delete l) (move l) l items return () where lookupItem :: Ident -> [(Ident,(String,Int))] -> [(String,Int)] lookupItem i xs = zip (map fst $ lookupIdent i xs) (findIndices ((== i) . fst) xs) insert :: Listbox -> String -> PlacePos (String,Int) -> GUI (String,Int) insert l li pos = liftIO $ fmap (const (li,0)) $ insertLBox l li $ fmap snd pos delete :: Listbox -> (String,Int) -> GUI () delete l (_,n) = liftIO $ deleteLBox l n reset :: Listbox -> [(String,Int)] -> [String] -> GUI [(String,Int)] reset l _ xs = liftIO $ fmap (const $ zip xs [1..]) $ resetListbox l xs -- if the item was originally earlier in the list than its new -- location then deleting it will have altered the list order. -- To fix this we alter the location to be one earlier move :: Listbox -> (String,Int) -> PlacePos (String,Int) -> GUI () move l (s,x) pos = liftIO $ do deleteLBox l x case pos of PlaceBefore (_,y) | y >= x -> insertLBox l s $ PlaceBefore (y-1) PlaceAfter (_,y) | y >= x -> insertLBox l s $ PlaceAfter (y-1) pos -> insertLBox l s $ fmap snd pos \end{code} Display a dynamic list of items given an insert, reset, delete and move action, along with the widget and the list. We maintain local data for each item. We pass in a lookup function that finds the info associated with an item, given the identifier of the item and the list of items. There shouldn't be more than one item with the same identifier, but just in case handle it by returning a list of info. \begin{code} dispL :: (WidgetItem w) => (Ident -> [(Ident,info)] -> [info]) -> (a -> PlacePos info -> GUI info) -> ([info] -> [a] -> GUI [info]) -> (info -> GUI ()) -> (info -> PlacePos info -> GUI ()) -> w -> ListB a -> GUI () dispL lookupItems (renderItem :: a -> PlacePos info -> GUI info) resetItems deleteItem moveItem w items = do ls <- liftIO $ do t <- getFranTime behaviorC items `at` t ref <- liftIO $ newIORef [] place ref (ResetList ls) op <- mkGUIL $ place ref addListenerW (eventC items) op w return () where fromPos :: [(Ident,info)] -> PlacePos Ident -> (PlacePos info) fromPos fm pos = case pos of PlaceBottom -> PlaceBottom PlaceTop -> PlaceTop PlaceBefore v -> maybe PlaceTop (PlaceBefore) $ listToMaybe $ lookupItems v fm PlaceAfter v -> maybe PlaceBottom (PlaceAfter) $ listToMaybe $ lookupItems v fm place :: IORef [(Ident,info)] -> ListOp a -> GUI () place ref (InsertList ident a pos) = do pos' <- liftIO $ fmap (flip fromPos pos) $ readIORef ref info <- renderItem a pos' liftIO $ updIORef ref $ \ls -> insertWith (ident,info) pos ls return () place ref (DeleteList ident) = do fm <- liftIO $ readIORef ref case lookupItems ident fm of [] -> liftIO $ print "deleting non existent elt" infos -> do liftIO $ writeIORef ref $ filter (\ (n',_) -> n' /= ident) fm mapM_ deleteItem infos place ref (ResetList as) = do elts <- liftIO $ fmap (map snd) $ readIORef ref case unzip $ fromList as of (is,as) -> do ms <- resetItems elts as liftIO $ writeIORef ref $ zip is ms place ref (MoveList id pos) = do fm <- liftIO $ readIORef ref case partition ((== id) . fst) fm of ([],as) -> return () (xs,as) -> do let pos' = fromPos fm pos liftIO $ writeIORef ref $ foldl' (\as x -> insertWith x pos as) as xs mapM_ (flip moveItem pos') $ lookupItems id fm \end{code} Entry widget components. \begin{code} mkEntry' :: [Conf Entry] -> Component mkEntry' cs = fmap widget $ entry cs --There is a creation function which takes an event and listener, and sends out --the state of the entry when a value is heard on the event. mkEntry :: [Conf Entry] -> Event () -> Listener String -> Component mkEntry cs ev l = mkEntry' (snapEntry (\_ s -> s) ev l:cs) --There is also a creation function that makes an entry that tells its argument --listener the value of the entry, every time the return key is pressed. mkEntryRtrn :: [Conf Entry] -> Listener String -> Component mkEntryRtrn cs l = do w <- mkWire keyPress Return (input w) $ mkEntry cs (event w) l \end{code} We can sample the value of a text area using snapEntry. Given an event, a listener and a composition function we can set up a sampling function. When a value is heard on the event, the state of the entry is sampled. The value is composed with the String from the entry, using the composition function, and is then told to the listener. \begin{code} snapEntry :: (a -> String -> b) -> Event a -> Listener b -> Conf Entry snapEntry f ev l = confGUI $ \ent -> do addListenerW ev (mapIOL (\a -> do s <- getEntry ent return (f a s)) l) ent return () \end{code} -- The Edit widget --------------------------- make an edit component and a scrollable edit component \begin{code} mkEdit :: [Conf Edit] -> Component mkEdit cs = fmap widget $ edit cs mkScrollableEdit :: [Conf Edit] -> [Conf Scrollbar] -> [Conf Scrollbar] -> Component mkScrollableEdit cs hs vs = do v <- mkVScroll h <- mkHScroll (mkEdit ([useScroll v,useScroll h] ++ cs) `beside` (fill FillY $ mkVScrollbar $ [useScroll v] ++ vs)) `above` (fill FillX $ mkHScrollbar $ [useScroll h] ++ hs) \end{code} We can snapshot the complete current text of the edit widget with snapEdit \begin{code} snapEdit :: (a -> String -> b) -> Event a -> Listener b -> Conf Edit snapEdit f ev l = confGUI $ \ed -> do addListenerW ev (mapIOL (\a -> do s <- getEdit ed return (f a s)) l) ed return () \end{code} Edit widgets have access to the clipboard, to copy cut and paste current selection \begin{code} data ClipboardAction = Copy | Cut | Paste clipboardE :: Event ClipboardAction -> Conf Edit clipboardE e = confGUI $ \w -> do addListenerW e (mkL $ handle w) w where handle w Copy = copyClip w handle w Cut = cutClip w handle w Paste = pasteClip w \end{code} As well as setting the edit widget using text configuration, we can also set the edit widget with structured text. This allows: 1) standard text 2) tagged text which represent subsections of text, which can be activated individually, for example, to make hyperlinks for example. 3) Marks added to text which mark specific points in the text We can do this through the EditVal data type There are number of functions to construct values of type EditVal. We can insert text at a given point (insertEditVal), delete text between two given points (deleteEditVal), reset the text (resetEditVal), or clear it (clearEditVal). We can insert an EditMark, or an EditTag. An EditMark puts a mark at particular point in some text. An EditTag is a way of tagging, and therefore changing the attributes of some section of text. We can also insert some text at a given point, with an EditTag associated with. Note that a given EditTag or EditMark can only be added to a single edit widget. This is why we pass in a value of type GUI EditTag (or EditMark), which is an action that produces an EditTag (or EditMark). \begin{code} -- Add structured text consisting of a list of structured items -- These can be standard text, tagged structured text (note this is -- recursive, allowing us tags within tags), and marks. data Structured = SText String | SGroup [Structured] | STextTagged [Structured] (GUI EditTag) | STextMark (GUI EditMark) deriving Show structured :: Structured -> IDoc structured s = [ResetStructured noEdit s] insertStructured :: Structured -> TIndex -> IDoc -> IDoc insertStructured s t idocs = InsertStructured noEdit s t : idocs replaceStructured :: Structured -> Ident -> IDoc -> IDoc replaceStructured s i idocs = ReplaceStructured noEdit s i : idocs emptyIDoc :: IDoc emptyIDoc = structured $ SText "" fromStringIDoc :: String -> IDoc fromStringIDoc s = structured $ SText s insertIDoc :: String -> TIndex -> IDoc -> IDoc insertIDoc s t i = insertStructured (SText s) t i resetIDoc :: String -> IDoc -> IDoc resetIDoc s _ = fromStringIDoc s clearIDoc :: IDoc -> IDoc clearIDoc _ = emptyIDoc deleteIDoc :: TIndex -> TIndex -> IDoc -> IDoc deleteIDoc t1 t2 idocs = DeleteBetween noEdit t1 t2 : idocs insertTagIDoc :: GUI EditTag -> IDoc -> IDoc insertTagIDoc g x = InsertEditTag nullId g : x insertMarkIDoc :: GUI EditMark -> IDoc -> IDoc insertMarkIDoc g x = InsertEditMark nullId g : x deleteFrom :: TIndex -> Int -> IDoc -> IDoc deleteFrom t n i = deleteFrom' noEdit t n : i deleteFrom' :: Ident -> TIndex -> Int -> DocUpd deleteFrom' ed i n = if n < 0 then DeleteBetween ed (TModMove i (ModChars n)) i else DeleteBetween ed i (TModMove i (ModChars n)) \end{code} \begin{code} type IDoc = [DocUpd] data DocUpd = InsertStructured Ident Structured TIndex | ReplaceStructured Ident Structured Ident | ResetStructured Ident Structured | DeleteBetween Ident TIndex TIndex | InsertEditMark Ident (GUI EditMark) | InsertEditTag Ident (GUI EditTag) -- these last two are used by edit widgets when translating -- user input into edit val updates. | SetMark Ident (Maybe TIndex) Ident | SetTag Ident (Maybe (TIndex,TIndex)) Ident deriving Show noEdit :: Ident noEdit = nullId editId :: DocUpd -> Ident editId (InsertStructured id _ _) = id editId (ReplaceStructured id _ _) = id editId (ResetStructured id _) = id editId (DeleteBetween id _ _) = id editId (InsertEditMark id _) = id editId (InsertEditTag id _) = id editId (SetMark id _ _) = id editId (SetTag id _ _) = id instance Show (GUI a) where show a = "" \end{code} \begin{code} data DocumentB = DocumentB { docBInit :: IDoc, docBEvent :: Event IDoc, docBBehavior :: Behavior EditState } data DocumentBVar = DocumentBVar { docB :: DocumentB, docBVarUpdInput :: Listener (IDoc -> IDoc) } -- currently we're not implementing updates to the edit state properly -- so we just make an initial list of edit val updates and then apply them -- to the widget data EditState = EditState { editmarks :: IDoc, -- updates to add marks editState :: [EditEntry] -- the state of the edit widget } data EditEntry = EditText String | EditMark Ident | EditTagStart Ident | EditTagStop Ident emptyEditState :: EditState emptyEditState = EditState [] [] \end{code} Configuration options for using Document behaviors \begin{code} useDocBVar :: DocumentBVar -> Conf Edit useDocBVar docbv = composeConf (listenDoc (docBVarUpdInput docbv)) (setDocB (docB docbv)) setDocB :: DocumentB -> Conf Edit setDocB = confGUI . setDocBGUI listenDoc :: Listener (IDoc -> IDoc) -> Conf Edit listenDoc = confGUI . listenDocGUI updIDoc :: (IDoc -> IDoc) -> Conf Edit updIDoc = confGUI . setIDocGUI setIDoc :: IDoc -> Conf Edit setIDoc xs = updIDoc $ \_ -> xs ++ emptyIDoc setIDocE :: Event (IDoc -> IDoc) -> Conf Edit setIDocE = confGUI . setDocEGUI setIDocB :: Behavior IDoc -> Conf Edit setIDocB b = confGUI $ \w -> setB' (\_ _ -> False) w setIDoc b \end{code} \begin{code} mkDocumentBVar :: IDoc -> GUI DocumentBVar mkDocumentBVar init = do w <- mkWire b <- mkDocumentB init (event w) return $ DocumentBVar b (input w) mkDocumentB :: IDoc -> Event (IDoc -> IDoc) -> GUI DocumentB mkDocumentB init e = liftIO $ mkDocumentB' init emptyEditState e mkDocumentB' :: IDoc -> EditState -> Event (IDoc -> IDoc) -> IO DocumentB mkDocumentB' init st e = do ev <- F.accumE init (e ==> \f _ -> reverse (f [])) bh <- F.stepAccum st (ev ==> \is e -> foldr applyEditState e is) return $ DocumentB init ev bh applyEditState :: DocUpd -> EditState -> EditState applyEditState ins@(InsertEditMark _ _) st@(EditState marks _) = st {editmarks = ins:marks} applyEditState ins@(InsertEditTag _ _) st@(EditState marks _) = st {editmarks = ins:marks} applyEditState ins@(SetMark _ _ _) st@(EditState marks _) = st {editmarks = ins:marks} applyEditState ins@(SetTag _ _ _) st@(EditState marks _) = st {editmarks = ins:marks} applyEditState e st = st \end{code} \begin{code} setDocEGUI :: Event (IDoc -> IDoc) -> Edit -> GUI () setDocEGUI e w = do ref <- liftIO $ newIORef [] runIDocEvent (mapE (reverse . ($ [])) e) w ref setDocBGUI :: DocumentB -> Edit -> GUI () setDocBGUI (DocumentB inits e b) w = do (EditState mrks s) <- liftIO $ getFranTime >>= at b ref <- liftIO $ newIORef [] mapM_ (fromDocUpd ref w) $ inits ++ reverse mrks runIDocEvent e w ref setIDocGUI :: (IDoc -> IDoc) -> Edit -> GUI () setIDocGUI i e = do ref <- liftIO $ newIORef [] setIDocG (reverse $ i []) e ref \end{code} we can insert IDoc updates into an edit widget using fromDocUpd and using runEditVal to insert an event list of them \begin{code} setIDocG :: IDoc -> Edit -> IORef StructuredData -> GUI () setIDocG xs e ref = do liftIO $ resetEdit e "" mapM_ (fromDocUpd ref e) xs runIDocEvent :: Event IDoc -> Edit -> IORef StructuredData -> GUI () runIDocEvent e ed ref = do gl <- mkGUIL $ mapM_ (fromDocUpd ref ed) addListenerW e gl ed return () -- a list of trees. We maintain a copy of the tree, and a list -- of all the identifiers in each tree. type StructuredData = [TreeIO (Ident,IO ())] fromDocUpd :: IORef StructuredData -> Edit -> DocUpd -> GUI () fromDocUpd ref ed x | uniqueId ed == editId x = return () fromDocUpd ref ed (DeleteBetween edid i1 i2) = liftIO $ deleteEdit ed i1 i2 fromDocUpd ref ed (InsertStructured _ xs i) = do pre <- liftIO $ readIORef ref ts <- doStructured ed xs i [] ts <- liftIO $ mapM initTreeIO ts liftIO $ writeIORef ref $ ts ++ pre fromDocUpd ref ed (ResetStructured nid s) = do liftIO $ resetEdit ed "" ts <- doStructured ed s tindexStart [] ts <- liftIO $ mapM initTreeIO ts liftIO $ writeIORef ref $ ts fromDocUpd ref ed (ReplaceStructured nid x idold) = do pre <- liftIO $ readIORef ref add pre where add :: [TreeIO (Ident,IO ())] -> GUI () add (t:ts) = do (mb,dead,abv) <- liftIO $ replaceInTree ((== idold) . fst) t case mb of Nothing -> add ts Just op -> do i1' <- liftIO $ fromTIndex ed (tindexTagFirst idold) case i1' of (Just (x1,y1)) -> do liftIO $ deleteEdit ed (tindex x1 y1) (tindexTagLast idold) liftIO $ mapM_ snd dead sts <- doStructured ed x (tindex x1 y1) (map fst abv) case sts of (st:_) -> liftIO $ op st _ -> return () Nothing -> return () fromDocUpd ref ed (InsertEditMark edid gui) = do mrk <- gui liftIO $ addEditMark mrk ed fromDocUpd ref ed (InsertEditTag edid gui) = do tg <- gui liftIO $ addEditTag tg ed fromDocUpd ref ed (SetMark edid i m) = liftIO $ mark ed m i >> return () fromDocUpd ref ed (SetTag edid mb i) = liftIO $ setTag' ed i mb \end{code} -- add structured text, associating with each individual bit of text -- all the parent tags in ts, as well as any provided by Structured -- Why? Tcl-tk has the rule that when inserting text it only retains -- the tags that cover text on both the left and right side of the -- insertion position. If you're replacing text and want to insert -- the new text inside all the tags the old text was in, and you're doing this -- at the beginning or end of the tag it won't work. So we can pass -- in a list of tags that the old text was in instead. \begin{code} doStructured :: Edit -> Structured -> TIndex -> [Ident] -> GUI [SubTree (Ident,IO ())] doStructured ed xs i ts = do let i' = if i == TIndexEnd then TModMove i (ModChars (-1)) else i liftIO $ setMark' ed (identify "tmpmark") (Just $ i') ts <- handle xs liftIO $ setMark' ed (identify "tmpmark") Nothing return ts where mpos = tindexMark (identify "tmpmark") handle :: Structured -> GUI [SubTree (Ident,IO ())] handle st@(SText s) = do liftIO $ insertEditTagged ed mpos s ts return [] handle (STextMark mkmrk) = do mrk <- mkmrk liftIO $ do setEditMarkIndex mrk (Just mpos) addEditMark mrk ed return [] handle st@(STextTagged xs mktg) = do tg <- mktg (x,y) <- fmap (maybe (1,0) id) $ liftIO $ fromTIndex ed mpos liftIO $ addEditTag tg ed cs <- fmap concat $ mapM handle xs liftIO $ setEditTagIndex tg (Just (tindex x y,mpos)) return $! [SubTree (uniqueId tg,destroy tg) cs] handle st@(SGroup xs) = fmap concat $ mapM handle xs \end{code} This edit listener implementation uses some general mechanisms but is fairly tcl specific. Not sure how specific yet though. \begin{code} listenDocGUI :: Listener (IDoc -> IDoc) -> Edit -> GUI () listenDocGUI l' ed = do let l :: Listener DocUpd l = comapL (:) l' -- record what modifiers are currently pressed -- as we don't hear some text when control or alt is pressed active <- mkBVar [] -- record the current selection. We can use this then for delete actions. -- as behaviors get updated immediately after the event, on a given input, -- this will be the selection status immediately before the event selection <- mkBVar Nothing -- the actual selection status, this will change with user input. -- and so is the selection status immediately after the input sel <- selectEditTag [] liftIO $ addEditTag sel ed -- the insertion mark. again its state after the input has been applied. ins <- insertEditMark liftIO $ addEditMark ins ed -- we pass on an insertion mark and tag to the edit valB. This represents -- the mark and selection area of this widget in the shared state. n <- getUniqueName let otherins = identify $ "ins" ++ show n let othersel = identify $ "sel" ++ show n liftIO $ do send <- fireListener l send $ InsertEditMark edid (mkEditMark [useIdent otherins]) send $ InsertEditTag edid (mkEditTag [useIdent othersel]) -- when the selection state changes pass on the new selection value to -- the shared edit state. addListenerW (event selection) (comapL (flip (SetTag edid) othersel) l) ed -- get the index of the insertion cursor let getIndx :: IO (Maybe TIndex) getIndx = fmap (fmap (uncurry TIndex)) $ getEditMarkIndex ins -- get the indices of the selection tag let getSelIndx :: IO (Maybe (TIndex,TIndex)) getSelIndx = fmap (fmap toTIndex) $ getEditTagIndex sel -- when we hear some input that may have changed the selection -- we sample the old selection (from the BVar), and the new selection -- from the tag. If they differ update the BVar. let select :: Listener a select = snapshotL_ (bvarBehavior selection) $ mapMaybeIOL (\oldsel -> do newsel <- getSelIndx if oldsel /= newsel then return $ Just newsel else return Nothing) (input selection) -- when we hear a key press. sample the modifier state, and selection state. -- also sample the actual insertion cursor and selection cursor. -- then make an update message using fromKeyL based on the key and all this info let keyMessage :: Listener Key keyMessage = snapshotL (pairB (bvarBehavior active) (bvarBehavior selection)) (mapIOL (\k -> do x <- getIndx newsel <- getSelIndx return (k,x,newsel)) $ mapL (\ k eds -> reverse (fromKeyL otherins (tindexMark otherins) k) ++ eds) l') -- when we get a button 1 press update the insertion cursor position mousePress 1 (mapIOL (const $ fmap (flip (SetMark edid) otherins) $ getIndx) l) $ -- when button 1 is released we have probably updated the selection mouseRelease 1 select $ -- on a key press we do several things, send an update message saying the text -- may have changed with keyMessage, update the selection, and update the -- modifier key state keyPressAny (keyMessage `mergeL` select `mergeL` (mapMaybeL setInactive (bvarUpdInput active))) $ -- when the Alt or Control key is released update the modifer key state. keyRelease Alt (tellL (bvarUpdInput active) (delete Alt)) $ keyRelease Control (tellL (bvarUpdInput active) (delete Control)) $ (return ed) return () where -- the unique identifier of the edit widget. We pass this along with all -- messages so that when sharing we know if the update was sent by -- this widget edid :: Ident edid = uniqueId ed -- if a modifier key is pressed update the modifier state setInactive :: Key -> Maybe ([Key] -> [Key]) setInactive Alt = Just $ insertOrd cmp const Alt setInactive Control = Just $ insertOrd cmp const Control setInactive _ = Nothing -- compare two keys for ordering, if they're not equal, keep searching cmp :: Key -> Key -> Ordering cmp a b = if a == b then EQ else GT -- converting a keypress into an update fromKeyL :: Ident -- the shared insertion tag's identifier -> TIndex -- the index associated with this shared insertion tag -> ((Key, -- the key pressed ([Key], -- the modifier state Maybe (TIndex,TIndex))), -- the selection indices immediately before this key press Maybe TIndex, -- the actual insertion cursor position after key press Maybe (TIndex,TIndex)) -- the actual selection indices after key press -> [DocUpd] -- the updates we generate fromKeyL oins oinsM ((v,(mods,sel)),ti,newsel) = catMaybes [fromDel oinsM v sel mods, -- generate deletes if null mods then fromKey oinsM v else Nothing, -- insert text fromMod oins v sel ti] -- change our mark in the shared mark -- generate a delete update fromDel :: TIndex -- the index associated with our shared insertion tag -> Key -- the key pressed -> Maybe (TIndex,TIndex) -- the selection indices immediately before this key press -> [Key] -- the modifier state -> Maybe DocUpd -- on a delete press if there is a selection delete it otherwise delete the -- next character fromDel oinsM Delete sel _ = Just $ maybeDel oinsM sel 1 -- on a backspace if there is a selection delete it otherwise delete the -- last character fromDel oinsM BackSpace sel _ = Just $ maybeDel oinsM sel (-1) -- if there is a selection, and we pressed an actual character -- without a modifier on delete the selection fromDel _ v sel [] | isInsertDels v = fmap (uncurry $ DeleteBetween edid) sel -- otherwise delete nothing fromDel _ _ _ _ = Nothing -- an actual character ie real text to be inserted isInsertDels :: Key -> Bool isInsertDels v = v `elem` [Tab,Return,Space] || (case v of KeyChar _ -> True Key _ -> True _ -> False) -- when we hear a delete if there is a selection delete it -- otherwise delete starting from our cursor for some number of characters maybeDel :: TIndex -- the index associated with our shared insertion tag -> Maybe (TIndex,TIndex) -- the selection indices immediately before this key press -> Int -- some distance worth of characters to delete -- (negative means backwards) -> DocUpd maybeDel oinsM sel n = maybe (deleteFrom' edid oinsM n) (uncurry $ DeleteBetween edid) sel -- is the key a movement key, changing the cursor position but not inserting -- text isMover :: Key -> Bool isMover v = v `elem` movers where movers :: [Key] movers = [Home,End,CursorLeft,CursorRight,CursorUp,CursorDown,Next,Prior] -- change our shared insertion cursor in the DocumentB state fromMod :: Ident -- the identifier associated with our shared insertion cursor -> Key -- the key pressed -> Maybe (TIndex,TIndex) -- the selection indices immediately before this key press -> Maybe TIndex -- the actual insertion cursor position after key press -> Maybe DocUpd -- if there was a movement character or there was just some selected text -- then our cursor has probably moved. -- if so then set the new position of the cursor -- it is assumed that marks will move about sensibly in the edit val state -- and in edit widgets without needing to send explicit updates fromMod oins v sel ti | isMover v || isJust sel = Just $ SetMark edid ti oins fromMod _ _ _ _ = Nothing -- when we hear a key, if it represents textual input, such as tabs, return keys, -- or a character send an insertion request to insert the String equivalent, -- at our insertion cursor fromKey :: TIndex -- the index associated with our shared insertion cursor -> Key -- the key pressed -> Maybe DocUpd fromKey oinsM Tab = Just (insertString edid oinsM "\t") fromKey oinsM Return = Just (insertString edid oinsM "\n") fromKey oinsM (KeyChar c) = Just (insertString edid oinsM [c]) fromKey oinsM (Key s) = Just (insertString edid oinsM s) fromKey oinsM Space = Just (insertString edid oinsM " ") fromKey _ k = Nothing insertString :: Ident -> TIndex -> String -> DocUpd insertString i t s = InsertStructured i (SText s) t \end{code} introduce selections properly setSelection :: Maybe (TIndex,TIndex) -> IDoc getSelection :: IDoc -> Maybe (Maybe (TIndex,TIndex)) getSelectedTags :: IDoc -> Maybe ([Ident],[Ident]) but prefer getSelectedTags :: IDoc -> Maybe [Ident] selection :: [Conf EditTag] -> Selection instance Has_Input Selection TIndex -> (Int,Int) TIndex -> [Ident] sampling the indices of a mark with a listener or event \begin{code} snapMarkE :: Event a -> EditMark -> Event (a, Maybe (Int,Int)) snapMarkE e edi = F.withIOE e (\v -> do t <- getEditMarkIndex edi;return (v,t)) snapMarkE_ :: Event a -> EditMark -> Event (Maybe (Int,Int)) snapMarkE_ e edi = snapMarkE e edi ==> snd snapMarkL :: EditMark -> Listener (a,Maybe (Int,Int)) -> Listener a snapMarkL edi l = mapIOL (\v -> do t <- getEditMarkIndex edi;return (v,t)) l snapMarkL_ :: EditMark -> Listener (Maybe (Int,Int)) -> Listener a snapMarkL_ edi l = snapMarkL edi $ mapL snd l \end{code} sampling the indices of a tag with a listener or event \begin{code} snapTagE :: Event a -> EditTag -> Event (a, Maybe((Int,Int),(Int,Int))) snapTagE e edi = F.withIOE e (\v -> do t <- getEditTagIndex edi;return (v,t)) snapTagE_ :: Event a -> EditTag -> Event (Maybe((Int,Int),(Int,Int))) snapTagE_ e edi = snapTagE e edi ==> snd snapTagL :: EditTag -> Listener (a,Maybe ((Int,Int),(Int,Int))) -> Listener a snapTagL edi l = mapIOL (\v -> do t <- getEditTagIndex edi;return (v,t)) l snapTagL_ :: EditTag -> Listener (Maybe ((Int,Int),(Int,Int))) -> Listener a snapTagL_ edi l = snapTagL edi $ mapL snd l \end{code} \begin{code} instance Has_Input (GUI Edit) where bindAction a l w = do w <- w;liftIO $ bind w a l;return w \end{code} Deprecated functions \begin{code} type EditVal = DocUpd insertEditVal :: String -> TIndex -> EditVal insertEditVal s t = InsertStructured noEdit (SText s) t deleteEditVal :: TIndex -> TIndex -> EditVal deleteEditVal t1 t2 = DeleteBetween noEdit t1 t2 resetEditVal :: String -> EditVal resetEditVal = ResetStructured noEdit . SText clearEditVal :: EditVal clearEditVal = resetEditVal "" insertTagEditVal :: GUI EditTag -> EditVal insertTagEditVal = InsertEditTag nullId insertMarkEditVal :: GUI EditMark -> EditVal insertMarkEditVal = InsertEditMark nullId -- We can also insert some text at a given point, with an EditTag -- associated with. insertTaggedEditVal :: String -> TIndex -> GUI EditTag -> EditVal insertTaggedEditVal s ti g = InsertStructured nullId (STextTagged [SText s] g) ti insertStructuredEditVal :: [Structured] -> TIndex -> EditVal insertStructuredEditVal s t = InsertStructured nullId (SGroup s) t replaceStructuredEditVal :: Structured -> Ident -> EditVal replaceStructuredEditVal = ReplaceStructured nullId editValsB :: Behavior [EditVal] -> Conf Edit editValsB = setIDocB type EditValB = DocumentBVar type EditValBehavior = DocumentB editValEvent :: EditValB -> Event EditVal editValEvent = mapEs reverse . docBEvent . docB editValInput :: EditValB -> Listener EditVal editValInput = comapL (:) . docBVarUpdInput editValB :: EditValB -> Conf Edit editValB = setDocB . docB editVals :: [EditVal] -> Conf Edit editVals = setIDoc editValL :: Listener EditVal -> Conf Edit editValL l = listenDoc $ comapLs (\f -> reverse $ f []) l editValE :: Event EditVal -> Conf Edit editValE = setIDocE . mapE (:) useEditValBehavior :: EditValBehavior -> Conf Edit useEditValBehavior = setDocB -- initially reset the text area with the given text initEditState :: String -> EditState initEditState s = EditState [resetEditVal s] [] initEditVals :: [EditVal] -> EditState initEditVals es = EditState (reverse es) [] -- make an edit val state in the GUI monad mkEditValB :: EditState -> GUI EditValB mkEditValB init = liftIO $ newEditValB init -- make an edit val state, very like a dynamic collection newEditValB :: EditState -> IO EditValB newEditValB init@(EditState is _) = do w <- newWire b <- mkDocumentB' [] init (event w) return $ DocumentBVar b (input w) editValBehavior :: [EditVal] -> Event EditVal -> EditValBehavior editValBehavior init e = DocumentB init (e ==> \x -> [x]) (lift0 emptyEditState) \end{code}