<reponame>dafvid/menu
"""
Python Menu class
<NAME> 2019
"""

# TODO
# -reserved nav keys (eg. p,n,u,-,0)

import os

__version__ = '190923.1'


class MenuItem:
    """The base class of all menu items
    Args:
        label (str): Text shown on the menu item
    """
    def __init__(self, label):
        self.label = label


class NoNavItem(MenuItem):  # Item outside of navigation
    """Base class for all classes that are outside of navigation"""
    pass


class SpaceItem(NoNavItem):
    """Just adds an empty row in the list of items"""
    def __init__(self):
        super().__init__('')


class TextItem(NoNavItem):
    """A simple text label in the menu
    Args:
        label (str): Text shown on the menu item
    """
    def __init__(self, label):
        super().__init__(label)


class NavItem(MenuItem):
    """Base class for all navigationitems
    Args:
        label (str): Text shown on the menu item
    Attributes:
        key (str): Shortcut for keyboard navigation
    """
    def __init__(self, label):
        super().__init__(label)
        self.key = ''
        self._msg = ''

    def has_msg(self):
        """Check if there is a message to show"""
        return self._msg != ''

    @property
    def msg(self):
        msg = self._msg
        self._msg = ''
        return msg

    @msg.setter
    def msg(self, msg):
        self._msg = msg


class ActionItem(NavItem):
    """Item calling a function with args and kwargs
    Attributes:
        msg (str): optional msg from the function
        action_func: reference to function to be called
    """
    def __init__(self, label, action_func, *args, **kwargs):
        super().__init__(label)
        self.action_func = action_func
        self.args = args
        self.kwargs = kwargs

    def action(self):
        self.action_func(self, *self.args, **self.kwargs)


class SubMenu(NavItem):
    """Base class for the hierarchical menu data model
    Args:
        label (str): Text shown on the menu item
        title (str): Text shown as title above the menu text
        text (str): Text show at the top of the menu
    Attributes:
        items (SubMenu): A list of child SubMenu items

    """
    def __init__(self, label, title='', text=''):
        super().__init__(label)
        self.itemsCount = 1
        self.items = []
        self.title = title if title else label
        self.text = text  # TODO replace with TextItem only
        self.parent = None

    def add_space_item(self):
        self.items.append(SpaceItem())

    def add_text_item(self, text, space_above=False):
        if space_above:  # It's common to add space above titles and labels
            self.add_space_item()
        self.items.append(TextItem(text))

    def add_item(self, item, key=''):
        if not hasattr(item, 'parent') or not item.parent:
            item.parent = self

        if isinstance(item, NoNavItem):
            self.items.append(item)
        elif key:
            item.key = key
            self.items.append(item)
        else:
            item.key = str(self.itemsCount)
            self.items.append(item)
            self.itemsCount += 1

    def clear_items(self):
        self.itemsCount = 1
        self.items = []

    def nav_items(self):
        return [i for i in self.items if isinstance(i, NavItem)]


class DynMenu(SubMenu):
    """A dynamic menu generated by a function"""
    def __init__(self, label, init_func, *args, **kwargs):
        super(DynMenu, self).__init__(label)
        self.init_func = init_func
        self.updatable = False
        self.args = args
        self.kwargs = kwargs

    def init_menu(self):
        """Call init_func with referenc do self, args and kwargs"""
        self.init_func(self, *self.args, **self.kwargs)

    def reset(self):
        """Clear all items and call init_func again"""
        self.clear_items()
        self.text = ''
        self.init_menu()


class SearchMenu(DynMenu):
    """A dynamic menu generated from a function by passing a parameter"""
    def __init__(self, label, search_func, *args, **kwargs):
        super(SearchMenu, self).__init__(label, None, *args, **kwargs)

        self.searchFunc = search_func

    def init_menu(self):
        pass

    def search(self, ans):
        return self.searchFunc(self, ans, *self.args, **self.kwargs)


class PageMenu(DynMenu):
    """Dynamic menu split upp into pages with prev/next navigation"""
    def __init__(self, label, item_init, idx=0, pages=None, title_func=None, *args, **kwargs):
        super(PageMenu, self).__init__(label, None, *args, **kwargs)
        if pages is None:
            pages = []
        self.pages = pages
        self.idx = idx
        self.item_init = item_init
        self.length = len(pages)
        self.title_func = title_func

    # override DynMenu init
    def init_menu(self):
        # call init for listitem
        page = self.pages[self.idx]
        if self.title_func:
            self.title = self.title_func(page)
        else:
            self.title = self.label
        self.item_init(self, page, *self.args, **self.kwargs)


class ListMenu(PageMenu):
    """Generete a PageMenu from a list of items and a page size"""
    def __init__(self, label, item_init, list_items, size=15, label_func=None, title_func=None, *args, **kwargs):
        pages = []
        for i in range(0, len(list_items), size):
            pages.append(list_items[i:i + size])
        super(ListMenu, self).__init__(label, None, 0, pages, *args, **kwargs)
        self.list = list_items
        self.size = size
        self.listLength = len(list_items)
        self.item_init = item_init
        self.label_func = label_func
        self.title_func = title_func

    def page_len(self):
        return len(self.pages[self.idx])

    def init_menu(self):
        page = self.pages[self.idx]  # list of items
        i = 0
        for page_item in page:
            # lbl = ''
            if self.label_func:
                lbl = self.label_func(page_item)
            else:
                lbl = str(page_item)
            idx = self.idx * self.size + i
            item = DynMenu(lbl, self.item_init, self.list[idx])
            if self.title_func:
                item.title = self.title_func(page_item)
            else:
                item.title = str(i)
            self.add_item(item)
            i += 1


class Menu(object):
    """
    Class for traversing an hierarchical menu structure
    Args:
        root_menu: A special SubMenu that acts as the root menu
        cls: Clears the screen between menus
        menu: The current shown menu
    """
    def __init__(self, title, cls=True):
        self._root_menu = SubMenu(title)
        self._root_menu.parent = None
        self._cls = cls
        self._current_menu = self._root_menu

    def add_item(self, item, key=None):
        """Adds an item to the root menu"""
        self._root_menu.add_item(item, key)

    def reset(self):
        """Resets the current menu"""
        if isinstance(self._current_menu, DynMenu):
            self._current_menu.reset()

    def show(self):
        """Show the current menu"""
        if self._cls:
            os.system('cls')
        if not isinstance(self._current_menu, SubMenu):
            raise Exception("self.menu not SubMenu")

        if isinstance(self._current_menu, ListMenu):
            firstidx = self._current_menu.idx * self._current_menu.size
            first = firstidx + 1
            last = firstidx + self._current_menu.page_len()
            print("### %s [%s/%s] [%s-%s/%s] ###" % (
                self._current_menu.title, self._current_menu.idx + 1, self._current_menu.length, first, last, self._current_menu.listLength))
        elif isinstance(self._current_menu, PageMenu):
            print("### %s [%s/%s] ###" % (self._current_menu.title, self._current_menu.idx + 1, self._current_menu.length))
        else:
            print("### %s ###" % self._current_menu.title)

        if self._current_menu.has_msg():
            print("{%s}" % (self._current_menu.msg.upper()))

        print()  # newline after title

        if self._current_menu.text:
            print("ALL YOUR TEXT ARE BELONG TO US!!! (use TextItem instead)")  # everything is a textitem now
        itemcount = len(self._current_menu.nav_items())
        key_offset = 1
        if itemcount >= 100:
            key_offset = 3
        elif itemcount >= 10:
            key_offset = 2

        format_str = "{:%sd}: [{}]" % key_offset

        for item in self._current_menu.items:
            if type(item) is SpaceItem:
                print()
            elif type(item) is TextItem:
                print("%s" % item.label)
            else:
                if item.key.isdigit():
                    print(format_str.format(int(item.key), item.label))
                    # print("%s%s: [%s]" % (' '*(key_offset - len(item.key) - 1),item.key,item.label))
                else:
                    print("%s: [%s]" % (item.key, item.label))

        print()
        if isinstance(self._current_menu, PageMenu):
            print("p: [Previous]")
            print("n: [Next]")
            print("o: [Go to page]\n")
        if isinstance(self._current_menu, DynMenu) and self._current_menu.updatable:
            print("u: [Update]\n")

        if self._current_menu.parent is not None:
            print("-: [Back]")

        print("0: [Home]")
        print("q: [Quit]")

    def read(self):
        """Waits for user input"""
        ans = input('\n#?: ')
        print()
        print()  # create space if cls = False

        # Navigation
        if ans == 'q':
            self._current_menu = None
            return
        elif ans == '0':
            self._current_menu = self._root_menu
            return
        elif ans == '-':
            self._current_menu = self._current_menu.parent
            self.reset()
            return
        elif ans == 'u' and isinstance(self._current_menu, DynMenu) and self._current_menu.updatable:
            self.reset()
            return
        elif isinstance(self._current_menu, PageMenu):
            if ans == 'p':
                if self._current_menu.idx > 0:
                    self._current_menu.idx -= 1
                self.reset()
                return
            elif ans == 'n':
                if self._current_menu.idx < len(self._current_menu.pages) - 1:
                    self._current_menu.idx += 1
                self.reset()
                return
            elif ans == 'o':
                pi = input('#num?: ')
                if pi.isdigit():
                    pi = int(pi)
                    if 0 < pi <= len(self._current_menu.pages):
                        self._current_menu.idx = pi - 1
                self.reset()
                return

        # Selection
        item = None

        if type(self._current_menu) is SearchMenu:
            item = self._current_menu.search(ans)
            if not item:  # return None if search failed
                return
        else:
            nav_items = self._current_menu.nav_items()
            if ans in [i.key for i in nav_items]:
                for i in nav_items:
                    if i.key == ans:
                        item = i
                        break
            else:
                return

        if type(item) is ActionItem:
            item.action()
            self.reset()
            self._current_menu.msg = item.msg

            # Maybe change current menu
        if item is not None:
            if type(item) is SubMenu:
                self._current_menu = item
            elif isinstance(item, DynMenu):
                self._current_menu = item
                self.reset()
                return

    def run(self):
        """Main loop that shows the current menu and waits for input"""
        run = True
        while run:
            self.show()
            self.read()
            if self._current_menu is None:
                run = False

"""
Python Menu class
<NAME> 2019
"""

# TODO
# -reserved nav keys (eg. p,n,u,-,0)

import os

__version__ = '190923.1'


class MenuItem:
    """The base class of all menu items
    Args:
        label (str): Text shown on the menu item
    """
    def __init__(self, label):
        self.label = label


class NoNavItem(MenuItem):  # Item outside of navigation
    """Base class for all classes that are outside of navigation"""
    pass


class SpaceItem(NoNavItem):
    """Just adds an empty row in the list of items"""
    def __init__(self):
        super().__init__('')


class TextItem(NoNavItem):
    """A simple text label in the menu
    Args:
        label (str): Text shown on the menu item
    """
    def __init__(self, label):
        super().__init__(label)


class NavItem(MenuItem):
    """Base class for all navigationitems
    Args:
        label (str): Text shown on the menu item
    Attributes:
        key (str): Shortcut for keyboard navigation
    """
    def __init__(self, label):
        super().__init__(label)
        self.key = ''
        self._msg = ''

    def has_msg(self):
        """Check if there is a message to show"""
        return self._msg != ''

    @property
    def msg(self):
        msg = self._msg
        self._msg = ''
        return msg

    @msg.setter
    def msg(self, msg):
        self._msg = msg


class ActionItem(NavItem):
    """Item calling a function with args and kwargs
    Attributes:
        msg (str): optional msg from the function
        action_func: reference to function to be called
    """
    def __init__(self, label, action_func, *args, **kwargs):
        super().__init__(label)
        self.action_func = action_func
        self.args = args
        self.kwargs = kwargs

    def action(self):
        self.action_func(self, *self.args, **self.kwargs)


class SubMenu(NavItem):
    """Base class for the hierarchical menu data model
    Args:
        label (str): Text shown on the menu item
        title (str): Text shown as title above the menu text
        text (str): Text show at the top of the menu
    Attributes:
        items (SubMenu): A list of child SubMenu items

    """
    def __init__(self, label, title='', text=''):
        super().__init__(label)
        self.itemsCount = 1
        self.items = []
        self.title = title if title else label
        self.text = text  # TODO replace with TextItem only
        self.parent = None

    def add_space_item(self):
        self.items.append(SpaceItem())

    def add_text_item(self, text, space_above=False):
        if space_above:  # It's common to add space above titles and labels
            self.add_space_item()
        self.items.append(TextItem(text))

    def add_item(self, item, key=''):
        if not hasattr(item, 'parent') or not item.parent:
            item.parent = self

        if isinstance(item, NoNavItem):
            self.items.append(item)
        elif key:
            item.key = key
            self.items.append(item)
        else:
            item.key = str(self.itemsCount)
            self.items.append(item)
            self.itemsCount += 1

    def clear_items(self):
        self.itemsCount = 1
        self.items = []

    def nav_items(self):
        return [i for i in self.items if isinstance(i, NavItem)]


class DynMenu(SubMenu):
    """A dynamic menu generated by a function"""
    def __init__(self, label, init_func, *args, **kwargs):
        super(DynMenu, self).__init__(label)
        self.init_func = init_func
        self.updatable = False
        self.args = args
        self.kwargs = kwargs

    def init_menu(self):
        """Call init_func with referenc do self, args and kwargs"""
        self.init_func(self, *self.args, **self.kwargs)

    def reset(self):
        """Clear all items and call init_func again"""
        self.clear_items()
        self.text = ''
        self.init_menu()


class SearchMenu(DynMenu):
    """A dynamic menu generated from a function by passing a parameter"""
    def __init__(self, label, search_func, *args, **kwargs):
        super(SearchMenu, self).__init__(label, None, *args, **kwargs)

        self.searchFunc = search_func

    def init_menu(self):
        pass

    def search(self, ans):
        return self.searchFunc(self, ans, *self.args, **self.kwargs)


class PageMenu(DynMenu):
    """Dynamic menu split upp into pages with prev/next navigation"""
    def __init__(self, label, item_init, idx=0, pages=None, title_func=None, *args, **kwargs):
        super(PageMenu, self).__init__(label, None, *args, **kwargs)
        if pages is None:
            pages = []
        self.pages = pages
        self.idx = idx
        self.item_init = item_init
        self.length = len(pages)
        self.title_func = title_func

    # override DynMenu init
    def init_menu(self):
        # call init for listitem
        page = self.pages[self.idx]
        if self.title_func:
            self.title = self.title_func(page)
        else:
            self.title = self.label
        self.item_init(self, page, *self.args, **self.kwargs)


class ListMenu(PageMenu):
    """Generete a PageMenu from a list of items and a page size"""
    def __init__(self, label, item_init, list_items, size=15, label_func=None, title_func=None, *args, **kwargs):
        pages = []
        for i in range(0, len(list_items), size):
            pages.append(list_items[i:i + size])
        super(ListMenu, self).__init__(label, None, 0, pages, *args, **kwargs)
        self.list = list_items
        self.size = size
        self.listLength = len(list_items)
        self.item_init = item_init
        self.label_func = label_func
        self.title_func = title_func

    def page_len(self):
        return len(self.pages[self.idx])

    def init_menu(self):
        page = self.pages[self.idx]  # list of items
        i = 0
        for page_item in page:
            # lbl = ''
            if self.label_func:
                lbl = self.label_func(page_item)
            else:
                lbl = str(page_item)
            idx = self.idx * self.size + i
            item = DynMenu(lbl, self.item_init, self.list[idx])
            if self.title_func:
                item.title = self.title_func(page_item)
            else:
                item.title = str(i)
            self.add_item(item)
            i += 1


class Menu(object):
    """
    Class for traversing an hierarchical menu structure
    Args:
        root_menu: A special SubMenu that acts as the root menu
        cls: Clears the screen between menus
        menu: The current shown menu
    """
    def __init__(self, title, cls=True):
        self._root_menu = SubMenu(title)
        self._root_menu.parent = None
        self._cls = cls
        self._current_menu = self._root_menu

    def add_item(self, item, key=None):
        """Adds an item to the root menu"""
        self._root_menu.add_item(item, key)

    def reset(self):
        """Resets the current menu"""
        if isinstance(self._current_menu, DynMenu):
            self._current_menu.reset()

    def show(self):
        """Show the current menu"""
        if self._cls:
            os.system('cls')
        if not isinstance(self._current_menu, SubMenu):
            raise Exception("self.menu not SubMenu")

        if isinstance(self._current_menu, ListMenu):
            firstidx = self._current_menu.idx * self._current_menu.size
            first = firstidx + 1
            last = firstidx + self._current_menu.page_len()
            print("### %s [%s/%s] [%s-%s/%s] ###" % (
                self._current_menu.title, self._current_menu.idx + 1, self._current_menu.length, first, last, self._current_menu.listLength))
        elif isinstance(self._current_menu, PageMenu):
            print("### %s [%s/%s] ###" % (self._current_menu.title, self._current_menu.idx + 1, self._current_menu.length))
        else:
            print("### %s ###" % self._current_menu.title)

        if self._current_menu.has_msg():
            print("{%s}" % (self._current_menu.msg.upper()))

        print()  # newline after title

        if self._current_menu.text:
            print("ALL YOUR TEXT ARE BELONG TO US!!! (use TextItem instead)")  # everything is a textitem now
        itemcount = len(self._current_menu.nav_items())
        key_offset = 1
        if itemcount >= 100:
            key_offset = 3
        elif itemcount >= 10:
            key_offset = 2

        format_str = "{:%sd}: [{}]" % key_offset

        for item in self._current_menu.items:
            if type(item) is SpaceItem:
                print()
            elif type(item) is TextItem:
                print("%s" % item.label)
            else:
                if item.key.isdigit():
                    print(format_str.format(int(item.key), item.label))
                    # print("%s%s: [%s]" % (' '*(key_offset - len(item.key) - 1),item.key,item.label))
                else:
                    print("%s: [%s]" % (item.key, item.label))

        print()
        if isinstance(self._current_menu, PageMenu):
            print("p: [Previous]")
            print("n: [Next]")
            print("o: [Go to page]\n")
        if isinstance(self._current_menu, DynMenu) and self._current_menu.updatable:
            print("u: [Update]\n")

        if self._current_menu.parent is not None:
            print("-: [Back]")

        print("0: [Home]")
        print("q: [Quit]")

    def read(self):
        """Waits for user input"""
        ans = input('\n#?: ')
        print()
        print()  # create space if cls = False

        # Navigation
        if ans == 'q':
            self._current_menu = None
            return
        elif ans == '0':
            self._current_menu = self._root_menu
            return
        elif ans == '-':
            self._current_menu = self._current_menu.parent
            self.reset()
            return
        elif ans == 'u' and isinstance(self._current_menu, DynMenu) and self._current_menu.updatable:
            self.reset()
            return
        elif isinstance(self._current_menu, PageMenu):
            if ans == 'p':
                if self._current_menu.idx > 0:
                    self._current_menu.idx -= 1
                self.reset()
                return
            elif ans == 'n':
                if self._current_menu.idx < len(self._current_menu.pages) - 1:
                    self._current_menu.idx += 1
                self.reset()
                return
            elif ans == 'o':
                pi = input('#num?: ')
                if pi.isdigit():
                    pi = int(pi)
                    if 0 < pi <= len(self._current_menu.pages):
                        self._current_menu.idx = pi - 1
                self.reset()
                return

        # Selection
        item = None

        if type(self._current_menu) is SearchMenu:
            item = self._current_menu.search(ans)
            if not item:  # return None if search failed
                return
        else:
            nav_items = self._current_menu.nav_items()
            if ans in [i.key for i in nav_items]:
                for i in nav_items:
                    if i.key == ans:
                        item = i
                        break
            else:
                return

        if type(item) is ActionItem:
            item.action()
            self.reset()
            self._current_menu.msg = item.msg

            # Maybe change current menu
        if item is not None:
            if type(item) is SubMenu:
                self._current_menu = item
            elif isinstance(item, DynMenu):
                self._current_menu = item
                self.reset()
                return

    def run(self):
        """Main loop that shows the current menu and waits for input"""
        run = True
        while run:
            self.show()
            self.read()
            if self._current_menu is None:
                run = False
Python Menu class <NAME> 2019 # TODO # -reserved nav keys (eg. p,n,u,-,0) The base class of all menu items     Args:         label (str): Text shown on the menu item # Item outside of navigation Base class for all classes that are outside of navigation Just adds an empty row in the list of items A simple text label in the menu     Args:         label (str): Text shown on the menu item Base class for all navigationitems     Args:         label (str): Text shown on the menu item     Attributes:         key (str): Shortcut for keyboard navigation Check if there is a message to show Item calling a function with args and kwargs     Attributes:         msg (str): optional msg from the function         action_func: reference to function to be called Base class for the hierarchical menu data model     Args:         label (str): Text shown on the menu item         title (str): Text shown as title above the menu text         text (str): Text show at the top of the menu     Attributes:         items (SubMenu): A list of child SubMenu items # TODO replace with TextItem only # It's common to add space above titles and labels A dynamic menu generated by a function Call init_func with referenc do self, args and kwargs Clear all items and call init_func again A dynamic menu generated from a function by passing a parameter Dynamic menu split upp into pages with prev/next navigation # override DynMenu init # call init for listitem Generete a PageMenu from a list of items and a page size # list of items # lbl = '' Class for traversing an hierarchical menu structure     Args:         root_menu: A special SubMenu that acts as the root menu         cls: Clears the screen between menus         menu: The current shown menu Adds an item to the root menu Resets the current menu Show the current menu ## %s [%s/%s] [%s-%s/%s] ###" % ( ## %s [%s/%s] ###" % (self._current_menu.title, self._current_menu.idx + 1, self._current_menu.length)) ## %s ###" % self._current_menu.title) # newline after title # everything is a textitem now # print("%s%s: [%s]" % (' '*(key_offset - len(item.key) - 1),item.key,item.label)) Waits for user input #?: ') # create space if cls = False # Navigation # Selection # return None if search failed # Maybe change current menu Main loop that shows the current menu and waits for input