Merge pull request #304 from CanonicalLtd/mwhudson/non-latin-keyboard-setting

handle non-latin keyboard layouts more intelligently
This commit is contained in:
Michael Hudson-Doyle 2018-03-28 14:44:13 +13:00 committed by GitHub
commit b98244b8c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 242 additions and 45 deletions

View File

@ -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')

View File

@ -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'])

View File

@ -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(),
},
},
}

View File

@ -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

View File

@ -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)