sifflet-0.1.5: Workspace/Workspace.hs
{- Currently, this module contains functions for VPUI, VPUIWindow,
Workspace, VCanvas, and more. A reorganization seems called for,
but for now I will just keep adding functions here. -}
module Workspace.Workspace
(
-- VPUI and Workspace
vpuiNew
, defaultVPUIToolkits
, Workspace(..), workspaceNewDefault, workspaceNewEditing
, addArgToolButtons
, addApplyCloseButtons
, defineFunction
, openNode
-- Quitting:
, removeWindow
, vpuiQuit
-- Windows:
, forallWindowsIO
)
where
import Control.Monad
import Data.Map (Map, (!), fromList, keys)
import qualified Data.Map as Map (empty)
import Data.Graph.Inductive as G
import Graphics.UI.Gtk as Gtk
hiding (Frame, Function, Style, Size, buttonPressed, fill, lineWidth,
disconnect, function, remove)
import CBMgr
import Examples
import Expr
import Geometry
import GtkUtil
import TreeGraph
import TreeLayout
import UITypes
import Util
import Workspace.Canvas
import Workspace.Frame
import Workspace.Functoid
import Workspace.Tools
import Workspace.WGraph
-- | Create a new VPUI.
-- This used to set up the basic "q to quit" and "on exposed" callbacks,
-- but now does not even do that.
-- The 'init' function argument
-- may perform additional initialization;
-- if there is none, simply use 'return'.
-- The following comment is out of date,
-- but may explain some bizarre features historically:
-- Note that if you want to set up callbacks,
-- there is some trickiness: the vpui contains the workspace,
-- and the layout (which is on the workspace) needs to have callbacks
-- which know the uiref. So, create the workspace, vpui, and uiref,
-- in that order, and then set up the callbacks.
vpuiNew :: Style -> Env -> IO VPUI
vpuiNew style env = do
group <- radioToolButtonNew
return VPUI {vpuiWindows = Map.empty,
vpuiToolkits = Map.empty,
vpuiButtonGroup = group,
vpuiFilePath = Nothing,
vpuiFileChanged = False,
vpuiStyle = style,
vpuiInitialEnv = env,
vpuiGlobalEnv = env
}
baseFunctionsRows :: [[String]]
baseFunctionsRows = [["+", "-", "*", "div", "mod", "add1", "sub1", "/"],
["==", "/=", "<", ">", "<=", ">="],
["zero?", "positive?", "negative?"],
["null", "head", "tail", ":"]]
defaultVPUIToolkits :: Map String VPToolkit
defaultVPUIToolkits =
let toolkits =
-- each item has name, width, list of rows tools
[VPToolkit "Base" 500 (functionToolsFromLists baseFunctionsRows),
VPToolkit "Examples" 500
(functionToolsFromLists [exampleFunctionNames]),
VPToolkit "My Functions" 500 (functionToolsFromLists [[]])]
in fromList (zip (map toolkitName toolkits) toolkits)
-- | Create a new "main" workspace window, with a given style.
-- The second argument should set up a menu bar and place it on the vbox,
-- or do nothing if no menu is wanted.
workspaceNewDefault :: Style -> (VBox -> IO ()) -> IO Workspace
workspaceNewDefault style =
workspaceNew style (Size 3600.0 2400.0) (Just (Size 900.0 600.0))
workspaceNewEditing :: Style -> Env -> Function -> IO Workspace
workspaceNewEditing style initEnv func = do
{
; let argValues = []
funcFrame = fedFuncFrame style func argValues initEnv -- throw-away
Size fwidth fheight = bbSize (cfBox funcFrame)
canvSize = Size (max fwidth 300) (max fheight 300)
mViewSize = Nothing
addNoMenu _ = return ()
; workspaceNew style canvSize mViewSize addNoMenu
}
addArgToolButtons :: CBMgr -> WinId -> [String] -> VPUI -> IO ()
addArgToolButtons cbmgr winId labels vpui =
case vpuiGetWindow vpui winId of
VPUIWorkWin ws _ ->
let bbar = wsButtonBar ws
in mapM_ (addArgToolButton cbmgr winId bbar) labels
_ -> return ()
addArgToolButton :: CBMgr -> WinId -> HBox -> String -> IO ()
addArgToolButton cbmgr winId buttonBox label = do
{
button <- buttonNewWithLabel label
; boxPackStart buttonBox button PackNatural 3 -- spacing between buttons
; widgetShow button
; cbmgr (AfterButtonClicked button
(\ uiref ->
modifyIORefIO uiref (vpuiSetTool (ToolArg label) winId)))
; return ()
}
-- | Add "Apply" and "Close" buttons to a function-editor window
addApplyCloseButtons :: CBMgr -> WinId -> VPUI -> IO ()
addApplyCloseButtons cbmgr winId vpui =
case vpuiGetWindow vpui winId of
VPUIWorkWin ws window ->
addApplyCloseButtons2 cbmgr winId ws window
_ -> return ()
addApplyCloseButtons2 :: CBMgr -> WinId -> Workspace -> Window -> IO ()
addApplyCloseButtons2 cbmgr winId ws window =
let bbar = wsButtonBar ws
applyFrame :: VPUI -> IO VPUI
applyFrame vpui =
case vcFrames (vpuiWindowGetCanvas (vpuiGetWindow vpui winId)) of
[frame] -> defineFunction winId frame vpui
_ -> info "ApplyFrame: no unique frame found" >> return vpui
-- addButton :: String -> (IORef VPUI -> IO ())
addButton label action = do
{
button <- buttonNewWithLabel label
; boxPackEnd bbar button PackNatural 3
; widgetShow button
; cbmgr (AfterButtonClicked button action)
}
in addButton "Close" (\ _uiref -> widgetDestroy window) >>
addButton "Apply" (\ uiref -> modifyIORefIO uiref applyFrame)
-- | fedFuncFrame generates a throw-away frame for the sole purplse
-- of obtaining its measurements before initializing the canvas
fedFuncFrame :: Style -> Function -> [Value] -> Env -> CanvFrame
fedFuncFrame style func values prevEnv =
let (frame, _) =
frameNewWithLayout style (Position 0 0) 0
(FunctoidFunc func) values
CallFrame -- mode may change below
0 prevEnv Nothing
in frame
-- | If mViewSize is Nothing, no scrollbars are put on the canvas,
-- and its display size request is its natural size.
-- If mViewSize = Just viewSize, then scrollbars are wrapped around
-- the canvas, and its displayed size request is viewSize.
-- addMenuBar is an action which, if desired, adds a menu bar;
-- if you don't want one, just pass (\ _ -> return ()).
workspaceNew :: Style -> Size -> Maybe Size -> (VBox -> IO ()) -> IO Workspace
workspaceNew style canvSize mViewSize addMenuBar = do
{
; let Size dcWidth dcHeight = canvSize -- Double, Double
(icWidth, icHeight) = (round dcWidth, round dcHeight)
scrolled :: Gtk.Layout -> Size -> IO ScrolledWindow
scrolled layout viewSize = do
{
let Size dvWidth dvHeight = viewSize -- Double, Double
(iViewWidth, iViewHeight) = (round dvWidth, round dvHeight)
-- Wrap layout directly in a ScrolledWindow .
-- Adjustments: value lower upper stepIncr pageIncr pageSize
; xAdj <- adjustmentNew 0.0 0.0 dcWidth 10.0 dvWidth dvWidth
; yAdj <- adjustmentNew 0.0 0.0 dcHeight 10.0 dvHeight dvHeight
; scrollWin <- scrolledWindowNew (Just xAdj) (Just yAdj)
-- show scrollbars? (never, always, or if needed)
; scrolledWindowSetPolicy scrollWin PolicyAutomatic PolicyAutomatic
-- request view size for _layout_
; widgetSetSizeRequest layout iViewWidth iViewHeight
; set scrollWin [containerChild := layout]
; return scrollWin
}
bare :: Gtk.Layout -> IO Gtk.Layout
bare layout = do
{
-- request canvas size for _layout_
; widgetSetSizeRequest layout icWidth icHeight -- new
; return layout
}
-- The canvas itself
; vcanvas <- vcanvasNew style dcWidth dcHeight
; let layout = vcLayout vcanvas
-- Set the actual size of the canvas layout, which may be more
-- than is displayed if scrollbars are used
; layoutSetSize layout icWidth icHeight
-- An empty HBox for buttons (or it may remain empty)
; buttonBar <- hBoxNew False 3
-- The Statusbar
; statusBar <- statusbarNew
-- All together in a VBox
; vbox <- vBoxNew False 0 -- not equal space allotments, 0 spacing
; addMenuBar vbox
; let packGrow :: WidgetClass w => w -> IO ()
packGrow w = boxPackStart vbox w PackGrow 0
; case mViewSize of
Nothing -> bare layout >>= packGrow
Just viewSize -> scrolled layout viewSize >>= packGrow
; boxPackStart vbox buttonBar PackNatural 0
; boxPackStart vbox statusBar PackNatural 0
; return $ Workspace vbox vcanvas buttonBar statusBar
}
vpuiQuit :: VPUI -> IO VPUI
vpuiQuit vpui = do
{
-- *** This should also check for unsaved changes? ***
vpui' <- foldM (\ vp winId -> removeWindow vp True winId)
vpui
(vpuiAllWindowKeys vpui)
; mainQuit
; return vpui'
}
-- | List of all window ids of the vpui,
vpuiAllWindowKeys :: VPUI -> [WinId]
vpuiAllWindowKeys = keys . vpuiWindows
-- | Perform action on all windows
-- (actually (WinId, VPUIWindow) pairs.
-- Returns updated VPUI (in case any windows are changed).
forallWindowsIO :: (VPUIWindow -> IO VPUIWindow) -> VPUI -> IO VPUI
forallWindowsIO action vpui =
let loop ks vpui' =
case ks of
[] -> return vpui'
k : ks' ->
let w = vpuiGetWindow vpui' k
in do
{
w' <- action w
; loop ks' (vpuiReplaceWindow vpui' k w')
}
in loop (vpuiAllWindowKeys vpui) vpui
-- | This function is called either when a window *has been* destroyed,
-- with destroy = False,
-- or when you *want to* destroy a window, with destroy = True.
-- WOULD BE BETTER to have two functions, windowRemoved and removeWindow???
-- | removeWindow actually *closes* the window if destroy = True,
-- as well as removing it from the vpui's windows map.
removeWindow :: VPUI -> Bool -> WinId -> IO VPUI
removeWindow vpui destroy winId = do
{
-- Remove the window from vpui;
-- if destroy is true, also destroy it.
-- It is an error if the window id does not exist.
let vwMap = vpuiWindows vpui
; when destroy $ widgetDestroy (vpuiWindowWindow (vwMap ! winId))
; return $ vpuiRemoveVPUIWindow winId vpui
}
-- | Context menu command to apply the function definition
-- of an EditFrame.
-- | "Execute" the definition currently represented in the frame,
-- i.e., bind the function name in the global environment
-- to the function definition found in the frame.
defineFunction :: WinId -> CanvFrame -> VPUI -> IO VPUI
defineFunction winId frame vpui =
case frameType frame of
CallFrame ->
showErrorMessage "Software error\nNot in an edit frame!"
>> return vpui
EditFrame ->
case cfFunctoid frame of
FunctoidFunc _function ->
return vpui
fparts@FunctoidParts {} ->
let env = vpuiGlobalEnv vpui
vw = vpuiGetWindow vpui winId
canv = vpuiWindowGetCanvas vw
graph = vcGraph canv
frameNode = cfFrameNode frame
in case functoidToFunction fparts graph frameNode env of
Fail errmsg ->
showErrorMessage errmsg >>
return vpui
Succ function ->
let BBox x y _ _ = cfBox frame
z = cfLevel frame
fname = functionName function
env' = envSet env fname (VFun function)
vpui' = vpui {vpuiGlobalEnv = env'}
in do
{
; canv' <- vcCloseFrame canv frame
-- ** can this use vpuiAddFrame?
; canv'' <-
vcAddFrame canv' (FunctoidFunc function) []
EditFrame
env' x y z Nothing
; let vw' = vpuiWindowSetCanvas vw canv''
vpui'' = vpuiReplaceWindow vpui' winId vw'
; vpuiUpdateCallFrames vpui'' fname
}
-- | In the workspace window, update each frame calling the named function
-- to reflect the current function definition
vpuiUpdateCallFrames :: VPUI -> String -> IO VPUI
vpuiUpdateCallFrames vpui fname =
let winId = "Sifflet Workspace"
in case vpuiTryGetWindow vpui winId of
Nothing -> return vpui
Just w -> do
{
; let canvas = vpuiWindowGetCanvas w
env = vpuiGlobalEnv vpui
frames = callFrames canvas fname
update canv frame = canvasUpdateCallFrame canv frame fname env
; canvas' <- foldM update canvas frames
; let w' = vpuiWindowSetCanvas w canvas'
; return $ vpuiReplaceWindow vpui winId w'
}
-- | In the canvas, update a call frame with the current function
-- definition from the environment, returning a new canvas.
-- Root call frames are torn down and rebuilt with the new function definition.
-- Call frames that are called by other call frames are simply torn down.
canvasUpdateCallFrame :: VCanvas -> CanvFrame -> String -> Env -> IO VCanvas
canvasUpdateCallFrame canvas frame fname env = do
{
-- Tear down old frame
canvas' <- vcCloseFrame canvas frame
; case cfParent frame of
Nothing ->
-- root frame; build up new frame
let Position x y = bbPosition (cfBox frame)
z = cfLevel frame
functoid = FunctoidFunc {fpFunc = envGetFunction env fname}
in vcAddFrame canvas' functoid [] CallFrame env x y z Nothing
Just _ ->
-- frame with a parent; finished
return canvas'
}
openNode :: VPUIWindow -> G.Node -> IO VPUIWindow
openNode vw node = do
let canvas = vpuiWindowGetCanvas vw
graph = vcGraph canvas
if not (nodeIsSimple graph node)
then return vw -- WFrame node -- is this possible?
else if nodeIsOpen graph node
then info "Already open" >> return vw
else let frame = nodeContainerFrame canvas graph node
in case nodeCompoundFunction graph frame node of
Nothing ->
info "Cannot be opened" >> return vw
Just function ->
case nodeInputValues graph node of
EvalOk (VList values) ->
let env = extendEnv (functionArgNames function)
values (cfEnv frame)
Position x y =
frameOffset (vcStyle canvas) frame
z = succ (cfLevel frame)
in vwAddFrame vw
(FunctoidFunc function) values CallFrame
env x y z (Just node)
EvalOk x ->
error $ "openNode: non-VList result: " ++ show x
_ ->
info "Cannot be opened: lacking input values" >>
return vw