From b00907546786c2d5ba95fdc13c32b8578642eb92 Mon Sep 17 00:00:00 2001
From: "Cody A.W. Somerville" <cody.somerville@gmail.com>
Date: Thu, 3 Mar 2016 23:46:18 -0500
Subject: [PATCH] Add navigation breadcrumb footer

This change adds a navigational breadcrumb trail to the bottom of the interface
to allow the user to easily keep track of their location in Gertty.

Change-Id: I40de19084bf66d095c14ac7a4a901f293b30b9be
---
 gertty/app.py     | 67 ++++++++++++++++++++++++++++++++++++++---------
 gertty/palette.py |  1 +
 2 files changed, 55 insertions(+), 13 deletions(-)

diff --git a/gertty/app.py b/gertty/app.py
index 3c10abb..1794d10 100644
--- a/gertty/app.py
+++ b/gertty/app.py
@@ -16,6 +16,7 @@
 import argparse
 import datetime
 import dateutil
+import functools
 import logging
 import os
 import re
@@ -135,6 +136,41 @@ class StatusHeader(urwid.WidgetWrap):
             self.sync_widget.set_text(u' Sync: %i' % self._sync)
 
 
+class BreadCrumbBar(urwid.WidgetWrap):
+    BREADCRUMB_SYMBOL = u'\N{BLACK RIGHT-POINTING SMALL TRIANGLE}'
+    BREADCRUMB_WIDTH = 25
+
+    def __init__(self):
+        self.prefix_text = urwid.Text(u' \N{WATCH}  ')
+        self.breadcrumbs = urwid.Columns([], dividechars=3)
+        self.display_widget = urwid.Columns(
+            [('pack', self.prefix_text), self.breadcrumbs])
+        super(BreadCrumbBar, self).__init__(self.display_widget)
+
+    def _get_breadcrumb_text(self, screen):
+        title = getattr(screen, 'title', str(screen))
+        text = "%s %s" % (BreadCrumbBar.BREADCRUMB_SYMBOL, title)
+        if len(text) > 20:
+            text = "%s..." % text[:20]
+        return urwid.Text(text, wrap='clip')
+
+    def _get_breadcrumb_column_options(self):
+        return self.breadcrumbs.options("given", BreadCrumbBar.BREADCRUMB_WIDTH)
+
+    def _update(self, screens):
+        breadcrumb_contents = []
+        for screen in screens:
+            breadcrumb_contents.append((
+                self._get_breadcrumb_text(screen),
+                self._get_breadcrumb_column_options()))
+        self.breadcrumbs.contents = breadcrumb_contents
+        # Update focus so we always have the right end of the breadcrumb trail
+        # in view. Urwid will gracefully handle clipping from the left when
+        # there is overflow.as trail grows, shrinks, or screen is resized.
+        if len(self.breadcrumbs.contents):
+            self.breadcrumbs.focus_position = len(self.breadcrumbs.contents) - 1
+
+
 class SearchDialog(mywid.ButtonDialog):
     signals = ['search', 'cancel']
     def __init__(self, app, default):
@@ -247,13 +283,18 @@ class App(object):
         self.db = db.Database(self, self.config.dburi, self.search)
         self.sync = sync.Sync(self, disable_background_sync)
 
-        self.screens = []
         self.status = StatusHeader(self)
         self.header = urwid.AttrMap(self.status, 'header')
+        self.screens = urwid.MonitoredList()
+        self.breadcrumbs = BreadCrumbBar()
+        self.screens.set_modified_callback(
+            functools.partial(self.breadcrumbs._update, self.screens))
+        self.footer = urwid.AttrMap(self.breadcrumbs, 'footer')
         screen = view_project_list.ProjectListView(self)
         self.status.update(title=screen.title)
         self.updateStatusQueries()
-        self.loop = urwid.MainLoop(screen, palette=self.config.palette.getPalette(),
+        self.frame = urwid.Frame(body=screen, footer=self.footer)
+        self.loop = urwid.MainLoop(self.frame, palette=self.config.palette.getPalette(),
                                    handle_mouse=self.config.handle_mouse,
                                    unhandled_input=self.unhandledInput)
 
@@ -342,9 +383,9 @@ class App(object):
         self.log.debug("Changing screen to %s" % (widget,))
         self.status.update(error=False, title=widget.title)
         if push:
-            self.screens.append(self.loop.widget)
+            self.screens.append(self.frame.body)
         self.clearInputBuffer()
-        self.loop.widget = widget
+        self.frame.body = widget
 
     def backScreen(self, target_widget=None):
         if not self.screens:
@@ -357,7 +398,7 @@ class App(object):
         if hasattr(widget, 'title'):
             self.status.update(title=widget.title)
         self.clearInputBuffer()
-        self.loop.widget = widget
+        self.frame.body = widget
         self.refresh(force=True)
 
     def findChangeList(self):
@@ -371,10 +412,10 @@ class App(object):
         while self.screens:
             widget = self.screens.pop()
             self.clearInputBuffer()
-            self.loop.widget = widget
+            self.frame.body = widget
 
     def refresh(self, data=None, force=False):
-        widget = self.loop.widget
+        widget = self.frame.body
         while isinstance(widget, urwid.Overlay):
             widget = widget.contents[0][0]
         interested = force
@@ -408,23 +449,23 @@ class App(object):
             width = ('relative', relative_width)
         if height is None:
             height = ('relative', relative_height)
-        overlay = urwid.Overlay(widget, self.loop.widget,
+        overlay = urwid.Overlay(widget, self.frame.body,
                                 'center', width,
                                 'middle', height,
                                 min_width=min_width, min_height=min_height)
-        self.log.debug("Overlaying %s on screen %s" % (widget, self.loop.widget))
-        self.screens.append(self.loop.widget)
-        self.loop.widget = overlay
+        self.log.debug("Overlaying %s on screen %s" % (widget, self.frame.body))
+        self.screens.append(self.frame.body)
+        self.frame.body = overlay
 
     def help(self):
-        if not hasattr(self.loop.widget, 'help'):
+        if not hasattr(self.frame.body, 'help'):
             return
         global_help = [(self.config.keymap.formatKeys(k), t)
                        for (k, t) in mywid.GLOBAL_HELP]
         for d in self.config.dashboards.values():
             global_help.append((keymap.formatKey(d['key']), d['name']))
         parts = [('Global Keys', global_help),
-                 ('This Screen', self.loop.widget.help())]
+                 ('This Screen', self.frame.body.help())]
         keylen = 0
         for title, items in parts:
             for keys, text in items:
diff --git a/gertty/palette.py b/gertty/palette.py
index 0313d4b..645f7de 100644
--- a/gertty/palette.py
+++ b/gertty/palette.py
@@ -29,6 +29,7 @@ DEFAULT_PALETTE={
     'focused-min-label': ['light red,standout', ''],
     'link': ['dark blue', ''],
     'focused-link': ['light blue', ''],
+    'footer': ['light gray', 'dark gray'],
     # Diff
     'context-button': ['dark magenta', ''],
     'focused-context-button': ['light magenta', ''],