Skip to content

Commit d29d58d

Browse files
authored
Further KeyboardPage improvements (#225)
*‌ re-style the re-orderable listview *‌ add input source service * add delete/add input source feature
1 parent 0060a98 commit d29d58d

File tree

8 files changed

+257
-29
lines changed

8 files changed

+257
-29
lines changed

lib/main.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:settings/l10n/l10n.dart';
66
import 'package:settings/schemas/schemas.dart';
77
import 'package:settings/services/bluetooth_service.dart';
88
import 'package:settings/services/hostname_service.dart';
9+
import 'package:settings/services/input_source_service.dart';
910
import 'package:settings/services/power_profile_service.dart';
1011
import 'package:settings/services/power_settings_service.dart';
1112
import 'package:settings/services/settings_service.dart';
@@ -61,6 +62,9 @@ void main() async {
6162
Provider<BlueZClient>(
6263
create: (_) => BlueZClient(),
6364
dispose: (_, client) => client.close(),
65+
),
66+
Provider<InputSourceService>(
67+
create: (_) => InputSourceService(),
6468
)
6569
],
6670
child: const UbuntuSettingsApp(),
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import 'dart:io';
2+
3+
import 'package:xml/xml.dart';
4+
5+
class InputSourceService {
6+
static const pathToXml = '/usr/share/X11/xkb/rules/base.xml';
7+
late final List<InputSource> inputSources;
8+
9+
InputSourceService() {
10+
inputSources = _loadInputSources();
11+
}
12+
13+
List<InputSource> _loadInputSources() {
14+
final document = XmlDocument.parse(File(pathToXml).readAsStringSync());
15+
16+
final layouts = document.findAllElements('layout');
17+
return layouts
18+
.map(
19+
(layout) => InputSource(
20+
variants: layout.getElement('variantList') != null
21+
? layout
22+
.getElement('variantList')!
23+
.childElements
24+
.map((variant) => InputSourceVariant(
25+
name: variant
26+
.getElement('configItem')!
27+
.getElement('name')!
28+
.innerText,
29+
description: variant
30+
.getElement('configItem')!
31+
.getElement('description')!
32+
.innerText,
33+
))
34+
.toList()
35+
: [],
36+
description: layout
37+
.getElement('configItem')
38+
?.getElement('description')
39+
?.innerText,
40+
name: layout
41+
.getElement('configItem')
42+
?.getElement('name')
43+
?.innerText),
44+
)
45+
.toList();
46+
}
47+
}
48+
49+
class InputSource {
50+
final String? name;
51+
final String? description;
52+
final List<InputSourceVariant> variants;
53+
54+
InputSource({this.name, this.description, required this.variants});
55+
}
56+
57+
class InputSourceVariant {
58+
final String? name;
59+
final String? description;
60+
61+
InputSourceVariant({this.name, this.description});
62+
}

lib/view/pages/keyboard/input_source_model.dart

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
1+
import 'dart:io';
2+
13
import 'package:dbus/dbus.dart';
24
import 'package:gsettings/gsettings.dart';
35
import 'package:safe_change_notifier/safe_change_notifier.dart';
46
import 'package:settings/schemas/schemas.dart';
7+
import 'package:settings/services/input_source_service.dart';
58
import 'package:settings/services/settings_service.dart';
69

710
class InputSourceModel extends SafeChangeNotifier {
811
final Settings? _inputSourceSettings;
912
static const _perWindowKey = 'per-window';
1013
static const _sourcesKey = 'sources';
1114
static const _mruSourcesKey = 'mru-sources';
15+
final List<InputSource> inputSources;
1216

13-
InputSourceModel(SettingsService service)
14-
: _inputSourceSettings = service.lookup(schemaInputSources) {
17+
InputSourceModel(
18+
SettingsService settingsService, InputSourceService inputSourceService)
19+
: _inputSourceSettings = settingsService.lookup(schemaInputSources),
20+
inputSources = inputSourceService.inputSources {
1521
_inputSourceSettings?.addListener(notifyListeners);
1622
}
1723

@@ -44,7 +50,7 @@ class InputSourceModel extends SafeChangeNotifier {
4450
return inputTypes ?? [];
4551
}
4652

47-
setInputSources(List<String>? inputTypes) async {
53+
Future<void> setInputSources(List<String>? inputTypes) async {
4854
final settings = GSettings(schemaInputSources);
4955

5056
final DBusArray array = DBusArray(DBusSignature('(ss)'), [
@@ -59,4 +65,23 @@ class InputSourceModel extends SafeChangeNotifier {
5965

6066
notifyListeners();
6167
}
68+
69+
Future<void> removeInputSource(String inputType) async {
70+
final types = await getInputSources();
71+
if (types!.length > 1) {
72+
types.remove(inputType);
73+
await setInputSources(types);
74+
}
75+
}
76+
77+
Future<void> addInputSource(String inputSource) async {
78+
final sources = await getInputSources();
79+
sources?.add(inputSource);
80+
await setInputSources(sources);
81+
}
82+
83+
Future<void> showKeyboardLayout(String inputType) async {
84+
await Process.run('gkbd-keyboard-display',
85+
['-l', inputType.split('+').first, inputType.split('+').last, '&']);
86+
}
6287
}

lib/view/pages/keyboard/input_source_section.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@ class InputSourceSection extends StatelessWidget {
1313
return Column(children: [
1414
YaruSection(headline: 'Change input sources', children: [
1515
RadioListTile(
16+
shape:
17+
RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
1618
title: const Text('Use the same input for all windows'),
1719
value: false,
1820
groupValue: inputSourceModel.perWindow,
1921
onChanged: (_) => inputSourceModel.perWindow = false),
2022
RadioListTile(
23+
shape:
24+
RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
2125
title: const Text('Give each window its own input source'),
2226
value: true,
2327
groupValue: inputSourceModel.perWindow,
Lines changed: 149 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter/material.dart';
22
import 'package:provider/provider.dart';
33
import 'package:settings/view/pages/keyboard/input_source_model.dart';
4+
import 'package:yaru_icons/yaru_icons.dart';
45
import 'package:yaru_widgets/yaru_widgets.dart';
56

67
class InputSourceSelectionSection extends StatelessWidget {
@@ -18,28 +19,155 @@ class InputSourceSelectionSection extends StatelessWidget {
1819
if (!snapshot.hasData) {
1920
return const CircularProgressIndicator();
2021
}
21-
return YaruSection(headline: 'Input Sources', children: [
22-
ReorderableListView(
23-
shrinkWrap: true,
24-
children: <Widget>[
25-
for (int index = 0; index < snapshot.data!.length; index++)
26-
ListTile(
27-
key: Key('$index'),
28-
title: Text('${index + 1}. ${snapshot.data![index]}'),
29-
),
30-
],
31-
onReorder: (int oldIndex, int newIndex) async {
32-
final sources = snapshot.data!;
33-
if (oldIndex < newIndex) {
34-
newIndex -= 1;
35-
}
36-
final item = snapshot.data!.removeAt(oldIndex);
37-
sources.insert(newIndex, item);
38-
model.setInputSources(sources);
39-
},
40-
)
41-
]);
22+
return YaruSection(
23+
headline: 'Input Sources',
24+
headerWidget: SizedBox(
25+
height: 40,
26+
width: 40,
27+
child: TextButton(
28+
onPressed: () => showDialog(
29+
context: context,
30+
builder: (context) => ChangeNotifierProvider.value(
31+
value: model,
32+
child: const _AddKeymapDialog(),
33+
)),
34+
child: const Icon(YaruIcons.plus)),
35+
),
36+
children: [
37+
ReorderableListView(
38+
buildDefaultDragHandles: false,
39+
shrinkWrap: true,
40+
children: <Widget>[
41+
for (int index = 0; index < snapshot.data!.length; index++)
42+
ReorderableDragStartListener(
43+
key: Key('$index'),
44+
index: index,
45+
child: ChangeNotifierProvider.value(
46+
value: model,
47+
child: _InputTypeRow(
48+
inputType: snapshot.data![index],
49+
),
50+
),
51+
),
52+
],
53+
onReorder: (int oldIndex, int newIndex) async {
54+
final sources = snapshot.data!;
55+
if (oldIndex < newIndex) {
56+
newIndex -= 1;
57+
}
58+
final item = snapshot.data!.removeAt(oldIndex);
59+
sources.insert(newIndex, item);
60+
model.setInputSources(sources);
61+
},
62+
),
63+
]);
4264
},
4365
);
4466
}
4567
}
68+
69+
class _InputTypeRow extends StatelessWidget {
70+
const _InputTypeRow({
71+
Key? key,
72+
required this.inputType,
73+
}) : super(key: key);
74+
75+
final String inputType;
76+
77+
@override
78+
Widget build(BuildContext context) {
79+
final model = context.watch<InputSourceModel>();
80+
return YaruRow(
81+
actionWidget: Row(
82+
children: [
83+
YaruOptionButton(
84+
onPressed: () => model.showKeyboardLayout(inputType),
85+
iconData: YaruIcons.input_keyboard),
86+
const SizedBox(
87+
width: 10,
88+
),
89+
YaruOptionButton(
90+
onPressed: () => model.removeInputSource(inputType),
91+
iconData: YaruIcons.trash)
92+
],
93+
),
94+
trailingWidget: Text(
95+
inputType,
96+
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.normal),
97+
),
98+
leadingWidget: Icon(
99+
YaruIcons.drag_handle,
100+
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
101+
));
102+
}
103+
}
104+
105+
class _AddKeymapDialog extends StatefulWidget {
106+
const _AddKeymapDialog({Key? key}) : super(key: key);
107+
108+
@override
109+
State<_AddKeymapDialog> createState() => _AddKeymapDialogState();
110+
}
111+
112+
class _AddKeymapDialogState extends State<_AddKeymapDialog> {
113+
int tabbedIndex = 0;
114+
bool variantsLoad = false;
115+
116+
@override
117+
Widget build(BuildContext context) {
118+
final model = context.watch<InputSourceModel>();
119+
return variantsLoad == false
120+
? YaruSimpleDialog(
121+
title: 'Add Keymap',
122+
closeIconData: YaruIcons.window_close,
123+
children: [
124+
for (var i = 0; i < model.inputSources.length; i++)
125+
InkWell(
126+
borderRadius: BorderRadius.circular(4.0),
127+
onTap: () => setState(() {
128+
tabbedIndex = i;
129+
variantsLoad = true;
130+
}),
131+
child: YaruRow(
132+
width: 100,
133+
description: model.inputSources[i].name,
134+
actionWidget: const SizedBox(),
135+
trailingWidget: Text(model.inputSources[i].description!),
136+
),
137+
),
138+
])
139+
: YaruSimpleDialog(
140+
title: (model.inputSources[tabbedIndex].name ?? '') +
141+
': ' +
142+
(model.inputSources[tabbedIndex].description ?? ''),
143+
closeIconData: YaruIcons.window_close,
144+
children: [
145+
for (var variant in model.inputSources[tabbedIndex].variants)
146+
InkWell(
147+
onTap: () {
148+
if (model.inputSources[tabbedIndex].name != null &&
149+
variant.name != null) {
150+
model.addInputSource(
151+
model.inputSources[tabbedIndex].name! +
152+
'+' +
153+
variant.name!);
154+
}
155+
156+
Navigator.of(context).pop();
157+
setState(() {});
158+
},
159+
borderRadius: BorderRadius.circular(4.0),
160+
child: YaruRow(
161+
width: 100,
162+
trailingWidget: Text(variant.description ?? ''),
163+
description: variant.name ?? '',
164+
actionWidget: const SizedBox()),
165+
),
166+
TextButton(
167+
onPressed: () => setState(() {
168+
variantsLoad = false;
169+
}),
170+
child: const Icon(YaruIcons.pan_start))
171+
]);
172+
}
173+
}

lib/view/pages/keyboard/keyboard_settings_page.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:provider/provider.dart';
3+
import 'package:settings/services/input_source_service.dart';
34
import 'package:settings/services/settings_service.dart';
45
import 'package:settings/view/pages/keyboard/input_source_model.dart';
56
import 'package:settings/view/pages/keyboard/input_source_section.dart';
@@ -14,20 +15,23 @@ class KeyboardSettingsPage extends StatelessWidget {
1415

1516
@override
1617
Widget build(BuildContext context) {
17-
final service = Provider.of<SettingsService>(context, listen: false);
18+
final settingsService =
19+
Provider.of<SettingsService>(context, listen: false);
20+
final inputSourceService =
21+
Provider.of<InputSourceService>(context, listen: false);
1822

1923
return Column(
2024
children: [
2125
ChangeNotifierProvider(
22-
create: (_) => InputSourceModel(service),
26+
create: (_) => InputSourceModel(settingsService, inputSourceService),
2327
child: const InputSourceSelectionSection(),
2428
),
2529
ChangeNotifierProvider(
26-
create: (_) => InputSourceModel(service),
30+
create: (_) => InputSourceModel(settingsService, inputSourceService),
2731
child: const InputSourceSection(),
2832
),
2933
ChangeNotifierProvider(
30-
create: (_) => SpecialCharactersModel(service),
34+
create: (_) => SpecialCharactersModel(settingsService),
3135
child: const SpecialCharactersSection(),
3236
),
3337
],

pubspec.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -859,7 +859,7 @@ packages:
859859
source: hosted
860860
version: "0.2.0"
861861
xml:
862-
dependency: transitive
862+
dependency: "direct main"
863863
description:
864864
name: xml
865865
url: "https://pub.dartlang.org"

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ dependencies:
4848
flutter_svg: ^1.0.0
4949
flutter_spinbox: ^0.7.0
5050
http: ^0.13.4
51+
xml: ^5.3.1
5152

5253
dev_dependencies:
5354
build_runner: ^2.1.2

0 commit comments

Comments
 (0)