editcurses: fixes and cleanup
Change-Id: I55a1a12a18f16beceaa62881a915701c11c14270
This commit is contained in:
+2
-1
@@ -30,7 +30,7 @@ repo = Path(__file__).absolute().parents[1]
|
||||
sys.path.insert(0, str(repo))
|
||||
|
||||
from frappy.lib import generalConfig
|
||||
from frappy.tools.cfgedit import EditorMain
|
||||
from frappy.editcurses.cfgedit import EditorMain
|
||||
|
||||
# merge cfg dirs from env variable and the ones typically used at psi
|
||||
# use dicts instead of sets, as we want to keep order
|
||||
@@ -43,3 +43,4 @@ for cfgdir in 'cfg', 'cfg/main', 'cfg/stick', 'cfg/addons':
|
||||
os.environ['FRAPPY_CONFDIR'] = ':'.join(cfgdirs)
|
||||
generalConfig.init()
|
||||
EditorMain(sys.argv[1]).run()
|
||||
|
||||
|
||||
@@ -4,14 +4,18 @@ import time
|
||||
from subprocess import Popen, PIPE
|
||||
from pathlib import Path
|
||||
from psutil import pid_exists
|
||||
import frappy
|
||||
from frappy.errors import ConfigError
|
||||
from frappy.lib import generalConfig
|
||||
from frappy.lib import formatExtendedTraceback
|
||||
from frappy.config import to_config_path
|
||||
from frappy.tools.configdata import Value, cfgdata_to_py, cfgdata_from_py, get_datatype, site, ModuleClass, stringtype
|
||||
from frappy.tools.completion import class_completion, recommended_prs
|
||||
import frappy.tools.terminalgui as tg
|
||||
from frappy.tools.terminalgui import Main, MenuItem, TextEdit, PushButton, ModalDialog, KEY
|
||||
from frappy.editcurses.configdata import Value, cfgdata_to_py, cfgdata_from_py, \
|
||||
get_datatype, site, ModuleClass, stringtype, class_completion, recommended_prs, \
|
||||
make_value, ModuleNameCompletion, Module
|
||||
from frappy.io import IOBase
|
||||
import frappy.editcurses.terminalgui as tg
|
||||
from frappy.editcurses.terminalgui import Main, MenuItem, TextEdit, PushButton, \
|
||||
ModalDialog, KEY
|
||||
|
||||
|
||||
KEY.add(
|
||||
@@ -56,6 +60,13 @@ class StringValue: # TODO: unused?
|
||||
class TopWidget:
|
||||
parent_cls = Main
|
||||
|
||||
def handle(self, main):
|
||||
key = super().handle(main)
|
||||
if key == KEY.LEFT:
|
||||
main.toggle_detailed()
|
||||
return None
|
||||
return key
|
||||
|
||||
|
||||
class Child(tg.Widget):
|
||||
"""child widget of NodeWidget ot ModuleWidget"""
|
||||
@@ -80,25 +91,33 @@ class HasValue(Child):
|
||||
def init_value_widget(self, parent, valobj):
|
||||
self.init_parent(parent)
|
||||
self.valobj = valobj
|
||||
if isinstance(valobj.completion, ModuleNameCompletion):
|
||||
valobj.completion.get_names = self.get_module_list
|
||||
|
||||
def validate(self, strvalue, main=None):
|
||||
pname = self.get_name()
|
||||
valobj = self.valobj
|
||||
prev = valobj.value, valobj.strvalue, valobj.error
|
||||
try:
|
||||
if pname != 'cls':
|
||||
if self.clsobj != self.parent.clsobj:
|
||||
self.clsobj = self.parent.clsobj
|
||||
valobj.datatype, valobj.error = get_datatype(
|
||||
self.get_name(), self.clsobj, valobj.value)
|
||||
valobj.validate_from_string(strvalue)
|
||||
self.valobj = make_value(self.get_name(), self.clsobj, valobj.value)
|
||||
if isinstance(self.valobj.completion, ModuleNameCompletion):
|
||||
valobj.completion.get_names = self.get_module_list
|
||||
self.valobj.validate_from_string(strvalue)
|
||||
self.error = None
|
||||
except Exception as e:
|
||||
self.error = str(e)
|
||||
if main and (valobj.value, valobj.strvalue, valobj.error) != prev:
|
||||
if main and valobj != self.valobj:
|
||||
main.touch()
|
||||
return valobj.strvalue
|
||||
|
||||
def get_module_list(self, basecls):
|
||||
assert isinstance(self.valobj.completion, ModuleNameCompletion)
|
||||
module = self.parent
|
||||
main = module.parent
|
||||
return main.get_module_list(basecls, module.get_name())
|
||||
|
||||
def check_data(self):
|
||||
self.validate(self.valobj.strvalue)
|
||||
|
||||
@@ -108,6 +127,7 @@ class HasValue(Child):
|
||||
|
||||
class ValueWidget(HasValue, tg.LineEdit):
|
||||
fixedname = None
|
||||
error = None
|
||||
|
||||
def __init__(self, parent, name, valobj, label=None):
|
||||
"""init a value widget
|
||||
@@ -132,7 +152,8 @@ class ValueWidget(HasValue, tg.LineEdit):
|
||||
|
||||
def validate_name(self, name, main):
|
||||
widget_dict = self.parent.widget_dict
|
||||
if name.isidentifier():
|
||||
pname, dot, prop = name.partition('.')
|
||||
if pname.isidentifier() and (prop.isidentifier or dot == ''):
|
||||
other = widget_dict.get(name)
|
||||
if other and other != self:
|
||||
self.error = f'duplicate name {name!r}'
|
||||
@@ -158,6 +179,15 @@ class ValueWidget(HasValue, tg.LineEdit):
|
||||
if name:
|
||||
as_dict[name] = self.valobj
|
||||
|
||||
def draw(self, wr, in_focus=False):
|
||||
super().draw(wr, in_focus)
|
||||
valobj = self.valobj
|
||||
if valobj.strvalue == '':
|
||||
wr.dim(valobj.datatype.to_string(valobj.default))
|
||||
elif self.error:
|
||||
wr.norm(' ')
|
||||
wr.error(self.error)
|
||||
|
||||
|
||||
class DocWidget(HasValue, tg.MultiLineEdit):
|
||||
parent_cls = TopWidget
|
||||
@@ -308,6 +338,11 @@ class ModuleWidget(BaseWidget):
|
||||
if clsobj:
|
||||
self.fixed_names.update({k: k for k, v in recommended_prs(clsobj).items() if v})
|
||||
|
||||
def get_class(self):
|
||||
clsvalue = self.widget_dict['cls'].valobj
|
||||
clsvalue.set_from_string(clsvalue.strvalue)
|
||||
return self.clsobj or Module
|
||||
|
||||
def new_widget(self, name='', pos=None):
|
||||
self.add_widget(name, Value('', *get_datatype('', self.clsobj, ''), from_string=True), self.focus)
|
||||
|
||||
@@ -414,6 +449,9 @@ class IOWidget(ModuleWidget):
|
||||
modules = [w.get_name() for w in self.parent.widgets if w.get_widget_value('io') == ioname]
|
||||
wr.norm(f"{self.get_widget_value('uri').ljust(half)} for {','.join(modules)} ")
|
||||
|
||||
def get_class(self):
|
||||
return IOBase
|
||||
|
||||
def collect(self, result):
|
||||
name = self.get_name()
|
||||
if name:
|
||||
@@ -455,7 +493,7 @@ class EndLine(Child):
|
||||
key = main.get_key()
|
||||
if key in (KEY.RETURN, KEY.ENTER):
|
||||
self.parent.add_module(True)
|
||||
elif key in (KEY.UP, KEY.DOWN, KEY.QUIT):
|
||||
elif key in (KEY.LEFT, KEY.UP, KEY.DOWN, KEY.QUIT):
|
||||
return key
|
||||
elif key == 'i':
|
||||
self.parent.add_iomodule(True)
|
||||
@@ -609,43 +647,48 @@ class EditorMain(Main):
|
||||
cut_extend = False
|
||||
|
||||
def __init__(self, cfg):
|
||||
self.titlebar = tg.TitleBar('Frappy Cfg Editor')
|
||||
super().__init__([], tg.Writer, [self.titlebar], [tg.StatusBar(self)])
|
||||
# self.select_menu = MenuItem('select module', CUT_KEY)
|
||||
self.version_menu = [
|
||||
MenuItem('previous version', KEY.PREV_VERSION, self.prev_version),
|
||||
MenuItem('next version', KEY.NEXT_VERSION, self.next_version),
|
||||
MenuItem('restore this version', 'r', self.restore_version),
|
||||
MenuItem('copy module', KEY.CUT, self.cut_module),
|
||||
]
|
||||
self.main_menu = [
|
||||
MenuItem('show previous version', KEY.PREV_VERSION, self.prev_version),
|
||||
]
|
||||
self.detailed_menuitem = MenuItem('toggle detailed', KEY.TOGGLE_DETAILED, self.toggle_detailed)
|
||||
self.cut_menuitem = MenuItem('insert cut modules', KEY.PASTE, self.insert_cut)
|
||||
self.version_dir = Path('~/.local/share/frappy_config_editor').expanduser()
|
||||
self.version_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
self.cfgname = None
|
||||
self.pidfile = None
|
||||
self.dirty = False
|
||||
# cleanup pidfiles
|
||||
for file in self.version_dir.glob('*.pid'):
|
||||
pidstr = file.read_text()
|
||||
if not pid_exists(int(pidstr)):
|
||||
file.unlink()
|
||||
cfgpath = Path(cfg)
|
||||
if cfg != cfgpath.stem: # this is a filename
|
||||
if cfg.endswith('_cfg.py'):
|
||||
cfg = cfgpath.name[:-7]
|
||||
try:
|
||||
self.titlebar = tg.TitleBar('Frappy Cfg Editor')
|
||||
super().__init__([], tg.Writer, [self.titlebar], [tg.StatusBar(self)],
|
||||
help_file=Path(frappy.__file__).parents[1] / 'resources/editcurses/help.txt')
|
||||
# self.select_menu = MenuItem('select module', CUT_KEY)
|
||||
self.version_menu = [
|
||||
MenuItem('previous version', KEY.PREV_VERSION, self.prev_version),
|
||||
MenuItem('next version', KEY.NEXT_VERSION, self.next_version),
|
||||
MenuItem('restore this version', 'r', self.restore_version),
|
||||
MenuItem('copy module', KEY.CUT, self.cut_module),
|
||||
]
|
||||
self.main_menu = [
|
||||
MenuItem('show previous version', KEY.PREV_VERSION, self.prev_version),
|
||||
]
|
||||
self.detailed_menuitem = MenuItem('toggle detailed', KEY.TOGGLE_DETAILED, self.toggle_detailed)
|
||||
self.cut_menuitem = MenuItem('insert cut modules', KEY.PASTE, self.insert_cut)
|
||||
self.version_dir = Path('~/.local/share/frappy_config_editor').expanduser()
|
||||
self.version_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
self.cfgname = None
|
||||
self.pidfile = None
|
||||
self.dirty = False
|
||||
# cleanup pidfiles
|
||||
for file in self.version_dir.glob('*.pid'):
|
||||
pidstr = file.read_text()
|
||||
if not pid_exists(int(pidstr)):
|
||||
file.unlink()
|
||||
cfgpath = Path(cfg)
|
||||
if cfg != cfgpath.stem: # this is a filename
|
||||
if cfg.endswith('_cfg.py'):
|
||||
cfg = cfgpath.name[:-7]
|
||||
else:
|
||||
cfg = self.cfgpath.stem
|
||||
else:
|
||||
cfg = self.cfgpath.stem
|
||||
else:
|
||||
cfgpath = None
|
||||
self.filecontent = None
|
||||
self.set_node_name(cfg, cfgpath)
|
||||
self.init_from_content(self.filecontent)
|
||||
self.module_clipboard = {}
|
||||
self.pr_clipboard = {}
|
||||
cfgpath = None
|
||||
self.filecontent = None
|
||||
self.set_node_name(cfg, cfgpath)
|
||||
self.modules = {} # dict <module name> of <module class>
|
||||
self.init_from_content(self.filecontent)
|
||||
self.module_clipboard = {}
|
||||
self.pr_clipboard = {}
|
||||
except Exception:
|
||||
print(formatExtendedTraceback())
|
||||
|
||||
def get_menu(self):
|
||||
self.detailed_menuitem.name = 'summary view' if self.detailed else 'detailed view'
|
||||
@@ -671,6 +714,15 @@ class EditorMain(Main):
|
||||
self.widgets = widgets
|
||||
# self.dirty = False
|
||||
|
||||
def get_module_list(self, basecls, ownname):
|
||||
"""get module list for Attached.
|
||||
|
||||
filter by basecls ad exclude own name
|
||||
"""
|
||||
return [w.get_name() for w in self.widgets
|
||||
if w.get_name() != ownname and isinstance(w, ModuleWidget)
|
||||
and issubclass(w.get_class(), basecls)]
|
||||
|
||||
def toggle_detailed(self):
|
||||
self.detailed = not self.detailed
|
||||
self.offset = None # recalculate offset from screen pos
|
||||
|
||||
@@ -22,43 +22,39 @@
|
||||
import re
|
||||
import frappy
|
||||
import inspect
|
||||
import socket
|
||||
from pathlib import Path
|
||||
from ast import literal_eval
|
||||
from importlib import import_module
|
||||
from frappy.lib.comparestring import compare
|
||||
from frappy.config import process_file, Node
|
||||
from frappy.core import Module, Parameter, Property
|
||||
from frappy.core import Module, Parameter, Property, Attached
|
||||
from frappy.datatypes import DataType, EnumType
|
||||
from frappy.properties import Property, UNSET
|
||||
|
||||
|
||||
HEADER = "# please edit this file with frappy edit"
|
||||
HEADER = "# please edit this file with bin/frappy-edit"
|
||||
|
||||
|
||||
class Site:
|
||||
domain = 'psi.ch'
|
||||
frappy_subdir = 'frappy_psi'
|
||||
base = Path(frappy.__file__).parent.parent
|
||||
default_interface = 'tcp://10767'
|
||||
domain = None
|
||||
frappy_subdir = None
|
||||
equipment_postfix = 'your.postfix'
|
||||
|
||||
def __init__(self, domain='psi.ch', frappy_subdir='frappy_psi', default_interface='tcp://10767'):
|
||||
self.init(domain, frappy_subdir, default_interface)
|
||||
# for individual sites
|
||||
|
||||
def init(self, domain=None, frappy_subdir=None, default_interface=None):
|
||||
if domain:
|
||||
self.domain = domain
|
||||
if default_interface:
|
||||
self.default_interface = default_interface
|
||||
if frappy_subdir:
|
||||
self.packages = [v.name for v in self.base.glob('frappy_*')]
|
||||
try: # psi should be first
|
||||
self.packages.remove(frappy_subdir)
|
||||
self.packages.insert(0, frappy_subdir)
|
||||
def __init__(self):
|
||||
self.packages = [v.name for v in self.base.glob('frappy_*')]
|
||||
if self.frappy_subdir:
|
||||
try: # own subdir should be first
|
||||
self.packages.remove(self.frappy_subdir)
|
||||
self.packages.insert(0, self.frappy_subdir)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
site = Site()
|
||||
|
||||
|
||||
class NonStringType:
|
||||
"""any type except string"""
|
||||
def validate(self, value):
|
||||
@@ -67,7 +63,6 @@ class NonStringType:
|
||||
def __call__(self, value):
|
||||
return value
|
||||
|
||||
|
||||
def from_string(self, strvalue):
|
||||
"""convert from string """
|
||||
try:
|
||||
@@ -129,15 +124,26 @@ class Value:
|
||||
strvalue = None
|
||||
modulecls = None
|
||||
datatype = None
|
||||
default = None
|
||||
value = None
|
||||
completion = None
|
||||
|
||||
def __init__(self, value, datatype=None, error=None, from_string=False, callback=None):
|
||||
def __init__(self, value, datatype=None, error=None, pr=None,
|
||||
from_string=False, callback=None):
|
||||
if value is None:
|
||||
raise ValueError(datatype)
|
||||
self.datatype = datatype
|
||||
if isinstance(pr, Property):
|
||||
if pr.value is UNSET:
|
||||
self.default = None if pr.default is UNSET else pr.default
|
||||
else:
|
||||
self.default = pr.value
|
||||
elif isinstance(pr, Parameter):
|
||||
self.default = pr.default if pr.value is None else pr.value
|
||||
if isinstance(datatype, EnumType):
|
||||
self.completion = NameCompletion([v.name for v in datatype._enum.members])
|
||||
elif isinstance(pr, Attached):
|
||||
self.completion = ModuleNameCompletion(pr.basecls)
|
||||
self.error = error
|
||||
if callback:
|
||||
self.callback = callback
|
||||
@@ -193,6 +199,10 @@ class Value:
|
||||
pass
|
||||
return repr(self.strvalue)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.value, self.error, self.strvalue
|
||||
) == (other.value, other.error, other.strvalue)
|
||||
|
||||
def __repr__(self):
|
||||
return f'{type(self).__name__}({self.value!r}, {self.datatype!r})'
|
||||
|
||||
@@ -203,7 +213,7 @@ def get_datatype(pname, cls, value):
|
||||
:param pname: <property name> or <parameter name> or <parametger name>.<property name>
|
||||
:param cls: a frappy Module class or None
|
||||
:param value: the given value (needed only in case the datatype can not be determined)
|
||||
:return:
|
||||
:return: datatype, error or None, property or parameter or None
|
||||
"""
|
||||
param, _, prop = pname.partition('.')
|
||||
error = None
|
||||
@@ -215,18 +225,19 @@ def get_datatype(pname, cls, value):
|
||||
if propobj is None:
|
||||
error = f'{cls.__module__}.{cls.__qualname__}.{param}.{prop} is not configurable'
|
||||
else:
|
||||
return propobj.datatype, None
|
||||
return propobj.datatype, None, propobj
|
||||
elif prop:
|
||||
error = f'{cls.__module__}.{cls.__qualname__}.{param} is not a parameter'
|
||||
else:
|
||||
return prop_param.datatype, None
|
||||
return prop_param.datatype, None, prop_param
|
||||
# return prop_param.datatype, None, None
|
||||
except AttributeError:
|
||||
error = f'{cls.__module__}.{cls.__qualname__} is not a Frappy Module'
|
||||
except KeyError:
|
||||
error = f'{cls.__module__}.{cls.__qualname__}.{param} is not configurable'
|
||||
if isinstance(value, str):
|
||||
return stringtype, error
|
||||
return nonstringtype, error
|
||||
return stringtype, error, None
|
||||
return nonstringtype, error, None
|
||||
|
||||
|
||||
def make_value(pname, cls, value):
|
||||
@@ -394,7 +405,7 @@ def fix_equipment_id(name, equipment_id):
|
||||
"""normalize equipment id"""
|
||||
if re.match(r'[a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+)*$', equipment_id):
|
||||
return equipment_id
|
||||
return f'{name}.{site.domain}'
|
||||
return f'{name}.{site.equipment_postfix}'
|
||||
|
||||
|
||||
def fix_node_class(cls):
|
||||
@@ -603,11 +614,35 @@ def class_completion(value):
|
||||
class NameCompletion:
|
||||
def __init__(self, names):
|
||||
self.names = names
|
||||
self.nameset = set(names)
|
||||
|
||||
def __call__(self, value):
|
||||
if value in self.nameset:
|
||||
if value in self.names:
|
||||
return len(value), []
|
||||
return 0, [value] + list(get_suggested(value, self.names))
|
||||
|
||||
|
||||
class ModuleNameCompletion:
|
||||
def __init__(self, basecls):
|
||||
self.basecls = basecls
|
||||
|
||||
def get_names(self, basecls):
|
||||
return []
|
||||
|
||||
def __call__(self, value):
|
||||
names = self.get_names(self.basecls)
|
||||
if value in names:
|
||||
return len(value), []
|
||||
return 0, [value] + list(get_suggested(value, names))
|
||||
|
||||
|
||||
class SitePSI(Site):
|
||||
domain = '.psi.ch'
|
||||
frappy_subdir = 'frappy_psi'
|
||||
equipment_postfix = 'psi.ch'
|
||||
|
||||
|
||||
hostname = socket.getfqdn()
|
||||
for sitecls in [SitePSI]: # add here other sites
|
||||
if hostname.endswith(sitecls.domain):
|
||||
break
|
||||
site = sitecls()
|
||||
|
||||
@@ -100,6 +100,7 @@ class Key(int):
|
||||
return self.name
|
||||
|
||||
|
||||
# keep this file independent of frappy
|
||||
def clamp(*args):
|
||||
return sorted(args)[len(args) // 2]
|
||||
|
||||
@@ -303,6 +304,7 @@ class TextEdit(Widget):
|
||||
self.pos = None # cursor pos or None when not editing
|
||||
if value is None:
|
||||
raise ValueError('textedit')
|
||||
assert isinstance(value, str)
|
||||
self.value = value
|
||||
self.col_offset = 0
|
||||
self.finish_callback = finish_callback
|
||||
@@ -382,15 +384,6 @@ class TextEdit(Widget):
|
||||
self.value = self.finish(self.value, main)
|
||||
else:
|
||||
self.value = self.prev_value
|
||||
# if save:
|
||||
# try:
|
||||
# self.value = self.validator(self.value)
|
||||
# self.error = None
|
||||
# except Exception as e:
|
||||
# self.error = str(e)
|
||||
# # main.touch()
|
||||
# else:
|
||||
# self.value = self.prev_value
|
||||
self.pos = None
|
||||
|
||||
|
||||
@@ -415,6 +408,7 @@ class Completion:
|
||||
|
||||
class TextEditCompl(TextEdit):
|
||||
menu = None
|
||||
exc_info = None
|
||||
|
||||
def __init__(self, value, finish_callback, completion):
|
||||
super().__init__(value, finish_callback)
|
||||
@@ -425,25 +419,29 @@ class TextEditCompl(TextEdit):
|
||||
main.popupmenu = None
|
||||
|
||||
def get_selection_menu(self, main, value):
|
||||
self.completion_pos, selection = self.completion(value)
|
||||
if (self.pos >= self.completion_pos and
|
||||
self == main.completion_widget): # widget has not changed
|
||||
if selection:
|
||||
main.popup_offset = self.completion_pos - self.pos
|
||||
main.popupmenu = CompletionMenu(selection)
|
||||
else:
|
||||
main.popupmenu = None
|
||||
self.completion_widget = None
|
||||
self.exc_info = None
|
||||
try:
|
||||
self.completion_pos, selection = self.completion(value)
|
||||
if (self.pos >= self.completion_pos and
|
||||
self == main.completion_widget): # widget has not changed
|
||||
if selection:
|
||||
main.popup_offset = self.completion_pos - self.pos
|
||||
main.popupmenu = CompletionMenu(selection)
|
||||
else:
|
||||
main.popupmenu = None
|
||||
self.completion_widget = None
|
||||
except Exception:
|
||||
self.exc_info = sys.exc_info()
|
||||
|
||||
def get_key(self, main):
|
||||
# if self.completion is None:
|
||||
# return super().get_key(main)
|
||||
if self.highlighted:
|
||||
return super().get_key(main)
|
||||
main.completion_widget = self
|
||||
mkthread(self.get_selection_menu, main, self.value)
|
||||
self.selection_thread = mkthread(self.get_selection_menu, main, self.value)
|
||||
# self.get_selection_menu(main, self.value)
|
||||
key = super().get_key(main)
|
||||
if self.exc_info:
|
||||
raise self.exc_info[1].with_traceback(self.exc_info[2])
|
||||
self.menu = menu = main.popupmenu
|
||||
if not menu or self.highlighted or key != KEY.DOWN or self.pos < self.completion_pos:
|
||||
self.menu = main.popupmenu = None
|
||||
@@ -470,8 +468,6 @@ class TextEditCompl(TextEdit):
|
||||
|
||||
|
||||
class TextWidget(Widget):
|
||||
# minwidth = 16
|
||||
|
||||
def __init__(self, text):
|
||||
self.value = text
|
||||
|
||||
@@ -483,8 +479,6 @@ class TextWidget(Widget):
|
||||
|
||||
|
||||
class LineEdit(Widget):
|
||||
error = None
|
||||
|
||||
def __init__(self, labelwidget, valuewidget):
|
||||
self.labelwidget = labelwidget
|
||||
self.valuewidget = valuewidget
|
||||
@@ -503,7 +497,7 @@ class LineEdit(Widget):
|
||||
if key == KEY.LEFT:
|
||||
if name is not None:
|
||||
self.focus = 0
|
||||
continue
|
||||
continue
|
||||
return key
|
||||
key = self.labelwidget.handle(main)
|
||||
self.focus = 1
|
||||
@@ -517,9 +511,6 @@ class LineEdit(Widget):
|
||||
wr.norm(': ')
|
||||
wr.col = max(wr.col, wr.left + wr.leftwidth)
|
||||
self.valuewidget.draw(wr, in_focus and self.focus == 1)
|
||||
if self.error:
|
||||
wr.norm(' ')
|
||||
wr.error(self.error)
|
||||
|
||||
|
||||
class MultiLineEdit(Container):
|
||||
@@ -564,6 +555,9 @@ class MultiLineEdit(Container):
|
||||
self.highlighted = True
|
||||
key = main.get_key()
|
||||
self.highlighted = False
|
||||
return self.handle_inner(main, key)
|
||||
|
||||
def handle_inner(self, main, key, focus=None):
|
||||
if key in (KEY.RIGHT, KEY.TAB):
|
||||
self.set_focus(None, -1)
|
||||
self.next_key = KEY.RIGHT
|
||||
@@ -580,6 +574,8 @@ class MultiLineEdit(Container):
|
||||
elif key == KEY.LEFT:
|
||||
self.set_focus(None, 0)
|
||||
self.next_key = KEY.LEFT
|
||||
elif focus is not None:
|
||||
self.set_focus(focus, 0)
|
||||
else:
|
||||
return key
|
||||
while True:
|
||||
@@ -596,6 +592,65 @@ class MultiLineEdit(Container):
|
||||
return key
|
||||
|
||||
|
||||
class HelpWidget(MultiLineEdit):
|
||||
winsize = 25
|
||||
|
||||
def __init__(self, main):
|
||||
self.init_parent(main)
|
||||
if main.help_file:
|
||||
text = main.help_file.read_text()
|
||||
else:
|
||||
text = main.help_text
|
||||
self.original = text
|
||||
self.readonly = True
|
||||
self.readonly_menu = [
|
||||
MenuItem('edit help text', KEY.END_LINE),
|
||||
MenuItem('quit help', '^q'),
|
||||
]
|
||||
self.edit_menu = [
|
||||
MenuItem('quit help', '^q'),
|
||||
]
|
||||
super().__init__('', text, '')
|
||||
|
||||
def draw(self, wr, in_focus=False):
|
||||
wr.leftwidth = 0
|
||||
self.winsize = wr.height
|
||||
super().draw(wr, True)
|
||||
|
||||
def get_menu(self):
|
||||
if self.readonly:
|
||||
return self.readonly_menu
|
||||
return self.cut_menuitems + self.edit_menu
|
||||
|
||||
def handle(self, main):
|
||||
self.focus = 0
|
||||
main.offset = 0
|
||||
height = len(self.widgets)
|
||||
step = self.winsize // 2
|
||||
if self.readonly:
|
||||
while True:
|
||||
key = main.get_key()
|
||||
if key == KEY.UP:
|
||||
main.offset = max(0, main.offset - step)
|
||||
elif key == KEY.DOWN:
|
||||
main.offset = max(0, main.offset + step)
|
||||
else:
|
||||
if key != KEY.END_LINE:
|
||||
return
|
||||
self.readonly = False
|
||||
break
|
||||
self.focus = clamp(0, main.offset + step, height)
|
||||
try:
|
||||
super().handle_inner(main, None, self.focus)
|
||||
return
|
||||
finally:
|
||||
if self.original != self.value:
|
||||
main.popupmenu = menu = ConfirmDialog('save changes to help text?')
|
||||
if menu.handle(main):
|
||||
main.help_file.write_text(self.value)
|
||||
main.status(f'{main.help_file} changed')
|
||||
|
||||
|
||||
class LineOfMultiline(TextEdit):
|
||||
minwidth = -1
|
||||
change_high = None
|
||||
@@ -739,9 +794,6 @@ class DialogInput(Widget):
|
||||
return self.valuewidget.value
|
||||
|
||||
def draw(self, wr, in_focus=False):
|
||||
# gap = wr.width - self.width() - 2 - wr.left
|
||||
# if gap > 0:
|
||||
# self.valuewidget.minwidth = self.valuewidget.width() + gap
|
||||
wr.startrow()
|
||||
wr.menu(f'{self.label}: ')
|
||||
self.valuewidget.draw(wr, in_focus)
|
||||
@@ -789,10 +841,6 @@ class PushButton(Widget):
|
||||
if key in (KEY.RETURN, KEY.ENTER, 'y', 'Y'):
|
||||
self.parent.result = self.arg
|
||||
return KEY.GOTO_MAIN
|
||||
# if key == KEY.LEFT:
|
||||
# return KEY.UP
|
||||
# if key == KEY.RIGHT:
|
||||
# return KEY.DOWN
|
||||
return key
|
||||
|
||||
|
||||
@@ -900,7 +948,6 @@ class ContextMenu(PopUpMenu):
|
||||
"""
|
||||
self.menuitems = [MenuItem('')] + menuitems
|
||||
self.height = len(self.menuitems)
|
||||
# self.selection = [v.name for v in menuitems]
|
||||
self.hotkeys = {k: m for m in menuitems for k in m.keys}
|
||||
|
||||
def do_hotkey(self, key):
|
||||
@@ -935,7 +982,7 @@ class ContextMenu(PopUpMenu):
|
||||
|
||||
class BaseWriter:
|
||||
"""base for writer. does nothing else than keeping track of position"""
|
||||
highstyle = brightstyle = errorstyle = barstyle = menustyle = querystyle = warnstyle = buttonstyle = None
|
||||
highstyle = brightstyle = errorstyle = barstyle = menustyle = querystyle = warnstyle = buttonstyle = dimstyle = None
|
||||
errorflag = '! '
|
||||
|
||||
def __init__(self):
|
||||
@@ -955,6 +1002,9 @@ class BaseWriter:
|
||||
def norm(self, text, width=0):
|
||||
self.wr(text, extend_to=width)
|
||||
|
||||
def dim(self, text, width=0):
|
||||
self.wr(text, self.dimstyle, extend_to=width)
|
||||
|
||||
def bar(self, text, width=0):
|
||||
self.wr(text, self.barstyle, extend_to=width)
|
||||
|
||||
@@ -1011,6 +1061,7 @@ class Writer(BaseWriter):
|
||||
newoffset = None
|
||||
querystyle = curses.A_BOLD
|
||||
warnstyle = curses.A_REVERSE
|
||||
dimstyle = 0
|
||||
pairs = {}
|
||||
colors = {}
|
||||
nextcolor = 16
|
||||
@@ -1094,6 +1145,8 @@ class Writer(BaseWriter):
|
||||
cls.querystyle = cls.make_pair(black, light_green)
|
||||
yellow = 1000, 1000, 0
|
||||
cls.warnstyle = cls.make_pair(black, yellow)
|
||||
grey = 400, 400, 400
|
||||
cls.dimstyle = cls.make_pair(grey, dim_white)
|
||||
|
||||
def edit(self, text, width, pos):
|
||||
"""write text and set cursor at given pos
|
||||
@@ -1214,11 +1267,7 @@ class Main(HasWidgets):
|
||||
log = Widget.log
|
||||
leftwidth = 0.2 # if < 1: a fraction
|
||||
|
||||
def __init__(self, widgets, writercls, headers=(), footers=()):
|
||||
#KEY.add(
|
||||
# ESC=27, TAB=9, DEL=127, RETURN=13,
|
||||
# QUIT='^x', BEG_LINE='^a', END_LINE='^e', MENU='^c', CUT='^k', PASTE='^v', HELP='^g',
|
||||
# UNHANDLED=-1, GOTO_MAIN=-2, GO_UP=-3)
|
||||
def __init__(self, widgets, writercls, headers=(), footers=(), help_file=None):
|
||||
self.writercls = writercls
|
||||
self.focus = 0
|
||||
self.headers = headers
|
||||
@@ -1237,6 +1286,7 @@ class Main(HasWidgets):
|
||||
self.quit_action = None
|
||||
self.cut_lines = []
|
||||
self.cut_extend = False
|
||||
self.help_file = help_file
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
@@ -1252,7 +1302,7 @@ class Main(HasWidgets):
|
||||
self.scr.keypad(True)
|
||||
KEY.init()
|
||||
self.context_menu = [
|
||||
MenuItem('help', KEY.HELP, self.handle_help, None),
|
||||
MenuItem('help', KEY.HELP, self.handle_help, KEY.GOTO_MAIN),
|
||||
MenuItem('exit', KEY.QUIT, self.do_quit),
|
||||
]
|
||||
while self.quit_action is None or not self.quit_action():
|
||||
@@ -1332,20 +1382,30 @@ class Main(HasWidgets):
|
||||
self.cut_extend = False
|
||||
return key
|
||||
|
||||
def handle(self, main):
|
||||
if self.help_mode:
|
||||
widgets = self.widgets
|
||||
try:
|
||||
help_widget = HelpWidget(self,)
|
||||
self.widgets = [help_widget]
|
||||
key = super().handle(main)
|
||||
return key
|
||||
finally:
|
||||
self.widgets = widgets
|
||||
self.help_mode = False
|
||||
else:
|
||||
return super().handle(main)
|
||||
|
||||
def handle_help(self):
|
||||
self.help_mode = True
|
||||
self.popupmenu = None
|
||||
key = self.get_key()
|
||||
self.help_mode = False
|
||||
return key if key == KEY.QUIT else None
|
||||
|
||||
def persistent_status(self):
|
||||
return 'ctrl-X: context menu/help ctrl-Q: exit'
|
||||
|
||||
def current_row(self):
|
||||
if self.help_mode:
|
||||
# TODO: scroll help
|
||||
return 0
|
||||
return self.widgets[0].current_row()
|
||||
return super().current_row()
|
||||
|
||||
def refresh(self):
|
||||
@@ -1364,7 +1424,7 @@ class Main(HasWidgets):
|
||||
bot_limit = max(self.screen_row, int(end_scroll * 0.7))
|
||||
top_limit = min(self.screen_row, int(end_scroll * 0.2) + 1, bot_limit)
|
||||
if self.offset is None:
|
||||
self.offset = current_row - self.screen_row
|
||||
self.offset = current_row - max(topmargin, self.screen_row)
|
||||
offset = clamp(self.offset, current_row - top_limit, current_row - bot_limit)
|
||||
self.offset = clamp(offset, 0, offsetlimit)
|
||||
self.screen_row = current_row - self.offset
|
||||
@@ -1380,18 +1440,12 @@ class Main(HasWidgets):
|
||||
widget.draw(wr)
|
||||
wr.offset = self.offset
|
||||
wr.top, wr.bot = topmargin, bot
|
||||
if self.help_mode:
|
||||
for line in self.help_text.split('\n'):
|
||||
wr.startrow()
|
||||
wr.norm(line)
|
||||
else:
|
||||
self.draw_widgets(wr, True)
|
||||
self.draw_widgets(wr, True)
|
||||
wr.top, wr.bot = 0, wr.height
|
||||
if self.popupmenu:
|
||||
col = self.popup_offset
|
||||
col += self.cursor_pos[1]
|
||||
wr.move(current_row, col)
|
||||
# wr.move(*self.popupmenu.row_col)
|
||||
self.popupmenu.draw(wr)
|
||||
if wr.cursor_visible:
|
||||
self.scr.move(*self.cursor_pos)
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
#
|
||||
# (C) 2005, Rob W. W. Hooft (rob@hooft.net)
|
||||
#
|
||||
# Comparison algorithm as implemented by Reinhard Schneider and Chris Sander
|
||||
# for comparison of protein sequences, but implemented to compare two
|
||||
# ASCII strings. This can be very useful for command interpreters to account
|
||||
# for mistyped commands (use the routine "compare(s1, s2)" in here to get
|
||||
# a score for each possible command, and see if one stands out). The comparison
|
||||
# makes use of a similarity matrix for letters: in the protein case this is
|
||||
# normally a chemical functionality similarity, for our case this is a matrix
|
||||
# based on keys next to each other on a US Qwerty keyboard and on "capital
|
||||
# letters are similar to their lowercase equivalent"
|
||||
#
|
||||
# The algorithm does not cut corners: it is sure to find the absolute best
|
||||
# match between the two strings.
|
||||
#
|
||||
# No attempt has been made to make this efficient in time or memory. Time taken
|
||||
# and memory used is proportional to the product of the length of the input
|
||||
# strings. Use for strings longer than 25 characters is entirely for your own
|
||||
# risk.
|
||||
#
|
||||
# Use freely, but please attribute when using.
|
||||
# from http://starship.python.net/crew/hooft/
|
||||
|
||||
# How much does it cost to make a hole in one of the strings?
|
||||
GAPOPENPENALTY = -0.3
|
||||
# How much does it cost to elongate a hole in one of the strings?
|
||||
GAPELONGATIONPENALTY = -0.2
|
||||
# How much alike (0.0-1.0) are small and capital letters?
|
||||
CAPITALIZESCORE = 0.8
|
||||
# How much alike (0.0-1.0) are characters next to each other on a (US) keyboard?
|
||||
NEXTKEYSCORE = 0.6
|
||||
|
||||
comparematrix = {}
|
||||
|
||||
|
||||
def _makekeyboardmap():
|
||||
# different characters score 0.0, equal characters score 1.0
|
||||
for i in range(33, 126 + 1):
|
||||
for j in range(33, 126 + 1):
|
||||
comparematrix[i, j] = 0.0
|
||||
comparematrix[i, i] = 1.0
|
||||
# Capital and small letters are CAPITALIZESCORE alike
|
||||
capdist = ord('A') - ord('a')
|
||||
for i in range(ord('a'), ord('z') + 1):
|
||||
comparematrix[i, i + capdist] = CAPITALIZESCORE
|
||||
comparematrix[i + capdist, i] = CAPITALIZESCORE
|
||||
|
||||
# Keyboard layout, add some score for letters that are close together
|
||||
line1 = '`1234567890-= '
|
||||
line2 = ' qwertyuiop[] '
|
||||
line3 = ' asdfghjkl; '
|
||||
line4 = ' zxcvbnm,./ '
|
||||
for i in range(len(line1) - 1):
|
||||
_keyboardneighbour(line1[i], line1[i + 1])
|
||||
_keyboardneighbour(line2[i], line2[i + 1])
|
||||
_keyboardneighbour(line3[i], line3[i + 1])
|
||||
_keyboardneighbour(line4[i], line4[i + 1])
|
||||
_keyboardneighbour(line1[i], line2[i])
|
||||
_keyboardneighbour(line2[i], line3[i])
|
||||
_keyboardneighbour(line3[i], line4[i])
|
||||
_keyboardneighbour(line1[i], line2[i + 1])
|
||||
_keyboardneighbour(line2[i], line3[i + 1])
|
||||
_keyboardneighbour(line3[i], line4[i + 1])
|
||||
|
||||
|
||||
def _keyboardneighbour(c1, c2):
|
||||
i1 = ord(c1)
|
||||
i2 = ord(c2)
|
||||
if 33 <= i1 <= 126 and 33 <= i2 <= 126:
|
||||
comparematrix[i1, i2] = NEXTKEYSCORE
|
||||
comparematrix[i2, i1] = NEXTKEYSCORE
|
||||
|
||||
_makekeyboardmap()
|
||||
|
||||
|
||||
def compare(s1, s2):
|
||||
lh = {}
|
||||
gapped = {}
|
||||
|
||||
l1 = len(s1)
|
||||
l2 = len(s2)
|
||||
|
||||
if s1 == s2:
|
||||
return l1 + 1
|
||||
|
||||
# Top left of the matrix is "before the first character" in both directions
|
||||
lh[1, 1] = 0.0
|
||||
gapped[1, 1] = False
|
||||
|
||||
# Start with a gap in s1
|
||||
lh[2, 1] = GAPOPENPENALTY
|
||||
gapped[2, 1] = True
|
||||
|
||||
for ii in range(3, l1 + 2):
|
||||
lh[ii, 1] = lh[ii - 1, 1] + GAPELONGATIONPENALTY
|
||||
gapped[ii, 1] = True
|
||||
|
||||
# Start with a gap in s2
|
||||
lh[1, 2] = GAPOPENPENALTY
|
||||
gapped[1, 2] = True
|
||||
|
||||
for jj in range(3, l2 + 2):
|
||||
lh[1, jj] = lh[1, jj - 1] + GAPELONGATIONPENALTY
|
||||
gapped[1, jj] = True
|
||||
|
||||
# The main algorithm: for each point in the matrix decide what the best
|
||||
# route so far is, by comparing the diagonal route forward with the
|
||||
# possibility to open or elongate a gap either way.
|
||||
for jj in range(1, l2 + 1):
|
||||
for ii in range(1, l1 + 1):
|
||||
oc1 = ord(s1[ii - 1])
|
||||
oc2 = ord(s2[jj - 1])
|
||||
if 33 <= oc1 <= 126 and 33 <= oc2 <= 126:
|
||||
ld = comparematrix[oc1, oc2]
|
||||
elif oc1 == oc2:
|
||||
ld = 1.0
|
||||
else:
|
||||
ld = 0.0
|
||||
|
||||
if gapped[ii + 1, jj]:
|
||||
gph = GAPELONGATIONPENALTY
|
||||
else:
|
||||
gph = GAPOPENPENALTY
|
||||
|
||||
if gapped[ii, jj + 1]:
|
||||
gpv = GAPELONGATIONPENALTY
|
||||
else:
|
||||
gpv = GAPOPENPENALTY
|
||||
|
||||
s = lh[ii, jj] + ld
|
||||
sh = lh[ii + 1, jj] + gph
|
||||
sv = lh[ii, jj + 1] + gpv
|
||||
sd = max(sh, sv)
|
||||
if s >= sd:
|
||||
lh[ii + 1, jj + 1] = s
|
||||
gapped[ii + 1, jj + 1] = False
|
||||
else:
|
||||
lh[ii + 1, jj + 1] = sd
|
||||
gapped[ii + 1, jj + 1] = True
|
||||
# The highest alignment score is in the bottom right corner of the matrix,
|
||||
# behind the last character in both strings
|
||||
return lh[l1 + 1, l2 + 1]
|
||||
|
||||
|
||||
def test():
|
||||
assert compare('test', 'test') == len('test') + 1.0
|
||||
assert compare('Test', 'test') == len('test') - 1.0 + CAPITALIZESCORE
|
||||
assert compare('rest', 'test') == len('test') - 1.0 + NEXTKEYSCORE
|
||||
assert compare('rest', 'crest') == len('rest') + GAPOPENPENALTY
|
||||
Reference in New Issue
Block a user