scope-0.5.0.0: src/GUI.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS -Wall -fno-warn-unused-do-bind -fno-warn-orphans #-}
--
-- Based on Gtk2Hs/demo/cairo/Drawing2.hs
-- Author: Johan Bockgård <bojohan@dd.chalmers.se>
--
-- This code is in the public domain.
--
module GUI (
guiMain
) where
import Prelude hiding (catch)
import Control.Applicative ((<$>))
import Control.Concurrent
import Control.Monad.CatchIO
import Control.Monad.Reader
import Data.Function (on)
import qualified Data.IntMap as IM
import Data.IORef
import Data.List (groupBy)
import Data.Maybe
import qualified Data.Iteratee as I
import Data.ZoomCache.Numeric
import qualified Graphics.UI.Gtk as G
import qualified Graphics.Rendering.Cairo as C
import Graphics.Rendering.Cairo.Internal (Render(..))
import Graphics.Rendering.Cairo.Types (Cairo)
import qualified Graphics.Rendering.Cairo.Matrix as M
import qualified System.Random.MWC as MWC
-- import Text.Printf
import Paths_scope as My
import Scope.View
import Scope.Types
----------------------------------------------------------------------
windowWidth, windowHeight :: Int
windowWidth = 500
windowHeight = 500
{-
-- Write image to file
_writePng :: IO ()
_writePng =
C.withImageSurface C.FormatARGB32 width height $ \ result -> do
C.renderWith result $ plotWindow width height
C.surfaceWriteToPNG result "Draw.png"
where width = windowWidth
height = windowHeight
-}
-- Display image in window
guiMain :: Chan String -> [String] -> IO ()
guiMain chan args = do
_ <- G.initGUI
window <- G.windowNew
G.widgetSetSizeRequest window windowWidth windowHeight
G.widgetSetAppPaintable window True
G.widgetSetDoubleBuffered window True
vbox <- G.vBoxNew False 0
G.containerAdd window vbox
ui <- G.uiManagerNew
filename <- My.getDataFileName "data/actions.ui"
G.uiManagerAddUiFromFile ui filename
let getWidget = fmap fromJust . G.uiManagerGetWidget ui
-- Menubar
fma <- G.actionNew "FMA" "File" Nothing Nothing
ema <- G.actionNew "EMA" "Edit" Nothing Nothing
vma <- G.actionNew "VMA" "View" Nothing Nothing
hma <- G.actionNew "HMA" "Help" Nothing Nothing
-- File menu
newa <- G.actionNew "NEWA" "New" (Just "Just a Stub") (Just G.stockNew)
newa `G.on` G.actionActivated $ myNew
opena <- G.actionNew "OPENA" "Open" (Just "Just a Stub") (Just G.stockOpen)
savea <- G.actionNew "SAVEA" "Save" (Just "Just a Stub") (Just G.stockSave)
saveasa <- G.actionNew "SAVEASA" "Save As" (Just "Just a Stub") (Just G.stockSaveAs)
quita <- G.actionNew "QUITA" "Quit" (Just "Just a Stub") (Just G.stockQuit)
quita `G.on` G.actionActivated $ myQuit window chan
let fChooser action label = G.fileChooserDialogNew Nothing (Just window) action
[(G.stockCancel, G.ResponseCancel), (label, G.ResponseAccept)]
openDialog <- fChooser G.FileChooserActionOpen G.stockOpen
demoPath <- My.getDataFileName "demo"
G.fileChooserSetCurrentFolder openDialog demoPath
opena `G.on` G.actionActivated $ G.widgetShow openDialog
saveDialog <- fChooser G.FileChooserActionSave G.stockSave
savea `G.on` G.actionActivated $ G.widgetShow saveDialog
saveasa `G.on` G.actionActivated $ G.widgetShow saveDialog
saveDialog `G.on` G.response $ myFileSave saveDialog
-- Edit menu
cut1 <- G.actionNew "cut1" "Cut" (Just "Just a Stub") (Just G.stockCut)
cut1 `G.on` G.actionActivated $ myCut
copy1 <- G.actionNew "copy1" "Copy" (Just "Just a Stub") (Just G.stockCopy)
copy1 `G.on` G.actionActivated $ myCopy
paste1 <- G.actionNew "paste1" "Paste" (Just "Just a Stub") (Just G.stockPaste)
paste1 `G.on` G.actionActivated $ myPaste
delete1 <- G.actionNew "delete1" "Delete" (Just "Just a Stub") (Just G.stockDelete)
delete1 `G.on` G.actionActivated $ myDelete
-- Help menu
-- About dialog
aboutdialog <- G.aboutDialogNew
abouta <- G.actionNew "ABOUTA" "About" (Just "Just a Stub") Nothing
abouta `G.on` G.actionActivated $ G.widgetShow aboutdialog
aboutdialog `G.on` G.response $ const $ G.widgetHide aboutdialog
-- Action group
agr <- G.actionGroupNew "AGR"
mapM_ (G.actionGroupAddAction agr) [fma, ema, vma, hma]
mapM_ (\act -> G.actionGroupAddActionWithAccel agr act Nothing)
[ newa, opena, savea, saveasa, quita
, cut1, copy1, paste1, delete1
, abouta
]
G.uiManagerInsertActionGroup ui agr 0
menubar <- getWidget "/ui/menubar1"
G.boxPackStart vbox menubar G.PackNatural 0
adj <- G.adjustmentNew (0.0) (0.0) (1.0) (0.1) 1.0 1.0
drawingArea <- G.drawingAreaNew
let scope = scopeNew drawingArea adj
scopeRef <- newIORef scope
mapM_ (modifyIORefM scopeRef . addLayersFromFile) args
openDialog `G.on` G.response $ myFileOpen scopeRef openDialog
adj `G.onValueChanged` (scroll scopeRef)
G.boxPackStart vbox drawingArea G.PackGrow 0
drawingArea `G.on` G.buttonPressEvent $ G.tryEvent $ buttonDown scopeRef
drawingArea `G.on` G.buttonReleaseEvent $ G.tryEvent $ buttonRelease scopeRef
drawingArea `G.on` G.scrollEvent $ G.tryEvent $ wheel scopeRef
drawingArea `G.on` G.motionNotifyEvent $ G.tryEvent $ motion scopeRef
drawingArea `G.on` G.keyPressEvent $ G.tryEvent $ keyDown scopeRef
G.widgetAddEvents drawingArea
[ G.KeyPressMask
, G.KeyReleaseMask
-- , G.PointerMotionMask
, G.ButtonMotionMask
, G.ScrollMask
]
G.widgetSetCanFocus drawingArea True
drawingArea `G.on` G.exposeEvent $ G.tryEvent $ do
liftIO $ updateCanvas scopeRef
return ()
scrollbar <- G.hScrollbarNew adj
G.boxPackStart vbox scrollbar G.PackNatural 0
statusbar <- G.statusbarNew
G.boxPackStart vbox statusbar G.PackNatural 0
G.onDestroy window ((myWriteChan chan "quit") >> G.mainQuit)
G.widgetShowAll window
G.mainGUI
myQuit :: G.WidgetClass cls => cls -> Chan String -> IO ()
myQuit window chan = do
G.widgetDestroy window
myWriteChan chan "quit"
myWriteChan :: Chan String -> String -> IO ()
myWriteChan chan s = do writeChan chan s
yield
myNew :: IO ()
myNew = putStrLn "New"
myFileOpen :: IORef Scope -> G.FileChooserDialog -> G.ResponseId -> IO ()
myFileOpen scopeRef fcdialog response = do
case response of
G.ResponseAccept -> do
Just filename <- G.fileChooserGetFilename fcdialog
scopeModifyMRedraw scopeRef (addLayersFromFile filename)
_ -> return ()
G.widgetHide fcdialog
myFileSave :: G.FileChooserDialog -> G.ResponseId -> IO ()
myFileSave _ _ = return ()
myCut :: IO ()
myCut = putStrLn "Cut"
myCopy :: IO ()
myCopy = putStrLn "Copy"
myPaste :: IO ()
myPaste = putStrLn "Paste"
myDelete :: IO ()
myDelete = putStrLn "Delete"
updateCanvas :: IORef Scope -> IO Bool
updateCanvas ref = do
scope <- readIORef ref
let c = canvas . view $ scope
win <- G.widgetGetDrawWindow c
(width, height) <- G.widgetGetSize c
G.renderWithDrawable win $ plotWindow width height scope
return True
----------------------------------------------------------------
scopeAlign :: IORef Scope -> CanvasX -> DataX -> IO ()
scopeAlign ref cx dx = scopeModifyUpdate ref (scopeModifyView (viewAlign cx dx))
scopeMoveLeft :: IORef Scope -> IO ()
scopeMoveLeft ref = do
scope <- readIORef ref
let View{..} = view scope
scopeAlign ref (CanvasX 0.0) viewX2
scopeMoveRight :: IORef Scope -> IO ()
scopeMoveRight ref = do
scope <- readIORef ref
let View{..} = view scope
scopeAlign ref (CanvasX 1.0) viewX1
----------------------------------------------------------------
scopeZoomIn :: IORef Scope -> Double -> IO ()
scopeZoomIn ref = scopeZoomInOn ref (CanvasX 0.5)
scopeZoomOut :: IORef Scope -> Double -> IO ()
scopeZoomOut ref = scopeZoomOutOn ref (CanvasX 0.5)
scopeZoomInOn :: IORef Scope -> CanvasX -> Double -> IO ()
scopeZoomInOn ref focus mult = scopeZoomOutOn ref focus (1.0/mult)
scopeZoomOutOn :: IORef Scope -> CanvasX -> Double -> IO ()
scopeZoomOutOn ref focus mult =
scopeModifyUpdate ref (scopeModifyView (viewZoomOutOn focus mult))
scopeModifyMRedraw :: IORef Scope -> (Scope -> IO Scope) -> IO ()
scopeModifyMRedraw ref f = do
modifyIORefM ref f
G.widgetQueueDraw =<< canvas . view <$> readIORef ref
scopeModifyUpdate :: IORef Scope -> (Scope -> Scope) -> IO ()
scopeModifyUpdate ref f = do
modifyIORef ref f
View{..} <- view <$> readIORef ref
G.adjustmentSetValue adj (toDouble viewX1)
G.adjustmentSetPageSize adj $ toDouble (distance viewX1 viewX2)
G.widgetQueueDraw canvas
----------------------------------------------------------------
_canvasToScreen :: G.DrawingArea -> CanvasX -> IO ScreenX
_canvasToScreen c (CanvasX cX) = do
(width, _height) <- G.widgetGetSize c
return $ ScreenX (fromIntegral width * cX)
screenToCanvas :: G.DrawingArea -> ScreenX -> IO CanvasX
screenToCanvas c (ScreenX sX) = do
(width, _height) <- G.widgetGetSize c
return $ CanvasX (sX / fromIntegral width)
----------------------------------------------------------------
buttonDown :: IORef Scope -> G.EventM G.EButton ()
buttonDown ref = do
(x, _y) <- G.eventCoordinates
liftIO $ do
c <- canvas . view <$> readIORef ref
cX <- screenToCanvas c (ScreenX x)
modifyIORef ref (scopeModifyView (viewButtonDown cX))
buttonRelease :: IORef Scope -> G.EventM G.EButton ()
buttonRelease ref = liftIO $ modifyIORef ref (scopeModifyView viewButtonRelease)
motion :: IORef Scope -> G.EventM G.EMotion ()
motion ref = do
(x, _y) <- G.eventCoordinates
liftIO $ do
View{..} <- view <$> readIORef ref
cX <- screenToCanvas canvas (ScreenX x)
scopeModifyUpdate ref $ scopeModifyView (viewButtonMotion cX)
wheel :: IORef Scope -> G.EventM G.EScroll ()
wheel ref = do
(x, _y) <- G.eventCoordinates
dir <- G.eventScrollDirection
liftIO $ do
scope <- readIORef ref
let View{..} = view scope
cX <- screenToCanvas canvas (ScreenX x)
case dir of
G.ScrollUp -> scopeZoomInOn ref cX 1.2
G.ScrollDown -> scopeZoomOutOn ref cX 1.2
_ -> return ()
scroll :: IORef Scope -> IO ()
scroll ref = do
val <- G.adjustmentGetValue =<< adj . view <$> readIORef ref
scopeModifyUpdate ref $ scopeModifyView (viewMoveTo val)
----------------------------------------------------------------
-- Some keys we are interested in, from:
-- http://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h
#define XK_Home 0xff50
#define XK_Left 0xff51 /* Move left, left arrow */
#define XK_Up 0xff52 /* Move up, up arrow */
#define XK_Right 0xff53 /* Move right, right arrow */
#define XK_Down 0xff54 /* Move down, down arrow */
#define XK_Page_Up 0xff55
#define XK_Page_Down 0xff56
#define XK_End 0xff57 /* EOL */
keyDown :: IORef Scope -> G.EventM G.EKey ()
keyDown ref = do
v <- G.eventKeyVal
-- n <- G.eventKeyName
-- liftIO . putStrLn $ printf "Key %s (%d) pressed" n v
liftIO $ case v of
XK_Home -> scopeAlign ref (CanvasX 0.0) (DataX 0.0)
XK_End -> scopeAlign ref (CanvasX 1.0) (DataX 1.0)
XK_Up -> scopeZoomIn ref 2.0
XK_Down -> scopeZoomOut ref 2.0
XK_Left -> scopeMoveRight ref
XK_Right -> scopeMoveLeft ref
_ -> return ()
----------------------------------------------------------------
foreach :: (Monad m) => [a] -> (a -> m b) -> m [b]
foreach = flip mapM
keepState :: C.Render t -> C.Render ()
keepState render = do
C.save
_ <- render
C.restore
----------------------------------------------------------------
plotWindow :: Int -> Int -> Scope -> C.Render ()
plotWindow width height scope = do
prologue width height (view scope)
plotLayers scope
-- Set up stuff
prologue :: Int -> Int -> View -> C.Render ()
prologue wWidth wHeight View{..} = do
-- Define viewport coords as (-1.0, -1.0) - (1.0, 1.0)
let width = 1.0
height = 2.0
xmax = 1.0
xmin = 0.0
ymax = 1.0
ymin = -1.0
scaleX = realToFrac wWidth / width
scaleY = realToFrac wHeight / height
-- style and color
C.setLineCap C.LineCapRound
C.setLineJoin C.LineJoinRound
C.setLineWidth $ 1 / max scaleX scaleY
C.setSourceRGBA 0.5 0.7 0.5 0.5
-- Set up user coordinates
C.scale scaleX scaleY
-- center origin vertically
C.translate 0 (height / 2)
-- positive y-axis upwards
let flipY = M.Matrix 1 0 0 (-1) 0 0
C.transform flipY
grid xmin xmax ymin ymax
-- Grid and axes
grid :: Double -> Double -> Double -> Double -> C.Render ()
grid xmin xmax ymin ymax =
keepState $ do
C.setSourceRGBA 0 0 0 0.7
-- axes
C.moveTo 0 ymin; C.lineTo 0 ymax; C.stroke
C.moveTo xmin 0; C.lineTo xmax 0; C.stroke
-- grid
C.setDash [0.01, 0.99] 0
foreach [xmin .. xmax] $ \ x ->
do C.moveTo x ymin
C.lineTo x ymax
C.stroke
----------------------------------------------------------------
instance MonadCatchIO C.Render where
m `catch` f = mapRender (\m' -> m' `catch` \e -> runRender $ f e) m
block = mapRender block
unblock = mapRender unblock
mapRender :: (ReaderT Cairo IO m1 -> ReaderT Cairo IO m) -> Render m1 -> Render m
mapRender f = Render . f . runRender
----------------------------------------------------------------
plotLayers :: Scope -> C.Render ()
plotLayers scope = mapM_ f layersByFile
where
f :: [ScopeLayer] -> C.Render ()
f ls = keepState $ plotFileLayers (fn . head $ ls) ls scope
layersByFile = groupBy ((==) `on` fn) (layers scope)
fn (ScopeLayer l) = filename l
plotFileLayers :: FilePath -> [ScopeLayer] -> Scope -> C.Render ()
plotFileLayers path layers scope =
I.fileDriverRandom (I.joinI $ enumCacheFile identifiers (I.sequence_ is)) path
where
identifiers = standardIdentifiers
is = map (plotLayer scope) layers
plotLayer :: Scope -> ScopeLayer -> I.Iteratee [Stream] Render ()
plotLayer scope (ScopeLayer Layer{..}) =
I.joinI . filterTracks [trackNo] . I.joinI . convEnee $ foldData
where
View{..} = view scope
foldData = do
I.drop skipLength
I.joinI . I.take visibleLength $ render plotter
render (LayerMap f) = do
I.foldM renderMap canvasX0
return ()
where
renderMap x d = do
f x stepWidth d
return (x + stepWidth)
render (LayerFold f b00) = do
I.foldM renderFold (canvasX0, b00)
return ()
where
renderFold (x, b0) d = do
b <- f x stepWidth b0 d
return (x + stepWidth, b)
-- | Canvas X coordinate of first data point
canvasX0 = (fromIntegral skipLength - skip) * stepWidth
-- | Count of data points to drop before rendering
skipLength = floor skip
-- | DataX coordinate of start of view
skip = fromIntegral dataLength * toDouble viewX1
-- | Count of data points visible in view
visibleLength = ceiling viz + 2
-- | Canvas x length per data point
stepWidth = 1.0 / viz
-- | Fractional number of data points visible in view
viz = fromIntegral dataLength * toDouble (distance viewX1 viewX2)
----------------------------------------------------------------------
-- Raw data
plotRaw :: Double -> LayerFoldFunc (TimeStamp, Double) (Maybe Double)
plotRaw yR x w Nothing (ts, y) = plotRaw yR x w (Just y) (ts, y)
plotRaw yR x w (Just y0) (_ts, y) = do
C.moveTo x y0
C.lineTo (x+w) (y * 2.0 {- (viewY2 v - viewY1 v)-} / yR)
return (Just y)
----------------------------------------------------------------------
-- Summary data
plotSummary :: Double -> Double -> Double -> Double
-> LayerFoldFunc (Summary Double) (Maybe (Summary Double))
plotSummary dYRange r g b x w Nothing s =
plotSummary dYRange r g b x w (Just s) s
plotSummary dYRange r g b x w (Just s0) s = do
C.setSourceRGBA r g b 0.3
C.moveTo x (y (numMax sd0))
C.lineTo (x+w) (y (numMax sd))
C.lineTo (x+w) (y (numMin sd))
C.lineTo x (y (numMin sd0))
C.fill
C.setSourceRGB (r*0.6) (g*0.6) (b*0.6)
C.moveTo x (y (numAvg sd0))
C.lineTo (x+w) (y (numAvg sd))
C.stroke
return (Just s)
where
sd0 = summaryData s0
sd = summaryData s
y v = v * 4.0 / dYRange
----------------------------------------------------------------------
scopeModifyView :: (View -> View) -> Scope -> Scope
scopeModifyView f scope = scope{ view = f (view scope) }
----------------------------------------------------------------------
-- Random, similar colors
type RGB = (Double, Double, Double)
genColor :: RGB -> Double -> MWC.GenIO -> IO RGB
genColor (r, g, b) a gen = do
let a' = 1.0 - a
r' <- MWC.uniformR (0.0, a') gen
g' <- MWC.uniformR (0.0, a') gen
b' <- MWC.uniformR (0.0, a') gen
return (r*a + r', g*a + g', b*a * b')
genColors :: Int -> RGB -> Double -> IO [RGB]
genColors n rgb a = MWC.withSystemRandom (replicateM n . genColor rgb a)
----------------------------------------------------------------------
layersFromFile :: FilePath -> IO [ScopeLayer]
layersFromFile path = do
tracks <- IM.keys . cfSpecs <$> I.fileDriverRandom (iterHeaders standardIdentifiers) path
colors <- genColors (length tracks) (0.9, 0.9, 0.9) (0.5)
concat <$> mapM (\t -> I.fileDriverRandom (iterLayers t) path) (zip tracks colors)
where
iterLayers (trackNo, color) = layers trackNo color <$>
wholeTrackSummaryDouble standardIdentifiers trackNo
layers :: TrackNo -> RGB -> Summary Double -> [ScopeLayer]
layers trackNo rgb s = [ ScopeLayer (rawLayer trackNo s)
, ScopeLayer (sLayer trackNo rgb s)
]
rawLayer :: TrackNo -> Summary Double -> Layer (TimeStamp, Double)
rawLayer trackNo s = Layer path trackNo 5000 enumDouble (LayerFold (plotRaw (yRange s)) Nothing)
sLayer :: TrackNo -> RGB -> Summary Double -> Layer (Summary Double)
sLayer trackNo (r, g, b) s = Layer path trackNo 600 (enumSummaryDouble 1)
(LayerFold (plotSummary (yRange s) r g b) Nothing)
yRange :: Summary Double -> Double
yRange s = 2 * ((abs . numMin . summaryData $ s) + (abs . numMax . summaryData $ s))
addLayersFromFile :: FilePath -> Scope -> IO Scope
addLayersFromFile path scope = do
newLayers <- layersFromFile path
return $ scope { layers = layers scope ++ newLayers }
modifyIORefM :: IORef a -> (a -> IO a) -> IO ()
modifyIORefM ref f = do
x <- readIORef ref
x' <- f x
writeIORef ref x'
----------------------------------------------------------------