module GraphicsWindow ( module GraphicsWindow ) where import qualified Xlib as X import Bits import Word import IOExts import Concurrent( MVar, newMVar, takeMVar, putMVar, readMVar ) import Exception (finally) import Monad( unless, when ) import Concurrent (forkIO, yield) import System( getEnv ) import IO( try ) import Maybe import qualified Graphics_Utilities as Utils import Graphics_Utilities( modMVar, modMVar_ ) import GraphicsEvent import qualified GraphicsEvents as E import GraphicsDC import qualified GraphicsTimer as T import GraphicsKey (Key(..)) import NumExts (showHex) ---------------------------------------------------------------- -- Interface ---------------------------------------------------------------- type Title = String runGraphics :: IO () -> IO () -- SOE, p48 data Window = MkWindow { wnd :: X.Window -- the real window , ref_dc :: MVar (Maybe DC) -- "device context" , exposed :: IORef Bool -- have we had an expose event yet? , events :: E.Events -- the event stream , graphic :: MVar Graphic -- the current graphic , redraw :: RedrawStuff , timer :: Maybe T.Timer } openWindowEx :: Title -> Maybe Point -> Size -> RedrawMode -> Maybe T.Time -> IO Window closeWindow :: Window -> IO () getWindowRect :: Window -> IO (Point,Point) getWindowEvent :: Window -> IO Event maybeGetWindowEvent :: Window -> IO (Maybe Event) type Graphic = Draw () -- note that modGraphic doesn't force a redraw setGraphic :: Window -> Graphic -> IO () getGraphic :: Window -> IO Graphic modGraphic :: Window -> (Graphic -> Graphic) -> IO () directDraw :: Window -> Graphic -> IO () getTime :: IO T.Time ---------------------------------------------------------------- -- Implementation ---------------------------------------------------------------- runGraphics m = do disp <- try (getEnv "DISPLAY") runGraphicsEx (either (const ":0.0") id disp) m ---------------------------------------------------------------- -- Windows ---------------------------------------------------------------- closeWindow' :: Bool -> Window -> IO () closeWindow' destroyXWindow w = do mb_dc <- takeMVar (ref_dc w) case mb_dc of Just dc -> do putMVar (ref_dc w) Nothing -- mark it for dead X.freeGC (disp dc) (textGC dc) X.freeGC (disp dc) (paintGC dc) X.freeGC (disp dc) (brushGC dc) case (redraw w) of UnbufferedStuff -> return () BufferedStuff gc _ ref_mbuffer -> do X.freeGC (disp dc) gc removeBuffer dc ref_mbuffer when destroyXWindow $ do X.destroyWindow (disp dc) (drawable dc) -- ths dc had better hold a window! minor_eloop (disp dc) Nothing -> do putMVar (ref_dc w) Nothing removeBuffer :: DC -> IORef (Maybe X.Pixmap) -> IO () removeBuffer dc ref_mbuffer = do mbuffer <- readIORef ref_mbuffer case mbuffer of Nothing -> return () Just buffer -> X.freePixmap (disp dc) buffer writeIORef ref_mbuffer Nothing removeDeadWindows :: IO () removeDeadWindows = do ws <- takeMVar wnds ws' <- remove ws [] putMVar wnds ws' where remove [] r = return r remove (w:ws) r = do mb_dc <- readMVar (ref_dc w) if (isJust mb_dc) then remove ws (w:r) else remove ws r closeAllWindows :: IO () closeAllWindows = do ws <- readMVar wnds mapM_ (closeWindow' True) ws removeDeadWindows -- bring out your dead sendTicks :: IO () sendTicks = do ws <- readMVar wnds sequence_ [ E.sendTick (events w) | w <- ws ] getWindowTick :: Window -> IO () getWindowTick w = E.getTick (events w) -- persistent list of open windows wnds :: MVar [Window] wnds = unsafePerformIO (newMVar []) -- persistent list of timers timers :: T.Timers timers = unsafePerformIO T.newTimers runGraphicsEx :: String -> IO () -> IO () runGraphicsEx host m = do X.setDefaultErrorHandler display <- openDisplay host closeAllWindows T.clearTimers timers -- color_map <- X.getStandardColormap display root X.a_RGB_BEST_MAP -- HN 2001-01-30 -- There is a race condition here since the event loop terminates if it -- encounters an empty window list (in the global, imperative, variable -- wnds). Thus, if m has not yet opened a window (assuming it will!) -- when the event_loop is entered, it will exit immediately. -- Solution: wait until either the window list is non-empty, or until -- m exits (in case it does not open a window for some reason). mDone <- newIORef False forkIO (catchErrors m `finally` writeIORef mDone True) let loop = do yield ws <- readMVar wnds d <- readIORef mDone if not (null ws) then main_eloop display else if not d then loop else return () catchErrors loop -- X.sync display True closeAllWindows -- X.sync display True -- A final yield to make sure there's no threads thinking of -- accessing the display yield closeDisplay -- This is what we use on Win32 -- type RedrawMode = Graphic -> DrawFun -- drawGraphic :: RedrawMode -- drawBufferedGraphic :: RedrawMode -- drawBufferedGraphicBC :: Win32.COLORREF -> RedrawMode catchErrors :: IO () -> IO () catchErrors m = do r <- Utils.safeTry m case r of Left e -> do -- putStr "Uncaught Error: " print e Right r -> return () return () -- On X, we're a bit more abstract data RedrawMode = Unbuffered | DoubleBuffered ---------------------------------------------------------------- -- Implementation ---------------------------------------------------------------- openWindowEx name pos size redrawMode tickRate = do display <- getDisplay let (x,y) = fromPoint (fromMaybe (0,0) pos) (w,h) = fromSize size let screen = X.defaultScreenOfDisplay display fg_color = X.whitePixelOfScreen screen bg_color = X.blackPixelOfScreen screen depth = X.defaultDepthOfScreen screen root = X.rootWindowOfScreen screen visual = X.defaultVisualOfScreen screen -- ToDo: resurrect the old code for constructing attribute sets attributes <- X.allocXSetWindowAttributes X.set_background_pixel attributes bg_color let event_mask = ( X.buttonPressMask .|. X.buttonReleaseMask .|. X.keyPressMask .|. X.keyReleaseMask .|. X.pointerMotionMask .|. X.exposureMask .|. X.structureNotifyMask ) X.set_event_mask attributes event_mask -- We use backing store to reduce the number of expose events due to -- raising/lowering windows. X.set_backing_store attributes X.whenMapped -- We use bit-gravity to avoid generating exposure events when a window is -- made smaller (they can't be avoided when the window is enlarged). -- The choice of NW is somewhat arbitrary but hopefully works often -- enough to be worth it. X.set_bit_gravity attributes X.northWestGravity let attrmask = X.cWBackPixel .|. X.cWEventMask .|. X.cWBackingStore .|. X.cWBitGravity window <- X.createWindow display root x y -- x, y w h -- width, height 1 -- border_width depth -- use CopyFromParent?? X.inputOutput visual -- use CopyFromParent?? attrmask attributes X.free attributes -- AC, 1/9/2000: Tell the window manager that we want to use the -- DELETE_WINDOW protocol delWinAtom <- X.internAtom display "WM_DELETE_WINDOW" False X.setWMProtocols display window [delWinAtom] X.setTextProperty display window name X.a_WM_ICON_NAME X.setTextProperty display window name X.a_WM_NAME X.mapWindow display window X.raiseWindow display window text_gc <- X.createGC display window X.setBackground display text_gc bg_color X.setForeground display text_gc fg_color pen_gc <- X.createGC display window X.setBackground display pen_gc bg_color X.setForeground display pen_gc fg_color brush_gc <- X.createGC display window X.setBackground display brush_gc bg_color X.setForeground display brush_gc fg_color redraw <- case redrawMode of Unbuffered -> return UnbufferedStuff DoubleBuffered -> do gc <- X.createGC display window X.setForeground display gc bg_color -- gc for clearing the screen ref_mbuffer <- newIORef Nothing return (BufferedStuff gc depth ref_mbuffer) w <- newWindow display window fg_color text_gc pen_gc brush_gc ((x,y),(w,h)) redraw tickRate -- It might be some time till we get back to the event loop -- so we try to process as many events as possible now. -- This is a bit of a hack and partly aimed at avoiding the bug that -- directDraw might try to draw something before the first expose event -- is processed. -- To make the hack even more effective, we wait a short time (allegedly -- 1uS) and synchronise before looking for the event. -- -- NB: -- This whole thing is based on the implicit notion that the server thread -- is "lower priority" than the user threads. That is, the server thread -- will only run when no user threads are runnable. -- -- Or, more concretely, only the server thread calls yield so it's safe -- to call the minor_eloop (which doesn't yield or block) but not the -- major_eloop because, amongst other things, it may yield or block. X.waitForEvent display 1 X.sync display False minor_eloop display return w closeWindow w = do closeWindow' True w removeDeadWindows -- bring out your dead getWindowRect w = do mb_dc <- readMVar (ref_dc w) case mb_dc of Just dc -> do (pt,sz) <- readMVar (ref_rect dc) return (toPoint pt, toSize sz) Nothing -> return ((0,0),(0,0)) -- ToDo? -- main_eloop :: X.Display -> IO () -- main_eloop d = do -- xevent <- X.allocXEvent -- let loop = do -- -- X.sync d False -- wild attempt to fix the broken X connection problem -- count <- X.pending d -- if (count > 0) then do -- -- X.sync d False -- wild attempt to fix the broken X connection problem -- X.nextEvent d xevent -- window <- X.get_Window xevent -- wnd <- findWindow window -- etype <- X.get_EventType xevent -- -- print (window,etype) -- dispatchEvent wnd etype xevent -- ws <- readMVar wnds -- unless (null ws) loop -- else -- loop -- loop -- X.free xevent -- This is the main event loop in the program main_eloop :: X.Display -> IO () main_eloop d = do xevent <- X.allocXEvent let handleEvent = do count <- X.pending d next <- T.nextTick timers if (count > 0 || not (isJust next)) then do -- Event in queue or no tick pending. X.nextEvent d xevent window <- X.get_Window xevent etype <- X.get_EventType xevent -- showEvent etype withWindow window $ \ wnd -> do dispatchEvent d wnd etype xevent else do -- No event and tick pending. let delay = fromJust next t0 <- getTime timedOut <- X.waitForEvent d (fromIntegral (delay * 1000)) t1 <- getTime T.fireTimers timers (t1 - t0) let loop = do -- We yield at this point because we're (potentially) -- about to block so we should give other threads a chance -- to run. yield ws <- readMVar wnds if (null ws) then return () else do handleEvent loop loop X.free xevent -- This event loop is the same as above except that it is -- non-blocking: it only handles those events that have already arrived. -- And this is important because it means we don't have to yield which -- means it can safely be called by user code (see comment in openWindowEx). minor_eloop :: X.Display -> IO () minor_eloop d = do xevent <- X.allocXEvent let handleEvent = do X.nextEvent d xevent window <- X.get_Window xevent etype <- X.get_EventType xevent -- print etype withWindow window $ \ wnd -> do dispatchEvent d wnd etype xevent return () loop = do ws <- readMVar wnds if null ws then return () else do -- Note: _do not_ call pending if null ws count <- X.pending d if count == 0 then return () else do handleEvent loop loop X.free xevent -- Time in milliseconds getTime = X.gettimeofday_in_milliseconds -- The DC is wrapped inside (MVar (Maybe ...)) so that we can mark -- windows as being dead the moment they die and so that we don't -- try to keep writing to them afterwards. -- The events remain valid after the window dies. -- It might be wiser to clear all events(???) and start returning -- Closed whenever events are read - or (more GC friendly?), when -- first read occurs but block thereafter? data RedrawStuff = UnbufferedStuff | BufferedStuff X.GC -- GC with foreground = background_color Int -- depth (IORef (Maybe X.Pixmap)) -- The buffer, allocated on demand -- drawBuffered. drawOnDC :: DC -> Draw () -> RedrawStuff -> IO () drawOnDC dc p redraw = case redraw of UnbufferedStuff -> drawUnbuffered dc p BufferedStuff gc depth ref_mbuffer -> drawBuffered dc p gc depth ref_mbuffer newWindow :: X.Display -> X.Window -> X.Pixel -> X.GC -> X.GC -> X.GC -> ((X.Position,X.Position),(X.Dimension,X.Dimension)) -> RedrawStuff -> Maybe T.Time -> IO Window newWindow display window fg_color tgc pgc bgc rect redraw tickRate = do es <- E.newEvents pic <- newMVar (return ()) -- failed attempts to find the default font -- f' <- X.fontFromGC display tgc -- f <- X.queryFont display f' -- Since we can't ask the server what default font it chooses to bless -- us with, we have to set an explicit font. f <- X.loadQueryFont display "9x15" -- a random choice X.setFont display tgc (X.fontFromFontStruct f) bits <- newMVar DC_Bits { textColor = RGB 255 255 255 , bkColor = RGB 0 0 0 , bkMode = Transparent , textAlignment = (Left',Top) , brush = RGB 255 255 255 , pen = defaultPen fg_color , font = f } ref_rect <- newMVar rect dc <- newMVar (Just MkDC{disp=display,drawable=window,textGC=tgc,paintGC=pgc,brushGC=bgc,ref_rect=ref_rect,ref_bits=bits}) timer <- case tickRate of Just t -> T.new timers t (E.sendTick es) >>= return.Just Nothing -> return Nothing ref_exposed <- newIORef False let wnd = MkWindow{wnd=window,ref_dc=dc,exposed=ref_exposed,events=es,graphic=pic,redraw=redraw,timer=timer} modMVar wnds (wnd:) return wnd setGraphic w p = do modMVar (graphic w) (const p) canDraw <- readIORef (exposed w) when canDraw $ do mb_dc <- readMVar (ref_dc w) case mb_dc of Just dc -> drawOnDC dc p (redraw w) Nothing -> return () getGraphic w = readMVar (graphic w) modGraphic w = modMVar_ (graphic w) directDraw w p = do mb_dc <- readMVar (ref_dc w) canDraw <- readIORef (exposed w) when canDraw $ do case mb_dc of Just dc -> unDraw p dc Nothing -> return () findWindow :: X.Window -> IO Window findWindow xw = do ws <- readMVar wnds return (head [ w | w <- ws, xw == wnd w ]) -- ToDo: don't use head withWindow :: X.Window -> (Window -> IO ()) -> IO () withWindow xw k = do ws <- readMVar wnds case [ w | w <- ws, xw == wnd w ] of (w:_) -> k w _ -> return () send :: Window -> Event -> IO () send w e = E.sendEvent (events w) e getWindowEvent w = E.getEvent (events w) maybeGetWindowEvent w = do noEvent <- E.isNoEvent(events w) if noEvent then return Nothing else do ev <- E.getEvent (events w) return (Just ev) dispatchEvent :: X.Display -> Window -> X.EventType -> X.XEventPtr -> IO () dispatchEvent display w etype xevent | etype == X.graphicsExpose || etype == X.expose = paint | etype == X.motionNotify = mouseMove | etype == X.buttonPress = button True | etype == X.buttonRelease = button False | etype == X.keyPress = key True | etype == X.keyRelease = key False | etype == X.configureNotify = reconfig | etype == X.destroyNotify = destroy -- AC, 1/9/2000: treat a ClientMesage as a destroy event -- TODO: really need to examine the event in more detail, -- and ensure that xevent.xclient.message_type==ATOM_WM_PROTOCOLS && -- xevent.xclient.data.l[0]==ATOM_WM_DELETE_WINDOW -- where ATOM_XXX is obtained from XInternAtom(dpy,"XXX",False) | etype == X.clientMessage = destroy -- ToDo: consider printing a warning message | otherwise = return () where -- Redrawing is awkward because the request comes as a number of -- separate events. We need to do one of the following (we currently -- do a combination of (1) and (3)): -- 1) Do a single redraw of the entire window but first delete all other -- expose events for this window from the queue. -- 2) Use all expose events for this window to build a Region object -- and use that to optimise redraws. -- 3) When double-buffering, use the buffer and information about -- whether it is up to date to serve redraws from the buffer. -- When single-buffering, use the server's backing store to reduce -- the number of expose events. (Combine with bit-gravity info to -- handle resize requests.) paint :: IO () paint = do let stompOnExposeEvents = do -- X.get_ExposeEvent xevent >>= print gotOne <- X.checkTypedWindowEvent display (wnd w) X.expose xevent when gotOne stompOnExposeEvents writeIORef (exposed w) True -- now safe to draw directly stompOnExposeEvents p <- readMVar (graphic w) mb_dc <- readMVar (ref_dc w) case mb_dc of Just dc -> drawOnDC dc p (redraw w) Nothing -> return () button :: Bool -> IO () button isDown = do (_,_,_,x,y,_,_,_,b,_) <- X.get_ButtonEvent xevent let isLeft = b == 1 -- assume that button 1 = left button send w Button{pt = (x,y), isLeft=isLeft, isDown=isDown} -- An X KeySym is *not* a character; not even a Unicode character! And -- since characters in Hugs only are 8-bit, we get a runtime error -- below. There is an underlying assumption that key events only -- involve characters. But of course there are function keys, arrow -- keys, etc. too. While this will be a problem if one wants to get at -- e.g. arrow keys (e.g. for some drawing application) or at -- dead/multi-keys for doing proper input, we'll ignore them -- completely for now. Furthermore, one really needs to call -- XlookupString (not XkeysymToString!) to do the processing! We'll -- ignore that too, and do a static mapping of just a few keysyms. key :: Bool -> IO () key isDown = do -- Should really use XmbLookupString here to make compose work. -- It's OK to call X.lookupString both on key up and down events. -- Not true for X.mbLookupString. In that case, use e.g. X.lookup -- on key up events. (mks, s) <- X.lookupString xevent case mks of Just ks -> send w (Key {keysym = MkKey ks, isDown = isDown}) Nothing -> return () if isDown then (mapM_ (\c -> send w (Char {char = c})) s) else return () mouseMove ::IO () mouseMove = do (_,_,_,x,y,_,_,_,_,_) <- X.get_MotionEvent xevent send w MouseMove{ pt = (x,y) } reconfig :: IO () reconfig = do (x,y,width,height) <- X.get_ConfigureEvent xevent mb_dc <- readMVar (ref_dc w) case mb_dc of Just dc -> do modMVar (ref_rect dc) (const ((x,y),(width,height))) case (redraw w) of UnbufferedStuff -> return () BufferedStuff _ _ ref_mbuffer -> removeBuffer dc ref_mbuffer Nothing -> return () -- don't send new size, it may be out of date by the time we -- get round to reading the event send w Resize destroy :: IO () destroy = do -- putStrLn "Window Destroyed" -- todo closeWindow' True w removeDeadWindows -- bring out your dead send w Closed ---------------------------------------------------------------- -- Utilities ---------------------------------------------------------------- modIORef :: IORef a -> (a -> a) -> IO () modIORef r f = do a <- readIORef r writeIORef r (f a) -- Only for debugging showEvent :: X.EventType -> IO () showEvent etype | etype == X.keyPress = putStrLn "keyPress" | etype == X.keyRelease = putStrLn "keyRelease" | etype == X.buttonPress = putStrLn "buttonPress" | etype == X.buttonRelease = putStrLn "buttonRelease" | etype == X.motionNotify = putStrLn "motionNotify" | etype == X.enterNotify = putStrLn "enterNotify" | etype == X.leaveNotify = putStrLn "leaveNotify" | etype == X.focusIn = putStrLn "focusIn" | etype == X.focusOut = putStrLn "focusOut" | etype == X.keymapNotify = putStrLn "keymapNotify" | etype == X.expose = putStrLn "expose" | etype == X.graphicsExpose = putStrLn "graphicsExpose" | etype == X.noExpose = putStrLn "noExpose" | etype == X.visibilityNotify = putStrLn "visibilityNotify" | etype == X.createNotify = putStrLn "createNotify" | etype == X.destroyNotify = putStrLn "destroyNotify" | etype == X.unmapNotify = putStrLn "unmapNotify" | etype == X.mapNotify = putStrLn "mapNotify" | etype == X.mapRequest = putStrLn "mapRequest" | etype == X.reparentNotify = putStrLn "reparentNotify" | etype == X.configureNotify = putStrLn "configureNotify" | etype == X.configureRequest = putStrLn "configureRequest" | etype == X.gravityNotify = putStrLn "gravityNotify" | etype == X.resizeRequest = putStrLn "resizeRequest" | etype == X.circulateNotify = putStrLn "circulateNotify" | etype == X.circulateRequest = putStrLn "circulateRequest" | etype == X.propertyNotify = putStrLn "propertyNotify" | etype == X.selectionClear = putStrLn "selectionClear" | etype == X.selectionRequest = putStrLn "selectionRequest" | etype == X.selectionNotify = putStrLn "selectionNotify" | etype == X.colormapNotify = putStrLn "colormapNotify" | etype == X.clientMessage = putStrLn "clientMessage" | etype == X.mappingNotify = putStrLn "mappingNotify" | etype == X.lASTEvent = putStrLn "lASTEvent" | otherwise = putStrLn ("Unknown X event type: " ++ show etype) ---------------------------------------------------------------- -- End ----------------------------------------------------------------