scroll the whole graph tree

IMPORTANT: CURSOR_LEFT, CURSOR_RIGHT now slides the graphtree. The
           cannot be used to change focused pane anymore.

The main purpose of this feature is to see the graph when branches
overflow the viewport. so, scrolling only one line doesn't help.
2 files changed, 61 insertions(+), 13 deletions(-)

M lairucrem/controler.py
M lairucrem/mixin.py
M lairucrem/controler.py +2 -0
@@ 222,6 222,7 @@ class changesetwalker(
         '{node}', '{rev}', '{graphnode}', '{p1rev}', '{children}', config.TEMPLATE])
 
     class graph_line_widget(
+            mixin.horizontal_scroll_text,
             mixin.one_line_widget,
             urwid.Text):
         pass

          
@@ 448,6 449,7 @@ class descriptionwalker(_patchdetailwalk
 
 
 class graphlistbox(
+        mixin.horizontal_scroll_listbox,
         mixin.changeable_listbox,
         mixin.filterable_listbox,
         mixin.searchable_listbox,

          
M lairucrem/mixin.py +59 -13
@@ 5,6 5,7 @@ 
 """UI widgets for lairucrem."""
 
 import asyncio
+import itertools
 import re
 from functools import reduce
 from operator import or_

          
@@ 203,7 204,27 @@ class link(_key_to_signal):
     }
 
 
-class horizontal_scroll_text(modifiable_widget, _key_to_signal):
+class horizontal_scroll_text(modifiable_widget):
+    """Scroll horizontally one line text."""
+
+    _horizontal_offset = 0
+
+    def _calc_line_translation(self, text, maxcol):
+        trans = super()._calc_line_translation(text, maxcol)
+        if not self._horizontal_offset:
+            return trans
+        assert len(trans) == 1, 'horizontal_scroll_text works on on_line_text only.'
+        amount = self._horizontal_offset - 1
+        return ([shift_line(trans[0], -amount)])
+
+    def setup_horizontal_offset(self, size, offset):
+        (maxcol, *_dummy) = size
+        self._horizontal_offset = offset
+        self._modified()
+        return (maxcol + self._horizontal_offset - 1) >= len(self._text)
+
+
+class horizontal_scroll_listbox(modifiable_widget, _key_to_signal):
 
     signals = ['slideleft', 'slideright', 'modified']
     key_to_signal_map = {

          
@@ 211,6 232,8 @@ class horizontal_scroll_text(modifiable_
         config.CURSOR_RIGHT: 'slideright',
     }
     _horizontal_offset = 0
+    _horizontal_end_reached = False
+    _amount = 5
 
     def _connect_signals(self):
         urwid.signals.connect_signal(

          
@@ 219,23 242,46 @@ class horizontal_scroll_text(modifiable_
             self, 'slideright', self.__class__.slide_right)
 
     def slide_left(self, size):
-        self._horizontal_offset = max(0, self._horizontal_offset - 2)
+        # self._horizontal_offset = max(0, self._horizontal_offset - 2)
+        if self._horizontal_offset <= 0:
+            return
+        self._horizontal_end_reached = False
+        self._horizontal_offset -= self._amount
+        self._propagate_horizontal_offset(size)
         self._modified()
 
     def slide_right(self, size):
-        (maxcol, *dummy) = size
-        self._horizontal_offset = min((len(self.text) - maxcol) + 1, self._horizontal_offset + 2)
+        if self._horizontal_end_reached:
+            return
+        self._horizontal_offset += self._amount
+        self._propagate_horizontal_offset(size)
         self._modified()
 
-    def _calc_line_translation(self, text, maxcol):
-        trans = super()._calc_line_translation(text, maxcol)
-        x, y = calc_coords(text, trans, maxcol + self._horizontal_offset - 2)
-        # raise ValueError(x, y, trans, len(text), maxcol)
-        if x < 0:
-            trans = (trans[:y] + [shift_line(trans[y], -x)] + trans[y+1:])
-        elif x >= maxcol:
-            trans = (trans[:y] + [shift_line(trans[y], -(x-maxcol+1))] + trans[y+1:])
-        return trans
+    def render(self, size, focus=False):
+        self._propagate_horizontal_offset(size, focus=focus)
+        return super().render(size, focus=focus)
+
+    def _propagate_horizontal_offset(self, size, focus=False):
+        middle, top, bottom = self.calculate_visible(size, focus=focus)
+        if not middle:
+            return  # There's no widget
+        end_reached = True
+        _dummy, focus_widget, *_dummy = middle
+        try:
+            end_reached &= focus_widget.setup_horizontal_offset(
+                size, self._horizontal_offset
+            )
+        except AttributeError:
+            pass
+        visible_widgets = itertools.chain(top[1], bottom[1])
+        for widget, _dummy, _dummy in visible_widgets:
+            try:
+                end_reached &= widget.setup_horizontal_offset(
+                    size, self._horizontal_offset
+                )
+            except AttributeError:
+                pass
+        self._horizontal_end_reached = end_reached
 
 
 class filterable_listbox(_key_to_signal):