4cc4eae367
When there are more rows than will fit in a ButtonDialog Linebox, having them wrapped within a ListBox makes it possible to scroll through them rather than merely displaying the last lines. Change-Id: Id38fb999e2eceda03bc15d0928765acb936dd8c8
327 lines
11 KiB
Python
327 lines
11 KiB
Python
# Copyright 2014 OpenStack Foundation
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import urwid
|
|
|
|
from gertty import keymap
|
|
from gertty.view import mouse_scroll_decorator
|
|
|
|
GLOBAL_HELP = (
|
|
(keymap.HELP,
|
|
"Display help"),
|
|
(keymap.PREV_SCREEN,
|
|
"Back to previous screen"),
|
|
(keymap.TOP_SCREEN,
|
|
"Back to project list"),
|
|
(keymap.QUIT,
|
|
"Quit Gertty"),
|
|
(keymap.CHANGE_SEARCH,
|
|
"Search for changes"),
|
|
(keymap.LIST_HELD,
|
|
"List held changes"),
|
|
)
|
|
|
|
class TextButton(urwid.Button):
|
|
def selectable(self):
|
|
return True
|
|
|
|
def __init__(self, text, on_press=None, user_data=None):
|
|
super(TextButton, self).__init__('', on_press=on_press, user_data=user_data)
|
|
self.text = urwid.Text(text)
|
|
self._w = urwid.AttrMap(self.text, None, focus_map='focused')
|
|
|
|
class FixedButton(urwid.Button):
|
|
def sizing(self):
|
|
return frozenset([urwid.FIXED])
|
|
|
|
def pack(self, size, focus=False):
|
|
return (len(self.get_label())+4, 1)
|
|
|
|
class FixedRadioButton(urwid.RadioButton):
|
|
def sizing(self):
|
|
return frozenset([urwid.FIXED])
|
|
|
|
def pack(self, size, focus=False):
|
|
return (len(self.get_label())+4, 1)
|
|
|
|
class TableColumn(urwid.Pile):
|
|
def pack(self, size, focus=False):
|
|
maxcol = size[0]
|
|
mx = max([i[0].pack((maxcol,), focus)[0] for i in self.contents])
|
|
return (min(mx+2, maxcol), len(self.contents))
|
|
|
|
class Table(urwid.WidgetWrap):
|
|
def __init__(self, headers=[], columns=None):
|
|
if columns is None:
|
|
cols = [('pack', TableColumn([('pack', w)])) for w in headers]
|
|
else:
|
|
cols = [('pack', TableColumn([])) for x in range(columns)]
|
|
super(Table, self).__init__(
|
|
urwid.Columns(cols))
|
|
|
|
def addRow(self, cells=[]):
|
|
for i, widget in enumerate(cells):
|
|
self._w.contents[i][0].contents.append((widget, ('pack', None)))
|
|
|
|
@mouse_scroll_decorator.ScrollByWheel
|
|
class ButtonDialog(urwid.WidgetWrap):
|
|
def __init__(self, title, message, entry_prompt=None, entry_text='', buttons=[]):
|
|
button_widgets = []
|
|
for button in buttons:
|
|
button_widgets.append(('pack', button))
|
|
button_columns = urwid.Columns(button_widgets, dividechars=2)
|
|
rows = []
|
|
rows.append(urwid.Text(message))
|
|
if entry_prompt:
|
|
self.entry = urwid.Edit(entry_prompt, edit_text=entry_text)
|
|
rows.append(self.entry)
|
|
else:
|
|
self.entry = None
|
|
rows.append(urwid.Divider())
|
|
rows.append(button_columns)
|
|
listbox = urwid.ListBox(rows)
|
|
super(ButtonDialog, self).__init__(urwid.LineBox(listbox, title))
|
|
|
|
class TextEditDialog(urwid.WidgetWrap):
|
|
signals = ['save', 'cancel']
|
|
def __init__(self, title, prompt, button, text):
|
|
save_button = FixedButton(button)
|
|
cancel_button = FixedButton('Cancel')
|
|
urwid.connect_signal(save_button, 'click',
|
|
lambda button:self._emit('save'))
|
|
urwid.connect_signal(cancel_button, 'click',
|
|
lambda button:self._emit('cancel'))
|
|
button_widgets = [('pack', save_button),
|
|
('pack', cancel_button)]
|
|
button_columns = urwid.Columns(button_widgets, dividechars=2)
|
|
rows = []
|
|
self.entry = urwid.Edit(edit_text=text, multiline=True)
|
|
rows.append(urwid.Text(prompt))
|
|
rows.append(self.entry)
|
|
rows.append(urwid.Divider())
|
|
rows.append(button_columns)
|
|
pile = urwid.Pile(rows)
|
|
fill = urwid.Filler(pile, valign='top')
|
|
super(TextEditDialog, self).__init__(urwid.LineBox(fill, title))
|
|
|
|
class MessageDialog(ButtonDialog):
|
|
signals = ['close']
|
|
def __init__(self, title, message):
|
|
ok_button = FixedButton('OK')
|
|
urwid.connect_signal(ok_button, 'click',
|
|
lambda button:self._emit('close'))
|
|
super(MessageDialog, self).__init__(title, message, buttons=[ok_button])
|
|
|
|
class YesNoDialog(ButtonDialog):
|
|
signals = ['yes', 'no']
|
|
def __init__(self, title, message):
|
|
yes_button = FixedButton('Yes')
|
|
no_button = FixedButton('No')
|
|
urwid.connect_signal(yes_button, 'click',
|
|
lambda button:self._emit('yes'))
|
|
urwid.connect_signal(no_button, 'click',
|
|
lambda button:self._emit('no'))
|
|
super(YesNoDialog, self).__init__(title, message, buttons=[yes_button,
|
|
no_button])
|
|
def keypress(self, size, key):
|
|
r = super(YesNoDialog, self).keypress(size, key)
|
|
if r in ('Y', 'y'):
|
|
self._emit('yes')
|
|
return None
|
|
if r in ('N', 'n'):
|
|
self._emit('no')
|
|
return None
|
|
return r
|
|
|
|
class HyperText(urwid.Text):
|
|
_selectable = True
|
|
|
|
def __init__(self, markup, align=urwid.LEFT, wrap=urwid.SPACE, layout=None):
|
|
self._mouse_press_item = None
|
|
self.selectable_items = []
|
|
self.focused_index = None
|
|
self.last_focused_index = 0
|
|
super(HyperText, self).__init__(markup, align, wrap, layout)
|
|
|
|
def focusFirstItem(self):
|
|
if len(self.selectable_items) == 0:
|
|
return False
|
|
self.focusItem(0)
|
|
return True
|
|
|
|
def focusLastItem(self):
|
|
if len(self.selectable_items) == 0:
|
|
return False
|
|
self.focusItem(len(self.selectable_items)-1)
|
|
return True
|
|
|
|
def focusPreviousItem(self):
|
|
if len(self.selectable_items) == 0:
|
|
return False
|
|
if self.focused_index is None:
|
|
self.focusItem(self.last_focused_index)
|
|
item = max(0, self.focused_index-1)
|
|
if item != self.focused_index:
|
|
self.focusItem(item)
|
|
return True
|
|
return False
|
|
|
|
def focusNextItem(self):
|
|
if len(self.selectable_items) == 0:
|
|
return False
|
|
if self.focused_index is None:
|
|
self.focusItem(self.last_focused_index)
|
|
item = min(len(self.selectable_items)-1, self.focused_index+1)
|
|
if item != self.focused_index:
|
|
self.focusItem(item)
|
|
return True
|
|
return False
|
|
|
|
def focusItem(self, item):
|
|
self.last_focused_index = self.focused_index
|
|
self.focused_index = item
|
|
self.set_text(self._markup)
|
|
self._invalidate()
|
|
|
|
def select(self):
|
|
if self.focused_index is not None:
|
|
self.selectable_items[self.focused_index][0].select()
|
|
|
|
def keypress(self, size, key):
|
|
if self._command_map[key] == urwid.CURSOR_UP:
|
|
if self.focusPreviousItem():
|
|
return False
|
|
return key
|
|
elif self._command_map[key] == urwid.CURSOR_DOWN:
|
|
if self.focusNextItem():
|
|
return False
|
|
return key
|
|
elif self._command_map[key] == urwid.ACTIVATE:
|
|
self.select()
|
|
return False
|
|
return key
|
|
|
|
def getPosAtCoords(self, maxcol, col, row):
|
|
trans = self.get_line_translation(maxcol)
|
|
colpos = 0
|
|
line = trans[row]
|
|
for t in line:
|
|
if len(t) == 2:
|
|
width, pos = t
|
|
if colpos <= col < colpos + width:
|
|
return pos
|
|
else:
|
|
width, start, end = t
|
|
if colpos <= col < colpos + width:
|
|
return start + (col - colpos)
|
|
colpos += width
|
|
return None
|
|
|
|
def getItemAtCoords(self, maxcol, col, row):
|
|
pos = self.getPosAtCoords(maxcol, col, row)
|
|
index = 0
|
|
for item, start, end in self.selectable_items:
|
|
if start <= pos <= end:
|
|
return index
|
|
index += 1
|
|
return None
|
|
|
|
def mouse_event(self, size, event, button, col, row, focus):
|
|
if ((button not in [0, 1]) or
|
|
(event not in ['mouse press', 'mouse release'])):
|
|
return False
|
|
item = self.getItemAtCoords(size[0], col, row)
|
|
if item is None:
|
|
if self.focused_index is None:
|
|
self.focusFirstItem()
|
|
return False
|
|
if event == 'mouse press':
|
|
self.focusItem(item)
|
|
self._mouse_press_item = item
|
|
if event == 'mouse release':
|
|
if self._mouse_press_item == item:
|
|
self.select()
|
|
self._mouse_press_item = None
|
|
return True
|
|
|
|
def processLinks(self, markup, data=None):
|
|
if data is None:
|
|
data = dict(pos=0)
|
|
if isinstance(markup, list):
|
|
return [self.processLinks(i, data) for i in markup]
|
|
if isinstance(markup, tuple):
|
|
return (markup[0], self.processLinks(markup[1], data))
|
|
if isinstance(markup, Link):
|
|
self.selectable_items.append((markup, data['pos'], data['pos']+len(markup.text)))
|
|
data['pos'] += len(markup.text)
|
|
focused = len(self.selectable_items)-1 == self.focused_index
|
|
link_attr = markup.getAttr(focused)
|
|
if link_attr:
|
|
return (link_attr, markup.text)
|
|
else:
|
|
return markup.text
|
|
data['pos'] += len(markup)
|
|
return markup
|
|
|
|
def set_text(self, markup):
|
|
self._markup = markup
|
|
self.selectable_items = []
|
|
super(HyperText, self).set_text(self.processLinks(markup))
|
|
|
|
def move_cursor_to_coords(self, size, col, row):
|
|
if self.focused_index is None:
|
|
if row:
|
|
self.focusLastItem()
|
|
else:
|
|
self.focusFirstItem()
|
|
return True
|
|
|
|
def render(self, size, focus=False):
|
|
if (not focus) and (self.focused_index is not None):
|
|
self.focusItem(None)
|
|
return super(HyperText, self).render(size, focus)
|
|
|
|
class Link(urwid.Widget):
|
|
signals = ['selected']
|
|
|
|
def __init__(self, text, attr=None, focused_attr=None):
|
|
self.text = text
|
|
self.attr = attr
|
|
self.focused_attr = focused_attr
|
|
|
|
def select(self):
|
|
self._emit('selected')
|
|
|
|
def getAttr(self, focus):
|
|
if focus:
|
|
return self.focused_attr
|
|
return self.attr
|
|
|
|
# A workaround for the issue fixed in
|
|
# https://github.com/wardi/urwid/pull/74
|
|
# included here until thi fix is released
|
|
class MyGridFlow(urwid.GridFlow):
|
|
def generate_display_widget(self, size):
|
|
p = super(MyGridFlow, self).generate_display_widget(size)
|
|
for item in p.contents:
|
|
if isinstance(item[0], urwid.Padding):
|
|
c = item[0].original_widget
|
|
if isinstance(c, urwid.Columns):
|
|
if c.focus_position == 0 and not c.contents[0][0].selectable():
|
|
for i, w in enumerate(c.contents):
|
|
if w[0].selectable():
|
|
c.focus_position = i
|
|
break
|
|
return p
|