Serenity Operating System
1/*
2 * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
3 * Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
4 * Copyright (c) 2022, the SerenityOS developers.
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include "KeyboardSettingsWidget.h"
10#include <AK/JsonObject.h>
11#include <AK/QuickSort.h>
12#include <Applications/KeyboardSettings/KeyboardWidgetGML.h>
13#include <Applications/KeyboardSettings/KeymapDialogGML.h>
14#include <LibConfig/Client.h>
15#include <LibCore/DeprecatedFile.h>
16#include <LibCore/Directory.h>
17#include <LibGUI/Application.h>
18#include <LibGUI/ComboBox.h>
19#include <LibGUI/Dialog.h>
20#include <LibGUI/ItemListModel.h>
21#include <LibGUI/Label.h>
22#include <LibGUI/MessageBox.h>
23#include <LibGUI/Model.h>
24#include <LibGUI/Process.h>
25#include <LibGUI/Widget.h>
26#include <LibGUI/Window.h>
27#include <LibGfx/Font/FontDatabase.h>
28#include <LibKeyboard/CharacterMap.h>
29#include <spawn.h>
30
31class KeymapSelectionDialog final : public GUI::Dialog {
32 C_OBJECT(KeymapSelectionDialog)
33public:
34 virtual ~KeymapSelectionDialog() override = default;
35
36 static DeprecatedString select_keymap(Window* parent_window, Vector<DeprecatedString> const& selected_keymaps)
37 {
38 auto dialog = KeymapSelectionDialog::construct(parent_window, selected_keymaps);
39 dialog->set_title("Add a keymap");
40
41 if (dialog->exec() == ExecResult::OK) {
42 return dialog->selected_keymap();
43 }
44
45 return DeprecatedString::empty();
46 }
47
48 DeprecatedString selected_keymap() { return m_selected_keymap; }
49
50private:
51 KeymapSelectionDialog(Window* parent_window, Vector<DeprecatedString> const& selected_keymaps)
52 : Dialog(parent_window)
53 {
54 auto widget = set_main_widget<GUI::Widget>().release_value_but_fixme_should_propagate_errors();
55 widget->load_from_gml(keymap_dialog_gml).release_value_but_fixme_should_propagate_errors();
56
57 set_resizable(false);
58 resize(190, 54);
59
60 set_icon(parent_window->icon());
61
62 auto iterator_result = Core::Directory::for_each_entry("/res/keymaps/"sv, Core::DirIterator::Flags::SkipDots, [&](auto const& entry, auto&) -> ErrorOr<IterationDecision> {
63 auto basename = entry.name.replace(".json"sv, ""sv, ReplaceMode::FirstOnly);
64 if (selected_keymaps.find(basename).is_end())
65 m_character_map_files.append(basename);
66 return IterationDecision::Continue;
67 });
68
69 if (iterator_result.is_error()) {
70 GUI::MessageBox::show(nullptr, DeprecatedString::formatted("Error on reading mapping file list: {}", iterator_result.error()), "Keyboard settings"sv, GUI::MessageBox::Type::Error);
71 GUI::Application::the()->quit(-1);
72 }
73
74 quick_sort(m_character_map_files);
75
76 m_selected_keymap = m_character_map_files.first();
77
78 m_keymaps_combobox = *widget->find_descendant_of_type_named<GUI::ComboBox>("keymaps_combobox");
79 m_keymaps_combobox->set_only_allow_values_from_model(true);
80 m_keymaps_combobox->set_model(*GUI::ItemListModel<DeprecatedString>::create(m_character_map_files));
81 m_keymaps_combobox->set_selected_index(0);
82
83 m_keymaps_combobox->on_change = [&](auto& keymap, auto) {
84 m_selected_keymap = keymap;
85 };
86
87 auto& ok_button = *widget->find_descendant_of_type_named<GUI::Button>("ok_button");
88 ok_button.on_click = [this](auto) {
89 done(ExecResult::OK);
90 };
91
92 auto& cancel_button = *widget->find_descendant_of_type_named<GUI::Button>("cancel_button");
93 cancel_button.on_click = [this](auto) {
94 done(ExecResult::Cancel);
95 };
96 }
97
98 RefPtr<GUI::ComboBox> m_keymaps_combobox;
99 Vector<DeprecatedString> m_character_map_files;
100 DeprecatedString m_selected_keymap;
101};
102
103class KeymapModel final : public GUI::Model {
104public:
105 KeymapModel() {};
106
107 int row_count(GUI::ModelIndex const&) const override { return m_data.size(); }
108 int column_count(GUI::ModelIndex const&) const override { return 1; }
109
110 GUI::Variant data(GUI::ModelIndex const& index, GUI::ModelRole role) const override
111 {
112 DeprecatedString const& data = m_data.at(index.row());
113 if (role == GUI::ModelRole::Font && data == m_active_keymap)
114 return Gfx::FontDatabase::default_font().bold_variant();
115
116 return data;
117 }
118
119 void remove_at(size_t index)
120 {
121 m_data.remove(index);
122 invalidate();
123 }
124
125 void add_keymap(DeprecatedString const& keymap)
126 {
127 m_data.append(keymap);
128 invalidate();
129 }
130
131 void set_active_keymap(DeprecatedString const& keymap)
132 {
133 m_active_keymap = keymap;
134 invalidate();
135 }
136
137 DeprecatedString const& active_keymap() { return m_active_keymap; }
138
139 DeprecatedString const& keymap_at(size_t index)
140 {
141 return m_data[index];
142 }
143
144 Vector<DeprecatedString> const& keymaps() const { return m_data; }
145
146private:
147 Vector<DeprecatedString> m_data;
148 DeprecatedString m_active_keymap;
149};
150
151KeyboardSettingsWidget::KeyboardSettingsWidget()
152{
153 load_from_gml(keyboard_widget_gml).release_value_but_fixme_should_propagate_errors();
154
155 auto proc_keymap = Core::DeprecatedFile::construct("/sys/kernel/keymap");
156 if (!proc_keymap->open(Core::OpenMode::ReadOnly))
157 VERIFY_NOT_REACHED();
158
159 auto json = JsonValue::from_string(proc_keymap->read_all()).release_value_but_fixme_should_propagate_errors();
160 auto const& keymap_object = json.as_object();
161 VERIFY(keymap_object.has("keymap"sv));
162 m_initial_active_keymap = keymap_object.get_deprecated_string("keymap"sv).value();
163 dbgln("KeyboardSettings thinks the current keymap is: {}", m_initial_active_keymap);
164
165 auto mapper_config(Core::ConfigFile::open("/etc/Keyboard.ini").release_value_but_fixme_should_propagate_errors());
166 auto keymaps = mapper_config->read_entry("Mapping", "Keymaps", "");
167
168 auto keymaps_vector = keymaps.split(',');
169
170 m_selected_keymaps_listview = find_descendant_of_type_named<GUI::ListView>("selected_keymaps");
171 m_selected_keymaps_listview->horizontal_scrollbar().set_visible(false);
172 m_selected_keymaps_listview->set_model(adopt_ref(*new KeymapModel()));
173 auto& keymaps_list_model = static_cast<KeymapModel&>(*m_selected_keymaps_listview->model());
174
175 for (auto& keymap : keymaps_vector) {
176 m_initial_keymap_list.append(keymap);
177 keymaps_list_model.add_keymap(keymap);
178 }
179
180 keymaps_list_model.set_active_keymap(m_initial_active_keymap);
181
182 m_activate_keymap_button = find_descendant_of_type_named<GUI::Button>("activate_keymap_button");
183
184 m_activate_keymap_event = [&]() {
185 auto& selection = m_selected_keymaps_listview->selection();
186 if (!selection.is_empty()) {
187 auto& selected_keymap = keymaps_list_model.keymap_at(selection.first().row());
188 keymaps_list_model.set_active_keymap(selected_keymap);
189 set_modified(true);
190 }
191 };
192
193 m_activate_keymap_button->on_click = [&](auto) {
194 m_activate_keymap_event();
195 };
196
197 m_selected_keymaps_listview->on_activation = [&](auto) {
198 m_activate_keymap_event();
199 };
200
201 m_add_keymap_button = find_descendant_of_type_named<GUI::Button>("add_keymap_button");
202
203 m_add_keymap_button->on_click = [&](auto) {
204 auto keymap = KeymapSelectionDialog::select_keymap(window(), keymaps_list_model.keymaps());
205 if (!keymap.is_empty()) {
206 keymaps_list_model.add_keymap(keymap);
207 set_modified(true);
208 }
209 };
210
211 m_remove_keymap_button = find_descendant_of_type_named<GUI::Button>("remove_keymap_button");
212
213 m_remove_keymap_button->on_click = [&](auto) {
214 auto& selection = m_selected_keymaps_listview->selection();
215 bool active_keymap_deleted = false;
216 for (auto& index : selection.indices()) {
217 if (keymaps_list_model.keymap_at(index.row()) == keymaps_list_model.active_keymap())
218 active_keymap_deleted = true;
219 keymaps_list_model.remove_at(index.row());
220 }
221 if (active_keymap_deleted)
222 keymaps_list_model.set_active_keymap(keymaps_list_model.keymap_at(0));
223 set_modified(true);
224 };
225
226 m_selected_keymaps_listview->on_selection_change = [&]() {
227 auto& selection = m_selected_keymaps_listview->selection();
228 m_remove_keymap_button->set_enabled(!selection.is_empty() && keymaps_list_model.keymaps().size() > 1);
229 if (selection.is_empty()) {
230 m_activate_keymap_button->set_enabled(false);
231 } else {
232 auto& highlighted_keymap = keymaps_list_model.keymap_at(selection.first().row());
233 auto& active_keymap = keymaps_list_model.active_keymap();
234 m_activate_keymap_button->set_enabled(highlighted_keymap != active_keymap);
235 }
236 };
237
238 m_test_typing_area = *find_descendant_of_type_named<GUI::TextEditor>("test_typing_area");
239 m_test_typing_area->on_focusin = [&]() {
240 set_keymaps(keymaps_list_model.keymaps(), keymaps_list_model.active_keymap());
241 };
242
243 m_test_typing_area->on_focusout = [&]() {
244 set_keymaps(m_initial_keymap_list, m_initial_active_keymap);
245 };
246
247 m_clear_test_typing_area_button = find_descendant_of_type_named<GUI::Button>("button_clear_test_typing_area");
248 m_clear_test_typing_area_button->on_click = [this](auto) {
249 m_test_typing_area->clear();
250 m_test_typing_area->set_focus(true);
251 };
252
253 m_num_lock_checkbox = find_descendant_of_type_named<GUI::CheckBox>("num_lock_checkbox");
254 m_num_lock_checkbox->set_checked(Config::read_bool("KeyboardSettings"sv, "StartupEnable"sv, "NumLock"sv, true));
255 m_num_lock_checkbox->on_checked = [&](auto) {
256 set_modified(true);
257 };
258}
259
260KeyboardSettingsWidget::~KeyboardSettingsWidget()
261{
262 set_keymaps(m_initial_keymap_list, m_initial_active_keymap);
263}
264
265void KeyboardSettingsWidget::window_activated(bool is_active_window)
266{
267 if (is_active_window && m_test_typing_area->is_focused()) {
268 auto& keymaps_list_model = static_cast<KeymapModel&>(*m_selected_keymaps_listview->model());
269 set_keymaps(keymaps_list_model.keymaps(), keymaps_list_model.active_keymap());
270 } else {
271 set_keymaps(m_initial_keymap_list, m_initial_active_keymap);
272 }
273}
274
275void KeyboardSettingsWidget::apply_settings()
276{
277 auto& m_keymaps_list_model = static_cast<KeymapModel&>(*m_selected_keymaps_listview->model());
278 set_keymaps(m_keymaps_list_model.keymaps(), m_keymaps_list_model.active_keymap());
279 m_initial_keymap_list.clear();
280 for (auto& keymap : m_keymaps_list_model.keymaps()) {
281 m_initial_keymap_list.append(keymap);
282 }
283 m_initial_active_keymap = m_keymaps_list_model.active_keymap();
284 Config::write_bool("KeyboardSettings"sv, "StartupEnable"sv, "NumLock"sv, m_num_lock_checkbox->is_checked());
285}
286
287void KeyboardSettingsWidget::set_keymaps(Vector<DeprecatedString> const& keymaps, DeprecatedString const& active_keymap)
288{
289 auto keymaps_string = DeprecatedString::join(',', keymaps);
290 GUI::Process::spawn_or_show_error(window(), "/bin/keymap"sv, Array { "-s", keymaps_string.characters(), "-m", active_keymap.characters() });
291}