view getan/states.py @ 151:1e35c24708dd

Allow to configure the action keys Use new config and keybinding classes to allow to configure the action keys in the states. This change decouples the actual action keys from the state transtitions.
author Björn Ricks <bjoern.ricks@intevation.de>
date Thu, 06 Dec 2012 12:30:27 +0100
parents 7ab5b887a7c5
children 7ffcd2ea92e3
line wrap: on
line source
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# (c) 2010 by Ingo Weinzierl <ingo.weinzierl@intevation.de>
# (c) 2011, 2012 by Björn Ricks <bjoern.ricks@intevation.de>
#
# This is Free Software licensed under the terms of GPLv3 or later.
# For details see LICENSE coming with the source of 'getan'.
#


import locale
import logging
import signal
from   datetime import datetime, timedelta

from getan.resources import gettext as _
from getan.utils     import human_time, safe_int

logger = logging.getLogger()

class State(object):
    messages = {
    }

    def __init__(self, controller, view):
        self.controller = controller
        self.view       = view
        self.config = controller.get_config()

    def msg(self, key):
        return self.messages[key]


class ProjectState(State):
    def keypress(self, key):
        keys = self.config.get_keybinding()
        logger.debug("ProjectState: handle key '%r'" % key)
        if keys.get_switch_time_mode() in key:
            self.view.switch_time_mode()
            return self

        if keys.get_switch_project_order() in key:
            self.view.switch_project_order()
            return self

        if keys.get_switch_lists() in key:
            if not self.controller.entries_view.rows:
                return self
            self.controller.entries_view.focused = 0
            self.controller.entries_view.update_focus(0)
            return DefaultEntryListState(self, self.controller,
                                  self.controller.entries_view)

        if keys.get_entry_up() in key:
            return self.up()

        if keys.get_entry_down() in key:
            return self.down()

    def up(self):
        self.view.up()
        self.controller.update_entries(self.view.item_in_focus())
        return self

    def down(self):
        self.view.down()
        self.controller.update_entries(self.view.item_in_focus())
        return self

class PausedProjectsState(ProjectState):
    messages = {
        'choose_proj': _('Choose a project: '),
    }

    def keypress(self, key):
        logger.debug("PausedProjectsState: handle key '%r'" % key)
        keys = self.config.get_keybinding()
        ret = super(PausedProjectsState, self).keypress(key)
        if ret:
            return ret

        if keys.get_enter() in key:
            return self.select()

        if keys.get_insert() in key:
            return AddProjectNameState(self.controller, self.view)

        if keys.get_escape() in key:
            return ExitState(self.controller, self.view)

        else:
            if len(key) > 0 and len(key[0]) == 1:
                select_proj = SelectProjectState(self.controller, self.view, key[0])
                return select_proj.check_key()
        return self


    def select(self):
        proj = self.view.item_in_focus()
        self.controller.start_project(proj)
        return RunningProjectsState(self.controller, self.view, proj)

class SelectProjectState(State):

    def __init__(self, controller, view, key):
        super(SelectProjectState, self).__init__(controller, view)
        self.proj_keys = key
        self.set_footer_text()

    def reset(self):
        self.view.set_footer_text("", "entry_footer")

    def set_footer_text(self):
        self.view.set_footer_text("Selecting project from key: %s" % self.proj_keys, "running")

    def check_key(self):
        proj = self.controller.project_by_key(self.proj_keys)
        if proj:
            self.reset()
            self.view.select_project(proj)
            self.controller.start_project(self.view.item_in_focus())
            self.controller.update_entries(
                    self.view.item_in_focus())
            return RunningProjectsState(self.controller, self.view, proj)
        return self

    def keypress(self, key):
        keys = self.config.get_keybinding()
        if keys.get_escape() in key:
            self.reset()
            return PausedProjectsState(self.controller, self.view)

        if 'backspace' in key:
            if len(self.proj_keys) > 0:
                self.proj_keys = self.proj_keys[:-1]
                self.set_footer_text()
            return self

        else:
            if len(key) > 0 and len(key[0]) == 1:
                self.proj_keys += key[0]
                self.set_footer_text()
                return self.check_key()
        return self

class ExitState(ProjectState):
    messages = {
        'quit'  : _(" Really quit? (y/n)"),
        'choose': _(" Choose a project:")
    }

    def __init__(self, controller, view):
        super(ExitState, self).__init__(controller, view)
        self.controller.view.set_footer_text(self.msg('quit'), 'question')

    def keypress(self, key):
        logger.debug("ExitState: handle key '%r'" % key)
        ret = super(ExitState, self).keypress(key)
        if ret:
            return ret

        if 'y' in key or 'Y' in key:
            self.controller.exit()

        if 'n' in key or 'N' in key:
            self.controller.view.set_footer_text(self.msg('choose'), 'question')
            return PausedProjectsState(self.controller, self.view)

        return self


class RunningProjectsState(ProjectState):
    messages = {
        'description': _("Enter a description: "),
        'add_time'   : _("Enter time to add [min]: "),
        'min_time'   : _("Enter time to subtract [min]: "),
        'continue'   : _("Press 'Space' to continue."),
        'running'    : _("Running ( %s ) on '%s'."),
        'paused'     : _(" Break   ( %s )%s."),
    }

    sec         = 0
    break_start = None

    def __init__(self, controller, view, project):
        super(RunningProjectsState, self).__init__(controller, view)
        self.project = project
        signal.signal(signal.SIGALRM, self.handle_signal)
        signal.alarm(1)

    def handle_signal(self, signum, frame):
        proj = self.project
        keys = self.config.get_keybinding()
        if not proj:
            return
        if not self.break_start:
            self.controller.view.set_footer_text(self.msg('running') %
                                                 (human_time(self.sec),
                                                  proj.desc),
                                                 'running')
            self.controller.view.loop.draw_screen()
            self.sec = self.sec + 1
        else:
            self.view.set_footer_text(
                self.msg('paused') %
                (human_time((datetime.now()-self.break_start).seconds),
                 self.msg('continue')),
                'paused_running')
            self.controller.view.loop.draw_screen()

        signal.signal(signal.SIGALRM, self.handle_signal)
        signal.alarm(1)

    def keypress(self, key):
        logger.debug("RunningProjectsState: handle key '%r'" % key)
        keys = self.config.get_keybinding()
        ret = super(RunningProjectsState, self).keypress(key)
        if ret:
            return ret

        if keys.get_enter() in key:
            return self.stop()
        if keys.get_add_time() in key:
            self.view.set_footer_text(self.msg('add_time'),
                                               'question', 1)
            self.view.frame.set_focus('footer')
            return AddTimeState(self.controller, self.view, self)
        if keys.get_subtract_time() in key:
            self.view.set_footer_text(self.msg('min_time'),
                                                 'question', 1)
            self.view.frame.set_focus('footer')
            return SubtractTimeState(self.controller, self.view, self)

        if keys.get_pause_project() in key:
            if not self.break_start:
                self.break_start = datetime.now()
                return self
            else:
                self.view._total_time()
                proj = self.project
                if proj:
                    proj.start += datetime.now() - self.break_start
                    self.break_start = None
                    signal.signal(signal.SIGALRM, self.handle_signal)
                    signal.alarm(1)
        return self

    def stop(self):
        signal.alarm(0)
        self.view.select()
        if self.break_start:
            proj = self.project
            if proj: proj.start += datetime.now() - self.break_start
        self.controller.view.set_footer_text(self.msg('description'),'question',1)
        self.controller.view.get_frame().set_focus('footer')
        return DescriptionProjectsState(
            self.controller, self.view, self,
            self.controller.view.get_frame().get_footer())


class HandleUserInputState(State):
    def __init__(self, controller, view, state, footer):
        super(HandleUserInputState, self).__init__(controller, view)
        self.state      = state
        self.footer     = footer

    def keypress(self, key):
        logger.debug("HandleUserInputState: handle key '%r'" % key)
        pos = self.footer.edit_pos
        keys = self.config.get_keybinding()

        if keys.get_escape() in key:
            return self.exit()
        elif keys.get_enter() in key:
            return self.enter()
        elif 'left' in key:
            self.footer.set_edit_pos(pos-1)
        elif 'right' in key:
            self.footer.set_edit_pos(pos+1)
        elif 'backspace' in key:
            text = self.footer.edit_text
            self.footer.set_edit_text(
                '%s%s' % (text[0:pos-1], text[pos:len(text)]))
            self.footer.set_edit_pos(pos-1)
        elif 'delete' in key:
            text = self.footer.edit_text
            self.footer.set_edit_text(
                '%s%s' % (text[0:pos], text[pos+1:len(text)]))
            self.footer.set_edit_pos(pos)
        elif len(key) >= 1 and len(key[0]) == 1:
            return self.insert(key)
        return self

    def enter(self):
        raise Exception("Not implemented")

    def exit(self):
        return self.state

    def insert(self, key):
        logger.debug("Enter key: %r" % key)
        text = "".join(key)
        # check for unicode here
        # urwid (at least up to version 1.0.2) will crash if a non-unicode
        # string with a char > 128 is passed here
        if not isinstance(text, unicode):
            text = unicode(text, locale.getpreferredencoding())
        self.footer.insert_text(text)
        return self


class BaseTimeState(HandleUserInputState):
    def __init__(self, controller, view, running_state):
        super(BaseTimeState, self).__init__(controller, view, running_state,
                                           view.frame.get_footer())
        self.project = running_state.project

    def exit(self):
        self.view._total_time()
        return self.state

    def insert(self, key):
        if key[0] in ['0','1','2','3','4','5','6','7','8','9']:
            self.footer.insert_text(key[0])
        else:
            logger.debug("BaseTimeState: invalid character for "\
                         "adding/subtracting time: '%r'" % key)
        return self


class AddTimeState(BaseTimeState):
    def enter(self):
        minutes         = safe_int(self.view.frame.get_footer().get_edit_text())
        project         = self.project
        project.start  -= timedelta(minutes=minutes)
        self.state.sec += minutes * 60
        logger.info("AddTimeState: add %d minutes to project '%s'"
                    % (minutes, project.desc))
        self.view._total_time()
        return self.state


class SubtractTimeState(BaseTimeState):
    def enter(self):
        minutes         = safe_int(self.view.frame.get_footer().get_edit_text())
        sec = minutes * 60
        if sec > self.state.sec:
            self.view._total_time()
            return self.state
        project         = self.project
        project.start  += timedelta(minutes=minutes)
        self.state.sec -= sec
        logger.info("SubtractTimeState: subtract %d minutes from project '%s'"
                    % (minutes, project.desc))
        self.view._total_time()
        return self.state


class DescriptionProjectsState(HandleUserInputState):
    messages = {
        'choose_proj': _(" Choose a project."),
    }

    def enter(self):
        text = self.footer.get_edit_text()
        if text == '':
            return self
        self.controller.stop_project(text)
        self.controller.view.set_footer_text(self.msg('choose_proj'), 'question')
        return PausedProjectsState(self.controller, self.view)

    def exit(self):
        project = self.view.item_in_focus()
        if project:
            time = (datetime.now() - project.start).seconds
            self.state.sec = time
            signal.signal(signal.SIGALRM, self.state.handle_signal)
            signal.alarm(1)
        return self.state


class EntryListState(State):
    def __init__(self, state, controller, view):
        super(EntryListState, self).__init__(controller, view)
        self.projectlist_state = state

    def keypress(self, key):
        logger.debug("EntryListState: pressed key '%r'" % key)
        keys = self.config.get_keybinding()
        if keys.get_switch_lists() in key:
            self.view.clear()
            return self.projectlist_state
        if keys.get_entry_up() in key:
            return self.up()
        if keys.get_entry_down() in key:
            return self.down()
        if keys.get_enter() in key:
            return self.select()
        return None

    def up(self):
        self.view.up()
        return self

    def down(self):
        self.view.down()
        return self

    def select(self):
        self.view.select()
        return self

    def renew_focus(self):
        e_len = self.view.row_count()
        f     = self.view.focused
        if f >= e_len:
            f = e_len - 1
        self.view.focused = f
        self.view.update_focus(f)


class DefaultEntryListState(EntryListState):
    def keypress(self, key):
        ret = super(DefaultEntryListState, self).keypress(key)
        if ret:
            return ret

        keys = self.config.get_keybinding()
        if keys.get_escape() in key:
            self.view.clear()
            return self.projectlist_state

        if keys.get_entry_delete() in key:
            if self.view.selection:
                return DeleteEntryState(self.projectlist_state,
                                        self.controller, self.view)
            else:
                entry = self.view.item_in_focus()
                return DeleteEntryState(self.projectlist_state,
                                        self.controller, self.view, [entry])
        if keys.get_entry_move() in key:
            if self.view.selection:
                return MoveEntryState(self.projectlist_state,
                                      self.controller, self.view)
        if keys.get_entry_edit() in key:
            entry = self.view.item_in_focus()
            if entry:
                return EditEntryState(self.projectlist_state,
                                      self.controller, self.view, entry)
        return self


class DeleteEntryState(EntryListState):
    messages = {
        'delete'  : _("Really delete this entry? (y/n)"),
    }

    def __init__(self, state, controller, view, entries=None):
        super(DeleteEntryState, self).__init__(state, controller, view)
        self.view.set_footer_text(self.msg('delete'), 'question')
        self.entries = entries
        if not self.entries:
            self.entries = [x.item for x in self.view.selection]

    def keypress(self, key):
        if 'y' in key:
            if self.entries:
                self.controller.delete_entries(self.entries)
                self.renew_focus()
            self.view.set_footer_text("", 'entry_footer')
            return DefaultEntryListState(self.projectlist_state,
                                         self.controller, self.view)

        if 'n' in key:
            self.view.set_footer_text("", 'entry_footer')
            return DefaultEntryListState(self.projectlist_state,
                                         self.controller, self.view)

        return self


class MoveEntryState(EntryListState):
    messages = {
        'project': _(" Into which project do you want to move these entries?"),
        'really':  _(" Are you sure? (y/n)"),
    }

    proj = None

    def __init__(self, state, controller, view):
        super(MoveEntryState, self).__init__(state, controller, view)
        self.view.set_footer_text(self.msg('project'), 'question')
        self.proj_keys = ""
        self.project_view = controller.project_view

    def set_project_footer(self):
        self.project_view.set_footer_text("Selecting project from " \
           "key: %s" % self.proj_keys, "running")

    def reset_project_footer(self):
        self.project_view.reset_footer()

    def keypress(self, key):
        keys = self.config.get_keybinding()
        if 'y' in key and self.proj:
            logger.debug("MoveEntryState: move selected entries.")
            self.controller.move_selected_entries(self.proj)
            self.renew_focus()
            self.view.set_footer_text('', 'entry_footer')
            self.proj = None
            return DefaultEntryListState(self.projectlist_state,
                                         self.controller, self.view)

        if 'n' in key and self.proj:
            self.view.set_footer_text('', 'entry_footer')
            self.reset_project_footer()
            return DefaultEntryListState(self.projectlist_state,
                                         self.controller, self.view)

        if keys.get_escape() in key:
            self.view.set_footer_text('', 'entry_footer')
            self.reset_project_footer()
            return DefaultEntryListState(self.projectlist_state,
                                         self.controller, self.view)
        if 'backspace' in key:
            if len(self.proj_keys) > 0:
                self.proj_keys = self.proj_keys[:-1]
                self.set_project_footer()
            return self

        if len(key) > 0 and len(key[0]) == 1 and self.proj is None:
            self.proj_keys += key[0]
            self.set_project_footer()
            self.proj = self.controller.project_by_key(self.proj_keys)
            if self.proj:
                self.reset_project_footer()
                logger.debug("MoveEntryState: prepared entries to be moved to "\
                             "project '%s'" % self.proj.desc)
                self.view.set_footer_text(self.msg('really'), 'question')

        return self

class CreateProjectState(HandleUserInputState):

    messages = {
            'choose_proj': _(' Choose a project.'),
            }

    def __init__(self, controller, view):
        super(CreateProjectState, self).__init__(controller, view, None,
                controller.view.view.get_footer())

    def exit(self):
        self.controller.view.set_footer_text(self.msg('choose_proj'), 'question')
        return PausedProjectsState(self.controller, self.view)

class AddProjectNameState(CreateProjectState):
    messages = {
        'choose_proj': _(' Choose a project.'),
        'proj_name': _('Insert new project name: '),
    }

    def __init__(self, controller, view):
        controller.view.set_footer_text(self.msg('proj_name'),
                                             'question', 1)
        super(AddProjectNameState, self).__init__(controller, view)
        self.view.frame.set_focus('footer')

    def enter(self):
        project = self.footer.get_edit_text()
        if project == '':
            return self
        return AddProjectKeyState(self.controller, self.view, project)


class AddProjectKeyState(CreateProjectState):

    messages = {
        'proj_name': _('Insert new project name: '),
        'proj_key': _('Insert key for new project: '),
    }

    def __init__(self, controller, view, project):
        controller.view.set_footer_text(self.msg('proj_key'),
                                             'question', 1)
        super(AddProjectKeyState, self).__init__(controller, view)
        self.view.frame.set_focus('footer')
        self.project = project

    def enter(self):
        key = self.footer.get_edit_text()
        if key == '':
            return self
        return AddProjectDescriptionState(self.controller, self.view,
                self.project, key)

class AddProjectDescriptionState(CreateProjectState):
    messages = {
        'proj_description': _('Insert a description for project: '),
        'choose_proj': _(" Choose a project.")
        }

    def __init__(self, controller, view, project, key):
        controller.view.set_footer_text(self.msg('proj_description'),
                                             'question', 1)
        super(AddProjectDescriptionState, self).__init__(controller, view)
        self.view.frame.set_focus('footer')
        self.project = project
        self.key = key

    def enter(self):
        description = self.footer.get_edit_text()
        if description == '':
            return self
        self.controller.add_project(self.key, description)
        self.controller.view.set_footer_text(self.msg('choose_proj'), 'question')
        return PausedProjectsState(self.controller, self.view)


class EditEntryState(HandleUserInputState):
    messages = {
        'edit_entry': _('Edit entry text: '),
    }

    def __init__(self, state, controller, view, entry):
        view.set_footer_text(self.msg('edit_entry'),
                             'question', True)
        super(EditEntryState, self).__init__(controller, view,
                None, view.footer)
        self.footer.set_edit_text(entry.desc)
        self.footer.set_edit_pos(len(self.footer.edit_text))
        self.view.frame.set_focus('footer')
        self.entry = entry
        self.state = state
        logger.debug("EditEntryState: Entry %s" % entry)

    def enter(self):
        entry_desc = self.footer.get_edit_text()
        if entry_desc == '':
            return self
        entry = self.entry
        entry.desc = entry_desc
        self.controller.update_entry(entry)
        self.view.node_in_focus().update()
        return self.exit()

    def exit(self):
        self.view.set_footer_text("", 'entry_footer', False)
        return DefaultEntryListState(self.state, self.controller, self.view)
This site is hosted by Intevation GmbH (Datenschutzerklärung und Impressum | Privacy Policy and Imprint)