pianola-0.1.0: src/Pianola/Model/Swing.hs
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Pianola.Model.Swing (
GUI (..),
Window (..),
WindowInfo (..),
WindowLike (..),
Windowed (..),
ComponentW (..),
Component (..),
ComponentInfo (..),
ComponentType (..),
ComponentLike (..),
Cell (..),
Tab (..),
mainWindow,
childWindow,
windowTitled,
clickButtonByText,
clickButtonByToolTip,
rightClickByText,
popupItem,
selectInMenuBar,
toggleInMenuBar,
selectInComboBox,
selectTabByText,
selectTabByToolTip,
expand,
labeledBy
) where
import Prelude hiding (catch)
import Data.Tree
import Data.Function
import Data.Functor.Identity
import qualified Data.Text as T
import Control.Error
import Control.Monad
import Control.Comonad
import Control.Applicative
import Control.Monad.Trans.Class
import Control.Comonad.Trans.Class
import Control.Comonad.Trans.Env
import Data.List
import Pianola.Util
import Pianola.Pianola
import Pianola.Geometry
import Control.Monad.Logic
-- | A client-side representation of the state of a remote Swing GUI.
-- Interaction with the GUI is through actions in the monad /m/.
type GUI m = [Window m]
newtype Window m = Window { unWindow :: Tree (WindowInfo m) }
-- | Typeclass instantiated by windows and components aware of belonging to a
-- window.
class Windowed w where
window :: Monad n => w m -> n (Window m)
-- | Typeclass which provides convenience functions to supplement the bare fields of a 'WindowInfo' record.
class Windowed w => WindowLike w where
wInfo :: w m -> WindowInfo m
title :: (Monad m,Monad n) => (w m) -> n T.Text
title = return . _windowTitle . wInfo
-- | If the window has a title that satisfies the predicate, returns the
-- window, otherwise 'mzero'.
hasTitle :: MonadPlus n => (T.Text -> Bool) -> w m -> n (w m)
hasTitle f w = do
guard . f $ _windowTitle . wInfo $ w
return w
-- | Convenience function to access the components in the popup layer. Most
-- of the time, clients should use 'popupItem' instead of this function.
popupLayer :: Monad m => Glance m l (w m) (Component m)
popupLayer = replusify . _popupLayer . wInfo
-- | Convenience function to log an screenshot of a window.
logcapture :: Monad m => Pianola m LogEntry (w m) ()
logcapture = (peek $ liftN._capture.wInfo) >>= logimg
-- | Convenience function which returns the content pane component
-- augmented with a reference to the containing window.
contentPane :: Monad m => Glance m l (w m) (ComponentW m)
contentPane win =
let concrete = runIdentity $ window win
in return . ComponentW
. EnvT concrete
. unComponent
. _contentPane
. wInfo
$ win
-- | Brings the window to the front of the screen.
toFront :: Monad m => Glance m l (w m) (Sealed m)
toFront = return . _toFront . wInfo
-- | Sends an /escape/ keypress to the window.
escape :: Monad m => Glance m l (w m) (Sealed m)
escape = return . _escape . wInfo
-- | Sends an /enter/ keypress to the window.
enter :: Monad m => Glance m l (w m) (Sealed m)
enter = return . _enter . wInfo
close :: Monad m => Glance m l (w m) (Sealed m)
close = return . _close . wInfo
instance Treeish (Window m) where
children (Window c) = children c >>= return . Window
descendants (Window c) = descendants c >>= return . Window
instance WindowLike Window where
wInfo = rootLabel . unWindow
instance Windowed Window where
window = return . id
data WindowInfo m = WindowInfo
{ _windowTitle::T.Text
-- | Width, height.
, _windowDim::(Int,Int)
-- | List of components in the menu bar. See 'selectInMenuBar'.
, _menu::[Component m]
-- | List of components in the popup layer.
, _popupLayer:: [Component m]
-- | The contents pane. All non-popup components of the window are
-- descendants of the contents pane. See 'contentPane' and 'descendants'.
, _contentPane::Component m
-- | Action which returns a screenshot capture of the window. See 'logcapture'.
, _capture::Nullipotent m Image
-- | See 'escape'.
, _escape::Sealed m
-- | See 'enter'.
, _enter::Sealed m
-- | See 'close'.
, _close::Sealed m
-- | See 'toFront'.
, _toFront::Sealed m
}
-- | A component which carries a reference to the window to which it belongs.
-- See 'Windowed'.
newtype ComponentW m = ComponentW
{ unComponentW :: EnvT (Window m) Tree (ComponentInfo m) }
instance Treeish (ComponentW m) where
children (ComponentW c) = children c >>= return . ComponentW
descendants (ComponentW c) = descendants c >>= return . ComponentW
instance ComponentLike ComponentW where
cInfo = rootLabel . lower . unComponentW
instance Windowed ComponentW where
window = return . ask . unComponentW
newtype Component m = Component
{ unComponent :: Tree (ComponentInfo m) }
instance Treeish (Component m) where
children (Component c) = children c >>= return . Component
descendants (Component c) = descendants c >>= return . Component
instance ComponentLike Component where
cInfo = rootLabel . unComponent
data ComponentInfo m = ComponentInfo
{ -- | The position of the component within the containing window.
_pos::(Int,Int)
-- | Width and height.
, _dim::(Int,Int)
, _name::Maybe T.Text
, _tooltip::Maybe T.Text
-- | The textual value of the component.
, _text::Maybe T.Text
, _enabled::Bool
, _componentType::ComponentType m
, _click::Sealed m
, _doubleClick::Sealed m
, _rightClick::Sealed m
}
instance ComponentLike c => Geometrical (c m) where
nwcorner = _pos . cInfo
dimensions = _dim . cInfo
-- | Typeclass which provides convenience functions to supplement the bare fields of a 'ComponentInfo' record.
class ComponentLike c where
cInfo :: c m -> ComponentInfo m
cType :: c m -> ComponentType m
cType = _componentType . cInfo
-- | Returns the component's textual content or 'mzero' if it doesn't have
-- any.
text :: MonadPlus n => c m -> n T.Text
text = justZ . _text . cInfo
-- | If the component has some kind of textual content and the text
-- satisfies the predicate, returns the component, otherwise 'mzero'.
hasText:: MonadPlus n => (T.Text -> Bool) -> c m -> n (c m)
hasText f c = do
t <- text $ c
guard $ f t
return c
-- | Returns the component's tooltip or 'mzero' if it doesn't have any.
tooltip :: MonadPlus n => c m -> n T.Text
tooltip = justZ . _tooltip . cInfo
-- | If the component has a tooltip and the tooltip satisfies the
-- predicate, returns the component, otherwise 'mzero'.
hasToolTip:: MonadPlus n => (T.Text -> Bool) -> c m -> n (c m)
hasToolTip f c = do
t <- tooltip $ c
guard $ f t
return c
-- | If the component has a name and the name satisfies the predicate,
-- returns the component, otherwise 'mzero'.
hasName:: MonadPlus n => (T.Text -> Bool) -> c m -> n (c m)
hasName f c = do
t <- justZ._name.cInfo $ c
guard $ f t
return c
-- | Toggles the component to the desired state if the component is
-- toggleable, 'mzero' otherwise.
toggle:: MonadPlus n => Bool -> c m -> n (Sealed m)
toggle b (cType -> Toggleable _ f) = return $ f b
toggle _ _ = mzero
-- | Returns the click action of a component.
click:: Monad n => c m -> n (Sealed m)
click = return._click.cInfo
doubleClick:: Monad n => c m -> n (Sealed m)
doubleClick = return._doubleClick.cInfo
rightClick:: Monad n => c m -> n (Sealed m)
rightClick = return._rightClick.cInfo
-- | If the component is a button returns its click action, otherwise
-- 'mzero'.
clickButton:: MonadPlus n => c m -> n (Sealed m)
clickButton (cType -> Button a) = return a
clickButton _ = mzero
-- | If the component is a combo box returns its click action, otherwise
-- 'mzero'.
clickCombo:: MonadPlus n => c m -> n (Sealed m)
clickCombo (cType -> ComboBox _ a) = return a
clickCombo _ = mzero
-- | If the component is a list and has a cell whose renderer's text
-- satisfies the predicate, returns the cell, otherwise 'mzero'.
listCellByText:: MonadPlus n => (T.Text -> Bool) -> c m -> n (Cell m)
listCellByText f (cType -> List l) = do
cell <- replusify l
let renderer = _renderer cell
descendants >=> hasText f $ renderer
return cell
listCellByText _ _ = mzero
-- | If the component is a table and has a cell at the specified column
-- whose renderer's text satisfies the predicate, returns a pair of the
-- cell and the row to which it belongs, otherwise 'mzero'.
tableCellByText:: MonadPlus n => Int -> (T.Text -> Bool) -> c m -> n (Cell m,[Cell m])
tableCellByText colIndex f (cType -> Table listOfCols) = do
column <- atZ listOfCols colIndex
(rowfocus,row) <- replusify $ zip column $ transpose listOfCols
let renderer = _renderer rowfocus
descendants >=> hasText f $ renderer
return (rowfocus,row)
tableCellByText _ _ _ = mzero
-- | If the component is a tree and has a cell at the specified depth
-- (starting at 0 for the root) whose renderer's text satisfies the
-- predicate, returns the subtree which has the cell as a root, otherwise
-- 'mzero'.
treeCellByText :: MonadPlus n => Int -> (T.Text -> Bool) -> c m -> n (Tree (Cell m))
treeCellByText depth f (cType -> Treegui cellForest) = do
tree <- replusify cellForest
level <- flip atZ depth . levels . duplicate $ tree
subtree <- replusify level
let renderer = _renderer . rootLabel $ subtree
descendants >=> hasText f $ renderer
return subtree
treeCellByText _ _ _ = mzero
-- | Returns the tabs of a component if the component is a tabbed pane,
-- 'mzero' otherwise.
tab:: MonadPlus n => c m -> n (Tab m)
tab (cType -> TabbedPane p) = replusify p
tab _ = mzero
-- | If the component is a text field and is editable, set the text of the
-- text field. Otherwise 'mzero'.
setText:: MonadPlus n => T.Text -> c m -> n (Sealed m)
setText txt c = case (cType c) of
TextField (Just f) -> return $ f txt
_ -> mzero
-- | Represents data specific to each subclass of Swing components.
data ComponentType m =
Panel
-- | A check box, either in a window or in a popup menu. The bool value is the
-- current selection state.
|Toggleable Bool (Bool -> Sealed m)
-- | A button with its selection action. Menu items in popup menus are also
-- treated as buttons.
|Button (Sealed m)
-- | 'Nothing' when the textfield is not editable.
|TextField (Maybe (T.Text -> Sealed m))
|Label
-- | A combo box which may already have a selection, and which offers a click
-- action which shows the drop-down list. See 'selectInComboBox'.
|ComboBox (Maybe (Component m)) (Sealed m)
-- | See 'listCellByText'.
|List [Cell m]
-- | Tables are represented as lists of columns. See 'tableCellByText'.
|Table [[Cell m]]
-- | A list of trees of 'Cell'. It is a list of trees instead of a single tree
-- so that JTrees which do not show the root can be represented. See 'treeCellByText'.
|Treegui (Forest (Cell m))
-- | In Swing, popup menus reside in the popup layer of a window or, if the
-- popup extends beyond the window, in the contents pane of a child window
-- created to hold the popup. See 'popupItem'.
|PopupMenu
-- | See 'selectTabByText'.
|TabbedPane [Tab m]
-- | The text value holds the name of the class.
|Other T.Text
-- | Complex gui components like lists, tables and trees are represented as
-- list of cells, list of lists (list of columns) of cells, and trees of cells,
-- respectively.
--
-- Bear in mind that in Swing the renderer sub-components of a complex
-- component do /not/ count as children of the component. However, editor
-- components /do/ count as children of the component.
--
-- A common case is to double click on a table cell to activate the cell's
-- editor, and then having to look for that editor among the descendants of the
-- table.
data Cell m = Cell
{
-- | The rendering component. Clients should not try to invoke actions on
-- rendering components, as they are inert and only used for display
-- purposes.
_renderer::Component m
, _clickCell::Sealed m
, _doubleClickCell::Sealed m
, _rightClickCell::Sealed m
-- | Always 'Nothing' for cells not belonging to trees.
, _expand:: Maybe (Bool -> Sealed m)
}
data Tab m = Tab
{ _tabText::T.Text
, _tabToolTip::Maybe T.Text
, _isTabSelected:: Bool
, _selectTab::Sealed m
}
-- | Returns the main window of the application. Only works properly when there
-- is only one top-level window.
mainWindow :: Glance m l (GUI m) (Window m)
mainWindow = replusify
-- | Returns the children of a window.
childWindow :: Glance m l (Window m) (Window m)
childWindow = children
-- | Returns all visible windows whose title satisfies the predicate.
windowTitled :: (T.Text -> Bool) -> Glance m l (GUI m) (Window m)
windowTitled f = replusify >=> descendants >=> hasTitle f
-- | If the component or *any of its descendants* is a button whose text
-- satisfies the predicate, returns the click action. Otherwise 'mzero'.
clickButtonByText :: (Monad m,ComponentLike c,Treeish (c m)) => (T.Text -> Bool) -> Glance m l (c m) (Sealed m)
clickButtonByText f = descendants >=> hasText f >=> clickButton
-- | Similar to 'clickButtonByText'.
clickButtonByToolTip :: (Monad m,ComponentLike c,Treeish (c m)) => (T.Text -> Bool) -> Glance m l (c m) (Sealed m)
clickButtonByToolTip f = descendants >=> hasToolTip f >=> clickButton
-- | Similar to 'clickButtonByText'.
rightClickByText :: (Monad m,ComponentLike c,Treeish (c m)) => (T.Text -> Bool) -> Glance m l (c m) (Sealed m)
rightClickByText f = descendants >=> hasText f >=> rightClick
-- | Returns all the visible popup items belonging to a window (that is, not
-- only the popup components themselves, but all their clickable children).
-- Clients should use this function instead of trying to access the popup layer
-- directly.
popupItem :: Monad m => Glance m l (Window m) (Component m)
popupItem w =
let insidepop = children >=> contentPane >=> descendants >=> \c ->
case cType c of
PopupMenu -> descendants c
_ -> mzero
in (popupLayer >=> descendants $ w) `mplus`
(insidepop >=> return . Component . lower . unComponentW $ w)
-- | Performs a sequence of selections in a window menu, based to the text of
-- the options. Pass it something like
--
-- > map (==) ["menuitem1","menuitem2',...]
--
-- To match the exact names of the options.
selectInMenuBar :: Monad m => [T.Text -> Bool] -> Pianola m l (Window m) ()
selectInMenuBar ps =
let go (firstitem,middleitems,lastitem) = do
poke $ replusify._menu.wInfo >=> descendants >=> hasText firstitem >=> clickButton
let pairs = zip middleitems (clickButton <$ middleitems) ++
[(lastitem, clickButton)]
forM_ pairs $ \(txt,action) ->
pmaybe pfail $ retryPoke1s 7 $
popupItem >=> hasText txt >=> action
clip l = (,,) <$> headZ l <*> (initZ l >>= tailZ) <*> lastZ l
in maybe pfail go (clip ps)
-- | Like 'selectInMenuBar', but for when the last item is a toggleable
-- component. The boolean paramenter is the desired selection state.
toggleInMenuBar :: Monad m => Bool -> [T.Text -> Bool] -> Pianola m l (Window m) ()
toggleInMenuBar toggleStatus ps =
let go (firstitem,middleitems,lastitem) = do
poke $ replusify._menu.wInfo >=> descendants >=> hasText firstitem >=> clickButton
let pairs = zip middleitems (clickButton <$ middleitems) ++
[(lastitem, toggle toggleStatus)]
forM_ pairs $ \(txt,action) ->
pmaybe pfail $ retryPoke1s 7 $
popupItem >=> hasText txt >=> action
replicateM_ (length pairs) $ poke escape
clip l = (,,) <$> headZ l <*> (initZ l >>= tailZ) <*> lastZ l
in maybe pfail go (clip ps)
-- | If the component is a combo box, clicks on it and selects an option by its
-- text. Otherwise fails.
selectInComboBox :: (Monad m, ComponentLike c, Windowed c) => (T.Text -> Bool) -> Pianola m l (c m) ()
selectInComboBox f = do
poke $ clickCombo
poke $ window >=> popupItem >=> listCellByText f >=> return._clickCell
-- | If the component is a tabbed pane returns the select action of a tab whose
-- text matches the predicate. Returns 'mzero' if the component is not a tabbed
-- pane.
selectTabByText :: (Monad m,ComponentLike c) => (T.Text -> Bool) -> Glance m l (c m) (Sealed m)
selectTabByText f =
tab >=> \aTab -> do
guard $ f . _tabText $ aTab
return $ _selectTab aTab
-- | Similar to 'selecTabByText'.
selectTabByToolTip :: (Monad m,ComponentLike c) => (T.Text -> Bool) -> Glance m l (c m) (Sealed m)
selectTabByToolTip f =
tab >=> \aTab -> do
tooltip <- justZ . _tabToolTip $ aTab
guard $ f tooltip
return $ _selectTab aTab
-- | Returns the expand/collapse action of the root node of a tree of cells,
-- depending on a boolean parameter. Useful with gui trees.
expand :: Monad m => Bool -> Glance m l (Tree (Cell m)) (Sealed m)
expand b cell = (justZ . _expand . rootLabel $ cell) <*> pure b
-- | Takes a component, searches its descendants to find a label whose text
-- matches the predicate, finds the component to which the label applies, and
-- returns it.
--
-- Useful for targeting text fields in form-like dialogs.
labeledBy :: (Monad m,ComponentLike c,Treeish (c m)) => (T.Text -> Bool) -> Glance m l (c m) (c m)
labeledBy f o = do
ref <- descendants o
Label {} <- return . cType $ ref
hasText f ref
let
positioned = sameLevelRightOf ref
labellable c = case cType c of
Toggleable {} -> True
Button {} -> True
TextField {} -> True
ComboBox {} -> True
List {} -> True
Table {} -> True
Treegui {} -> True
_ -> False
candidates <- lift . observeAllT $ do
c <- descendants o
guard $ labellable c && positioned c
return c
headZ $ sortBy (compare `on` minX) candidates