Merge pull request #304 from CanonicalLtd/mwhudson/non-latin-keyboard-setting
handle non-latin keyboard layouts more intelligently
This commit is contained in:
commit
b98244b8c8
|
@ -17,6 +17,7 @@ import logging
|
|||
|
||||
from subiquitycore.controller import BaseController
|
||||
|
||||
from subiquity.models.keyboard import KeyboardSetting
|
||||
from subiquity.ui.views import KeyboardView
|
||||
|
||||
log = logging.getLogger('subiquity.controllers.keyboard')
|
||||
|
@ -57,15 +58,15 @@ class KeyboardController(BaseController):
|
|||
if 'layout' in self.answers:
|
||||
layout = self.answers['layout']
|
||||
variant = self.answers.get('variant', '')
|
||||
self.done(layout, variant)
|
||||
self.done(KeyboardSetting(layout=layout, variant=variant))
|
||||
|
||||
def done(self, layout, variant):
|
||||
def done(self, setting):
|
||||
self.run_in_bg(
|
||||
lambda: self.model.set_keyboard(layout, variant),
|
||||
lambda: self.model.set_keyboard(setting),
|
||||
self._done)
|
||||
|
||||
def _done(self, fut):
|
||||
self.signal.emit_signal('next-screen')
|
||||
self.loop.set_alarm_in(0.0, lambda loop, ud: self.signal.emit_signal('next-screen'))
|
||||
|
||||
def cancel(self):
|
||||
self.signal.emit_signal('prev-screen')
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
|
||||
from collections import defaultdict
|
||||
import gzip
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
import attr
|
||||
|
||||
from subiquitycore.utils import run_command
|
||||
|
||||
log = logging.getLogger("subiquity.models.keyboard")
|
||||
|
@ -18,41 +18,135 @@ etc_default_keyboard_template = """\
|
|||
XKBMODEL="pc105"
|
||||
XKBLAYOUT="{layout}"
|
||||
XKBVARIANT="{variant}"
|
||||
XKBOPTIONS=""
|
||||
XKBOPTIONS="{options}"
|
||||
|
||||
BACKSPACE="guess"
|
||||
"""
|
||||
|
||||
@attr.s
|
||||
class KeyboardSetting:
|
||||
layout = attr.ib()
|
||||
variant = attr.ib(default='')
|
||||
toggle = attr.ib(default=None)
|
||||
|
||||
def render(self):
|
||||
options = ""
|
||||
if self.toggle:
|
||||
options = "grp:" + self.toggle
|
||||
variant = self.variant
|
||||
return etc_default_keyboard_template.format(
|
||||
layout=self.layout, variant=variant, options=options)
|
||||
|
||||
def latinizable(self):
|
||||
"""
|
||||
If this setting does not allow the typing of latin characters,
|
||||
return a setting that can be switched to one that can.
|
||||
"""
|
||||
if self.layout == 'rs':
|
||||
if self.variant.startswith('latin'):
|
||||
return self
|
||||
else:
|
||||
if self.variant == 'yz':
|
||||
new_variant = 'latinyz'
|
||||
elif self.variant == 'alternatequotes':
|
||||
new_variant = 'latinalternatequotes'
|
||||
else:
|
||||
new_variant = 'latin'
|
||||
return KeyboardSetting(layout='rs,rs', variant=new_variant + ',' + self.variant)
|
||||
elif self.layout == 'jp':
|
||||
if self.variant in ('106', 'common', 'OADG109A', 'nicola_f_bs', ''):
|
||||
return self
|
||||
else:
|
||||
return KeyboardSetting(layout='jp,jp', variant=',' + self.variant)
|
||||
elif self.layout == 'lt':
|
||||
if self.variant == 'us':
|
||||
return KeyboardSetting(layout='lt,lt', variant='us,')
|
||||
else:
|
||||
return KeyboardSetting(layout='lt,lt', variant=self.variant + ',us')
|
||||
elif self.layout == 'me':
|
||||
if self.variant == 'basic' or self.variant.startswith('latin'):
|
||||
return self
|
||||
else:
|
||||
return KeyboardSetting(layout='me,me', variant=self.variant + ',us')
|
||||
elif self.layout in standard_non_latin_layouts:
|
||||
return KeyboardSetting(layout='us,' + self.layout, variant=',' + self.variant)
|
||||
else:
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_config_file(cls, config_file):
|
||||
content = open(config_file).read()
|
||||
def optval(opt, default):
|
||||
match = re.search('(?m)^\s*%s=(.*)$'%(opt,), content)
|
||||
if match:
|
||||
r = match.group(1).strip('"')
|
||||
if r != '':
|
||||
return r
|
||||
return default
|
||||
XKBLAYOUT = optval("XKBLAYOUT", "us")
|
||||
XKBVARIANT = optval("XKBVARIANT", "")
|
||||
XKBOPTIONS = optval("XKBOPTIONS", "")
|
||||
toggle = None
|
||||
for option in XKBOPTIONS.split(','):
|
||||
if option.startswith('grp:'):
|
||||
toggle = option[4:]
|
||||
return cls(layout=XKBLAYOUT, variant=XKBVARIANT, toggle=toggle)
|
||||
|
||||
def for_ui(self):
|
||||
"""
|
||||
Attempt to guess a setting the user chose which resulted in the current config.
|
||||
Basically the inverse of latinizable().
|
||||
"""
|
||||
if ',' in self.layout:
|
||||
layout1, layout2 = self.layout.split(',', 1)
|
||||
else:
|
||||
layout1, layout2 = self.layout, ''
|
||||
if ',' in self.variant:
|
||||
variant1, variant2 = self.variant.split(',', 1)
|
||||
else:
|
||||
variant1, variant2 = self.variant, ''
|
||||
if self.layout == 'lt,lt':
|
||||
layout = layout1
|
||||
variant = variant1
|
||||
elif self.layout in ('rs,rs', 'us,rs', 'jp,jp', 'us,jp'):
|
||||
layout = layout2
|
||||
variant = variant2
|
||||
elif layout1 == 'us' and layout2 in standard_non_latin_layouts:
|
||||
layout = layout2
|
||||
variant = variant2
|
||||
elif ',' in self.layout:
|
||||
# Something unrecognized
|
||||
layout = 'us'
|
||||
variant = ''
|
||||
else:
|
||||
return self
|
||||
return KeyboardSetting(layout=layout, variant=variant)
|
||||
|
||||
|
||||
# Non-latin keyboard layouts that are handled in a uniform way
|
||||
standard_non_latin_layouts = set(
|
||||
('af', 'am', 'ara', 'ben', 'bd', 'bg', 'bt', 'by', 'et', 'ge',
|
||||
'gh', 'gr', 'guj', 'guru', 'il', ''in'', 'iq', 'ir', 'iku', 'kan',
|
||||
'kh', 'kz', 'la', 'lao', 'lk', 'kg', 'ma', 'mk', 'mm', 'mn', 'mv',
|
||||
'mal', 'np', 'ori', 'pk', 'ru', 'scc', 'sy', 'syr', 'tel', 'th',
|
||||
'tj', 'tam', 'tib', 'ua', 'ug', 'uz')
|
||||
)
|
||||
|
||||
|
||||
class KeyboardModel:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.layout = 'us'
|
||||
self.variant = ''
|
||||
self._kbnames_file = os.path.join(os.environ.get("SNAP", '.'), 'kbdnames.txt')
|
||||
self._clear()
|
||||
if os.path.exists(self.config_path):
|
||||
content = open(self.config_path).read()
|
||||
pat_tmpl = '(?m)^\s*%s=(.*)$'
|
||||
log.debug("%r", content)
|
||||
layout_match = re.search(pat_tmpl%("XKBLAYOUT",), content)
|
||||
if layout_match:
|
||||
log.debug("%s", layout_match)
|
||||
self.layout = layout_match.group(1).strip('"')
|
||||
variant_match = re.search(pat_tmpl%("XKBVARIANT",), content)
|
||||
if variant_match:
|
||||
log.debug("%s", variant_match)
|
||||
self.variant = variant_match.group(1).strip('"')
|
||||
if self.variant == '':
|
||||
self.variant = None
|
||||
self.setting = KeyboardSetting.from_config_file(self.config_path)
|
||||
else:
|
||||
self.setting = KeyboardSetting(layout='us')
|
||||
|
||||
@property
|
||||
def config_path(self):
|
||||
return os.path.join(self.root, 'etc', 'default', 'keyboard')
|
||||
|
||||
@property
|
||||
def config_content(self):
|
||||
return etc_default_keyboard_template.format(layout=self.layout, variant=self.variant)
|
||||
|
||||
def has_language(self, code):
|
||||
self.load_language(code)
|
||||
return bool(self.layouts)
|
||||
|
@ -88,17 +182,16 @@ class KeyboardModel:
|
|||
def lookup(self, code):
|
||||
if ':' in code:
|
||||
layout_code, variant_code = code.split(":", 1)
|
||||
return self.layouts.get(layout_code, '?'), self.variants.get(layout_code).get(variant_code, '?')
|
||||
return self.layouts.get(layout_code, '?'), self.variants.get(layout_code, {}).get(variant_code, '?')
|
||||
else:
|
||||
return self.layouts.get(code, '?'), None
|
||||
|
||||
def set_keyboard(self, layout, variant):
|
||||
def set_keyboard(self, setting):
|
||||
path = self.config_path
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
self.layout = layout
|
||||
self.variant = variant
|
||||
self.setting = setting
|
||||
with open(path, 'w') as fp:
|
||||
fp.write(self.config_content)
|
||||
fp.write(self.setting.render())
|
||||
if self.root == '/':
|
||||
run_command(['setupcon', '--save', '--force'])
|
||||
run_command(['/snap/bin/subiquity.subiquity-loadkeys'])
|
||||
|
|
|
@ -129,7 +129,7 @@ class SubiquityModel:
|
|||
'write_files': {
|
||||
'etc_default_keyboard': {
|
||||
'path': 'etc/default/keyboard',
|
||||
'content': self.keyboard.config_content,
|
||||
'content': self.keyboard.setting.render(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -18,11 +18,17 @@ import logging
|
|||
from urwid import (
|
||||
connect_signal,
|
||||
LineBox,
|
||||
Padding as UrwidPadding,
|
||||
SolidFill,
|
||||
Text,
|
||||
WidgetWrap,
|
||||
)
|
||||
|
||||
from subiquitycore.ui.buttons import ok_btn, other_btn
|
||||
from subiquitycore.ui.buttons import (
|
||||
cancel_btn,
|
||||
ok_btn,
|
||||
other_btn,
|
||||
)
|
||||
from subiquitycore.ui.container import (
|
||||
Columns,
|
||||
ListBox,
|
||||
|
@ -32,10 +38,11 @@ from subiquitycore.ui.form import (
|
|||
ChoiceField,
|
||||
Form,
|
||||
)
|
||||
from subiquitycore.ui.selector import Option
|
||||
from subiquitycore.ui.selector import Selector, Option
|
||||
from subiquitycore.ui.utils import button_pile, Color, Padding
|
||||
from subiquitycore.view import BaseView
|
||||
|
||||
from subiquity.models.keyboard import KeyboardSetting
|
||||
from subiquity.ui.spinner import Spinner
|
||||
from subiquity.ui.views import pc105
|
||||
|
||||
|
@ -266,6 +273,80 @@ class ApplyingConfig(WidgetWrap):
|
|||
])))
|
||||
|
||||
|
||||
toggle_text = _("""\
|
||||
You will need a way to toggle the keyboard between the national layout and the standard Latin layout.
|
||||
|
||||
Right Alt or Caps Lock keys are often chosen for ergonomic reasons (in the latter case, use the combination Shift+Caps Lock for normal Caps toggle). Alt+Shift is also a popular combination; it will however lose its usual behavior in Emacs and other programs that use it for specific needs.
|
||||
|
||||
Not all listed keys are present on all keyboards. """)
|
||||
|
||||
|
||||
toggle_options = [
|
||||
(_('Caps Lock'), True, 'caps_toggle'),
|
||||
(_('Right Alt (AltGr)'), True, 'toggle'),
|
||||
(_('Right Control'), True, 'rctrl_toggle'),
|
||||
(_('Right Shift'), True, 'rshift_toggle'),
|
||||
(_('Right Logo key'), True, 'rwin_toggle'),
|
||||
(_('Menu key'), True, 'menu_toggle'),
|
||||
(_('Alt+Shift'), True, 'alt_shift_toggle'),
|
||||
(_('Control+Shift'), True, 'ctrl_shift_toggle'),
|
||||
(_('Control+Alt'), True, 'ctrl_alt_toggle'),
|
||||
(_('Alt+Caps Lock'), True, 'alt_caps_toggle'),
|
||||
(_('Left Control+Left Shift'), True, 'lctrl_lshift_toggle'),
|
||||
(_('Left Alt'), True, 'lalt_toggle'),
|
||||
(_('Left Control'), True, 'lctrl_toggle'),
|
||||
(_('Left Shift'), True, 'lshift_toggle'),
|
||||
(_('Left Logo key'), True, 'lwin_toggle'),
|
||||
(_('Scroll Lock key'), True, 'sclk_toggle'),
|
||||
(_('No toggling'), True, None),
|
||||
]
|
||||
|
||||
|
||||
class ToggleQuestion(WidgetWrap):
|
||||
|
||||
def __init__(self, parent, setting):
|
||||
self.parent = parent
|
||||
self.setting = setting
|
||||
self.selector = Selector(toggle_options)
|
||||
self.selector.value = 'alt_shift_toggle'
|
||||
if self.parent.model.setting.toggle:
|
||||
try:
|
||||
self.selector.value = self.parent.model.setting.toggle
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
pile = Pile([
|
||||
ListBox([
|
||||
Text(_(toggle_text)),
|
||||
]),
|
||||
(1, SolidFill(" ")),
|
||||
('pack', Padding.center_79(Columns([
|
||||
('pack', Text(_("Shortcut: "))),
|
||||
Color.string_input(self.selector),
|
||||
]))),
|
||||
(1, SolidFill(" ")),
|
||||
('pack', button_pile([
|
||||
ok_btn(label=_("OK"), on_press=self.ok),
|
||||
cancel_btn(label=_("Cancel"), on_press=self.cancel),
|
||||
])),
|
||||
])
|
||||
pile.focus_position = 4
|
||||
super().__init__(
|
||||
LineBox(
|
||||
UrwidPadding(
|
||||
pile,
|
||||
left=1, right=1),
|
||||
_("Select layout toggle")))
|
||||
|
||||
def ok(self, sender):
|
||||
self.parent.remove_overlay()
|
||||
self.setting.toggle = self.selector.value
|
||||
self.parent.really_done(self.setting)
|
||||
|
||||
def cancel(self, sender):
|
||||
self.parent.remove_overlay()
|
||||
|
||||
|
||||
class KeyboardForm(Form):
|
||||
|
||||
cancel_label = _("Back")
|
||||
|
@ -290,9 +371,10 @@ class KeyboardView(BaseView):
|
|||
connect_signal(self.form, 'cancel', self.cancel)
|
||||
connect_signal(self.form.layout.widget, "select", self.select_layout)
|
||||
self.form.layout.widget._options = opts
|
||||
setting = model.setting.for_ui()
|
||||
try:
|
||||
self.form.layout.widget.value = model.layout
|
||||
self.form.variant.widget.value = model.variant
|
||||
self.form.layout.widget.value = setting.layout
|
||||
self.form.variant.widget.value = setting.variant
|
||||
except AttributeError:
|
||||
# Don't crash on pre-existing invalid config.
|
||||
pass
|
||||
|
@ -337,9 +419,17 @@ class KeyboardView(BaseView):
|
|||
variant = ''
|
||||
if self.form.variant.widget.value is not None:
|
||||
variant = self.form.variant.widget.value
|
||||
setting = KeyboardSetting(layout=layout, variant=variant)
|
||||
new_setting = setting.latinizable()
|
||||
if new_setting != setting:
|
||||
self.show_overlay(ToggleQuestion(self, new_setting), height=('relative', 100))
|
||||
return
|
||||
self.really_done(setting)
|
||||
|
||||
def really_done(self, setting):
|
||||
ac = ApplyingConfig(self.controller.loop)
|
||||
self.show_overlay(ac, width=ac.width, min_width=None)
|
||||
self.controller.done(layout, variant)
|
||||
self.controller.done(setting)
|
||||
|
||||
def cancel(self, result=None):
|
||||
self.controller.cancel()
|
||||
|
@ -347,10 +437,17 @@ class KeyboardView(BaseView):
|
|||
def select_layout(self, sender, layout):
|
||||
log.debug("%s", layout)
|
||||
opts = []
|
||||
for variant, variant_desc in self.model.variants[layout].items():
|
||||
default_i = -1
|
||||
for i, (variant, variant_desc) in enumerate(self.model.variants[layout].items()):
|
||||
if variant == "":
|
||||
default_i = i
|
||||
opts.append(Option((variant_desc, True, variant)))
|
||||
opts.sort(key=lambda o:o.label)
|
||||
opts.insert(0, Option(("default", True, None)))
|
||||
if default_i < 0:
|
||||
opts.insert(0, Option(("default", True, "")))
|
||||
self.form.variant.widget._options = opts
|
||||
self.form.variant.widget.index = 0
|
||||
if default_i < 0:
|
||||
self.form.variant.widget.index = 0
|
||||
else:
|
||||
self.form.variant.widget.index = default_i
|
||||
self.form.variant.enabled = len(opts) > 1
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
Contains some default key navigations
|
||||
"""
|
||||
|
||||
from urwid import Columns, Overlay, Pile, Text, WidgetWrap
|
||||
from urwid import Columns, Overlay, Pile, SolidFill, Text, WidgetWrap
|
||||
|
||||
|
||||
class BaseView(WidgetWrap):
|
||||
|
@ -42,14 +42,20 @@ class BaseView(WidgetWrap):
|
|||
if isinstance(kw['width'], int):
|
||||
kw['width'] += 2*PADDING
|
||||
args.update(kw)
|
||||
if 'height' in kw:
|
||||
f = SolidFill(" ")
|
||||
p = 1
|
||||
else:
|
||||
f = Text("")
|
||||
p = 'pack'
|
||||
top = Pile([
|
||||
('pack', Text("")),
|
||||
(p, f),
|
||||
Columns([
|
||||
(PADDING, Text("")),
|
||||
(PADDING, f),
|
||||
overlay_widget,
|
||||
(PADDING, Text(""))
|
||||
(PADDING, f)
|
||||
]),
|
||||
('pack', Text("")),
|
||||
(p, f),
|
||||
])
|
||||
self._w = Overlay(top_w=top, bottom_w=self._w, **args)
|
||||
|
||||
|
|
Loading…
Reference in New Issue