2015-07-22 18:57:53 +00:00
|
|
|
# Copyright 2015 Canonical, Ltd.
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
""" Re-usable input widgets
|
|
|
|
"""
|
|
|
|
|
2017-02-14 02:16:12 +00:00
|
|
|
from functools import partial
|
|
|
|
import logging
|
|
|
|
import re
|
|
|
|
|
2017-01-09 03:11:32 +00:00
|
|
|
from urwid import (
|
|
|
|
ACTIVATE,
|
|
|
|
AttrWrap,
|
|
|
|
connect_signal,
|
|
|
|
Edit,
|
|
|
|
Filler,
|
|
|
|
IntEdit,
|
|
|
|
LineBox,
|
|
|
|
PopUpLauncher,
|
|
|
|
SelectableIcon,
|
2017-02-02 01:34:59 +00:00
|
|
|
Text,
|
2017-01-09 03:11:32 +00:00
|
|
|
TOP,
|
|
|
|
WidgetWrap,
|
|
|
|
)
|
2015-07-22 18:57:53 +00:00
|
|
|
|
2017-02-03 01:20:05 +00:00
|
|
|
from subiquitycore.ui.container import Pile
|
|
|
|
|
2016-06-30 18:17:01 +00:00
|
|
|
log = logging.getLogger("subiquitycore.ui.input")
|
2015-07-22 18:57:53 +00:00
|
|
|
|
|
|
|
|
2017-02-14 02:16:12 +00:00
|
|
|
class StringEditor(Edit):
|
2015-07-22 18:57:53 +00:00
|
|
|
""" Edit input class
|
|
|
|
|
2017-02-14 02:16:12 +00:00
|
|
|
Attaches its result to the `value` accessor.
|
2015-07-22 18:57:53 +00:00
|
|
|
"""
|
2015-08-28 19:50:17 +00:00
|
|
|
|
2015-07-22 18:57:53 +00:00
|
|
|
@property
|
|
|
|
def value(self):
|
2017-02-14 02:16:12 +00:00
|
|
|
return self.get_edit_text()
|
2015-07-22 18:57:53 +00:00
|
|
|
|
2015-08-21 18:49:45 +00:00
|
|
|
@value.setter # NOQA
|
|
|
|
def value(self, value):
|
2017-02-14 02:16:12 +00:00
|
|
|
self.set_edit_text(value)
|
2015-08-27 14:28:48 +00:00
|
|
|
|
2015-07-22 18:57:53 +00:00
|
|
|
|
2015-08-18 18:34:59 +00:00
|
|
|
class PasswordEditor(StringEditor):
|
|
|
|
""" Password input prompt with masking
|
|
|
|
"""
|
2017-02-14 02:16:12 +00:00
|
|
|
def __init__(self, mask="*"):
|
|
|
|
super().__init__(mask=mask)
|
2015-08-18 18:34:59 +00:00
|
|
|
|
|
|
|
|
2017-02-14 02:16:12 +00:00
|
|
|
class RestrictedEditor(StringEditor):
|
|
|
|
"""Editor that only allows certain characters."""
|
2015-11-03 20:00:05 +00:00
|
|
|
|
2017-02-14 02:16:12 +00:00
|
|
|
def __init__(self, allowed=None):
|
|
|
|
super().__init__()
|
|
|
|
self.matcher = re.compile(allowed)
|
2015-11-03 20:00:05 +00:00
|
|
|
|
2017-02-14 02:27:25 +00:00
|
|
|
def valid_char(self, ch):
|
2017-02-14 03:07:36 +00:00
|
|
|
return len(ch) == 1 and self.matcher.match(ch) is not None
|
2015-11-03 20:00:05 +00:00
|
|
|
|
|
|
|
|
2017-02-14 02:16:12 +00:00
|
|
|
RealnameEditor = partial(RestrictedEditor, r'[a-zA-Z0-9_\- ]')
|
|
|
|
EmailEditor = partial(RestrictedEditor, r'[-a-zA-Z0-9_.@+=]')
|
2016-07-27 10:15:22 +00:00
|
|
|
|
|
|
|
|
Implement username field protection
man 8 useradd says:
It is usually recommended to only use usernames that begin with a
lower case letter or an underscore, followed by lower case
letters, digits, underscores, or dashes. They can end with a
dollar sign. In regular expression terms: [a-z_][a-z0-9_-]*[$]?
On Debian, the only constraints are that usernames must neither
start with a dash ('-') nor plus ('+') nor tilde ('~') nor
contain a colon (':'), a comma (','), or a whitespace (space: ' ',
end of line: '\n', tabulation: '\t', etc.). Note that using a
slash ('/') may break the default algorithm for the definition
of the user's home directory.
Usernames may only be up to 32 characters long.
In this patch we implement most of this. Subset of the regular
expression suggested above is used to limit input into the username
field. We've not yet determined how to provide a fixed width widget
so at this time, user can input more than 32 characters, but upon
selectin done, we raise and error and reset the state.
Signed-off-by: Ryan Harper <ryan.harper@canonical.com>
2015-10-23 20:21:13 +00:00
|
|
|
class UsernameEditor(StringEditor):
|
|
|
|
""" Username input prompt with input rules
|
|
|
|
"""
|
|
|
|
|
|
|
|
def keypress(self, size, key):
|
|
|
|
''' restrict what chars we allow for username '''
|
2017-02-14 02:16:12 +00:00
|
|
|
if self._command_map[key] is not None:
|
|
|
|
return super().keypress(size, key)
|
|
|
|
new_text = self.insert_text_result(key)[0]
|
|
|
|
username = r'[a-z_][a-z0-9_-]*'
|
Implement username field protection
man 8 useradd says:
It is usually recommended to only use usernames that begin with a
lower case letter or an underscore, followed by lower case
letters, digits, underscores, or dashes. They can end with a
dollar sign. In regular expression terms: [a-z_][a-z0-9_-]*[$]?
On Debian, the only constraints are that usernames must neither
start with a dash ('-') nor plus ('+') nor tilde ('~') nor
contain a colon (':'), a comma (','), or a whitespace (space: ' ',
end of line: '\n', tabulation: '\t', etc.). Note that using a
slash ('/') may break the default algorithm for the definition
of the user's home directory.
Usernames may only be up to 32 characters long.
In this patch we implement most of this. Subset of the regular
expression suggested above is used to limit input into the username
field. We've not yet determined how to provide a fixed width widget
so at this time, user can input more than 32 characters, but upon
selectin done, we raise and error and reset the state.
Signed-off-by: Ryan Harper <ryan.harper@canonical.com>
2015-10-23 20:21:13 +00:00
|
|
|
# don't allow non username chars
|
2017-02-14 02:16:12 +00:00
|
|
|
if new_text != "" and re.match(username, new_text) is None:
|
Implement username field protection
man 8 useradd says:
It is usually recommended to only use usernames that begin with a
lower case letter or an underscore, followed by lower case
letters, digits, underscores, or dashes. They can end with a
dollar sign. In regular expression terms: [a-z_][a-z0-9_-]*[$]?
On Debian, the only constraints are that usernames must neither
start with a dash ('-') nor plus ('+') nor tilde ('~') nor
contain a colon (':'), a comma (','), or a whitespace (space: ' ',
end of line: '\n', tabulation: '\t', etc.). Note that using a
slash ('/') may break the default algorithm for the definition
of the user's home directory.
Usernames may only be up to 32 characters long.
In this patch we implement most of this. Subset of the regular
expression suggested above is used to limit input into the username
field. We've not yet determined how to provide a fixed width widget
so at this time, user can input more than 32 characters, but upon
selectin done, we raise and error and reset the state.
Signed-off-by: Ryan Harper <ryan.harper@canonical.com>
2015-10-23 20:21:13 +00:00
|
|
|
return False
|
|
|
|
return super().keypress(size, key)
|
2015-11-02 18:55:07 +00:00
|
|
|
|
|
|
|
|
2015-07-22 18:57:53 +00:00
|
|
|
class IntegerEditor(WidgetWrap):
|
|
|
|
""" IntEdit input class
|
|
|
|
"""
|
2017-02-14 02:16:12 +00:00
|
|
|
def __init__(self, default=0):
|
|
|
|
self._edit = IntEdit(default=default)
|
2015-07-22 18:57:53 +00:00
|
|
|
super().__init__(self._edit)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def value(self):
|
|
|
|
return self._edit.get_edit_text()
|
|
|
|
|
2017-02-13 01:48:53 +00:00
|
|
|
@value.setter
|
|
|
|
def value(self, val):
|
|
|
|
return self._edit.set_edit_text(str(val))
|
|
|
|
|
2015-07-22 18:57:53 +00:00
|
|
|
|
2017-01-09 03:11:32 +00:00
|
|
|
class _PopUpButton(SelectableIcon):
|
2017-01-25 03:20:46 +00:00
|
|
|
"""It looks a bit like a radio button, but it just emits 'click' on activation."""
|
2017-01-09 03:11:32 +00:00
|
|
|
|
|
|
|
signals = ['click']
|
|
|
|
|
|
|
|
states = {
|
2017-01-25 03:20:46 +00:00
|
|
|
True: "(+) ",
|
|
|
|
False: "( ) ",
|
2017-01-09 03:11:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(self, option, state):
|
2017-01-20 00:19:16 +00:00
|
|
|
p = self.states[state]
|
|
|
|
super().__init__(p + option, len(p))
|
2017-01-09 03:11:32 +00:00
|
|
|
|
|
|
|
def keypress(self, size, key):
|
|
|
|
if self._command_map[key] != ACTIVATE:
|
|
|
|
return key
|
|
|
|
self._emit('click')
|
|
|
|
|
|
|
|
|
|
|
|
class _PopUpSelectDialog(WidgetWrap):
|
|
|
|
"""A list of PopUpButtons with a box around them."""
|
|
|
|
|
|
|
|
def __init__(self, parent, cur_index):
|
|
|
|
self.parent = parent
|
|
|
|
group = []
|
|
|
|
for i, option in enumerate(self.parent._options):
|
2017-02-02 01:34:59 +00:00
|
|
|
if option[1]:
|
|
|
|
btn = _PopUpButton(option[0], state=i==cur_index)
|
|
|
|
connect_signal(btn, 'click', self.click, i)
|
|
|
|
group.append(AttrWrap(btn, 'menu_button', 'menu_button focus'))
|
|
|
|
else:
|
|
|
|
btn = Text(" " + option[0])
|
|
|
|
group.append(AttrWrap(btn, 'info_minor'))
|
2017-01-09 03:11:32 +00:00
|
|
|
pile = Pile(group)
|
|
|
|
pile.set_focus(group[cur_index])
|
|
|
|
fill = Filler(pile, valign=TOP)
|
2017-01-20 00:19:16 +00:00
|
|
|
super().__init__(LineBox(fill))
|
2017-01-09 03:11:32 +00:00
|
|
|
|
|
|
|
def click(self, btn, index):
|
|
|
|
self.parent.index = index
|
|
|
|
self.parent.close_pop_up()
|
|
|
|
|
2017-01-25 03:20:46 +00:00
|
|
|
def keypress(self, size, key):
|
|
|
|
if key == 'esc':
|
|
|
|
self.parent.close_pop_up()
|
|
|
|
else:
|
|
|
|
return super().keypress(size, key)
|
2017-01-09 03:11:32 +00:00
|
|
|
|
2017-02-02 01:34:59 +00:00
|
|
|
class SelectorError(Exception):
|
|
|
|
pass
|
|
|
|
|
2017-01-09 03:11:32 +00:00
|
|
|
class Selector(PopUpLauncher):
|
|
|
|
"""A widget that allows the user to chose between options by popping up this list of options.
|
|
|
|
|
|
|
|
(A bit like <select> in an HTML form).
|
2015-07-22 18:57:53 +00:00
|
|
|
"""
|
2017-01-09 03:11:32 +00:00
|
|
|
|
2017-01-25 03:20:46 +00:00
|
|
|
_prefix = "(+) "
|
2017-01-20 00:19:16 +00:00
|
|
|
|
2017-01-25 03:39:28 +00:00
|
|
|
signals = ['select']
|
|
|
|
|
2017-01-09 03:11:32 +00:00
|
|
|
def __init__(self, opts, index=0):
|
2017-02-02 01:34:59 +00:00
|
|
|
self._options = []
|
|
|
|
for opt in opts:
|
|
|
|
if not isinstance(opt, tuple):
|
|
|
|
if not isinstance(opt, str):
|
|
|
|
raise SelectorError("invalid option %r", opt)
|
|
|
|
opt = (opt, True, opt)
|
|
|
|
elif len(opt) == 1:
|
|
|
|
opt = (opt[0], True, opt[0])
|
|
|
|
elif len(opt) == 2:
|
|
|
|
opt = (opt[0], opt[1], opt[0])
|
|
|
|
elif len(opt) != 3:
|
|
|
|
raise SelectorError("invalid option %r", opt)
|
|
|
|
self._options.append(opt)
|
2017-01-20 00:19:16 +00:00
|
|
|
self._button = SelectableIcon(self._prefix, len(self._prefix))
|
2017-01-25 03:39:28 +00:00
|
|
|
self._set_index(index)
|
2017-01-09 03:11:32 +00:00
|
|
|
super().__init__(self._button)
|
|
|
|
|
|
|
|
def keypress(self, size, key):
|
|
|
|
if self._command_map[key] != ACTIVATE:
|
|
|
|
return key
|
|
|
|
self.open_pop_up()
|
|
|
|
|
2017-01-25 03:39:28 +00:00
|
|
|
def _set_index(self, val):
|
2017-02-02 01:34:59 +00:00
|
|
|
self._button.set_text(self._prefix + self._options[val][0])
|
2017-01-25 03:39:28 +00:00
|
|
|
self._index = val
|
|
|
|
|
2017-01-09 03:11:32 +00:00
|
|
|
@property
|
|
|
|
def index(self):
|
|
|
|
return self._index
|
|
|
|
|
|
|
|
@index.setter
|
|
|
|
def index(self, val):
|
2017-02-02 01:34:59 +00:00
|
|
|
self._emit('select', self._options[val][2])
|
2017-01-25 03:39:28 +00:00
|
|
|
self._set_index(val)
|
2015-07-22 18:57:53 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def value(self):
|
2017-02-02 01:34:59 +00:00
|
|
|
return self._options[self._index][2]
|
2017-01-09 03:11:32 +00:00
|
|
|
|
|
|
|
def create_pop_up(self):
|
|
|
|
return _PopUpSelectDialog(self, self.index)
|
|
|
|
|
|
|
|
def get_pop_up_parameters(self):
|
2017-02-02 01:34:59 +00:00
|
|
|
width = max([len(o[0]) for o in self._options]) \
|
|
|
|
+ len(self._prefix) + 3 # line on left, space, line on right
|
2017-01-20 00:19:16 +00:00
|
|
|
return {'left':-1, 'top':-self.index-1, 'overlay_width':width, 'overlay_height':len(self._options) + 2}
|
2015-09-09 19:18:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
class YesNo(Selector):
|
|
|
|
""" Yes/No selector
|
|
|
|
"""
|
|
|
|
def __init__(self):
|
|
|
|
opts = ['Yes', 'No']
|
|
|
|
super().__init__(opts)
|