Skip to content

Instantly share code, notes, and snippets.

@abhijangda
Last active December 21, 2015 16:19
Show Gist options
  • Select an option

  • Save abhijangda/6333030 to your computer and use it in GitHub Desktop.

Select an option

Save abhijangda/6333030 to your computer and use it in GitHub Desktop.
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem, TabbedPanelHeader, TabbedPanelContent
from kivy.properties import ObjectProperty, StringProperty, BooleanProperty, NumericProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.image import Image
from kivy.uix.bubble import Bubble, BubbleButton
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.uix.scrollview import ScrollView
Builder.load_string('''
<MenuHeader>
color: (1, 1, 1, 1) if self.state == 'normal' else (0, 0, 0, 1)
font_size: '12dp'
shorten: True
text_size: self.size
padding: '2dp', '2dp'
background_normal: 'atlas://data/images/defaulttheme/action_item'
background_disabled_normal: 'atlas://data/images/defaulttheme/action_item_disabled'
background_down: 'atlas://data/images/defaulttheme/action_item_down'
background_disabled_down: 'atlas://data/images/defaulttheme/action_item_down'
Image:
source: 'atlas://data/images/defaulttheme/tree_closed'
size: (20, 20)
center_y: root.center_y
x: (self.parent.right - self.width) if self.parent else 100
<ContextSubMenu>:
arrow_image: 'atlas://data/images/defaulttheme/tree_closed'
Image:
source: root.arrow_image
size: (20, 20) if root.attached_menu else (0,0)
y: self.parent.y + (self.parent.height/2) - (self.height/2)
x: self.parent.x + (self.parent.width - self.width)
<MenuButton>:
background_normal: 'atlas://data/images/defaulttheme/action_item'
background_disabled_normal: 'atlas://data/images/defaulttheme/action_item_disabled'
background_down: 'atlas://data/images/defaulttheme/action_item_down'
background_disabled_down: 'atlas://data/images/defaulttheme/action_item_down'
<ContextMenu>:
tab_pos:'top_right'
do_default_tab: False
<MenuBubble>:
background_image: 'atlas://data/images/defaulttheme/action_item'
''')
class MenuBubble(Bubble):
pass
class MenuHeader(TabbedPanelItem):
pass
class ContextMenuException(Exception):
pass
class MenuButton(Button):
pass
class ContextMenu(TabbedPanel):
container = ObjectProperty(None)
main_tab = ObjectProperty(None)
bubble_cls = ObjectProperty(MenuBubble)
header_cls = ObjectProperty(MenuHeader)
attach_to = ObjectProperty(allownone=True)
auto_width = BooleanProperty(True)
dismiss_on_select = BooleanProperty(True)
max_height = NumericProperty(None, allownone=True)
'''Indicate the maximum height that the dropdown can take. If None, it will
take the maximum height available, until the top or bottom of the screen
will be reached.
:data:`max_height` is a :class:`~kivy.properties.NumericProperty`, default
to None.
'''
__events__ = ('on_select', 'on_dismiss')
def __init__(self, **kwargs):
self._win = None
self.add_tab = super(ContextMenu, self).add_widget
self.bubble = self.bubble_cls(size_hint=(None, None))
super(ContextMenu, self).__init__(**kwargs)
self.bubble.add_widget(self)
self.bind(size=self._reposition)
def open(self, widget):
'''Open the dropdown list, and attach to a specific widget.
Depending the position of the widget on the window and the height of the
dropdown, the placement might be lower or higher off that widget.
'''
# ensure we are not already attached
if self.attach_to is not None:
self.dismiss()
# we will attach ourself to the main window, so ensure the widget we are
# looking for have a window
self._win = widget.get_parent_window()
if self._win is None:
raise ContextMenuException(
'Cannot open a dropdown list on a hidden widget')
self.attach_to = widget
widget.bind(pos=self._reposition, size=self._reposition)
self._reposition()
# attach ourself to the main window
self._win.add_widget(self.bubble)
def on_select(self, data):
pass
def dismiss(self, *largs):
'''Remove the dropdown widget from the iwndow, and detach itself from
the attached widget.
'''
if self.bubble.parent:
self.bubble.parent.remove_widget(self.bubble)
if self.attach_to:
self.attach_to.unbind(pos=self._reposition, size=self._reposition)
self.attach_to = None
self.dispatch('on_dismiss')
def select(self, data):
'''Call this method to trigger the `on_select` event, with the `data`
selection. The `data` can be anything you want.
'''
self.dispatch('on_select', data)
if self.dismiss_on_select:
self.dismiss()
def on_dismiss(self):
pass
def _reposition(self, *largs):
# calculate the coordinate of the attached widget in the window
# coordinate sysem
win = self._win
widget = self.attach_to
if not widget or not win:
return
wx, wy = widget.to_window(*widget.pos)
wright, wtop = widget.to_window(widget.right, widget.top)
# set width and x
if self.auto_width:
self.bubble.width = wright - wx
# ensure the dropdown list doesn't get out on the X axis, with a
# preference to 0 in case the list is too wide.
x = wx
if x + self.bubble.width > win.width:
x = win.width - self.bubble.width
if x < 0:
x = 0
self.bubble.x = x
# determine if we display the dropdown upper or lower to the widget
h_bottom = wy - self.bubble.height
h_top = win.height - (wtop + self.bubble.height)
if h_bottom > 0:
self.bubble.top = wy
self.bubble.arrow_pos = 'top_mid'
elif h_top > 0:
self.bubble.y = wtop
self.bubble.arrow_pos = 'bottom_mid'
else:
# none of both top/bottom have enough place to display the widget at
# the current size. Take the best side, and fit to it.
height = max(h_bottom, h_top)
if height == h_bottom:
self.bubble.top = wy
self.bubble.height = wy
self.bubble.arrow_pos = 'top_mid'
else:
self.bubble.y = wtop
self.bubble.height = win.height - wtop
self.bubble.arrow_pos = 'bottom_mid'
def on_touch_down(self, touch):
if super(ContextMenu, self).on_touch_down(touch):
return True
if self.collide_point(*touch.pos):
return True
self.dismiss()
def on_touch_up(self, touch):
if super(ContextMenu, self).on_touch_up(touch):
return True
self.dismiss()
def add_widget(self, widget, index=0):
if self.tab_list and widget == self.tab_list[0].content or\
widget == self._current_tab.content or self.content == widget or\
self._tab_layout == widget or\
isinstance(widget, TabbedPanelContent) or\
isinstance(widget, TabbedPanelHeader):
super(ContextMenu, self).add_widget(widget, index)
return
if not self.main_tab:
self.main_tab = self.header_cls(text='Main Menu')
self.add_tab(self.main_tab)
self.main_tab.content = ScrollView(size_hint=(1,1))
self.main_tab.content.bind(height=self.on_scroll_height)
self.main_box = GridLayout(orientation='vertical',
size_hint_y=None,
cols=1)
self.main_tab.content.add_widget(self.main_box)
self.main_box.bind(height=self.on_main_box_height)
self.main_box.add_widget(widget, index)
if hasattr(widget, 'cont_menu'):
widget.cont_menu = self
widget.bind(height=self.on_child_height)
def remove_widget(self, widget):
if widget in self.main_box.children:
self.main_box.remove_widget(widget)
else:
super(ContextMenu, self).remove_widget(widget)
def on_scroll_height(self, *args):
self.main_box.height = max(self.main_box.height,
self.main_tab.content.height)
def on_main_box_height(self, *args):
self.main_box.height = max(self.main_box.height,
self.main_tab.content.height)
if self.max_height:
self.bubble.height = min(self.main_box.height + self.tab_height + dp(10), self.max_height)
else:
self.bubble.height = self.main_box.height + self.tab_height + dp(10)
def on_child_height(self, *args):
height = 0
for i in self.main_box.children:
height += i.height
self.main_box.height = height
def add_tab(self, widget, index = 0):
super(ContextMenu, self).add_widget(widget, index)
class ContextSubMenu(MenuButton):
attached_menu = ObjectProperty(None)
cont_menu = ObjectProperty(None)
container = ObjectProperty(None)
def __init__(self, **kwargs):
super(ContextSubMenu, self).__init__(**kwargs)
def on_text(self, *args):
if self.attached_menu:
self.attached_menu.text = self.text
def on_attached_menu(self, *args):
self.attached_menu.text = self.text
def add_widget(self, widget, index = 0):
if isinstance(widget, Image):
Button.add_widget(self, widget, index)
return
if not self.attached_menu:
self.attached_menu = self.cont_menu.header_cls(text=self.text)
self.attached_menu.content = ScrollView(size_hint=(1,1))
self.attached_menu.content.bind(height=self.on_scroll_height)
self.container = GridLayout(orientation='vertical',
size_hint_y = None, cols=1)
self.attached_menu.content.add_widget(self.container)
self.container.bind(height=self.on_container_height)
self.container.add_widget(widget, index)
widget.cont_menu = self.cont_menu
widget.bind(height=self.on_child_height)
def on_scroll_height(self, *args):
self.container.height = max(self.container.height,
self.attached_menu.content.height)
def on_container_height(self, *args):
self.container.height = max(self.container.height,
self.attached_menu.content.height)
def on_child_height(self, *args):
height = 0
for i in self.container.children:
height += i.height
self.container.height = height
def on_release(self, *args):
if not self.attached_menu:
return
try:
index = self.cont_menu.tab_list.index(self.attached_menu)
self.cont_menu.switch_to(self.cont_menu.tab_list[index])
except:
curr_index = self.cont_menu.tab_list.index(self.cont_menu.current_tab)
for i in range(curr_index - 1, -1, -1):
self.cont_menu.remove_widget(self.cont_menu.tab_list[i])
self.cont_menu.add_tab(self.attached_menu)
self.cont_menu.switch_to(self.cont_menu.tab_list[0])
from kivy.clock import Clock
Clock.schedule_once(self._scroll, 0.1)
def _scroll(self, dt):
from kivy.animation import Animation
total_tabs = len(self.cont_menu.tab_list)
tab_list = self.cont_menu.tab_list
curr_index = total_tabs - tab_list.index(self.cont_menu.current_tab)
to_scroll = len(tab_list)/curr_index
anim = Animation(scroll_x=to_scroll, d=0.75)
anim.cancel_all(self.cont_menu.current_tab.parent.parent)
anim.start(self.cont_menu.current_tab.parent.parent)
if __name__=='__main__':
from kivy.app import App
Builder.load_string('''
<Test>:
Button:
text: 'press to launch menu'
size_hint: None, None
size: 200, 100
pos_hint: {'top': 1, 'x': 0.5}
on_release: root.add_menu(args[0])
<CMenu>:
MenuButton:
text: 'Item1'
size_hint_y: None
height: 30
on_release: root.select(self.text)
MenuButton:
text: 'Item1'
size_hint_y: None
height: 30
MenuButton:
text: 'Item1'
size_hint_y: None
height: 30
MenuButton:
text: 'Item1'
size_hint_y: None
height: 30
MenuButton:
text: 'Item1'
size_hint_y: None
height: 30
MenuButton:
text: 'Item1'
size_hint_y: None
height: 30
MenuButton:
text: 'Item1'
size_hint_y: None
height: 30
ContextSubMenu:
text: 'Item2'
size_hint_y: None
height: 30
MenuButton:
text: '2->1'
size_hint_y: None
height: 30
MenuButton:
text: '2->2'
size_hint_y: None
height: 30
MenuButton:
text: '2->2'
size_hint_y: None
height: 30
MenuButton:
text: '2->2'
size_hint_y: None
height: 30
MenuButton:
text: '2->2'
size_hint_y: None
height: 30
MenuButton:
text: '2->2'
size_hint_y: None
height: 30
MenuButton:
text: '2->2'
size_hint_y: None
height: 30
MenuButton:
text: '2->2'
size_hint_y: None
height: 30
ContextSubMenu:
text: '2->3'
size_hint_y: None
height: 30
ContextSubMenu:
text: '2->3->1'
size_hint_y: None
height: 30
ContextSubMenu:
text: 'Item3'
size_hint_y: None
height: 30
MenuButton:
text: '3->1'
size_hint_y: None
height: 30
MenuButton:
text: '3->2'
size_hint_y: None
height: 30
''')
class CMenu(ContextMenu):
pass
class Test(FloatLayout):
def __init__(self, **kwargs):
super(Test, self).__init__(**kwargs)
self.context_menu = CMenu()
def add_menu(self, obj, *l):
self.context_menu = CMenu()
self.context_menu.open(self.children[0])
class MyApp(App):
def build(self):
return Test()
MyApp().run()
@akshayaurora
Copy link
Copy Markdown

<SubMenuItem@Cmenu>

<CMenu>:
    Button:
        text: 'Item1'

    SubMenuItem:
        text: 'Item2'
        Button:
            text: '2->1'
        Button:
            text: '2->2'
        Button:
            text: '2->3'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment