-- Author: Andy Stewart -- Maintainer: Andy Stewart -- -- Copyright (C) 2010 Andy Stewart, all rights reserved. -- -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program. If not, see . {-# LANGUAGE ExistentialQuantification, RankNTypes, DeriveDataTypeable, ScopedTypeVariables #-} {-# LANGUAGE OverloadedStrings #-} module Manatee.Extension.IrcClient.IrcView where import Control.Applicative import Control.Concurrent.STM import Control.Monad import Data.Map (Map) import Data.ByteString (ByteString) import Data.Text.Lazy (Text) import Data.Typeable import Graphics.UI.Gtk hiding (Statusbar, statusbarNew, get, Language) import Graphics.UI.Gtk.Gdk.SerializedEvent import Graphics.UI.Gtk.SourceView import Language.Translate.Google import Manatee.Core.DBus import Manatee.Core.PageView import Manatee.Core.Types import Manatee.Extension.IrcClient.DBus import Manatee.Extension.IrcClient.HighlightNick import Manatee.Extension.IrcClient.IrcBuffer import Manatee.Extension.IrcClient.Types import Manatee.Toolkit.General.Basic import Manatee.Toolkit.General.ByteString import Manatee.Toolkit.General.Map import Manatee.Toolkit.General.Maybe import Manatee.Toolkit.General.Process import Manatee.Toolkit.General.STM import Manatee.Toolkit.General.String import Manatee.Toolkit.Gtk.Concurrent import Manatee.Toolkit.Gtk.Gtk import Manatee.Toolkit.Gtk.Multiline import Manatee.Toolkit.Gtk.ScrolledWindow import Network.FastIRC.Messages import Paths_manatee_ircclient import System.FilePath import Text.Morse import qualified Data.ByteString.Char8 as B import qualified Data.Map as M import qualified Data.Set as S import qualified Data.ByteString.UTF8 as UTF8 data IrcView = IrcView {ircViewPlugId :: TVar PagePlugId ,ircViewScrolledWindow :: ScrolledWindow ,ircViewView :: SourceView ,ircViewBuffer :: IrcBuffer ,ircViewBroadcastChannel :: ViewChannel IrcBufferSignal } deriving Typeable instance PageBuffer IrcBuffer where pageBufferGetName = return . ircBufferChannel pageBufferSetName _ _ = return () pageBufferClient = ircBufferClient pageBufferCreateView a pId = PageViewWrap <$> ircViewNew a pId pageBufferMode = ircBufferMode pageBufferPackageName _ = fmap takeFileName getDataDir instance PageView IrcView where pageViewBuffer = PageBufferWrap . ircViewBuffer pageViewPlugId = ircViewPlugId pageViewFocus = widgetGrabFocus . ircViewView pageViewCut = ircViewCut pageViewCopy = ircViewCopy pageViewPaste = ircViewPaste pageViewScrolledWindow = ircViewScrolledWindow pageViewHandleKeyAction = ircViewHandleKeyAction pageViewScrollToTop = ircViewScrollToTop pageViewScrollToBottom = ircViewScrollToBottom pageViewScrollVerticalPage = ircViewScrollVerticalPage pageViewScrollVerticalStep = ircViewScrollVerticalStep -- | Create new irc view. ircViewNew :: IrcBuffer -> PagePlugId -> IO IrcView ircViewNew sb plugId = do -- Create plug id. pId <- newTVarIO plugId -- Create UI frame. scrolledWindow <- scrolledWindowNew_ -- Create source view. ircView <- sourceViewNewWithBuffer (ircBufferBuffer sb) scrolledWindow `containerAdd` ircView forM_ [StateNormal, StateActive, StatePrelight, StateSelected, StateInsensitive] $ \state -> widgetModifyBg ircView state (nickColorToColor backgroundColor) -- Broadcast channel. channel <- createViewChannel (ircBufferBroadcastChannel sb) ircView let sv = IrcView pId scrolledWindow ircView sb channel -- Customize setup. sourceViewSetHighlightCurrentLine ircView True -- highlight current line. sourceViewSetInsertSpacesInsteadOfTabs ircView True -- use space instead tabs set ircView -- show line number [sourceViewShowLineNumbers :=> readTVarIO (ircCustomizeShowLineNumber $ ircBufferCustomize sb)] textViewSetCursorVisible ircView True -- make cursor visible textViewSetWrapMode ircView WrapWord -- auto wrap text -- Set fixed-width font, otherwise wrap mode can't work. fontDescr <- fontDescriptionFromString "Monospace" widgetModifyFont ircView (Just fontDescr) -- Update time stamp at right. timeStampPosition <- readTVarIO (ircCustomizeTimeStampPosition $ ircBufferCustomize sb) gutter <- sourceViewGetGutter ircView timeStampPosition cell <- cellRendererTextNew sourceGutterInsert gutter cell 0 -- Set gutter data. sourceGutterSetCellDataFunc gutter cell $ \ c l _ -> do -- Display time stamp. timeStampMap <- readTVarIO $ ircBufferTimeStamp sb let (timeStamp, timeColor) = case findMinMatch timeStampMap (\ line _ -> line == l) of Just x -> snd x Nothing -> (defaultTimeStamp, Color 0 0 0) set (castToCellRendererText c) [cellText := timeStamp] set (castToCellRendererText c) [cellTextForegroundColor := timeColor] -- Set gutter size. sourceGutterSetCellSizeFunc gutter cell $ \ c -> -- -1 mean cell renderer will adjust width with chars dynamically. set (castToCellRendererText c) [cellTextWidthChars := (-1)] -- Read channel. ircViewListenChannel sv return sv -- | Listen broadcast channel for draw view synchronous. ircViewListenChannel :: IrcView -> IO () ircViewListenChannel view = listenViewChannel (ircViewBroadcastChannel view) $ \ signal -> case signal of SwitchTranslateLanguage -> do -- Update language status lang <- readTVarIO $ ircBufferTranslateLanguage $ ircViewBuffer view pageViewUpdateInfoStatus view "Translate" ("Translate to (" ++ show lang ++ ")") BufferChanged -> do -- Scroll screen if prompt mark at left of insert mark. -- Otherwise don't screen, because user screen download to read old messages. let buffer = ircViewBuffer view textBuffer = ircBufferBuffer buffer ircView = ircViewView view readTVarIO (ircBufferScrollMark buffer) >?>= \ mark -> do iter <- textBufferGetIterAtMark textBuffer mark (y, _) <- textViewGetLineYrange ircView iter (Rectangle _ vy _ vh) <- textViewGetVisibleRect ircView when (y >= vy + vh) $ textViewScrollMarkOnscreen ircView mark -- | Swtich translate language. ircViewSwitchTranslateLanguage :: IrcView -> IO () ircViewSwitchTranslateLanguage IrcView {ircViewBuffer = buffer ,ircViewBroadcastChannel = channel} = do let customize = ircBufferCustomize buffer targetLanguage <- readTVarIO (ircCustomizeTargetLanguage customize) sourceLanguage <- readTVarIO (ircCustomizeSourceLanguage customize) modifyTVarIO (ircBufferTranslateLanguage buffer) $ \lang -> if lang == targetLanguage then sourceLanguage else targetLanguage writeTChanIO (viewChannel channel) SwitchTranslateLanguage -- | Handle key action. ircViewHandleKeyAction :: IrcView -> Text -> SerializedEvent -> IO () ircViewHandleKeyAction view keystoke sEvent = case M.lookup keystoke ircViewKeymap of -- Execute action when found in keymap. Just action -> action view -- Otherwise propagate event. Nothing -> widgetPropagateEvent (ircViewView view) sEvent -- | Begin. ircViewScrollToTop :: IrcView -> IO () ircViewScrollToTop a = do textViewBegin (ircViewView a) (ircViewScrolledWindow a) ircViewApplySelectionMark a -- | End. ircViewScrollToBottom :: IrcView -> IO () ircViewScrollToBottom a = do textViewEnd (ircViewView a) (ircViewScrolledWindow a) ircViewApplySelectionMark a -- | Scroll page vertically. ircViewScrollVerticalPage :: Bool -> IrcView -> IO () ircViewScrollVerticalPage isDown a = do let sw = ircViewScrolledWindow a tv = ircViewView a pageInc <- (<=<) adjustmentGetPageIncrement scrolledWindowGetVAdjustment sw textViewScrollVertical tv sw (if isDown then pageInc else (- pageInc)) ircViewApplySelectionMark a -- | Scroll step vertically. ircViewScrollVerticalStep :: Bool -> IrcView -> IO () ircViewScrollVerticalStep isDown a = do let sw = ircViewScrolledWindow a tv = ircViewView a ti <- textViewGetTextIter tv (_, lineHeight) <- textViewGetLineYrange tv ti let stepInc = i2d lineHeight textViewScrollVertical tv sw (if isDown then stepInc else (- stepInc)) ircViewApplySelectionMark a -- | Send morse code. ircViewSendMorse :: IrcView -> IO () ircViewSendMorse view@(IrcView {ircViewBuffer = buffer ,ircViewView = vView}) = do -- Scroll mark on screen visible area. let textBuffer = ircBufferBuffer buffer promptMark = ircBufferPromptMark buffer textViewScrollMarkOnscreen vView promptMark -- Get input string. startIter <- textBufferGetIterAtMark textBuffer promptMark endIter <- textBufferGetEndIter textBuffer text <- textBufferGetText textBuffer startIter endIter True -- Clean input. ircViewCleanInput view if isBlankString text -- Avoid send blank string, server will ignore blank message. then pageViewUpdateOutputStatus view "Ignored blank ..." Nothing -- Otherwise send message to server. else ircViewSend view (UTF8.fromString $ encodeMorse text) -- | Send irc message. ircViewSendMessage :: Bool -> IrcView -> IO () ircViewSendMessage isTranslate view@(IrcView {ircViewBuffer = buffer ,ircViewView = vView}) = do -- Scroll mark on screen visible area. let promptMark = ircBufferPromptMark buffer textViewScrollMarkOnscreen vView promptMark -- Get input string. text <- ircViewGetInput view -- Clean input. ircViewCleanInput view if isBlankByteString text -- Avoid send blank string, server will ignore blank message. then pageViewUpdateOutputStatus view "Ignored blank ..." Nothing -- Otherwise send message to server. else if isTranslate -- Try get translation. then forkGuiIO_ (do pageViewUpdateOutputStatus view "Translation ..." Nothing lang <- readTVarIO $ ircBufferTranslateLanguage buffer translate text Nothing lang) $ \result -> case result of -- Send original message if translate failed. Left _ -> do pageViewUpdateOutputStatus view "Get translation failed, send original message." Nothing ircViewSend view text -- Otherwise send translation. Right translation -> do pageViewUpdateOutputStatus view "Translation ... completed." Nothing ircViewSend view translation -- Send original message. else ircViewSend view text -- | Select all. ircViewSelectAll :: IrcView -> IO () ircViewSelectAll = textViewSelectAll . ircViewView -- | Wrap delete action. ircViewWrapDeleteAction :: IrcView -> IO Bool -> IO () ircViewWrapDeleteAction view action = unlessM action $ pageViewUpdateOutputStatus view "Can't delete uneditable area." Nothing -- | Delete lines. ircViewDelLines :: IrcView -> IO () ircViewDelLines view = ircViewWrapDeleteAction view (textViewDelLines (ircViewView view)) -- | Delete. ircViewDelete :: IrcView -> IO () ircViewDelete view = textViewDelete (ircViewView view) True True >> return () -- | Delete forward char. ircViewDeleteForwardChar :: IrcView -> IO () ircViewDeleteForwardChar view = ircViewWrapDeleteAction view (textViewDeleteForwardChar (ircViewView view) True) -- | Backward char. ircViewDeleteBackwardChar :: IrcView -> IO () ircViewDeleteBackwardChar view = ircViewWrapDeleteAction view (textViewDeleteBackwardChar (ircViewView view) True) -- | Forward word. ircViewDeleteForwardWord :: IrcView -> IO () ircViewDeleteForwardWord view = ircViewWrapDeleteAction view (textViewDeleteForwardWord (ircViewView view) True) -- | Backward word. ircViewDeleteBackwardWord :: IrcView -> IO () ircViewDeleteBackwardWord view = ircViewWrapDeleteAction view (textViewDeleteBackwardWord (ircViewView view) True) -- | Delete to line end. ircViewDeleteToLineEnd :: IrcView -> IO () ircViewDeleteToLineEnd view = ircViewWrapDeleteAction view (textViewDeleteToLineEnd (ircViewView view) True) -- | Delete to line start. ircViewDeleteToLineStart :: IrcView -> IO () ircViewDeleteToLineStart view = ircViewWrapDeleteAction view (textViewDeleteToLineStart (ircViewView view) True) -- | Cut. ircViewCut :: IrcView -> IO Bool ircViewCut view = do textViewCut $ ircViewView view return True -- | Copy. ircViewCopy :: IrcView -> IO Bool ircViewCopy view = do textViewCopy $ ircViewView view return True -- | Paste. ircViewPaste :: IrcView -> IO Bool ircViewPaste view = do textViewPaste $ ircViewView view return True -- | Forward line. ircViewForwardLine :: IrcView -> IO () ircViewForwardLine a = do textViewForwardLine (ircViewView a) (ircViewScrolledWindow a) ircViewApplySelectionMark a -- | Backward line. ircViewBackwardLine :: IrcView -> IO () ircViewBackwardLine a = do textViewBackwardLine (ircViewView a) (ircViewScrolledWindow a) ircViewApplySelectionMark a -- | Forward char. ircViewForwardChar :: IrcView -> IO () ircViewForwardChar a = do textViewForwardChar (ircViewView a) (ircViewScrolledWindow a) ircViewApplySelectionMark a -- | Backward char. ircViewBackwardChar :: IrcView -> IO () ircViewBackwardChar a = do textViewBackwardChar (ircViewView a) (ircViewScrolledWindow a) ircViewApplySelectionMark a -- | Forward word. ircViewForwardWord :: IrcView -> IO () ircViewForwardWord a = do textViewForwardWord (ircViewView a) (ircViewScrolledWindow a) ircViewApplySelectionMark a -- | Backward word. ircViewBackwardWord :: IrcView -> IO () ircViewBackwardWord a = do textViewBackwardWord (ircViewView a) (ircViewScrolledWindow a) ircViewApplySelectionMark a -- | Smart home. ircViewSmartHome :: IrcView -> IO () ircViewSmartHome a = do textViewSmartHome $ ircViewView a ircViewApplySelectionMark a -- | Smart end. ircViewSmartEnd :: IrcView -> IO () ircViewSmartEnd a = do textViewSmartEnd $ ircViewView a ircViewApplySelectionMark a -- | Set selection mark. ircViewToggleSelectionMark :: IrcView -> IO () ircViewToggleSelectionMark view = ifM (textViewToggleSelectionMark $ ircViewView view) (pageViewUpdateInfoStatus view "Selection" "Selection (Active)") (pageViewUpdateInfoStatus view "Selection" "Selection (Inactive)") -- | Exchange selection mark. ircViewExchangeSelectionMark :: IrcView -> IO () ircViewExchangeSelectionMark = textViewExchangeSelectionMark . ircViewView -- | Show selection mark. ircViewApplySelectionMark :: IrcView -> IO () ircViewApplySelectionMark = textViewApplySelectionMark . ircViewView -- | Newline. ircViewNewline :: IrcView -> IO () ircViewNewline = textViewNewLine . ircViewView -- | Get text buffer. ircViewGetTextBuffer :: IrcView -> IO TextBuffer ircViewGetTextBuffer = textViewGetBuffer . ircViewView -- | Get source buffer. ircViewGetSourceBuffer :: IrcView -> IO SourceBuffer ircViewGetSourceBuffer sb = castToSourceBuffer <$> ircViewGetTextBuffer sb -- | Move to prompt position. ircViewMoveToPrompt :: IrcView -> IO () ircViewMoveToPrompt IrcView {ircViewBuffer = buffer ,ircViewView = view} = textViewPlaceCursorWithMark view (ircBufferPromptMark buffer) -- | Clean input. ircViewCleanInput :: IrcView -> IO () ircViewCleanInput IrcView {ircViewBuffer = buffer} = do -- Get iter range. let textBuffer = ircBufferBuffer buffer promptMark = ircBufferPromptMark buffer promptIter <- textBufferGetIterAtMark textBuffer promptMark endIter <- textBufferGetEndIter textBuffer -- Clean input. textBufferDelete textBuffer promptIter endIter -- | Get input message. ircViewGetInput :: IrcView -> IO ByteString ircViewGetInput IrcView {ircViewBuffer = buffer} = do -- Get iter range. let textBuffer = ircBufferBuffer buffer promptMark = ircBufferPromptMark buffer promptIter <- textBufferGetIterAtMark textBuffer promptMark endIter <- textBufferGetEndIter textBuffer -- Return input message. textBufferGetByteString textBuffer promptIter endIter True -- | Send message. ircViewSend :: IrcView -> ByteString -> IO () ircViewSend view@(IrcView {ircViewBuffer = buffer}) message = do nick <- readTVarIO $ ircBufferNick buffer -- Push message to buffer. ircBufferReceivePrivate buffer (B.pack nick) message -- Send message to server. let msg = showCommand $ PrivMsgCmd (S.singleton (B.pack $ ircBufferChannel buffer)) message mkIrcDaemonSignal (pageViewClient view) SendMessage (SendMessageArgs (ircBufferServer buffer) msg) -- | Open url around pointer. ircViewOpenUrl :: IrcView -> IO () ircViewOpenUrl view@(IrcView {ircViewBuffer = buffer}) = textBufferGetTagText (ircBufferBuffer buffer) (ircCustomizeUrlColorTag $ ircBufferCustomize buffer) >?>= \ url -> mkDaemonSignal (pageViewClient view) NewTab (NewTabArgs "PageBrowser" url []) -- | Translate message around pointer. ircViewTranslateMessage :: IrcView -> IO () ircViewTranslateMessage view@(IrcView {ircViewBuffer = ircBuffer@(IrcBuffer {ircBufferBuffer = buffer ,ircBufferMessageTag = messageTag ,ircBufferTranslateLanguage = language})}) = do -- Use end line iter to match messageTag content (skip message head). iter <- textBufferGetCurrentLineEndIter_ buffer textIterBackwardChar iter textBufferGetTagByteStringWithIter buffer iter messageTag >?>= \ text -> forkGuiIO_ (do pageViewUpdateOutputStatus view "Fetch translation ..." Nothing -- Translate reverse. :) lang <- readTVarIO language let customize = ircBufferCustomize ircBuffer targetLanguage <- readTVarIO (ircCustomizeTargetLanguage customize) sourceLanguage <- readTVarIO (ircCustomizeSourceLanguage customize) translate text Nothing (if lang == targetLanguage then sourceLanguage else targetLanguage)) $ \result -> case result of Left _ -> pageViewUpdateOutputStatus view "Fetch translation ... failed." Nothing Right translation -> do pageViewUpdateOutputStatus view "Fetch translation ... completed." Nothing -- Get rectangle around cursor. point <- ircViewGetTooltipPoint view -- Show translation with tooltip. mkDaemonSignal (pageViewClient view) ShowTooltip (ShowTooltipArgs (UTF8.toString translation) (Just point) 10000 Nothing (Just (Color 0 65535 0)) True Nothing) -- | Translate morse code. ircViewTranslateMorse :: IrcView -> IO () ircViewTranslateMorse view@(IrcView {ircViewBuffer = IrcBuffer {ircBufferBuffer = buffer ,ircBufferMessageTag = messageTag}}) = do -- Use end line iter to match messageTag content (skip message head). iter <- textBufferGetCurrentLineEndIter_ buffer textIterBackwardChar iter textBufferGetTagTextWithIter buffer iter messageTag >?>= \ text -> do -- Get rectangle around cursor. point <- ircViewGetTooltipPoint view mkDaemonSignal (pageViewClient view) ShowTooltip (ShowTooltipArgs (decodeMorse text) (Just point) 10000 Nothing (Just (Color 65535 65535 0)) True Nothing) -- | Read message around pointer. ircViewReadMessage :: IrcView -> IO () ircViewReadMessage view@(IrcView {ircViewBuffer = IrcBuffer {ircBufferBuffer = buffer ,ircBufferMessageTag = messageTag}}) = do -- Use end line iter to match messageTag content (skip message head). iter <- textBufferGetCurrentLineEndIter_ buffer textIterBackwardChar iter textBufferGetTagTextWithIter buffer iter messageTag >?>= \ text -> ircViewReadText view text -- | Read irc message. ircViewReadText :: IrcView -> String -> IO () ircViewReadText view text = execute "festival" ("echo " ++ show text ++ " | %s --tts") True >?>= \ failedReason -> pageViewUpdateOutputStatus view failedReason Nothing -- | Get tooltip coordinate. ircViewGetTooltipPoint :: IrcView -> IO Point ircViewGetTooltipPoint IrcView {ircViewView = textView} = do textIter <- textViewGetTextIter textView (Rectangle x y width height) <- textViewGetIterLocation textView textIter (wx, wy) <- textViewBufferToWindowCoords textView TextWindowWidget (x, y) return (wx + width, wy + height) -- | Keymap. ircViewKeymap :: Map Text (IrcView -> IO ()) ircViewKeymap = M.fromList [("Return", ircViewSendMessage False) ,("M-m", ircViewSendMessage False) ,("C-m", ircViewSendMessage True) ,("C-n", ircViewSwitchTranslateLanguage) ,("C-N", ircViewSendMorse) ,("M-M", ircViewMoveToPrompt) ,("M-N", ircViewCleanInput) ,("M-a", ircViewSelectAll) ,("M-d", ircViewDelLines) ,("M-D", ircViewDelete) ,("M-,", ircViewDeleteBackwardChar) ,("M-.", ircViewDeleteForwardChar) ,("M-<", ircViewDeleteBackwardWord) ,("M->", ircViewDeleteForwardWord) ,("M-C-,", ircViewDeleteToLineStart) ,("M-C-.", ircViewDeleteToLineEnd) ,("M-j", ircViewForwardLine) ,("M-k", ircViewBackwardLine) ,("M-l", ircViewForwardChar) ,("M-h", ircViewBackwardChar) ,("Down", ircViewForwardLine) ,("Up", ircViewBackwardLine) ,("Right", ircViewForwardChar) ,("Left", ircViewBackwardChar) ,("S-Return", ircViewNewline) ,("M-L", ircViewForwardWord) ,("M-H", ircViewBackwardWord) ,("M-P-h", ircViewSmartHome) ,("M-P-l", ircViewSmartEnd) ,("C-c", ircViewToggleSelectionMark) ,("C-C", ircViewExchangeSelectionMark) ,("C-o", ircViewOpenUrl) ,("C-j", ircViewTranslateMessage) ,("C-J", ircViewTranslateMorse) ,("C-k", ircViewReadMessage) ]