-
Notifications
You must be signed in to change notification settings - Fork 0
Panes
A gtk2hs based window framework.
A window contains areas which contain notebooks which contain panes. The layout of a window can be arranged by the functionality to split any area horizontally or vertically, or as a reverse operation collapse it. Internally, the panes are arranged in a layout of a binary tree, where the leaves are horizontal or vertical splits. With the interface, the commands in the View menu, or with the keyboard you can manipulate this tree to change the layout.
A notebook cannot only contain single panes, but it can as well contain group panes, which have a layout on their own and may contain arbitrary other panes. An area can be detached, which moves an area to a separate window (which may be moved to a separate display). When you close the detached window, the pane goes back to the place where it was before detaching.
The division is adjustable by the user by dragging a handle. Panes can be moved between areas in the window by dragging the notebook tab, and release it on the frame of another notebook. Alternatively you can use keystrokes to move panes around. The tabs of notebooks can be positioned at any of the four directions, or the tabs can be switched off.
Note that holding the mouse over the tabs and selecting the right button brings up a menu of all panes in this area.
By defining action descriptions, the window can be instrumented with a menu and a toolbar.
The framework offers the possibility to save the current state of the layout and the panes, and to recover it.
To use the framework, you have to call the startupFrame function. Here is an example to start it from the main StartUp event:
leksahInit2 baseEvent myEvent = do
...
registerEvent' baseEvent (λ e → case e of
StartUp → startupLeksah
_ → return ())
startupLeksah ∷ StateAction
startupLeksah = do
initPrefs
startupFrame "Leksah main" beforeMainGUI
The basic gtk initialization happens in the init functions of the package. The startupFrame takes a hook function, which is called immediately before gtk takes over control via a call to mainGUI. We will probably add other hooks later. (TODO)
startupFrame ∷ String → (Window → VBox → Notebook → StateAction) → StateAction
To use this library add this import to your modules:
import Graphics.Pane
To add a pane, you have to construct two data types. One which represents your pane, and one, with which you can represent and recover the current state of your pane. Then you have to make them an instance of class PaneInterface and class Pane.
Every Pane needs a (not changing) name. If it is not unique, the framework adds a number (e.g. "name(1)"). Then it needs a unique String which identifies the kind of pane.
Most importantly you need to implement a builder function, which builds the contents of the pane. You can use the gtk2hs API to accomplish this. This function returns a pair, with just the new pane if everything goes well, and Nothing if the pane can't be constructed. The second part of the window are gtk signal handler ids, which are used to disconnect gtk signal handlers after closing the pane. (All connection handlers are cast to connections to widgets). Then you need to have a function, which gives the top widget of your pane to the framework. So the pane type will generally hold this for you.
The saveState and recoverState functions can be used to save and reconstruct the state of the pane and use the second data structure. (See section Session).
So below is a simple example that declares a new pane
- with a pane type holding the top widget.
- a type for holding the state (explained later, not doing anything here).
- implements a Pane instance, with the builder being the interesting part. The builder fills the pane with a single button you can press.
- The builder has a handler, so if you press the button it does something.
- We add a function, which opens the dummy pane.
-- | The state of the pane
data DummyPane = DummyPane {
sw ∷ VBox
} deriving Typeable
-- | The state for recovery (unused)
data DummyPaneState = DPState
deriving(Eq,Ord,Read,Show)
-- | The instance of the PaneInterface class
instance PaneInterface DummyPane where
data PaneState DummyPane = DummyPaneState
deriving(Eq,Ord,Read,Show)
getTopWidget = λ p → castToWidget (sw p)
primPaneName = λ dp → "Dummy"
paneType = λ _ → "**Dummy"
saveState = λ s → return Nothing
recoverState = λ s _ → return Nothing
builder = buildDummy
-- | The instance of the Pane class
instance Pane DummyPane
-- | The actual construction function for the pane
buildDummy panePath notebook window = do
reifyState $ λ stateR → do
ibox ← vBoxNew False 0
button ← buttonNew
buttonSetLabel button "Press here"
boxPackStartDefaults ibox button
let info = DummyPane{ sw = ibox }
button `onButtonPress` λ event → reflectState (dummyAction >> return True) stateR
return (Just info,[])
openDummy ∷ StateM ()
openDummy = (getOrBuildDisplay (Left []) True ∷ StateM (Maybe DummyPane)) >> return ()
So the important part is the PaneInterface class, which needs to get implemented for every new type of pane:
class (Typeable α, Show (PaneState α), Read (PaneState α)) ⇒ PaneInterface α where
data PaneState α ∷ *
primPaneName ∷ α → String
-- ^ gets a string which names this pane
paneType ∷ α → String
-- ^ gets a unique id for this type of pane
builder ∷ PanePath → Notebook → Window → StateM (Maybe α,Connections)
-- ^ A function, which builds this pane
getTopWidget ∷ α → Widget
-- ^ gets the top Widget of this pane
saveState ∷ α → StateM (Maybe (PaneState α))
-- ^ Returns the state of this pane
recoverState ∷ PanePath → PaneState α → StateM (Maybe α)
-- ^ Sets the state for this pane
In the class Pane you don`t have to implement any methods.
The pane class offers the following functions:
paneName ∷ α → PaneName
-- ^ gets a string which names this pane, which may include an added index ...
getAddedIndex ∷ α → Int
-- ^ ..., which is used if more then one pane has the same name
getPane ∷ StateM (Maybe α)
-- ^get a pane of this type, if one is open. Their is as well a getPanes
-- function, which returns a list of panes)
forceGetPane ∷ Either PanePath String → StateM α
-- ^get a pane of this type, if not one is open panic
getOrBuildPane ∷ Either PanePath String → StateM (Maybe α)
-- ^get a pane of this type, if one is open, or build one and for this specify either
-- a pane path to put it, or a group name, from which a pane path may be derived
displayPane ∷ α → Bool → StateM ()
-- ^ makes this pane visible
getOrBuildDisplay ∷ Either PanePath String → Bool → StateM (Maybe α)
-- ^ is a concatination of getOrBuildPane and displayPane
setChanged ∷ α → Bool → StateM ()
-- ^ Set the state of this pane to changed or not changed
closePane ∷ α → StateM Bool
-- ^ Closes this pane
Finally we have some functions, which operate on the currently active pane:
-- | Split the currently active pane in horizontal direction
viewSplitHorizontal ∷ StateM ()
-- | Split the currently active pane in vertical direction
viewSplitVertical ∷ StateM ()
-- | Two notebooks can be collapsed to one
viewCollapse ∷ StateM ()
-- | Moves the activePane in the given direction, if possible
-- | If their are many possibilities choose the leftmost and topmost
viewMove ∷ PaneDirection → StateM ()
viewNewGroup ∷ StateM ()
viewDetach ∷ StateM (Maybe (Window,Widget))
-- | Toggle the tabs of the current notebook
viewSwitchTabs ∷ StateM ()
-- | Sets the tab position in the current notebook
viewTabsPos ∷ PositionType → StateM ()
-- | Closes the current pane
viewClosePane ∷ StateM ()
Yet, the pane still doesn't show up, so we need to add a menu item or such to do this, which we treat now.
To add an item to the menu-bar, toolbar, or add an accelerator, you have to add
an action description for it. To do this, you handle the RegisterAction
frame
event, and add your actions to it:
dummyInit2 ∷ BaseEvent → EventChannel DummyEvent → StateM ()
dummyInit2 baseEvent myEvent =
getFrameEvent ↠ λev → registerEvent ev frameEventHandler >> return ()
frameEventHandler (RegisterActions actions) = return (RegisterActions $ actions ⊕ myActions)
frameEventHandler other = return other
To define the action, you name it, and in this example we add it as last entry in the Panes sub-menu:
myActions ∷ [ActionDescr]
myActions =
[AD "Dummy" "Dummy" Nothing Nothing openDummy Nothing ActionNormal
(Just $ MPLast ["Panes"] False) Nothing []]
Here is a complete description of the ActionDescr data type:
-- | ActionDescr is a data structure used for
-- menus, toolbars, and accelerator keystrokes. In this implementation
-- GtkActions are build from this description
data ActionDescr = AD {
adName ∷ String -- ^ has to be unique, so allways prepend the plugin name,
-- seperated with a dot.
, adLabel ∷ String -- ^ what is displayed in the menu
, adSynopsis ∷ Maybe String -- ^ maybe a text to display in a tooltip
, adStockID ∷ Maybe String -- ^ maybe a text for a gtk standard action
, adAction ∷ StateM () -- ^ the action to perform
-- which then may show a special icon, ...
, adAccelerator ∷ Maybe String -- ^ Keyboard accelerator
-- ^ The format looks like "<Control>a" or "<Shift><Alt>F1" or "<Release>z"
, adActionType ∷ ActionType
, adMenu ∷ Maybe MenuPosition
, adToolbar ∷ Maybe ToolPosition
, adSensitivities ∷ [GenSelector]
}
type WithSeparator = Bool
data MenuPosition =
MPFirst [String] -- ^ Add this item in the first position using path.
| MPLast [String] WithSeparator -- ^ Add this item in the last position using path.
| MPAfter [String]
WithSeparator -- ^ Add this item after the first arg string
-- If the Bool is true add a separator between.
| MPBefore [String] -- ^ Add this item before the first arg string
-- in the last position using path (second arg).
| MPAppend WithSeparator -- ^ Append this after the last added item.
-- If the Bool is true add a separator between.
| MPOr MenuPosition MenuPosition -- ^ Try the first position.
-- If this fails try the next.
deriving Eq
data ToolPosition =
TPFirst -- ^ Add this item in the first position.
| TPLast WithSeparator -- ^ Add this item in the last position.
| TPAfter String WithSeparator -- ^ Add this item after the first arg string
| TPBefore String -- ^ Add this item before the first arg string
-- in the last position using path (second arg).
| TPAppend WithSeparator -- ^ Append this after the last added item.
| TPOr ToolPosition ToolPosition -- ^ Try the first position.
-- If this fails try the next.
deriving Eq
-- | Beside Standard action we have actions which toggles a state or select from a
-- number of possible states
data ActionType = ActionNormal | ActionToggle | ActionSubmenu -- TODO ActionSelect alpha
The toolbar work just like the menu-bar, except that the position can't be hierarchic.
The sensitivity for actions can be set. The menu and toolbar entry for an nonsensitive entry is greyed out. To make actions sensitive
- define a Selector
- add the selector to all actions, which shall be sensible for this selector
- call setSensivity, with a list of pairs of selectors and a Boolean
setSensitivity ∷ Selector s ⇒ [(s, Bool)] → StateM ()
The recovering restores the original layout together with all panes that supports recovery. When the return value of the save state instance function of PaneInterface is Nothing, the pane will not be reopened, when the state is recovered.
To use this feature, you first have to register the pane type with the RegisterPane event. This helps the framework to find the right type, when de-serializing (read) the pane state. For every pane you need a tupel with the paneType string and a PaneC with some object of type pane. Since the second is only used for type-checking, you can pass an undefined with the right type (you can use an undefined as well to get the string, as the string is independent of the concrete instance). Not to forget, we have to register the pane type to the framework. To do this handle the RegisterPane event of the frameEvent channel in your init2 method, and add your pane types. As only the type is needed, you don't need to actually instantiate an object, but can use an undefined:
frameEventHandler (RegisterPane paneTypes) = return (RegisterPane $ paneTypes ⊕ myPaneTypes)
myPaneTypes ∷ [(String,GenPane)]
myPaneTypes = map asRegisterType [undefined ∷ DummyPane]
Then you can use the below code to retrieve and set the session information:
-- | Retrieves a string with all session information
saveSession ∷ StateM String
-- | Recovers a session from a previously saved string
recoverSession ∷ String → StateM ()
Here is an example code for implementing the saveState and recoverState functions of the PaneInterface class. In the saveState function we extract the state and pass it with the PPState constructor. In the recoverState function we build the pane, and if this was successful inject the value in the pane.
instance PaneInterface PluginPane where
data PaneState PluginPane = PPState (Maybe Plugin)
deriving(Read,Show)
...
saveState = λ p → do
mbVal ← ppExt p
return $ Just (PPState mbVal)
recoverState = λ pp ps → do
nb ← getNotebook pp
mbP ← buildPane pp nb builder
case mbP of
Nothing → return Nothing
Just p → case ps of
PPState Nothing → return $ Just p
PPState (Just v) → (ppInj p) v ↠ λ _ → return $ Just p
Since the application may want to add other information to the session, the session is extensible. To add an extension, you have to handle the RegisterSessionExt event, and add your extensions. An extension needs a unique name, a retriever to get the value, and an applicator to restore the value.
data (Read α, Show α) ⇒ SessionExtension α = SessionExtension {
seName ∷ String,
seRetriever ∷ StateM α,
seApplicator ∷ α → StateM ()}
data GenSessionExtension = forall α . (Read α, Show α) ⇒ GenS (SessionExtension α)
This code doesn't make much sense but should show how to use the mechanism:
...
frameEventHandler (RegisterSessionExt ext) = return (RegisterSessionExt $ ext ⊕ mySessionExt)
...
mySessionExt ∷ [GenSessionExtension]
mySessionExt = [GenS (SessionExtension "dummy" (return 5)
(λ i → liftIO $ putStrLn ("recovery " ⊕ show (i + 1)))),
GenS (SessionExtension "dummy2" (return 5.2)
(λ i → liftIO $ putStrLn ("recovery2 " ⊕ show (i + 0.1))))]
TODO