Serenity Operating System
1/*
2 * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
3 * Copyright (c) 2021, Rasmus Nylander <RasmusNylander.SerenityOS@gmail.com>
4 * Copyright (c) 2022, the SerenityOS developers.
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include "KeyboardMapperWidget.h"
10#include "KeyPositions.h"
11#include <LibCore/File.h>
12#include <LibGUI/BoxLayout.h>
13#include <LibGUI/InputBox.h>
14#include <LibGUI/MessageBox.h>
15#include <LibGUI/RadioButton.h>
16#include <LibKeyboard/CharacterMap.h>
17#include <LibKeyboard/CharacterMapFile.h>
18
19KeyboardMapperWidget::KeyboardMapperWidget()
20{
21 create_frame();
22}
23
24bool KeyboardMapperWidget::request_close()
25{
26 if (!window()->is_modified())
27 return true;
28 auto result = GUI::MessageBox::ask_about_unsaved_changes(window(), m_filename);
29 if (result == GUI::MessageBox::ExecResult::Yes) {
30 if (auto error_or = save(); error_or.is_error())
31 show_error_to_user(error_or.release_error());
32
33 if (!window()->is_modified())
34 return true;
35 }
36 return result == GUI::MessageBox::ExecResult::No;
37}
38
39void KeyboardMapperWidget::create_frame()
40{
41 set_fill_with_background_color(true);
42 set_layout<GUI::VerticalBoxLayout>(4);
43
44 auto& main_widget = add<GUI::Widget>();
45 main_widget.set_relative_rect(0, 0, 200, 200);
46
47 m_keys.resize(KEY_COUNT);
48
49 for (unsigned i = 0; i < KEY_COUNT; i++) {
50 Gfx::IntRect rect = { keys[i].x, keys[i].y, keys[i].width, keys[i].height };
51
52 auto& tmp_button = main_widget.add<KeyButton>();
53 tmp_button.set_relative_rect(rect);
54 tmp_button.set_text(String::from_deprecated_string(keys[i].name).release_value_but_fixme_should_propagate_errors());
55 tmp_button.set_enabled(keys[i].enabled);
56
57 tmp_button.on_click = [this, &tmp_button]() {
58 DeprecatedString value;
59 if (GUI::InputBox::show(window(), value, "New Character:"sv, "Select Character"sv) == GUI::InputBox::ExecResult::OK) {
60 int i = m_keys.find_first_index(&tmp_button).value_or(0);
61 VERIFY(i > 0);
62
63 auto index = keys[i].map_index;
64 VERIFY(index > 0);
65
66 tmp_button.set_text(String::from_deprecated_string(value).release_value_but_fixme_should_propagate_errors());
67 u32* map = map_from_name(m_current_map_name);
68
69 if (value.length() == 0)
70 map[index] = '\0'; // Empty string
71 else
72 map[index] = value[0];
73
74 window()->set_modified(true);
75 }
76 };
77
78 m_keys.insert(i, &tmp_button);
79 }
80
81 // Action Buttons
82 auto& bottom_widget = add<GUI::Widget>();
83 bottom_widget.set_layout<GUI::HorizontalBoxLayout>();
84 bottom_widget.set_fixed_height(40);
85
86 // Map Selection
87 m_map_group = bottom_widget.add<GUI::Widget>();
88 m_map_group->set_layout<GUI::HorizontalBoxLayout>();
89 m_map_group->set_fixed_width(450);
90
91 add_map_radio_button("map"sv, "Default"_short_string);
92 add_map_radio_button("shift_map"sv, "Shift"_short_string);
93 add_map_radio_button("altgr_map"sv, "AltGr"_short_string);
94 add_map_radio_button("alt_map"sv, "Alt"_short_string);
95 add_map_radio_button("shift_altgr_map"sv, "Shift+AltGr"_string.release_value_but_fixme_should_propagate_errors());
96
97 bottom_widget.add_spacer().release_value_but_fixme_should_propagate_errors();
98}
99
100void KeyboardMapperWidget::add_map_radio_button(const StringView map_name, String button_text)
101{
102 auto& map_radio_button = m_map_group->add<GUI::RadioButton>(button_text);
103 map_radio_button.set_name(map_name);
104 map_radio_button.on_checked = [map_name, this](bool) {
105 set_current_map(map_name);
106 };
107}
108
109u32* KeyboardMapperWidget::map_from_name(const StringView map_name)
110{
111 u32* map;
112 if (map_name == "map"sv) {
113 map = m_character_map.map;
114 } else if (map_name == "shift_map"sv) {
115 map = m_character_map.shift_map;
116 } else if (map_name == "alt_map"sv) {
117 map = m_character_map.alt_map;
118 } else if (map_name == "altgr_map"sv) {
119 map = m_character_map.altgr_map;
120 } else if (map_name == "shift_altgr_map"sv) {
121 map = m_character_map.shift_altgr_map;
122 } else {
123 VERIFY_NOT_REACHED();
124 }
125 return map;
126}
127
128ErrorOr<void> KeyboardMapperWidget::load_map_from_file(DeprecatedString const& filename)
129{
130 auto character_map = TRY(Keyboard::CharacterMapFile::load_from_file(filename));
131
132 m_filename = filename;
133 m_character_map = character_map;
134 set_current_map("map");
135
136 for (auto& widget : m_map_group->child_widgets()) {
137 auto& radio_button = static_cast<GUI::RadioButton&>(widget);
138 radio_button.set_checked(radio_button.name() == "map");
139 }
140
141 window()->set_modified(false);
142 update_window_title();
143 return {};
144}
145
146ErrorOr<void> KeyboardMapperWidget::load_map_from_system()
147{
148 auto character_map = TRY(Keyboard::CharacterMap::fetch_system_map());
149
150 m_filename = DeprecatedString::formatted("/res/keymaps/{}.json", character_map.character_map_name());
151 m_character_map = character_map.character_map_data();
152 set_current_map("map");
153
154 for (auto& widget : m_map_group->child_widgets()) {
155 auto& radio_button = static_cast<GUI::RadioButton&>(widget);
156 radio_button.set_checked(radio_button.name() == "map");
157 }
158
159 update_window_title();
160 return {};
161}
162
163ErrorOr<void> KeyboardMapperWidget::save()
164{
165 return save_to_file(m_filename);
166}
167
168ErrorOr<void> KeyboardMapperWidget::save_to_file(StringView filename)
169{
170 JsonObject map_json;
171
172 auto add_array = [&](DeprecatedString name, u32* values) {
173 JsonArray items;
174 for (int i = 0; i < 90; i++) {
175 StringBuilder sb;
176 if (values[i])
177 sb.append_code_point(values[i]);
178
179 JsonValue val(sb.to_deprecated_string());
180 items.append(move(val));
181 }
182 map_json.set(name, move(items));
183 };
184
185 add_array("map", m_character_map.map);
186 add_array("shift_map", m_character_map.shift_map);
187 add_array("alt_map", m_character_map.alt_map);
188 add_array("altgr_map", m_character_map.altgr_map);
189 add_array("shift_altgr_map", m_character_map.shift_altgr_map);
190
191 // Write to file.
192 DeprecatedString file_content = map_json.to_deprecated_string();
193 auto file = TRY(Core::File::open(filename, Core::File::OpenMode::Write));
194 TRY(file->write_until_depleted(file_content.bytes()));
195 file->close();
196
197 window()->set_modified(false);
198 m_filename = filename;
199 update_window_title();
200 return {};
201}
202
203void KeyboardMapperWidget::keydown_event(GUI::KeyEvent& event)
204{
205 for (int i = 0; i < KEY_COUNT; i++) {
206 if (keys[i].scancode != event.scancode())
207 continue;
208 auto& tmp_button = m_keys.at(i);
209 tmp_button->set_pressed(true);
210 tmp_button->update();
211 break;
212 }
213
214 if (m_automatic_modifier && event.modifiers() > 0) {
215 update_modifier_radio_buttons(event);
216 }
217
218 event.ignore();
219}
220
221void KeyboardMapperWidget::keyup_event(GUI::KeyEvent& event)
222{
223 for (int i = 0; i < KEY_COUNT; i++) {
224 if (keys[i].scancode != event.scancode())
225 continue;
226 auto& tmp_button = m_keys.at(i);
227 tmp_button->set_pressed(false);
228 tmp_button->update();
229 break;
230 }
231
232 if (m_automatic_modifier) {
233 update_modifier_radio_buttons(event);
234 }
235}
236
237void KeyboardMapperWidget::set_current_map(const DeprecatedString current_map)
238{
239 m_current_map_name = current_map;
240 u32* map = map_from_name(m_current_map_name);
241
242 for (unsigned k = 0; k < KEY_COUNT; k++) {
243 auto index = keys[k].map_index;
244 if (index == 0)
245 continue;
246
247 StringBuilder sb;
248 sb.append_code_point(map[index]);
249
250 m_keys.at(k)->set_text(sb.to_string().release_value_but_fixme_should_propagate_errors());
251 }
252
253 this->update();
254}
255
256void KeyboardMapperWidget::update_window_title()
257{
258 StringBuilder sb;
259 sb.append(m_filename);
260 sb.append("[*] - Keyboard Mapper"sv);
261
262 window()->set_title(sb.to_deprecated_string());
263}
264
265void KeyboardMapperWidget::show_error_to_user(Error error)
266{
267 GUI::MessageBox::show_error(window(), error.string_literal());
268}
269
270void KeyboardMapperWidget::set_automatic_modifier(bool checked)
271{
272 m_automatic_modifier = checked;
273}
274
275void KeyboardMapperWidget::update_modifier_radio_buttons(GUI::KeyEvent& event)
276{
277 GUI::RadioButton* radio_button;
278 if (event.shift() && event.altgr()) {
279 radio_button = m_map_group->find_child_of_type_named<GUI::RadioButton>("shift_altgr_map");
280 } else if (event.altgr()) {
281 radio_button = m_map_group->find_child_of_type_named<GUI::RadioButton>("altgr_map");
282 } else if (event.alt()) {
283 radio_button = m_map_group->find_child_of_type_named<GUI::RadioButton>("alt_map");
284 } else if (event.shift()) {
285 radio_button = m_map_group->find_child_of_type_named<GUI::RadioButton>("shift_map");
286 } else {
287 radio_button = m_map_group->find_child_of_type_named<GUI::RadioButton>("map");
288 }
289
290 if (radio_button)
291 radio_button->set_checked(true);
292}