# HG changeset patch # User Ingo Weinzierl # Date 1292956443 -3600 # Node ID 780bfda9c583948838cd52db8e5596f275e7b65c # Parent f1012651979787d4510e93a1b5947b8ae01884e1 Initialized a branch to work on a tree-like project view. diff -r f10126519797 -r 780bfda9c583 ChangeLog --- a/ChangeLog Wed Dec 15 14:47:40 2010 +0100 +++ b/ChangeLog Tue Dec 21 19:34:03 2010 +0100 @@ -1,3 +1,16 @@ +2010-12-21 Ingo Weinzierl + + * getan.py: Added a method to sort the projects based on its key. + + * getan/project.py: A project can now have children. Therefore, a new + method add_child() has been implemented. Important to build up a tree is + the project's key. If we try to append a new child to a project, but + there is already a child with the key that starts just like the new + child's, we will try to append the new child to the existing one. + + * getan/view.py: The project view looks now like a tree. Projects can have + children, that are indented relative to its parent. + 2010-12-15 Sascha L. Teichmann Fix for #1638 diff -r f10126519797 -r 780bfda9c583 getan.py --- a/getan.py Wed Dec 15 14:47:40 2010 +0100 +++ b/getan.py Tue Dec 21 19:34:03 2010 +0100 @@ -20,6 +20,18 @@ logger = logging.getLogger() +def order_projects(projects): + projects = sorted(projects, key=lambda proj: proj.key) + ordered = {} + for p in projects: + c = p.key[0] + if c in ordered: + ordered[c].add_child(p.key[1:], p) + else: + ordered[c] = p + return ordered + + class GetanController: def __init__(self, backend, pv_class, ev_class): self.ev_class = ev_class @@ -33,7 +45,7 @@ self.running = [] self.backend = backend - self.project_view = pv_class(self, self.projects) + self.project_view = pv_class(self, order_projects(self.projects)) self.entries_view = ev_class(entries) self.view = GetanView(self, self.project_view, self.entries_view) diff -r f10126519797 -r 780bfda9c583 getan/project.py --- a/getan/project.py Wed Dec 15 14:47:40 2010 +0100 +++ b/getan/project.py Tue Dec 21 19:34:03 2010 +0100 @@ -8,18 +8,37 @@ # This is Free Software licensed unter the terms of GPLv3 or later. # For details see LICENSE coming with the source of 'getan'. +import logging + from datetime import datetime, timedelta +logger = logging.getLogger() + class Project: def __init__(self, id, key, desc, total): - self.id = id - self.key = key - self.desc = desc - self.entries = [] - self.total = total - self.start = None - self.stop = None + self.id = id + self.key = key + self.desc = desc + self.entries = [] + self.children = {} + self.total = total + self.start = None + self.stop = None + + def add_child(self, key, project): + if not key or not project: + return + logger.debug("Parent: %s\tAdd child: %s" % (self.key, project.key)) + if key in self.children: + self.children[key].add_child(key[1:], project) + else: + self.children[key] = project + + def dump(self): + logger.debug(self.key) + for k, p in self.children.items(): + p.dump() def year(self): total = 0 diff -r f10126519797 -r 780bfda9c583 getan/states.py --- a/getan/states.py Wed Dec 15 14:47:40 2010 +0100 +++ b/getan/states.py Tue Dec 21 19:34:03 2010 +0100 @@ -37,6 +37,7 @@ return None if 'tab' in key: + if self.view.item_in_focus().children: return None self.controller.entries_view.focused = 0 self.controller.entries_view.update_focus(0) return DefaultEntryListState(self, self.controller, diff -r f10126519797 -r 780bfda9c583 getan/view.py --- a/getan/view.py Wed Dec 15 14:47:40 2010 +0100 +++ b/getan/view.py Tue Dec 21 19:34:03 2010 +0100 @@ -19,24 +19,53 @@ logger = logging.getLogger() + +def create_project_tree(projects): + nodes = [] + for k in sorted(projects.keys()): + nodes.extend(create_project_node(projects[k], 0)) + return nodes + +def create_project_node(project, indent=0): + nodes = [ProjectNode(project, indent)] + for k, p in project.children.items(): + nodes.extend(create_project_node(p, indent+3)) + return nodes + class ListWidget(urwid.BoxWidget): def _update_view(self): logger.debug("ListWidget: update view now.") + if self.size and len(self.rows) > self.size[1]: + start_idx = self.top + end_idx = start_idx + self.size[1] - 6 + else: + start_idx = self.top + end_idx = start_idx + len(self.rows) + listbox = urwid.LineBox(urwid.Padding( + urwid.ListBox(urwid.SimpleListWalker(self.rows[start_idx: + end_idx])), + ('fixed left', 1), + ('fixed right', 1))) + + self.body = urwid.AttrWrap(listbox, 'body') + self.frame.set_body(self.body) self.frame.set_footer(self.footer) self.frame.set_header(self.header) def update_focus(self, focus, unfocus=-1): - logger.debug("ListWidget: focus row (index = %d)" % focus) - logger.debug("ListWidget: unfocus row (index = %d)" % unfocus) + self.focused = focus if focus >= 0 and focus <= len(self.rows)-1: self.rows[focus].focus = True self.rows[focus].update_w() if unfocus >= 0: self.rows[unfocus].focus = False self.rows[unfocus].update_w() + if self.size: + self.validate_view() def render(self, size, focus=False): + self.size = size maxcol, maxrow = size return self.frame.render((maxcol, maxrow), focus) @@ -44,11 +73,10 @@ if edit: logger.debug("ListWidget: set footer text (edit) = '%s'" % text) self.footer = urwid.AttrWrap(urwid.Edit(text),attr) - self._update_view() else: logger.debug("ListWidget: set footer text = '%s'" % text) self.footer = urwid.AttrWrap(urwid.Text(text),attr) - self._update_view() + self._update_view() def row_count(self): if not self.rows: return 0 @@ -60,22 +88,31 @@ return None def up(self): - logger.debug("ListWidget: navigate to upper row.") if self.focused > 0: - self.focused = self.focused - 1 - self.update_focus(self.focused, self.focused+1) + logger.debug("ListWidget: navigate to upper row.") + self.update_focus(self.focused-1, self.focused) def down(self): - logger.debug("ListWidget: navigate to lower row.") if self.focused < len(self.rows) - 1: - self.focused = self.focused + 1 - self.update_focus(self.focused, self.focused-1) + logger.debug("ListWidget: navigate to lower row.") + self.update_focus(self.focused+1, self.focused) + + def validate_view(self): + if self.focused < self.top: + self.top = self.focused + self._update_view() + if self.focused + 8 >= self.top + self.size[1]: + self.top = self.focused - (self.size[1]) + 9 + if self.top < 0: + self.top = 0 + self._update_view() def select(self): if not self.rows: return None node = self.rows[self.focused] logger.debug("ListWidget: select row '%s'" % self.focused) - node.select() + selected = node.select() + if not selected: return None if node.selected: self.selection.append(node) else: @@ -101,9 +138,10 @@ (4, _('Day')) ] - def __init__(self, proj, mode=3): + def __init__(self, proj, indent=0, mode=3): self.selected = False self.focus = False + self.indent = indent self.mode = self.MODES[mode] self.item = proj w = self.update() @@ -113,8 +151,14 @@ def update(self): logger.debug("Update ProjectNode '%s'" % self.item.desc) time_str = self._get_formatted_time() - description = urwid.Text('%s %s' % (self.item.key, self.item.desc)) - time = urwid.Text('%s (%s)' % (self.mode[1], time_str)) + description = urwid.Text([' ' * self.indent, + ('project_key',self.item.key), + (' '), + (self.item.desc)]) + if self.selectable(): + time = urwid.Text('%s (%s)' % (self.mode[1], time_str)) + else: + time = urwid.Text('') self.widget = urwid.AttrWrap(urwid.Columns([description, time]),None) self._w = self.widget self.update_w() @@ -161,22 +205,30 @@ self._w.focus_attr = 'body' self._w.attr = 'body' + def selectable(self): + return not self.item.children + def select(self): - self.selected = not self.selected - self.update_w() + if self.selectable(): + self.selected = not self.selected + self.update_w() + return True + return False class ProjectList(ListWidget): def __init__(self, controller, rows): self.selection = [] self.focused = 0 + self.size = () + self.top = 0 self.controller = controller - self.raw_projects = rows + self.raw_rows = rows self.header = urwid.LineBox(urwid.AttrWrap(urwid.Text("\n%s\n" % _('List of registered projects')),'project_header')) self.footer = urwid.Edit() - self.rows = [ProjectNode(x) for x in rows] + self.rows = create_project_tree(rows) self.listbox = urwid.ListBox(urwid.SimpleListWalker(self.rows)) self.body = urwid.LineBox(urwid.Padding(urwid.AttrWrap( self.listbox, 'entries'),('fixed left',1),('fixed right',1))) @@ -193,9 +245,9 @@ tmp = proj._get_time() if tmp and type(tmp) == int: total += tmp - self.frame.set_footer(urwid.AttrWrap( - urwid.Text(_(' All projects: %s %s') - % (proj.mode[1],human_time(total))), 'project_footer')) + self.set_footer_text(_(' All projects: %s %s') + % (proj.mode[1],human_time(total)), + 'project_footer') def update(self): logger.debug("ProjectList: update focused project row now.") @@ -240,7 +292,6 @@ self.update_w() def update(self): - logger.debug("EntryNode: update entry '%s'." % self.item.desc) row = urwid.Text(' %s [%s] %s' \ % (format_datetime(self.item.start), short_time(self.item.duration().seconds), @@ -271,11 +322,14 @@ % self.item.desc) self.selected = not self.selected self.update_w() + return True class EntryList(ListWidget): def __init__(self, rows): self.selection = [] + self.top = 0 + self.size = () self.focused = 0 self.rows = [EntryNode(x) for x in rows] listbox = urwid.ListBox(urwid.SimpleListWalker(self.rows)) @@ -290,11 +344,7 @@ footer=self.footer) def set_rows(self, rows): - logger.debug("EntryList: set new entries.") self.rows = [EntryNode(x) for x in rows] - listbox = urwid.LineBox(urwid.ListBox(urwid.SimpleListWalker( - self.rows))) - self.body = urwid.AttrWrap(listbox, 'entry_body') self._update_view() @@ -306,6 +356,7 @@ ('footer', 'yellow', 'dark blue'), ('entry_footer', 'white', 'dark blue'), ('project_footer', 'white', 'dark blue'), + ('project_key', 'black', 'dark cyan'), ('body', 'white', 'black'), ('entry body', 'white', 'dark blue'), ('entries', 'white', 'black'),