Repository: kivy/kivy

Issue:
Default TabbedPanelHeader cannot be customize
The default tab in TabbedPanel is created with TabbedPanelHeader. If you want to have custom styling, you cannot use a specialized version for the default tab.

We could have a default_tab_cls property that contain the class to use for creating the default tab.



Discussion:

Resolving patch:
diff --git a/examples/widgets/tabbed_panel_showcase.py b/examples/widgets/tabbed_panel_showcase.py
--- a/examples/widgets/tabbed_panel_showcase.py
+++ b/examples/widgets/tabbed_panel_showcase.py
@@ -16,12 +16,12 @@ class StandingHeader(TabbedPanelHeader):
     pass
 
 
-class ClosableHeader(TabbedPanelHeader):
+class CloseableHeader(TabbedPanelHeader):
     pass
 
 
 Factory.register('StandingHeader', cls = StandingHeader)
-Factory.register('ClosableHeader', cls = ClosableHeader)
+Factory.register('CloseableHeader', cls = CloseableHeader)
 
 from kivy.lang import Builder
 
@@ -44,7 +44,7 @@ class ClosableHeader(TabbedPanelHeader):
         size_hint: None, None
         size: lbl.size
         center_x: root.center_x
-        center_y: root.center_y + (lbl.height/4)
+        center_y: root.center_y
         Label:
             id: lbl
             text: root.text
@@ -55,7 +55,8 @@ class ClosableHeader(TabbedPanelHeader):
     size_hint: (.45, .45)
     pos_hint: {'center_x': .25, 'y': .55}
     #replace the default tab with our custom tab
-    default_tab: def_tab
+    default_tab_cls: sh.__class__
+    default_tab_content: default_content
     tab_width: 40
     tab_height: 70
     FloatLayout:
@@ -74,17 +75,14 @@ class ClosableHeader(TabbedPanelHeader):
             size: self.parent.size
             source: 'data/images/image-loading.gif'
     StandingHeader:
-        id: def_tab
-        content: default_content
-        text: 'Default tab'
-    StandingHeader:
+        id: sh
         content: tab_2_content
         text: 'tab 2'
     StandingHeader:
         content: tab_3_content
         text: 'tab 3'
 
-<ClosableHeader>
+<CloseableHeader>
     color: 0,0,0,0
     # variable tab_width
     text: 'tabx'
@@ -100,8 +98,8 @@ class ClosableHeader(TabbedPanelHeader):
             text: root.text
         BoxLayout:
             size_hint: None, 1
-            width: 22
             orientation: 'vertical'
+            width: 22
             Widget:
             Button:
                 border: 0,0,0,0
@@ -114,13 +112,14 @@ class ClosableHeader(TabbedPanelHeader):
     tab_pos: 'top_right'
     size_hint: (.45, .45)
     pos_hint: {'center_x': .75, 'y': .55}
+    #replace the default tab with our custom tab
     default_tab: def_tab
     #allow variable tab width
     tab_width: None
     FloatLayout:
         RstDocument:
             id: default_content
-            text: '\\n'.join(("Closable tabs", "-------------",\
+            text: '\\n'.join(("Closeable tabs", "---------------",\
                 "- The tabs above are also scrollable",\
                 "- Tabs in \\'%s\\' position" %root.tab_pos))
         Image:
@@ -138,33 +137,33 @@ class ClosableHeader(TabbedPanelHeader):
             BubbleButton:
                 text: 'Press set this tab as default'
                 on_release: root.default_tab = tab3
-    ClosableHeader:
+    CloseableHeader:
         id: def_tab
         text: 'default tab'
         content:default_content
         panel: root
-    ClosableHeader:
+    CloseableHeader:
         text: 'tab2'
         content: tab_2_content
         panel: root
-    ClosableHeader:
+    CloseableHeader:
         id: tab3
         text: 'tab3'
         content: tab_3_content
         panel: root
-    ClosableHeader:
+    CloseableHeader:
         panel: root
-    ClosableHeader:
+    CloseableHeader:
         panel: root
-    ClosableHeader:
+    CloseableHeader:
         panel: root
-    ClosableHeader:
+    CloseableHeader:
         panel: root
-    ClosableHeader:
+    CloseableHeader:
         panel: root
-    ClosableHeader:
+    CloseableHeader:
         panel: root
-    ClosableHeader:
+    CloseableHeader:
         panel: root
 
 <PanelbLeft>
@@ -259,9 +258,7 @@ class ClosableHeader(TabbedPanelHeader):
 
 class Tp(TabbedPanel):
     #override tab switching method to animate on tab switch
-    def switch_to(self, header, *args):
-        if header.content is None:
-            return
+    def switch_to(self, header):
         anim = Animation(color=(1, 1, 1, 0), d =.24, t = 'in_out_quad')
 
         def start_anim(_anim, child, in_complete, *lt):
@@ -280,7 +277,7 @@ def _on_complete(*lt):
 
         anim.bind(on_complete = _on_complete)
         if self.content:
-            start_anim(anim, self.content.children[0], False)
+            start_anim(anim, self.current_tab.content, False)
         else:
             _on_complete()
 
@@ -292,7 +289,7 @@ class PanelLeft(Tp):
 class PanelRight(Tp):
 
     def add_header(self):
-        self.add_widget(ClosableHeader(panel = self))
+        self.add_widget(CloseableHeader(panel = self))
 
 
 class PanelbLeft(Tp):
diff --git a/examples/widgets/tabbedpanel.py b/examples/widgets/tabbedpanel.py
--- a/examples/widgets/tabbedpanel.py
+++ b/examples/widgets/tabbedpanel.py
@@ -7,9 +7,11 @@
 
 from kivy.app import App
 from kivy.uix.tabbedpanel import TabbedPanel
+from kivy.uix.floatlayout import FloatLayout
 from kivy.lang import Builder
 
 Builder.load_string("""
+
 <Test>:
     size_hint: .5, .5
     pos_hint: {'center_x': .5, 'center_y': .5}
diff --git a/kivy/uix/tabbedpanel.py b/kivy/uix/tabbedpanel.py
--- a/kivy/uix/tabbedpanel.py
+++ b/kivy/uix/tabbedpanel.py
@@ -30,11 +30,6 @@
 
     tab_pos = 'top_mid'
 
-By default, the widgets added to a tabbed panel are orderd horizontally, as in
-a Boxlayout. You can change that by::
-
-    orientation = 'vertical'
-
 An individual tab is called a TabbedPanelHeader. It is a special button
 containing a content property. You add the TabbedPanelHeader first, and set its
 content separately::
@@ -113,7 +108,7 @@
 '''
 
 __all__ = ('TabbedPanel', 'TabbedPanelContent', 'TabbedPanelHeader',
-    'TabbedPanelStrip')
+    'TabbedPanelStrip', 'TabbedPanelException')
 
 from functools import partial
 from kivy.clock import Clock
@@ -123,13 +118,21 @@
 from kivy.uix.scatter import Scatter
 from kivy.uix.scrollview import ScrollView
 from kivy.uix.gridlayout import GridLayout
+from kivy.uix.floatlayout import FloatLayout
 from kivy.logger import Logger
 from kivy.properties import ObjectProperty, StringProperty, OptionProperty, \
         ListProperty, NumericProperty, AliasProperty
 
 
+class TabbedPanelException(Exception):
+    '''The TabbedPanelException class.
+    '''
+    pass
+
+
 class TabbedPanelHeader(ToggleButton):
-    '''A button intended to be used as a Heading/Tab for TabbedPanel widget.
+    '''A Base for implementing a Tabbed Panel Head. A button intended to be
+    used as a Heading/Tab for TabbedPanel widget.
 
     You can use this TabbedPanelHeader widget to add a new tab to TabbedPanel.
     '''
@@ -137,7 +140,8 @@ class TabbedPanelHeader(ToggleButton):
     content = ObjectProperty(None)
     '''Content to be loaded when this tab header is selected.
 
-    :data:`content` is a :class:`~kivy.properties.ObjectProperty`
+    :data:`content` is a :class:`~kivy.properties.ObjectProperty` default
+    to None .
     '''
 
     # only allow selecting the tab if not already selected
@@ -151,8 +155,6 @@ def on_touch_down(self, touch):
             super(TabbedPanelHeader, self).on_touch_down(touch)
 
     def on_release(self, *l):
-        if not self.content:
-            return
         parent = self.parent
         while parent is not None and not isinstance(parent, TabbedPanel):
             parent = parent.parent
@@ -163,12 +165,18 @@ def on_release(self, *l):
 
 class TabbedPanelStrip(GridLayout):
     '''A strip intended to be used as background for Heading/Tab.
-    See module documentation for details.
     '''
     tabbed_panel = ObjectProperty(None)
+    '''link to the panel that tab strip is a part of.
 
+    :data:`tabbed_panel` is a :class:`~kivy.properties.ObjectProperty` default
+    to None .
+    '''
 
-class TabbedPanelContent(GridLayout):
+
+class TabbedPanelContent(FloatLayout):
+    '''The TabbedPanelContent class.
+    '''
     pass
 
 
@@ -202,13 +210,25 @@ class TabbedPanel(GridLayout):
     default to 'atlas://data/images/defaulttheme/tab'.
     '''
 
+    def get_current_tab(self):
+        return self._current_tab
+
+    current_tab = AliasProperty(get_current_tab, None)
+    '''Links to the currently select or active tab.
+
+    .. versionadded:: 1.4.0
+
+    :data:`current_tab` is a :class:`~kivy.AliasProperty`, read-only.
+    '''
+
     tab_pos = OptionProperty('top_left',
             options=('left_top', 'left_mid', 'left_bottom', 'top_left',
                 'top_mid', 'top_right', 'right_top', 'right_mid',
                 'right_bottom', 'bottom_left', 'bottom_mid', 'bottom_right'))
     '''Specifies the position of the tabs relative to the content.
-    Can be one of: left_top, left_mid, left_bottom top_left, top_mid, top_right
-    right_top, right_mid, right_bottom bottom_left, bottom_mid, bottom_right.
+    Can be one of: `left_top`, `left_mid`, `left_bottom`, `top_left`,
+    `top_mid`, `top_right`, `right_top`, `right_mid`, `right_bottom`,
+    `bottom_left`, `bottom_mid`, `bottom_right`.
 
     :data:`tab_pos` is a :class:`~kivy.properties.OptionProperty`,
     default to 'bottom_mid'.
@@ -218,14 +238,14 @@ class TabbedPanel(GridLayout):
     '''Specifies the height of the tab header.
 
     :data:`tab_height` is a :class:`~kivy.properties.NumericProperty`,
-    default to '20'.
+    default to 40.
     '''
 
     tab_width = NumericProperty(100, allownone=True)
     '''Specifies the width of the tab header.
 
     :data:`tab_width` is a :class:`~kivy.properties.NumericProperty`,
-    default to '100'.
+    default to 100.
     '''
 
     default_tab_text = StringProperty('Default tab')
@@ -235,6 +255,18 @@ class TabbedPanel(GridLayout):
     default to 'default tab'.
     '''
 
+    default_tab_cls = ObjectProperty(TabbedPanelHeader)
+    '''Specifies the class to use for the styling of the default tab.
+
+    .. versionadded:: 1.4.0
+
+    .. warning::
+        `default_tab_cls` should be subclassed from `TabbedPanelHeader`
+
+    :data:`default_tab_cls` is a :class:`~kivy.properties.ObjectProperty`,
+    default to `TabbedPanelHeader`.
+    '''
+
     def get_tab_list(self):
         if self._tab_strip:
             return self._tab_strip.children
@@ -258,13 +290,16 @@ def get_def_tab(self):
         return self._default_tab
 
     def set_def_tab(self, new_tab):
+        if not issubclass(new_tab.__class__, TabbedPanelHeader):
+            raise TabbedPanelException('`default_tab_class` should be\
+                subclassed from `TabbedPanelHeader`')
         if  self._default_tab == new_tab:
             return
         oltab = self._default_tab
         self._default_tab = new_tab
-        if self._original_tab == oltab:
+        if hasattr(self, '_original_tab') and self._original_tab == oltab:
             self.remove_widget(oltab)
-            self._origina_tab = None
+            self._original_tab = None
         self.switch_to(new_tab)
         new_tab.state = 'down'
 
@@ -290,15 +325,6 @@ def set_def_tab_content(self, *l):
     :data:`default_tab_content` is a :class:`~kivy.properties.AliasProperty`
     '''
 
-    orientation = OptionProperty('horizontal',
-            options=('horizontal', 'vertical'))
-    '''This specifies the manner in which the children of the panel content
-    are arranged. Can be one of 'vertical', 'horizontal'.
-
-    :data:`orientation` is a :class:`~kivy.properties.OptionProperty`,
-    default to 'horizontal'.
-    '''
-
     def __init__(self, **kwargs):
         self._tab_layout = GridLayout(rows = 1)
         self._bk_img = Image(
@@ -307,31 +333,56 @@ def __init__(self, **kwargs):
         self._tab_strip = _tabs = TabbedPanelStrip(tabbed_panel=self,
             rows=1, cols=99, size_hint=(None, None),\
             height=self.tab_height, width=self.tab_width)
-        self._original_tab = self._default_tab = default_tab = \
-            TabbedPanelHeader(text=self.default_tab_text,
-                height=self.tab_height, state='down',
-                width=self.tab_width)
-        _tabs.add_widget(default_tab)
-        default_tab.group = '__tab%r__' %_tabs.uid
-        self._partial_update_scrollview = None
 
+        self._partial_update_scrollview = None
         self.content = content = TabbedPanelContent()
+
+        self._current_tab = default_tab = self._original_tab\
+            = self._default_tab = TabbedPanelHeader()
         super(TabbedPanel, self).__init__(**kwargs)
-        self.add_widget(content)
+
+        content = self._default_tab.content
+        cls = self.default_tab_cls
+        if not issubclass(cls, TabbedPanelHeader):
+            raise TabbedPanelException('`default_tab_class` should be\
+                subclassed from `TabbedPanelHeader`')
+
+        if cls != type(default_tab):
+            self._default_tab = cls()
+        default_tab = self.default_tab
+        default_tab.text = self.default_tab_text
+        default_tab.height = self.tab_height
+        default_tab.group = '__tab%r__' %_tabs.uid
+        default_tab.state = 'down'
+        default_tab.width = self.tab_width if self.tab_width else 100
+        default_tab.content = content
+
+        tl = self.tab_list
+        if default_tab not in tl:
+            _tabs.add_widget(default_tab, len(tl))
+
+        if default_tab.content:
+            self.clear_widgets()
+            self.add_widget(self.default_tab.content)
+        else:
+            Clock.schedule_once(self._load_default_tab_content)
+        self._current_tab = default_tab
         self.on_tab_pos()
-        #make default tab the active tab
-        Clock.schedule_once(partial(self.switch_to, self._default_tab))
 
-    def on_default_tab_text(self, *l):
+    def _load_default_tab_content(self, dt):
+        self.switch_to(self.default_tab)
+
+    def on_default_tab_text(self, *args):
         self._default_tab.text = self.default_tab_text
 
-    def switch_to(self, header, *dt):
+    def switch_to(self, header):
         '''Switch to a specific panel header.
         '''
         header_content = header.content
+        self._current_tab = header
+        self.clear_widgets()
         if header_content is None:
             return
-        self.clear_widgets()
         # if content has a previous parent remove it from that parent
         parent = header_content.parent
         if parent:
@@ -346,11 +397,11 @@ def add_widget(self, widget, index=0):
             super(TabbedPanel, self).add_widget(widget, index)
         elif isinstance(widget, TabbedPanelHeader):
             self_tabs = self._tab_strip
-            self_tab_width = self.tab_width
             self_tabs.add_widget(widget)
             widget.group = '__tab%r__' % self_tabs.uid
             self.on_tab_width()
         else:
+            widget.pos_hint = {'x': 0, 'top': 1}
             content.add_widget(widget, index)
 
     def remove_widget(self, widget):
@@ -392,7 +443,7 @@ def clear_tabs(self, *l):
 
     def reposition_tabs(self, *l):
         Clock.unschedule(self.on_tab_pos)
-        Clock.schedule_once(self.on_tab_pos)
+        Clock.schedule_once(self.on_tab_pos, 0)
 
     def on_background_image(self, *l):
         self._bk_img.source = self.background_image
@@ -402,17 +453,6 @@ def on_background_color(self, *l):
             return
         self._bk_img.color = self.background_color
 
-    def on_orientation(self, *l):
-        content = self.content
-        if not content:
-            return
-        if self.orientation[0] == 'v':
-            content.cols = 1
-            content.rows = 99
-        else:
-            content.cols = 99
-            content.rows = 1
-
     def on_tab_width(self, *l):
         Clock.unschedule(self.update_tab_width)
         Clock.schedule_once(self.update_tab_width, 0)
@@ -430,7 +470,7 @@ def update_tab_width(self, *l):
                     # size_hint_x: x/.xyz
                     tab.size_hint_x = 1
                     #drop to default tab_width
-                    tsw = 100 * len(self._tab_strip.children)
+                    tsw += 100
                 else:
                     # size_hint_x: None
                     tsw += tab.width
@@ -540,13 +580,12 @@ def on_tab_pos(self, *l):
 
             if tab_pos[lentab_pos-4:] == '_top':
                 #on positions 'left_top' and 'right_top'
-                sctr.bind(pos = Clock.schedule_once(
-                    partial(self._update_top, sctr, 'top', None), -1))
+                sctr.bind(pos = partial(self._update_top, sctr, 'top', None))
                 tab_list = (sctr, )
             elif tab_pos[lentab_pos-4:] == '_mid':
                 #calculate top of scatter
-                sctr.bind(pos = Clock.schedule_once(
-                    partial(self._update_top, sctr, 'mid', scrl_v.width), -1))
+                sctr.bind(pos = partial(self._update_top, sctr, 'mid',
+                    scrl_v.width))
                 tab_list = (Widget(), sctr, Widget())
             elif tab_pos[lentab_pos-7:] == '_bottom':
                 tab_list = (Widget(), Widget(), sctr)
@@ -566,7 +605,12 @@ def on_tab_pos(self, *l):
         for widg in widget_list:
             add(widg)
 
-    def _update_top(self, sctr, top, scrl_v_width, dt):
+    def _update_top(self, *args):
+        sctr, top, scrl_v_width, x, y = args
+        Clock.unschedule(partial(self._updt_top, sctr, top, scrl_v_width))
+        Clock.schedule_once(partial(self._updt_top, sctr, top, scrl_v_width), 0)
+
+    def _updt_top(self, sctr, top, scrl_v_width, *args):
         if top[0] == 't':
             sctr.top = self.top
         else:


Test patch: