packages feed

rail-compiler-editor-0.3.0.0: src/RailEditor/TextArea.hs

{- |
Module      :  TextArea.hs
Description :  .
Maintainer  :  Kelvin Glaß, Chritoph Graebnitz, Kristin Knorr, Nicolas Lehmann (c)
License     :  MIT
Stability   :  experimental

The TextArea-module depicts the view on the data structure stored in the TextAreaContent-module for the editor.
-}


module TextArea(

-- * Types
  TextArea,
-- * Constructors
  initTextAreaWithContent,
-- * Constants
-- * Methods
  textAreaContent,
  currentPosition,
  setTextAreaContent,
  drawingArea,
  getTextAreaContainer, -- This function should be used to get a widget to place in MainWindow
  setInputMode,
  setHighlighting,
  redrawContent
  )where
    
    -- imports --
import qualified TextAreaContent as TAC
import qualified KeyHandler
import qualified Highlighter as HIGH
import qualified Selection

import qualified Graphics.Rendering.Cairo as CAIRO
import qualified Graphics.UI.Gtk as GTK
import qualified Graphics.UI.Gtk.Gdk.Events as Events
import Control.Concurrent (threadDelay)
import Data.IORef as IORef
import Data.Maybe
import qualified Data.Map as Map
import Control.Monad
import qualified Graphics.UI.Gtk.Gdk.GC as GC

{-
  This is the main datatype of TextArea 
-}
data TextArea = TA { drawingArea :: GTK.DrawingArea,
    textAreaContent :: IORef TAC.TextAreaContent, --The TAC
    scrolledWindow :: GTK.ScrolledWindow, --The part to bind it in a container
    currentPosition :: IORef TAC.Position, --The current position of cursor without hef and bef
    vAdjustment :: GTK.Adjustment, --needed to create a scrolledWindow
    hAdjustment :: GTK.Adjustment, --needed to create a scrolledWindow
    inputMode :: IORef KeyHandler.InputMode, -- needed to set and get the current input mode
    getHighlighted :: IORef Bool} -- needed to determine, whether the area is highlighted

--The cursor color
defaultCursorColor :: GC.Color
defaultCursorColor = GC.Color 65535 0 0 

--The background color
defaultBgColor :: GC.Color
defaultBgColor = GC.Color 65535 65535 65535

bef = 15 :: TAC.Coord --width of a character
hef = 15 :: TAC.Coord --height of a character

width = 400
height = 600

{-
  This function returns a scrolled Window which can be used
  to bind it on a widget.
-}
getTextAreaContainer :: TextArea -> IO GTK.ScrolledWindow
getTextAreaContainer textArea = return $ scrolledWindow textArea

setInputMode :: TextArea -> KeyHandler.InputMode -> IO ()
setInputMode area mode = do
  let modeRef = inputMode area
  writeIORef modeRef mode

{-
  This function sets the TextArea TAC to the TAC given in argument 2.
-}
setTextAreaContent :: TextArea -> TAC.TextAreaContent -> IO ()
setTextAreaContent textArea areaContent= do
  let 
    areaRef = textAreaContent textArea
    drawArea = drawingArea textArea
    highlightedIORef = getHighlighted textArea
  writeIORef areaRef areaContent
  highlighted <- readIORef highlightedIORef
  --expand the drawWindow when needed
  tac <- readIORef areaRef
  s@(xMax,yMax) <- TAC.size tac
  (w,h) <- GTK.widgetGetSizeRequest drawArea
  if yMax*hef > h && xMax*bef > w
  then GTK.widgetSetSizeRequest drawArea (w + (xMax*bef-w)) (h+(yMax*hef-w))
  else do
    when (yMax*hef > h ) $
      GTK.widgetSetSizeRequest drawArea w (h+((yMax*hef)-w))
    when (xMax*bef > w) $
      GTK.widgetSetSizeRequest drawArea (w + (xMax*bef-w))  h
  when highlighted $ HIGH.highlight tac
  redrawContent textArea

-- | sets (True) or unsets (False) the highlighting 
setHighlighting :: TextArea -> Bool -> IO ()
setHighlighting area val = do
  writeIORef (getHighlighted area) val
  tac <- readIORef $ textAreaContent area
  if val
  then HIGH.highlight tac
  else TAC.deleteColors tac
  redrawContent area

{-
  This function setup the TextArea. It Set up the drawWindow 
  related to textAreaContent. It also setup the different user events
  for editing the TextArea
-}
initTextAreaWithContent :: TAC.TextAreaContent -> IO TextArea
initTextAreaWithContent areaContent = do
  veAdjustment <- GTK.adjustmentNew 0 0 0 (fromIntegral hef) 0 0
  hoAdjustment <- GTK.adjustmentNew 0 0 0 (fromIntegral bef) 0 0
  scrwin <- GTK.scrolledWindowNew (Just hoAdjustment) (Just veAdjustment)
  areaRef <- newIORef areaContent
  drawArea <- setUpDrawingArea
  GTK.scrolledWindowAddWithViewport scrwin drawArea
  posRef <- newIORef (0,0)
  modRef <- newIORef KeyHandler.Insert
  highlighted <- newIORef True
  let textArea = TA drawArea areaRef scrwin posRef veAdjustment hoAdjustment modRef highlighted

  {-This function is called when the user press a mouse button.
    It calls the handleButtonPress function.  
  -}
  GTK.onButtonPress drawArea $ \event -> do
    let posRef = currentPosition textArea
    GTK.widgetGrabFocus drawArea
    readIORef posRef >>= clearCursor textArea
    handleButtonPress textArea (round(Events.eventX event),round(Events.eventY event)) >>= writeIORef posRef
    return $ Events.eventSent event
    
  GTK.on drawArea GTK.motionNotifyEvent $ do
    actualPos <- GTK.eventCoordinates
    CAIRO.liftIO $ do
      let areaRef = textAreaContent textArea
      tac <- readIORef areaRef
      currentPos <- readIORef posRef                            
      newPos <- updatePosition actualPos
      clearCursor textArea currentPos
      showCursor textArea newPos
      when (currentPos /= newPos) $ do
        (isAlreadySelected,selectedContent) <- Selection.handleSelection tac currentPos newPos
        if isAlreadySelected 
        then deselectEntries textArea tac selectedContent
        else selectEntries textArea selectedContent
        writeIORef posRef newPos
    GTK.eventRequestMotions
    return True
  {-
    This function is called when the user presses a key.
    It starts the heyHandler the highlighter and redraw the textAreaContent.
  -}
  GTK.onKeyPress drawArea $ \event -> do
    let 
      posRef   = currentPosition textArea
      areaRef  = textAreaContent textArea
      modif = Events.eventModifier event
      key = Events.eventKeyName event
      val = Events.eventKeyVal event
      modRef = inputMode textArea
    modus <- readIORef modRef
    tac <- readIORef areaRef --TextAreaContent
    readIORef posRef >>= clearCursor textArea
    posBef@(x,y) <- readIORef posRef
    pos@(kx,ky) <- KeyHandler.handleKey tac posBef modus modif key val
    --expand the drawWindow when needed
    extendDrawingAreaHorizontally textArea kx
    extendDrawingAreaVertically textArea ky
    writeIORef posRef pos
    highlighted <- readIORef (getHighlighted textArea)
    when highlighted $ HIGH.highlight tac
    redrawContent textArea
    showCursor textArea pos
    return $ Events.eventSent event
 
  {-
    Called when the application starts
    It setting the cursor and draw the textAreaContent
  -}
  GTK.onExpose drawArea $ \(Events.Expose sent _ _ _) -> do
    let 
      posRef   = currentPosition textArea
      areaRef  = textAreaContent textArea
    content <- readIORef areaRef
    redrawContent textArea
    readIORef posRef >>= showCursor textArea
    return sent
  return textArea


{-
  This function setup the DrawArea using GTK.DrawingArea.
-}
setUpDrawingArea :: IO GTK.DrawingArea
setUpDrawingArea = do
  drawingArea <- GTK.drawingAreaNew
  GTK.widgetModifyBg drawingArea GTK.StateNormal defaultBgColor
  GTK.set drawingArea [GTK.widgetCanFocus GTK.:= True]
  GTK.widgetAddEvents drawingArea [GTK.ButtonMotionMask]
  GTK.widgetSetSizeRequest drawingArea width height
  return drawingArea

--This handles a mouse button press event
handleButtonPress :: TextArea -> TAC.Position -> IO TAC.Position
handleButtonPress textArea (x,y) = do
  let tacIORef = textAreaContent textArea
      curPosIORef = currentPosition textArea
  showCursor textArea newPos
  tac <- readIORef tacIORef
  selection <- TAC.getSelectedPositons tac
  deselectEntries textArea tac selection
  Selection.updateCells tac selection False
  return newPos
  where newPos = (div x bef,div y hef)-- position without hef and bef

{-
  This function renders a character at the given position.
  It calculate the coords for drawingArea from TAC.Position.
-} 
renderScene :: TextArea -> TAC.Position -> Char -> TAC.RGBColor -> Bool -> Bool -> IO ()
renderScene textArea (x,y) char (TAC.RGBColor r g b) breakpoint isIP = do
  let drawArea = drawingArea textArea
  removeCharacter textArea (x,y)
  drawWindow <- GTK.widgetGetDrawWindow drawArea
  GTK.renderWithDrawable drawWindow $ do 
    CAIRO.setSourceRGBA r g b 1.0
    CAIRO.moveTo (fromIntegral (xCoord x + 2)) (fromIntegral (yCoord y - 3))
    CAIRO.setFontSize 14
    CAIRO.showText [char]
  gc <- GC.gcNew drawWindow
  GC.gcSetValues gc $ GC.newGCValues { GC.foreground = defaultCursorColor }
  when breakpoint $ GTK.drawRectangle drawWindow gc False (xCoord x-2) (yCoord (y-1)) (bef-2) (hef-2)
  when isIP $ GTK.drawArc drawWindow gc False (xCoord x-2) (yCoord (y-1)) (bef-2) (hef-2) 0 23040

--This function returns the y coord in relation to drawingArea    
yCoord :: TAC.Coord -> TAC.Coord
yCoord 0 = hef
yCoord y = y*hef + hef

--This function returns the  coord in relation to drawingArea  
xCoord :: TAC.Coord -> TAC.Coord
xCoord x = x*bef
  
{-
  This Function removes the caracter at given position
  The function should be called in renderScene to avoid overwriting.
-}
removeCharacter :: TextArea -> TAC.Position -> IO ()     
removeCharacter textArea (x,y) = do
  let drawArea = drawingArea textArea
  drawWindow <- GTK.widgetGetDrawWindow drawArea
  GTK.drawWindowClearArea drawWindow (fromIntegral $ xCoord x) 
          (fromIntegral(y*hef)) (fromIntegral bef) (fromIntegral hef) 

-- This Function draws every character in scrollable Frame.
redrawContent :: TextArea -> IO ()  
redrawContent textArea = do
  let 
    tacIORef = textAreaContent textArea
    drawArea = drawingArea textArea
    vAdj = vAdjustment textArea 
    hAdj = hAdjustment textArea
  vAdjValue <- GTK.adjustmentGetValue vAdj
  hAdjValue <- GTK.adjustmentGetValue hAdj
  drawFrameHeight <- GTK.adjustmentGetPageSize vAdj
  drawFrameWidth <- GTK.adjustmentGetPageSize hAdj
  drawWindow <- GTK.widgetGetDrawWindow drawArea
  s@(w,h) <- GTK.drawableGetSize drawWindow
  GTK.drawWindowClearArea drawWindow (round hAdjValue) (round vAdjValue) w h
  let 
    xFrom = div (round hAdjValue) bef
    yFrom = div (round vAdjValue) hef
    xTo =  div (round hAdjValue + round drawFrameWidth) bef
    yTo = div (round vAdjValue + round drawFrameHeight) hef
  tac <- readIORef tacIORef
  --Function from Control.Monad Monad m => [a] -> (a -> m b) -> m ()
  forM_ [xFrom..xTo] (\x -> forM_ [yFrom..yTo] (\y -> draw textArea (x,y)))
  where
    -- Call renderScene to 
    draw:: TextArea -> TAC.Position -> IO()
    draw textArea pos = do
      let tacIORef = textAreaContent textArea
      tac <- readIORef tacIORef
      mayCell <- TAC.getCell tac pos
      unless (isNothing mayCell) $ do 
        let ((char,isSelected), color) = fromJust mayCell
        cnt <- readIORef (TAC.context tac)
        renderScene textArea pos char color (Map.findWithDefault False pos (TAC.breakMap cnt)) (pos == TAC.curIPPos cnt)
        when isSelected $ selectEntry textArea pos

--Draws a cursor at the position.
--The function adds the TACU offsets
showCursor :: TextArea -> TAC.Position -> IO ()
showCursor textArea (x,y) = do
  let drawArea = drawingArea textArea
  drawWindow <- GTK.widgetGetDrawWindow drawArea
  gc <- GC.gcNew drawWindow
  GC.gcSetValues gc $ GC.newGCValues { GC.foreground = defaultCursorColor }
  GTK.drawRectangle drawWindow gc True (curX (x*bef)) (curY (y*hef)) 2 hef
  --http://hackage.haskell.org/package/gtk-0.12.5.7/docs/Graphics-UI-Gtk-Gdk-Drawable.html#t:Drawable
  return ()

-- delets the cursor at the given position.
clearCursor :: TextArea -> TAC.Position -> IO ()
clearCursor textArea (x,y) = do
  let drawArea = drawingArea textArea
  drawWindow <- GTK.widgetGetDrawWindow drawArea
  GTK.drawWindowClearArea drawWindow 
    (fromIntegral (curX (x*bef))) 
    (fromIntegral (curY (y*hef)))
    2
    (fromIntegral hef)

--Coords for the cursor to fit the cells of characters
curX x = abs(x - mod x bef-1)
curY y = y - mod y hef

{-
  This function extends the TextArea horizontal and set the scroll Frame.
  It should be called after KeyHandler 
-}
extendDrawingAreaHorizontally :: TextArea -> TAC.Coord -> IO ()
extendDrawingAreaHorizontally textArea x = do 
  let 
    drawArea = drawingArea textArea
    adjustment = hAdjustment textArea
  width <- GTK.adjustmentGetPageSize adjustment
  (w,h) <- GTK.widgetGetSizeRequest drawArea
  value <- GTK.adjustmentGetValue adjustment
  when ((x*bef)+bef >= w) $
    GTK.widgetSetSizeRequest drawArea ((x*bef)+bef) h
  when (x*bef +bef >= round width + round value || x*bef-bef < round value) $
    GTK.adjustmentSetValue adjustment $ fromIntegral (x*bef)
    
{-
  This function extends the TextArea vertically and set the scroll Frame.
  It should be called after KeyHandler 
-}
extendDrawingAreaVertically :: TextArea -> TAC.Coord -> IO ()
extendDrawingAreaVertically textArea y = do 
  let 
    drawArea = drawingArea textArea
    adjustment = vAdjustment textArea
  height <- GTK.adjustmentGetPageSize adjustment
  (w,h) <- GTK.widgetGetSizeRequest drawArea
  value <- GTK.adjustmentGetValue adjustment
  when (y*hef + hef >= h) $
    GTK.widgetSetSizeRequest drawArea w (y*hef + hef)
  when (y*hef+hef >= round height + round value) $
    GTK.adjustmentSetValue adjustment $ value + fromIntegral hef
  when (y*hef-hef < round value) $
    GTK.adjustmentSetValue adjustment $ value - fromIntegral hef


selectEntries :: TextArea -> [TAC.Position] -> IO ()
selectEntries _ [] = return ()
selectEntries textArea (x:xs) = do
  selectEntry textArea x
  selectEntries textArea xs

selectEntry :: TextArea -> TAC.Position -> IO ()
selectEntry textArea (x,y) = drawSelection textArea TAC.black 0.1 (x,y) (fromIntegral bef) (fromIntegral hef) 

deselectEntries :: TextArea -> TAC.TextAreaContent -> [TAC.Position] -> IO ()
deselectEntries _ _ [] = return ()
deselectEntries textArea tac (x:xs) = do
  deselectEntry textArea tac x
  deselectEntries textArea tac xs
    
deselectEntry :: TextArea -> TAC.TextAreaContent -> TAC.Position -> IO ()
deselectEntry textArea tac pos  = do
  drawSelection textArea TAC.white 1 pos (bef+2) hef 
  maybeCell <- TAC.getCell tac pos
  let ((char,isSelected),color) = fromJust maybeCell
  cnt <- readIORef (TAC.context tac)
  renderScene textArea pos char color (Map.findWithDefault False pos (TAC.breakMap cnt)) (pos == TAC.curIPPos cnt)

drawSelection :: TextArea -> TAC.RGBColor -> Double -> TAC.Position -> TAC.Coord -> TAC.Coord -> IO () 
drawSelection textArea (TAC.RGBColor r g b) alpha (x,y) width height = do
  drawWindow <- GTK.widgetGetDrawWindow $ drawingArea textArea
  GTK.renderWithDrawable drawWindow $ do
    CAIRO.setSourceRGBA r g b alpha
    CAIRO.rectangle (fromIntegral (xCoord x)) (fromIntegral (hef * y)) (fromIntegral width) (fromIntegral height)
    CAIRO.fill

updatePosition :: (Double,Double) -> IO TAC.Position
updatePosition (x,y) = 
  return (round leftX,round topY)
  where leftX = getMultiplier x (fromIntegral bef) 0
        topY  = getMultiplier y (fromIntegral hef) 0
  
getMultiplier :: Double -> Double -> Double -> Double
getMultiplier a b count
  | a > b * (count+1) = getMultiplier a b (count+1)
  | otherwise = count