implementation module FilteredListBox import StdBool, StdEnum, StdList, StdMisc, StdOrdList, StdTuple, StdFunc import StdControl, StdControlReceiver, StdId, StdPicture, StdPSt, StdReceiver, StdWindow import StdControlAttribute import ioutil from commondef import strictSeq :: FilteredListBoxState = { items :: ![String] // All items to be displayed , selection :: ![Index] // The current selection , listboxId :: !FilteredListBoxId // The ids related to this list box , lineHeight :: !Int , initHeight :: !Int , pen :: ![PenAttribute] , ifilter :: !{#Char} -> Bool // the item filter , aitems :: ![String] // all items (unfiltered) } :: FilteredListBoxId = { fcontrolId :: !Id // The Id of the outmost CompoundControl , freceiverId :: !R2Id FilteredMessageIn FilteredMessageOut // The Id of the Receiver2Control that handles message passing } openFilteredListBoxId :: !*env -> (!FilteredListBoxId,!*env) | Ids env openFilteredListBoxId env # (cid, env) = openId env # (rid,env) = openR2Id env = ({fcontrolId=cid,freceiverId=rid},env) :: FilteredMessageIn = FInGetSelection // Request to retrieve current selection | FInSetSelection [Index] // Request to set the selection to the given index | FInGetItems // Request to retrieve all current items | FInAppendItems [FilteredListBoxItem] // Request to add items behind the last element | FInCloseAllItems // Request to remove all current items | FInSetPen [PenAttribute] // Request to set control pen | FInGetPen // Request to get control pen | FInSetFilter (!String->Bool) | FInGetFilter :: FilteredMessageOut = FOutGetSelection [(String,Index)] // Reply to retrieve the current selection | FOutSetSelection // Reply to set the selection | FOutGetItems [String] // Reply to get all items | FOutAppendItems // Reply to append items | FOutCloseAllItems // Reply to remove all items | FOutSetPen // Reply to set the control pen | FOutGetPen [PenAttribute] // Reply to get the control pen | FOutSetFilter | FOutGetFilter (!String->Bool) :: FilteredListBoxItem :== String :: FilteredListBoxControl ls ps = FilteredListBoxControl [FilteredListBoxItem] [Int] FilteredListBoxId [ControlAttribute *(*(FilteredListBoxState,ls),ps)] instance Controls FilteredListBoxControl where getControlType _ = "FilteredListBoxControl" controlToHandles (FilteredListBoxControl items selection listboxId attrs) ps #! ((lineHeight,initHeight),ps) = accScreenPicture (liheights penAtts) ps #! (domain,ps) = calcControlDomain penAtts items ps = controlToHandles (imp lineHeight initHeight domain) ps where liheights pen pic # pic = setPenAttributes pen pic # (metrics,pic) = getPenFontMetrics pic # lineHeight = fontLineHeight metrics # initHeight = metrics.fAscent + metrics.fLeading = ((lineHeight,initHeight),pic) imp lineHeight initHeight domain # (customLook,listboxState) = customlook listboxState = { addLS = listboxState , addDef = CompoundControl (NilLS) ( [ ControlId listboxId.fcontrolId , ControlItemSpace 0 0 // No itemspace , ControlHScroll (altScrollFunction Horizontal 10) , ControlVScroll (altScrollFunction Vertical lineHeight) , ControlViewDomain domain , ControlLook True customLook : listboxAtts ] ) :+: Receiver2 listboxId.freceiverId receiver [] } where listboxState = { items = items , selection = selection , listboxId = listboxId , lineHeight = lineHeight , initHeight = initHeight , pen = penAtts , ifilter = const True , aitems = items } listboxAtts = map toLBCA (filter isListBoxControlAttribute attrs) penAtts = flatten (map getControlPenAtt (filter isControlPen attrs)) isListBoxControlAttribute :: !(ControlAttribute .ps) -> Bool isListBoxControlAttribute (ControlFunction _) = True isListBoxControlAttribute ControlHide = True isListBoxControlAttribute (ControlPos _) = True isListBoxControlAttribute (ControlPen _) = True isListBoxControlAttribute (ControlSelectState _) = True isListBoxControlAttribute (ControlViewSize _) = True isListBoxControlAttribute (ControlOuterSize _) = True isListBoxControlAttribute (ControlResize _) = True isListBoxControlAttribute (ControlMouse _ _ _) = True isListBoxControlAttribute (ControlKeyboard _ _ _) = True isListBoxControlAttribute _ = False toLBCA (ControlFunction f) = ControlFunction f toLBCA ControlHide = ControlHide toLBCA (ControlPos p) = ControlPos p toLBCA (ControlPen p) = ControlPen p toLBCA (ControlSelectState p) = ControlSelectState p toLBCA (ControlViewSize p) = ControlViewSize p toLBCA (ControlOuterSize p) = ControlOuterSize p toLBCA (ControlResize p) = ControlResize p toLBCA (ControlMouse a b c) = ControlMouse a b c toLBCA (ControlKeyboard a b c) = ControlKeyboard a b c toLBCA _ = abort "FilteredListBox:toLBCA: unsupported ControlAttribute" receiver (FInSetFilter filt) ((listboxState=:{pen,aitems},ls),ps) # items = filter filt aitems # listboxState = {listboxState & ifilter = filt, items = items} // refresh... # (newDomain,ps) = calcControlDomain pen items ps # (newLook,listboxState)= customlook listboxState # ps = appPIO (seq [ setControlViewDomain customId newDomain , setControlLooks [(customId,True,(True,newLook))] ]) ps = (FOutSetFilter,((listboxState,ls),ps)) where customId = listboxState.listboxId.fcontrolId receiver (FInGetFilter) ((listboxState=:{ifilter},ls),ps) = (FOutGetFilter ifilter,((listboxState,ls),ps)) // Return the current selection: receiver FInGetSelection ((listboxState=:{items,selection},ls),ps) = (FOutGetSelection (map (\index->(items!!(index-1),index)) selection),((listboxState,ls),ps)) // Set a new selection: receiver (FInSetSelection newSelection) ((listboxState=:{lineHeight,initHeight},ls),ps) # listboxState = {FilteredListBoxState | listboxState & selection=newSelection} # (newLook,listboxState)= customlook listboxState #! ps = scrolltosel ps #! ps = appPIO (setControlLooks [(customId,True,(True,newLook))]) ps = (FOutSetSelection,((listboxState,ls),ps)) where customId = listboxState.listboxId.fcontrolId singlesel = length newSelection == 1 selitem = hd newSelection scrolltosel ps | not singlesel = ps # top = (selitem-1) * lineHeight # bot = selitem * lineHeight # (wdef,ps) = accPIO (getParentWindow customId) ps | isNothing wdef = ps # wdef = fromJust wdef # (exists,frame) = getControlViewFrame customId wdef | not exists = ps | isNothing frame = ps # frame = fromJust frame # delta = top - frame.corner1.y | delta < 0 = appPIO (moveControlViewFrame customId {vx=0, vy=delta}) ps # delta = bot - frame.corner2.y | delta > 0 = appPIO (moveControlViewFrame customId {vx=0, vy=delta}) ps = ps // Return the current elements: receiver FInGetItems ((listboxState=:{items},ls),ps) = (FOutGetItems items,((listboxState,ls),ps)) // Append elements: receiver (FInAppendItems newItems) ((listboxState=:{pen,items,aitems,ifilter,lineHeight,initHeight},ls),ps) # listboxState = {listboxState & items=allItems, aitems = aitems++newItems} | length newItems`==0 = (FOutAppendItems,((listboxState,ls),ps)) # (newDomain,ps) = calcControlDomain pen allItems ps # (newLook,listboxState)= customlook listboxState # (delta,ps) = scrolltoend newDomain ps # ps = appPIO (seq [ setControlLooks [(customId,False,(True,newLook))] , setControlViewDomain customId newDomain , moveControlViewFrame customId {vx=0, vy=delta} ]) ps = (FOutAppendItems,((listboxState,ls),ps)) where customId = listboxState.listboxId.fcontrolId newItems` = filter ifilter newItems allItems = items++newItems` scrolltoend dom=:{corner2={y=bot}} ps # (wdef,ps) = accPIO (getParentWindow customId) ps | isNothing wdef = (zero,ps) # wdef = fromJust wdef # (exists,frame) = getControlViewFrame customId wdef | not exists || isNothing frame = (zero,ps) # frame = fromJust frame # delta = bot - frame.corner2.y = (delta,ps) // Remove elements: // Remove all: receiver (FInCloseAllItems) ((listboxState=:{listboxId,pen,items,selection,lineHeight,initHeight},ls),ps) # listboxState = {listboxState & items=[], aitems = [], selection=[]} # (newDomain,ps)= calcControlDomain pen [] ps # (newLook,listboxState)= customlook listboxState # ps = appPIO (seq [ setControlViewDomain listboxId.fcontrolId newDomain , setControlLooks [(listboxId.fcontrolId,True,(True,newLook))] ]) ps = (FOutCloseAllItems,((listboxState,ls),ps)) // Set control pen: receiver (FInSetPen newpen) ((listboxState=:{listboxId,items,pen},ls),ps) # pen = removeDupAtt (newpen++pen) # (newDomain,ps) = calcControlDomain pen items ps # ((lineHeight,initHeight),ps) = accScreenPicture (liheights pen) ps # listboxState = { listboxState & pen = pen , lineHeight = lineHeight , initHeight = initHeight } # (newLook,listboxState)= customlook listboxState # ps = appPIO (seq [ setControlViewDomain listboxId.fcontrolId newDomain , setControlLooks [(listboxId.fcontrolId,True,(True,newLook))] , setControlScrollFunction listboxId.fcontrolId Vertical (altScrollFunction Vertical lineHeight) ]) ps = (FOutSetPen,((listboxState,ls),ps)) // Get control pen: receiver (FInGetPen) ((listboxState=:{pen},ls),ps) = (FOutGetPen pen,((listboxState,ls),ps)) calcControlDomain :: ![.PenAttribute] ![.{#Char}] !*(PSt .a) -> *(!.Rectangle,!*PSt .a); calcControlDomain pen items ps # (newDomain,ps) = accPIO (accScreenPicture calc) ps = (newDomain,ps) where calc pic # pic = setPenAttributes pen pic # (metrics,pic) = getPenFontMetrics pic # (itemWidths,pic) = getPenFontStringWidths items pic # minWidth = 0 # maxWidth = maxList [minWidth:itemWidths] # nrItems = length items # height = nrItems*(fontLineHeight metrics) # newDomain = {corner1=zero,corner2={x=maxWidth,y=height}} // calculate new domain... = (newDomain,pic) removeDupAtt [x:xs] = [x:removeDupAtt (filter (diff x) xs)] where diff (PenSize _) (PenSize _) = False diff (PenPos _) (PenPos _) = False diff (PenColour _) (PenColour _) = False diff (PenBack _) (PenBack _) = False diff (PenFont _) (PenFont _) = False diff _ _ = True removeDupAtt _ = [] // The look of the custom control lists all items and the current selection customlook ls=:{items,selection,pen,lineHeight,initHeight} = (customlook,ls) where customlook _ {newFrame} pict # pict = setPenAttributes pen pict # pict = unfill newFrame pict # (_,pict) = strictSeq [drawline item \\ item <- items] (initHeight,pict) # pict = strictSeq [drawsel sel \\ sel <- selection] pict = pict where (x1,x2) = (newFrame.corner1.x,newFrame.corner2.x) drawsel i = hilite {corner1={x=x1,y=(i-1)*lineHeight}, corner2={x=x2,y=i*lineHeight-1}} drawline line (y,p) = (y+lineHeight,drawAt {x=0,y=y} line p) //-- flbMouse :: ({#Char} -> .(*(PSt .a) -> *PSt .a)) -> .ControlAttribute *((FilteredListBoxState,.b),*PSt .a); flbMouse efun = ControlMouse mouseFilter Able (mouse efun) flbKeyboard :: ({#Char} -> .(*(PSt .a) -> *PSt .a)) -> .ControlAttribute *((FilteredListBoxState,.b),*PSt .a); flbKeyboard efun = ControlKeyboard keyFilter Able (keyboard efun) keyFilter :: KeyboardState -> Bool keyFilter (SpecialKey _ (KeyDown True) _) = True keyFilter _ = False keyboard efun (SpecialKey key (KeyDown repeat) {shiftDown,controlDown}) ((lbState=:{selection,items,lineHeight},ls),ps) | key == enterKey | not hasSelection = ((lbState,ls),ps) // execute selection // shift-execute // ? what if mul-selection // | shiftDown // # ps = (shiftfuns!!(lastSelection-1)) ps // = ((lbState,ls),ps) # ps = efun (items!!(lastSelection-1)) ps = ((lbState,ls),ps) | key == upKey // set selection one earlier // if shift - extend selection one up // if control ... // if control-shift ... # newSelection = if hasSelection (max 1 (lastSelection - 1)) 1 # lbState = {lbState & selection = [newSelection]} # (newLook,lbState) = customlook lbState # ps = scrolltoselection True newSelection ps # ps = appPIO (setControlLooks [(customId,True,(True,newLook))]) ps = ((lbState,ls),ps) | key == downKey // set selection one later // if shift - extend selection one up // if control ... // if control-shift ... # newSelection = if hasSelection (min nrItems (lastSelection + 1)) nrItems # lbState = {lbState & selection = [newSelection]} # (newLook,lbState) = customlook lbState # ps = scrolltoselection True newSelection ps # ps = appPIO (setControlLooks [(customId,True,(True,newLook))]) ps = ((lbState,ls),ps) | key == beginKey // set selection first // if shift - extend selection one up // if control ... // if control-shift ... # newSelection = 1 # lbState = {lbState & selection = [newSelection]} # (newLook,lbState) = customlook lbState # ps = scrolltoselection True newSelection ps # ps = appPIO (setControlLooks [(customId,True,(True,newLook))]) ps = ((lbState,ls),ps) | key == endKey // set selection last // if shift - extend selection one up // if control ... // if control-shift ... # newSelection = nrItems # lbState = {lbState & selection = [newSelection]} # (newLook,lbState) = customlook lbState # ps = scrolltoselection True newSelection ps # ps = appPIO (setControlLooks [(customId,True,(True,newLook))]) ps = ((lbState,ls),ps) # (wstate,ps) = accPIO (getParentWindow customId) ps | isNothing wstate = ((lbState,ls),ps) # wstate = fromJust wstate # (ok,frame) = getControlViewFrame customId wstate | not ok = ((lbState,ls),ps) | isNothing frame = ((lbState,ls),ps) # frame = fromJust frame # linesOnPage = max 1 (dec ((frame.corner2.y - frame.corner1.y) / lineHeight)) | key == pgUpKey // set selection page-up // if shift - extend selection one up // if control ... // if control-shift ... # top = (lastSelection-2) * lineHeight # newSelection = if hasSelection (if (top <= frame.corner1.y) //topLine (max 1 (lastSelection - linesOnPage)) (2 + (frame.corner1.y / lineHeight)) //topOfPage ) 1 # lbState = {lbState & selection = [newSelection]} # (newLook,lbState) = customlook lbState # ps = scrolltoselection True newSelection ps # ps = appPIO (setControlLooks [(customId,True,(True,newLook))]) ps = ((lbState,ls),ps) | key == pgDownKey // set selection page-down // if shift - extend selection one up // if control ... // if control-shift ... # bot = (inc lastSelection) * lineHeight # newSelection = if hasSelection (if (bot >= frame.corner2.y) //bottomLine (min nrItems (lastSelection + linesOnPage)) (frame.corner2.y / lineHeight) //bottomOfPage ) nrItems # lbState = {lbState & selection = [newSelection]} # (newLook,lbState) = customlook lbState # ps = scrolltoselection True newSelection ps # ps = appPIO (setControlLooks [(customId,True,(True,newLook))]) ps = ((lbState,ls),ps) = ((lbState,ls),ps) where nrItems = length items customId = lbState.listboxId.fcontrolId hasSelection | isEmpty selection = False = True lastSelection = hd selection scrolltoselection singlesel selitem ps | not singlesel = ps # top = (selitem-1) * lineHeight # bot = selitem * lineHeight # (wdef,ps) = accPIO (getParentWindow customId) ps | isNothing wdef = ps # wdef = fromJust wdef # (exists,frame) = getControlViewFrame customId wdef | not exists = ps | isNothing frame = ps # frame = fromJust frame # delta = top - frame.corner1.y | delta < 0 = appPIO (moveControlViewFrame customId {vx=0, vy=delta}) ps # delta = bot - frame.corner2.y | delta > 0 = appPIO (moveControlViewFrame customId {vx=0, vy=delta}) ps = ps keyboard _ _ _ = abort "FilteredListBox: unsupported keyboard action" // The mouse responds only to MouseDowns: mouseFilter :: MouseState -> Bool mouseFilter (MouseDown _ _ _) = True mouseFilter _ = False // The mouse either sets, adds, or removes items to the selection: mouse efun (MouseDown pos {shiftDown,controlDown} 1) ((listboxState=:{items,selection,lineHeight,initHeight},ls),ps) # listboxState = {FilteredListBoxState | listboxState & selection=okSelection} # (newLook,listboxState) = customlook listboxState # ps = appPIO (setControlLooks [(customId,True,(True,newLook))]) ps = ((listboxState,ls),ps) where nrItems = length items hasSelection= not (isEmpty selection) [prevIndex:_]= selection listSelection | prevIndex < newIndex = [prevIndex..newIndex] = [newIndex..prevIndex] newIndex = pos.y/lineHeight+1 newSelection | shiftDown | hasSelection = removeDup [newIndex:listSelection++selection] = [newIndex] | controlDown | isMember newIndex selection = removeMembers selection [newIndex] = [newIndex:selection] = [newIndex] okSelection = filter (isBetween 1 nrItems) newSelection customId = listboxState.listboxId.fcontrolId mouse efun (MouseDown pos {shiftDown} _) ((listboxState=:{items,lineHeight,initHeight},ls),ps) // double click # listboxState = {FilteredListBoxState | listboxState & selection=okSelection} # (newLook,listboxState) = customlook listboxState # ps = appPIO (setControlLooks [(customId,True,(True,newLook))]) ps | newIndex > length listboxState.items = ((listboxState,ls),ps) // | shiftDown // # ps = (shiftfuns!!(newIndex-1)) ps // = ((listboxState,ls),ps) # ps = efun (items!!(newIndex-1)) ps = ((listboxState,ls),ps) where customId = listboxState.listboxId.fcontrolId newIndex = pos.y/lineHeight+1 nrItems = length items newSelection= [newIndex] okSelection = filter (isBetween 1 nrItems) newSelection mouse _ _ _ = abort "FilteredListBox: unsupported mouse action" // The functions below take care of the proper communication with the receiver that // belongs to the listbox control. getFilteredListBoxSelection :: !FilteredListBoxId !(PSt .l) -> (!(!Bool,![(String,!Index)]),!PSt .l) getFilteredListBoxSelection {freceiverId} pState # ((_,maybe_out),pState) = syncSend2 freceiverId FInGetSelection pState | isNothing maybe_out = ((False,[]),pState) # result = case (fromJust maybe_out) of (FOutGetSelection selection) -> (True,selection) _ -> (False,[]) | otherwise = (result,pState) setFilteredListBoxSelection :: !FilteredListBoxId ![Index] !(PSt .l) -> PSt .l setFilteredListBoxSelection {freceiverId} selection pState = snd (syncSend2 freceiverId (FInSetSelection selection) pState) getFilteredListBoxItems :: !FilteredListBoxId !(PSt .l) -> (!(!Bool,![String]),!PSt .l) getFilteredListBoxItems {freceiverId} pState # ((_,maybe_out),pState) = syncSend2 freceiverId FInGetItems pState | isNothing maybe_out = ((False,[]),pState) # result = case (fromJust maybe_out) of (FOutGetItems items) -> (True,items) _ -> (False,[]) | otherwise = (result,pState) appendFilteredListBoxItems :: !FilteredListBoxId ![FilteredListBoxItem] !(PSt .l) -> PSt .l appendFilteredListBoxItems {freceiverId} items pState = snd (syncSend2 freceiverId (FInAppendItems items) pState) setFilteredListBoxPen :: !FilteredListBoxId ![PenAttribute] !(PSt .l) -> PSt .l setFilteredListBoxPen {freceiverId} pen ps = snd (syncSend2 freceiverId (FInSetPen pen) ps) setFilter :: !FilteredListBoxId (!String->Bool) !(PSt .l) -> PSt .l setFilter {freceiverId} flt ps = snd (syncSend2 freceiverId (FInSetFilter flt) ps) getFilter :: !FilteredListBoxId !(PSt .l) -> (!String->Bool,PSt .l) getFilter {freceiverId} ps # ((_,out),ps) = (syncSend2 freceiverId (FInGetFilter) ps) | isNothing out = (const True,ps) # out = case (fromJust out) of (FOutGetFilter filt) -> filt _ -> const True = (out,ps) exec_next_filtered :: !Bool !FilteredListBoxId (!String !(PSt .l) -> (PSt .l)) !(PSt .l) -> (PSt .l) exec_next_filtered next lbId efun ps # ((ok,sel),ps) = getFilteredListBoxSelection lbId ps | not ok = ps # ((ok,lst),ps) = getFilteredListBoxItems lbId ps | not ok = ps | length lst == 0 = ps # idx = (if (isEmpty sel) (firsti) (nexti (snd(hd sel)) lst)) # ps = setFilteredListBoxSelection lbId [idx] ps # ((ok,sel),ps) = getFilteredListBoxSelection lbId ps | not ok = ps | isEmpty sel = ps # path = fst(hd sel) = efun path ps where firsti = 1 nexti idx lst # idx = fun idx # idx = normalise idx 1 l l = idx where l = length lst fun | next = inc = dec normalise num min max incr | num < min = normalise (num+incr) min max incr | num > max = normalise (num-incr) min max incr = num // Auxiliary functions: isBetween :: x x x -> Bool | Ord x isBetween low up x = low<=x && x<=up