owickstrom / gi-gtk-declarative

Declarative GTK+ programming in Haskell
https://owickstrom.github.io/gi-gtk-declarative/
288 stars 35 forks source link

Wrap Gtk.Notebook #56

Closed zenhack closed 4 years ago

zenhack commented 5 years ago

I found myself wanting to use a gtk notebook (tabs), but it looks like this widget will need a little bit of custom logic to be able to add children.

I tried adding the necessary instances myself, but here's where I got stuck: StateTree seems to assume all parent/child relationships fit into the widget/bin/container ontology, but notebooks have "pages" which have a main contents widget, but also the tab itself can be an arbitrary widget, and there's also a "menu label" widget that can be specified. Furthermore, there is no widget caled "page"; the relevant functions just take more than one widget when adding in the same call. So I don't know how to construct the SomeState for a notebook, or write the EventSource and Patchable instances.

Any insights?

Relevant gtk docs:

https://developer.gnome.org/gtk3/stable/GtkNotebook.html#gtk-notebook-append-page

owickstrom commented 5 years ago

OK! I haven't used the notebook widget, but I see how this is problematic.

Would the existing solution for menus be helpful, in https://github.com/owickstrom/gi-gtk-declarative/blob/master/gi-gtk-declarative/src/GI/Gtk/Declarative/Container/MenuItem.hs? Menus are simpler though, they have the same type of children. Maybe it's better if you create a custom widget wrapper for it, it seems hard to fit into the "happy path" of widget/bin/container, as you say. And extending that model just for one type of widget isn't very appealing.

Dretch commented 4 years ago

I want to use a Notebook, but I don't need the fancy stuff: just labelled tabs. I was able to get this working fairly easily by looking at the other containers - see below:

{-# OPTIONS_GHC -fno-warn-orphans #-}
module Notebook where

import           Control.Monad (void)
import           Data.Text (Text)
import           Data.Vector (Vector)
import qualified GI.Gtk as Gtk
import           GI.Gtk.Declarative.Container.Class
import           GI.Gtk.Declarative.EventSource
import           GI.Gtk.Declarative.Widget
import           GI.Gtk.Declarative.Patch

data NotebookChild event = NotebookChild
    { label :: Text
    , child :: Widget event
    }
    deriving (Functor)

instance Patchable NotebookChild where
    create = create . child
    patch s b1 b2 = patch s (child b1) (child b2)

instance EventSource NotebookChild where
    subscribe NotebookChild{..} = subscribe child

instance ToChildren Gtk.Notebook Vector NotebookChild

instance IsContainer Gtk.Notebook NotebookChild where
    appendChild notebook NotebookChild{label} widget = do
        lbl <- Gtk.labelNew (Just label)
        void $ Gtk.notebookAppendPage notebook widget (Just lbl)

    replaceChild notebook NotebookChild{label} i old new = do
        Gtk.widgetDestroy old
        lbl <- Gtk.labelNew (Just label)
        void $ Gtk.notebookInsertPage notebook new (Just lbl) i

I can send a pull request with docs and examples, if this is something you want in the library.

Dretch commented 4 years ago

I managed to get tab labels to be arbitrary widgets. I'm not 100% sure this is safe, but it works as far as I have tested it:

{-# OPTIONS_GHC -fno-warn-orphans #-}
module Gi.Gtk.Declarative.Notebook
  ( Page
  , page
  , pageWithTab
  , notebook
  ) where

import           Control.Monad (void)
import           Data.Maybe (isNothing)
import           Data.Text (Text, pack)
import           Data.Vector (Vector)
import qualified Data.Vector as Vector
import           GHC.Ptr (nullPtr)
import qualified GI.GLib as GLib
import qualified GI.Gtk as Gtk
import           GI.Gtk.Declarative
import           GI.Gtk.Declarative.Container
import           GI.Gtk.Declarative.Container.Class

data Page event = Page
  { tabLabel :: Widget event
  , child :: Widget event
  }

page :: Text -> Widget event -> Page event
page label = pageWithTab (widget Gtk.Label [#label := label])

pageWithTab :: Widget event -> Widget event -> Page event
pageWithTab = Page

notebook :: Vector (Attribute Gtk.Notebook event) -> Vector (Page event) -> Widget event
notebook attrs children =
    let tabsAndChildren = concat $ (\Page{..} -> [child, tabLabel]) <$> children
    in container Gtk.Notebook attrs $ Vector.fromList tabsAndChildren

instance ToChildren Gtk.Notebook Vector Widget

instance IsContainer Gtk.Notebook Widget where

    appendChild parent _ new = do
        lastPage <- Gtk.notebookGetNthPage parent (-1)
        case lastPage of
            Nothing -> do -- this is the first page to be added
                void $ Gtk.notebookAppendPage parent new (Nothing :: Maybe Gtk.Widget) 
            Just p -> do
                label <- Gtk.notebookGetTabLabel parent p
                if isNothing label then -- this page must already have a child, we just need to set the label
                    Gtk.notebookSetTabLabel parent p (Just new)
                else                    -- the last page has a child and a label, so create a new page instead
                    void $ Gtk.notebookAppendPage parent new (Nothing :: Maybe Gtk.Widget)

    replaceChild parent _ i old new = do
        let i' = i `div` 2
        pageI <- Gtk.notebookGetNthPage parent i'
        case pageI of
            Nothing -> do
                GLib.logDefaultHandler
                    (Just "gi-gtk-declarative")
                    [GLib.LogLevelFlagsLevelError]
                    (Just $ "Notebook.replaceChild called with an index where there is no child: " <> pack (show i))
                    nullPtr
            Just p -> do
                if i `mod` 2 == 0 then do -- we have to replace the child
                    label <- Gtk.notebookGetMenuLabel parent p
                    Gtk.widgetDestroy old
                    void $ Gtk.notebookInsertPage parent new label i'
                else do                   -- we have to replace the label
                    Gtk.notebookSetTabLabel parent p (Just new)

Usage:

notebook []
              [ page "First Page" (widget ...)
              , page "Second Page" (widget ...)
              ]
owickstrom commented 4 years ago

@Dretch I'm unsure about the Notebook API, specifically, but from a gi-gtk-declarative point of view (if that makes sense), your code looks good. :+1:

owickstrom commented 4 years ago

Now that it's generic, would you mind submitting this wrapper in a PR?

owickstrom commented 4 years ago

Closing this now, as @Dretch submitted a PR that's merged and released on Hackage as 0.6.2.