packages feed

yi-0.5.2: Yi/UI/Cocoa.hs

{-# LANGUAGE TemplateHaskell, ForeignFunctionInterface, ExistentialQuantification, TypeSynonymInstances, FlexibleInstances, MultiParamTypeClasses, DeriveDataTypeable #-}
--
-- Copyright (c) 2007, 2008 Jean-Philippe Bernardy
-- Copyright (c) 2008 Gustav Munkby
--

-- | This module defines a user interface implemented using Cocoa.

module Yi.UI.Cocoa (start) where

import Prelude hiding (init, length, error, sequence_, elem, mapM_, mapM, concat, concatMap)

import Yi.UI.Cocoa.Application
import Yi.UI.Cocoa.TextStorage
import Yi.UI.Cocoa.TextView
import Yi.UI.Cocoa.Utils

import Yi.Prelude hiding (init)
import Yi.Buffer
import Yi.Editor
import Yi.Keymap
import Yi.Monad
import Yi.Config
import Yi.Rectangle
import Yi.String
import qualified Yi.UI.Common as Common
import qualified Yi.WindowSet as WS
import qualified Yi.Style as Style
import Yi.Window
import Paths_yi (getDataFileName)

import Control.Monad.Reader (when)

import qualified Data.List as L
import Data.IORef
import Data.Maybe
import Data.Monoid
import Data.Unique

-- Specify Cocoa imports explicitly, to avoid name-clashes.
-- Since the number of functions recognized by HOC varies
-- between revisions, this seems like the safest choice.
import HOC
import Foundation (
  NSPoint(..),NSRect(..),NSRange(..),NSSize(..),nsHeight,nsWidth,
  _NSThread,detachNewThreadSelectorToTargetWithObject,alloc,init,
  NSObject,toNSString,respondsToSelector,_NSMutableArray,array,
  _NSValue,valueWithRange,addObject,NSMutableArray)
import AppKit (
  frame,bounds,setFrame,NSView,_NSView,NSTextField,_NSTextField,
  NSCell,_NSSplitView,_NSImage,NSApplication,sharedApplication,
  terminate_,run,setApplicationIconImage,NSWindow,_NSWindow,_NSMenu,
  activateIgnoringOtherApps,makeKeyAndOrderFront,setMainMenu,
  addSubview,removeFromSuperview,Has_setBackgroundColor,
  Has_setTextColor,NSSplitView,_NSFont,addSubviewPositionedRelativeTo,
  fontWithNameSize,setUserFixedPitchFont,userFixedPitchFontOfSize,
  adjustSubviews,cell,center,containerSize,nsWindowBelow,
  initWithContentRectStyleMaskBackingDefer,initWithContentsOfFile,
  initWithFrame,layoutManager,makeFirstResponder,
  nsBackingStoreBuffered,nsClosableWindowMask,nsLeftTextAlignment,
  nsLineBreakByTruncatingMiddle,nsMiniaturizableWindowMask,
  nsResizableWindowMask,nsTitledWindowMask,nsViewHeightSizable,
  nsViewMaxYMargin,nsViewNotSizable,nsViewWidthSizable,
  performMiniaturize,replaceTextStorage,scrollRangeToVisible,
  setAlignment,setAutodisplay,setAutohidesScrollers,
  setAutoresizingMask,setBackgroundColor,setBezeled,setBordered,
  setContainerSize,setDelegate,setDocumentView,setEditable,
  setFrameAutosaveName,setHasHorizontalScroller,setHasVerticalScroller,
  setHorizontallyResizable,setInsertionPointColor,setLineBreakMode,
  setRichText,setSelectable,setSelectedRanges,NSLayoutManager,
  setSelectedTextAttributes,setStringValue,setTextColor,setTitle,
  setVerticallyResizable,setWidthTracksTextView,setWindowsMenu,
  setWraps,sizeToFit,textColor,textContainer)

import qualified AppKit.NSWindow
import qualified AppKit.NSView

import Foreign.C
import Foreign hiding (new)

foreign import ccall "Processes.h TransformProcessType" transformProcessType :: Ptr (CInt) -> CInt -> IO (CInt)
foreign import ccall "Processes.h SetFrontProcess" setFrontProcess :: Ptr (CInt) -> IO (CInt)
foreign import ccall "Processes.h GetCurrentProcess" getCurrentProcess :: Ptr (CInt) -> IO (CInt)

-- Don't import this, since it is only available in Leopard...
$(declareRenamedSelector "setAllowsNonContiguousLayout:" "setAllowsNonContiguousLayout" [t| Bool -> IO () |])
instance Has_setAllowsNonContiguousLayout (NSLayoutManager a)
_silenceWarning :: ImpType_setAllowsNonContiguousLayout a b
_silenceWarning = undefined

------------------------------------------------------------------------

data UI = UI {uiWindow :: NSWindow ()
             ,uiBox :: NSSplitView ()
             ,uiCmdLine :: NSTextField ()
             ,windowCache :: IORef [WinInfo]
             ,uiActionCh :: Action -> IO ()
             ,uiFullConfig :: Config
             }

uiConfig :: UI -> UIConfig
uiConfig = configUI . uiFullConfig

data WinInfo = WinInfo
  { wikey    :: !Unique        -- ^ Uniquely identify each window
  , window   :: Window         -- ^ The editor window that we reflect
  , textview :: YiTextView ()
  , modeline :: NSTextField ()
  , widget   :: NSView ()      -- ^ Top-level widget for this window.
  , storage  :: TextStorage
  }

mkUI :: UI -> Common.UI
mkUI ui = Common.dummyUI
  { Common.main    = main
  , Common.end     = end
  , Common.suspend = uiWindow ui # performMiniaturize nil
  , Common.refresh = refresh ui
  }

mkRect :: Float -> Float -> Float -> Float -> NSRect
mkRect x y w h = NSRect (NSPoint x y) (NSSize w h)

allSizable, normalWindowMask :: CUInt
allSizable = nsViewWidthSizable .|. nsViewHeightSizable
normalWindowMask =
  nsTitledWindowMask .|. nsResizableWindowMask .|. nsClosableWindowMask .|. nsMiniaturizableWindowMask

initWithContentRect :: NSRect -> NewlyAllocated (NSWindow ()) -> IO (NSWindow ())
initWithContentRect r =
  initWithContentRectStyleMaskBackingDefer r normalWindowMask nsBackingStoreBuffered True

toNSView :: forall t. ID () -> NSView t
toNSView = castObject

toYiApplication :: forall t1 t2. NSApplication t1 -> YiApplication t2
toYiApplication = castObject
toYiController :: forall t1 t2. NSObject t1 -> YiController t2
toYiController = castObject

newTextLine :: IO (NSTextField ())
newTextLine = do
  tl <- new _NSTextField
  tl # setAlignment nsLeftTextAlignment
  tl # setAutoresizingMask (nsViewWidthSizable .|. nsViewMaxYMargin)
  tl # setMonospaceFont
  tl # setSelectable True
  tl # setEditable False
  tl # setBezeled False
  tl # sizeToFit
  cl <- castObject <$> tl # cell :: IO (NSCell ())
  cl # setWraps False
  cl # setLineBreakMode nsLineBreakByTruncatingMiddle
  return tl

addSubviewWithTextLine :: Maybe (NSView t0) -> NSView t1 -> NSView t2 -> IO (NSTextField (), NSView ())
addSubviewWithTextLine sibling view parent = do
  container <- new _NSView
  parent # bounds >>= flip setFrame container
  container # setAutoresizingMask allSizable
  view # setAutoresizingMask allSizable
  container # addSubview view

  text <- newTextLine
  container # addSubview text
  case sibling of
    Nothing -> parent # addSubview container
    Just v -> parent # addSubviewPositionedRelativeTo container nsWindowBelow v

  -- Adjust frame sizes, as superb cocoa cannot do this itself...
  txtbox <- text # frame
  winbox <- container # bounds
  view # setFrame (mkRect 0 (nsHeight txtbox) (nsWidth winbox) (nsHeight winbox - nsHeight txtbox))
  text # setFrame (mkRect 0 0 (nsWidth winbox) (nsHeight txtbox))

  return (text, container)

-- | Initialise the ui
start :: UIBoot
start cfg ch outCh _ed = do

  -- Ensure that our command line application is also treated as a gui application
  fptr <- mallocForeignPtrBytes 32 -- way to many bytes, but hey...
  withForeignPtr fptr $ getCurrentProcess
  withForeignPtr fptr $ (flip transformProcessType) 1
  withForeignPtr fptr $ setFrontProcess

  -- Publish Objective-C classes...
  initializeClass_Application
  initializeClass_YiTextView
  initializeClass_TextStorage
  initializeClass_YiScrollView

  app <- _YiApplication # sharedApplication >>= return . toYiApplication
  app # setIVar _eventChannel (Just ch)
  app # setIVar _runAction (Just $ outCh . singleton . makeAction)

  -- Multithreading in Cocoa is initialized by spawning a new thread
  -- This spawns a thread that immediately exits, but that's okay
  _NSThread # detachNewThreadSelectorToTargetWithObject
     (getSelectorForName "sharedApplication") _YiApplication nil

  -- Set the application icon accordingly
  icon <- getDataFileName "art/yi+lambda-fat.pdf"
  _NSImage # alloc >>=
    initWithContentsOfFile (toNSString icon) >>=
    flip setApplicationIconImage app

  -- Initialize the app delegate, which allows quit-on-window-close
  controller <- autonew _YiController >>= return . toYiController
  app # setDelegate controller

  -- init menus
  mm <- _NSMenu # alloc >>= init
  mm' <- _NSMenu # alloc >>= init
  mm'' <- _NSMenu # alloc >>= init
  app # setMainMenu mm
  app # setAppleMenu mm'
  app # setWindowsMenu mm''


  -- Create main cocoa window...
  win <- _NSWindow # alloc >>= initWithContentRect (mkRect 0 0 480 340)
  win # setTitle (toNSString "Yi")
  content <- win # AppKit.NSWindow.contentView >>= return . toNSView
  content # setAutoresizingMask allSizable

  -- Update the font configuration
  let fontSize = maybe 0 fromIntegral (configFontSize (configUI cfg))
  let fontGetter = maybe userFixedPitchFontOfSize (fontWithNameSize . toNSString) (configFontName (configUI cfg))
  _NSFont # fontGetter fontSize >>= flip setUserFixedPitchFont _NSFont

  -- Create yi window container
  winContainer <- new _NSSplitView
  (cmd,_) <- content # addSubviewWithTextLine Nothing winContainer

  -- Activate application window
  win # center
  win # setFrameAutosaveName (toNSString "main")
  win # makeKeyAndOrderFront nil
  app # activateIgnoringOtherApps False

  wc <- newIORef []

  let ui = UI win winContainer cmd wc (outCh . singleton) cfg

  cmd # setColors (Style.baseAttributes $ configStyle $ uiConfig ui)

  return (mkUI ui)

-- | Run the main loop
main :: IO ()
main = _YiApplication # sharedApplication >>= run

-- | Clean up and go home
end :: IO ()
end = _YiApplication # sharedApplication >>= terminate_ nil

syncWindows :: Editor -> UI -> [(Window, Bool)] -> [WinInfo] -> IO [WinInfo]
syncWindows e ui = sync
  where 
    sync ws [] = mapM (insert Nothing) ws
    sync [] cs = mapM_ remove cs >> return []
    sync (w:ws) (c:cs)
      | match w c          = (:) <$> update w c <*> sync ws cs
      | L.any (match w) cs = remove c >> sync (w:ws) cs
      | otherwise          = (:) <$> insert (Just $ widget c) w <*> sync ws (c:cs)

    match w c = winkey (fst w) == winkey (window c)

    winbuf = flip findBufferWith e . bufkey

    remove = removeFromSuperview . widget
    insert before (w,f) = update (w,f) =<< newWindow before ui w (winbuf w)
    update (w, False) i = return (i{window = w})
    update (w, True) i = do
      (textview i) # AppKit.NSView.window >>= makeFirstResponder (textview i)
      return (i{window = w})

setColors :: (Has_setBackgroundColor t, Has_setTextColor t) => Style.Attributes -> t -> IO ()
setColors s slf = do
  getColor True  (Style.foreground s) >>= flip setTextColor slf
  getColor False (Style.background s) >>= flip setBackgroundColor slf

-- | Make A new window
newWindow :: Maybe (NSView t) -> UI -> Window -> FBuffer -> IO WinInfo
newWindow before ui win b = do
  v <- alloc _YiTextView >>= initWithFrame (mkRect 0 0 100 100)
  v # setRichText False
  v # setSelectable True
  v # setAlignment nsLeftTextAlignment
  v # sizeToFit
  let sty = configStyle $ uiConfig ui
      ground = Style.baseAttributes sty
  attrs <- convertAttributes $ appEndo (Style.selectedStyle sty) $ ground
  v # setSelectedTextAttributes attrs
  v # setColors ground
  v # textColor >>= flip setInsertionPointColor v

  (ml, view) <- if (isMini win)
   then do
    v # setHorizontallyResizable False
    v # setVerticallyResizable False
    prompt <- newTextLine
    prompt # setStringValue (toNSString (name b))
    prompt # sizeToFit
    prompt # setAutoresizingMask nsViewNotSizable
    prompt # setBordered False
    prompt # setColors ground

    prect <- prompt # frame
    vrect <- v # frame

    hb <- _NSView # alloc >>= initWithFrame (mkRect 0 0 (nsWidth prect + nsWidth vrect) (nsHeight prect))
    v # setFrame (mkRect (nsWidth prect) 0 (nsWidth vrect) (nsHeight prect))
    v # setAutoresizingMask nsViewWidthSizable
    hb # addSubview prompt
    hb # addSubview v
    hb # setAutoresizingMask nsViewWidthSizable

    brect <- (uiBox ui) # bounds
    hb # setFrame (mkRect 0 0 (nsWidth brect) (nsHeight prect))

    (uiBox ui) # addSubview hb
    dummy <- _NSTextField # alloc >>= init

    return (dummy, hb)
   else do
    v # setHorizontallyResizable True
    v # setVerticallyResizable True
    
    when (not $ configLineWrap $ uiConfig ui) $ do
      tc <- v # textContainer
      NSSize _ h <- tc # containerSize
      tc # setContainerSize (NSSize 1.0e7 h)
      tc # setWidthTracksTextView False

    scroll <- new _YiScrollView
    scroll # setDocumentView v
    scroll # setAutoresizingMask allSizable

    scroll # setHasVerticalScroller True
    scroll # setHasHorizontalScroller False
    scroll # setAutohidesScrollers (configAutoHideScrollBar $ uiConfig ui)
    scroll # setIVar _leftScroller (configLeftSideScrollBar $ uiConfig ui)
    addSubviewWithTextLine before scroll (uiBox ui)

  -- TODO: Support focused modeline...
  ml # setColors (Style.modelineAttributes sty)
  s <- newTextStorage (configStyle $ uiConfig ui) (snd $ runBuffer win b revertPendingUpdatesB) win
  layoutManager v >>= replaceTextStorage s

  responds <- layoutManager v >>= respondsToSelector (getSelectorForName "setAllowsNonContiguousLayout:")
  when responds $ layoutManager v >>= setAllowsNonContiguousLayout True

  k <- newUnique
  flip (setIVar _runBuffer) v $ \act -> do
    wCache <- readIORef (windowCache ui)
    uiActionCh ui $ makeAction $ do
      modifyWindows $ WS.focusIndex $ fromJust $ L.findIndex ((k ==) . wikey) wCache
      withGivenBufferAndWindow0 win (keyB b) act

  return $ WinInfo 
    { wikey    = k
    , window   = win
    , textview = v
    , modeline = ml
    , widget   = view
    , storage  = s
    }

getSelectedRegions :: BufferM [Region]
getSelectedRegions = do
  rect <- getA rectangleSelectionA
  if (not rect)
    then singleton <$> (getSelectRegionB >>= charRegionB)
    else do
      (reg, x1, x2) <- getRectangle
      ls <- map (fromIntegral . L.length) <$> lines' <$> readRegionB reg
      mapM charRegionB $ catMaybes $ snd $ L.mapAccumL
        (lineRegions (fromIntegral x1) (fromIntegral x2)) (regionStart reg) ls
  where
    lineRegions :: Size -> Size -> Point -> Size -> (Point, Maybe Region)
    lineRegions x1 x2 p l 
      | l <= x1   = (p +~ succ l, Nothing)
      | l <= x2   = (p +~ succ l, Just $ mkRegion (p+~x1) (p +~ l))
      | otherwise = (p +~ succ l, Just $ mkRegion (p+~x1) (p +~ x2))


refresh :: UI -> Editor -> IO ()
refresh ui e = withAutoreleasePool $ logNSException "refresh" $ do
    _YiApplication # sharedApplication >>=
      pushClipboard (snd $ runEditor (uiFullConfig ui) getRegE e) . toYiApplication
  
    (uiCmdLine ui) # setStringValue (toNSString $ statusLine e)

    cache <- readRef $ windowCache ui
    (uiWindow ui) # setAutodisplay False -- avoid redrawing while window syncing
    cache' <- syncWindows e ui (toList $ WS.withFocus $ windows e) cache
    writeRef (windowCache ui) cache'
    (uiBox ui) # adjustSubviews -- FIX: maybe it is not needed
    (uiWindow ui) # setAutodisplay True -- reenable automatic redrawing

    forM_ cache' $ \w ->
        do let buf = findBufferWith (bufkey (window w)) e
           (storage w) # setMonospaceFont -- FIXME: Why is this needed for mini buffers?
           (storage w) # setTextStorageBuffer e buf

           let ((p0,txt,rs),_) = runBuffer (window w) buf $
                 (,,) <$> (pointB >>= charIndexB) <*> getModeLine <*> getSelectedRegions
           a <- castObject <$> _NSMutableArray # array :: IO (NSMutableArray ())
           mapM_ ((flip addObject a =<<) . flip valueWithRange _NSValue . mkRegionRange) rs
           (textview w) # setSelectedRanges (castObject a)
           (textview w) # scrollRangeToVisible (NSRange (fromIntegral p0) 0)
           (modeline w) # setStringValue (toNSString txt)
           (textview w) # visibleRange >>= flip visibleRangeChanged (storage w)