Pārlūkot izejas kodu

Applications: Add new KeyboardMapper application

New editing app for keymap files.
Hüseyin ASLITÜRK 5 gadi atpakaļ
vecāks
revīzija
d3578fdf9b

+ 1 - 0
Applications/CMakeLists.txt

@@ -9,6 +9,7 @@ add_subdirectory(FontEditor)
 add_subdirectory(Help)
 add_subdirectory(Help)
 add_subdirectory(HexEditor)
 add_subdirectory(HexEditor)
 add_subdirectory(IRCClient)
 add_subdirectory(IRCClient)
+add_subdirectory(KeyboardMapper)
 add_subdirectory(Piano)
 add_subdirectory(Piano)
 add_subdirectory(PixelPaint)
 add_subdirectory(PixelPaint)
 add_subdirectory(QuickShow)
 add_subdirectory(QuickShow)

+ 8 - 0
Applications/KeyboardMapper/CMakeLists.txt

@@ -0,0 +1,8 @@
+set(SOURCES
+    KeyboardMapperWidget.cpp
+    KeyButton.cpp
+    main.cpp
+)
+
+serenity_bin(KeyboardMapper)
+target_link_libraries(KeyboardMapper LibGUI LibKeyboard)

+ 68 - 0
Applications/KeyboardMapper/KeyButton.cpp

@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "KeyButton.h"
+#include <LibGUI/Button.h>
+#include <LibGUI/Painter.h>
+#include <LibGfx/Font.h>
+#include <LibGfx/Palette.h>
+
+KeyButton::~KeyButton()
+{
+}
+
+void KeyButton::paint_event(GUI::PaintEvent& event)
+{
+    GUI::Painter painter(*this);
+    painter.add_clip_rect(event.rect());
+
+    auto content_rect = rect().shrunken(5, 5);
+    auto& font = this->font();
+
+    Gfx::StylePainter::paint_button(painter, rect(), palette(), Gfx::ButtonStyle::Normal, is_being_pressed(), is_hovered(), is_checked(), true);
+
+    if (m_pressed)
+        painter.fill_rect(content_rect, Color::Cyan);
+    else if (!is_enabled())
+        painter.fill_rect(content_rect, Color::from_rgb(0x8C7272));
+
+    if (!text().is_empty()) {
+        Gfx::Rect text_rect { 0, 0, font.width(text()), font.glyph_height() };
+        text_rect.align_within(content_rect, Gfx::TextAlignment::Center);
+
+        auto clipped_rect = rect().intersected(this->rect());
+
+        painter.draw_text(clipped_rect, text(), font, Gfx::TextAlignment::Center, palette().button_text(), Gfx::TextElision::Right);
+        if (is_focused())
+            painter.draw_rect(clipped_rect.inflated(6, 4), palette().focus_outline());
+    }
+}
+
+void KeyButton::click(unsigned)
+{
+    if (on_click)
+        on_click();
+}

+ 47 - 0
Applications/KeyboardMapper/KeyButton.h

@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include <LibGUI/AbstractButton.h>
+
+class KeyButton : public GUI::AbstractButton {
+    C_OBJECT(KeyButton)
+
+public:
+    virtual ~KeyButton() override;
+
+    void set_pressed(bool value) { m_pressed = value; }
+
+    Function<void()> on_click;
+
+protected:
+    virtual void click(unsigned modifiers = 0) override;
+    virtual void paint_event(GUI::PaintEvent&) override;
+
+private:
+    bool m_pressed { false };
+};

+ 115 - 0
Applications/KeyboardMapper/KeyPositions.h

@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <AK/String.h>
+
+struct KeyPosition {
+    u32 scancode;
+    int x;
+    int y;
+    int width;
+    int height;
+    bool enabled;
+    int map_index;
+    AK::String name;
+};
+
+#define KEY_COUNT 63
+
+struct KeyPosition keys[KEY_COUNT] = {
+    [ 0] = {     0,   0,  0,   0,   0, false,  0, ""},
+
+    [ 1] = {  0x29,   0,  0,  50,  50,  true, 41, "`"},
+    [ 2] = {  0x02,  51,  0,  50,  50,  true,  2, "1"},
+    [ 3] = {  0x03, 102,  0,  50,  50,  true,  3, "2"},
+    [ 4] = {  0x04, 153,  0,  50,  50,  true,  4, "3"},
+    [ 5] = {  0x05, 204,  0,  50,  50,  true,  5, "4"},
+    [ 6] = {  0x06, 255,  0,  50,  50,  true,  6, "5"},
+    [ 7] = {  0x07, 306,  0,  50,  50,  true,  7, "6"},
+    [ 8] = {  0x08, 357,  0,  50,  50,  true,  8, "7"},
+    [ 9] = {  0x09, 408,  0,  50,  50,  true,  9, "8"},
+    [10] = {  0x0A, 459,  0,  50,  50,  true, 10, "9"},
+    [11] = {  0x0B, 510,  0,  50,  50,  true, 11, "0"},
+    [12] = {  0x0C, 561,  0,  50,  50,  true, 12, "-"},
+    [13] = {  0x0D, 612,  0,  50,  50,  true, 13, "="},
+    [14] = {  0x0E, 663,  0, 100,  50, false,  0, "back space"},
+
+
+    [15] = {  0x0F,   0,  52,  76,  50, false,  0, "tab"},
+    [16] = {  0x10,  77,  52,  50,  50,  true, 16, "q"},
+    [17] = {  0x11, 128,  52,  50,  50,  true, 17, "w"},
+    [18] = {  0x12, 179,  52,  50,  50,  true, 18, "e"},
+    [19] = {  0x13, 230,  52,  50,  50,  true, 19, "r"},
+    [20] = {  0x14, 281,  52,  50,  50,  true, 20, "t"},
+    [21] = {  0x15, 332,  52,  50,  50,  true, 21, "y"},
+    [22] = {  0x16, 383,  52,  50,  50,  true, 22, "u"},
+    [23] = {  0x17, 434,  52,  50,  50,  true, 23, "ı"},
+    [24] = {  0x18, 485,  52,  50,  50,  true, 24, "o"},
+    [25] = {  0x19, 536,  52,  50,  50,  true, 25, "p"},
+    [26] = {  0x1A, 587,  52,  50,  50,  true, 26, "["},
+    [27] = {  0x1B, 638,  52,  50,  50,  true, 27, "]"},
+    [28] = {  0x1C, 689,  52,  74,  50, false,  0, "enter"},
+
+
+    [29] = {  0x3A,   0, 104, 101,  50, false,  0, "caps lock"},
+    [30] = {  0x1E, 103, 104,  50,  50,  true, 30, "a"},
+    [31] = {  0x1F, 154, 104,  50,  50,  true, 31, "s"},
+    [32] = {  0x20, 205, 104,  50,  50,  true, 32, "d"},
+    [33] = {  0x21, 256, 104,  50,  50,  true, 33, "f"},
+    [34] = {  0x22, 307, 104,  50,  50,  true, 34, "g"},
+    [35] = {  0x23, 358, 104,  50,  50,  true, 35, "h"},
+    [36] = {  0x24, 409, 104,  50,  50,  true, 36, "j"},
+    [37] = {  0x25, 460, 104,  50,  50,  true, 37, "k"},
+    [38] = {  0x26, 511, 104,  50,  50,  true, 38, "l"},
+    [39] = {  0x27, 562, 104,  50,  50,  true, 39, ";"},
+    [40] = {  0x28, 614, 104,  50,  50,  true, 40, "\""},
+    [41] = {  0x2B, 665, 104,  50,  50,  true, 43, "\\"},
+
+
+
+    [42] = {  0x2A,   0, 156,  76,  50, false,  0, "left shift"},
+    [43] = {  0x56,  77, 156,  50,  50,  true, 86, "\\"},
+    [44] = {  0x2C, 128, 156,  50,  50,  true, 44, "z"},
+    [45] = {  0x2D, 179, 156,  50,  50,  true, 45, "x"},
+    [46] = {  0x2E, 230, 156,  50,  50,  true, 46, "c"},
+    [47] = {  0x2F, 281, 156,  50,  50,  true, 47, "v"},
+    [48] = {  0x30, 332, 156,  50,  50,  true, 48, "b"},
+    [49] = {  0x31, 383, 156,  50,  50,  true, 49, "n"},
+    [50] = {  0x32, 434, 156,  50,  50,  true, 50, "m"},
+    [51] = {  0x33, 485, 156,  50,  50,  true, 51, ","},
+    [52] = {  0x34, 536, 156,  50,  50,  true, 52, "."},
+    [53] = {  0x35, 587, 156,  50,  50,  true, 53, "/"},
+    [54] = {  0x36, 638, 156, 125,  50, false,  0, "right shift"},
+
+    [55] = {  0x1D,   0, 208,  76,  50, false,  0, "left ctrl"},
+    [56] = {0xE05B,  77, 208,  50,  50, false,  0, "left\nsuper"},
+    [57] = {  0x38, 128, 208,  50,  50, false,  0, "alt"},
+    [58] = {  0x39, 179, 208, 356,  50, false,  0, "space"},
+    [59] = {0xE038, 536, 208,  50,  50, false,  0, "alt gr"},
+    [60] = {0xE05C, 587, 208,  50,  50, false,  0, "right\nsuper"},
+    [61] = {0xE05D, 638, 208,  50,  50, false,  0, "menu"},
+    [62] = {0xE01D, 689, 208,  74,  50, false,  0, "right ctrl"}
+};

+ 289 - 0
Applications/KeyboardMapper/KeyboardMapperWidget.cpp

@@ -0,0 +1,289 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "KeyboardMapperWidget.h"
+#include "KeyPositions.h"
+#include <LibCore/File.h>
+#include <LibGUI/BoxLayout.h>
+#include <LibGUI/InputBox.h>
+#include <LibGUI/MessageBox.h>
+#include <LibGUI/RadioButton.h>
+#include <LibKeyboard/CharacterMapFile.h>
+#include <ctype.h>
+#include <fcntl.h>
+#include <stdio.h>
+
+KeyboardMapperWidget::KeyboardMapperWidget()
+{
+    create_frame();
+}
+
+KeyboardMapperWidget::~KeyboardMapperWidget()
+{
+}
+
+void KeyboardMapperWidget::create_frame()
+{
+    set_fill_with_background_color(true);
+    set_layout<GUI::VerticalBoxLayout>();
+    set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fill);
+    layout()->set_margins({ 4, 4, 4, 4 });
+
+    auto& main_widget = add<GUI::Widget>();
+    main_widget.set_relative_rect(0, 0, 200, 200);
+
+    m_keys.resize(KEY_COUNT);
+
+    for (unsigned i = 0; i < KEY_COUNT; i++) {
+        Gfx::Rect rect = { keys[i].x, keys[i].y, keys[i].width, keys[i].height };
+
+        auto& tmp_button = main_widget.add<KeyButton>();
+        tmp_button.set_relative_rect(rect);
+        tmp_button.set_text(keys[i].name);
+        tmp_button.set_enabled(keys[i].enabled);
+
+        tmp_button.on_click = [&]() {
+            auto input_box = GUI::InputBox::construct("New Character:", "Select Character", window());
+            if (input_box->exec() == GUI::InputBox::ExecOK) {
+                auto value = input_box->text_value();
+
+                int i = m_keys.find_first_index(&tmp_button).value_or(0);
+                ASSERT(i > 0);
+
+                auto index = keys[i].map_index;
+                ASSERT(index > 0);
+
+                tmp_button.set_text(value);
+                char* map;
+
+                if (m_current_map_name == "map") {
+                    map = m_character_map.map;
+                } else if (m_current_map_name == "shift_map") {
+                    map = m_character_map.shift_map;
+                } else if (m_current_map_name == "alt_map") {
+                    map = m_character_map.alt_map;
+                } else if (m_current_map_name == "altgr_map") {
+                    map = m_character_map.altgr_map;
+                } else {
+                    ASSERT_NOT_REACHED();
+                }
+
+                if (value.length() == 0)
+                    map[index] = '\0'; // Empty string
+                else
+                    map[index] = value[0];
+
+                m_modified = true;
+                update_window_title();
+            }
+        };
+
+        m_keys.insert(i, &tmp_button);
+    }
+
+    // Action Buttons
+    auto& bottom_widget = add<GUI::Widget>();
+    bottom_widget.set_layout<GUI::HorizontalBoxLayout>();
+    bottom_widget.set_size_policy(GUI::SizePolicy::Fill, GUI::SizePolicy::Fixed);
+    bottom_widget.set_preferred_size(0, 40);
+
+    // Map Selection
+    m_map_group = bottom_widget.add<GUI::Widget>();
+    m_map_group->set_layout<GUI::HorizontalBoxLayout>();
+    m_map_group->set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill);
+    m_map_group->set_preferred_size(250, 0);
+
+    auto& radio_map = m_map_group->add<GUI::RadioButton>("Default");
+    radio_map.set_name("map");
+    radio_map.on_checked = [&](bool) {
+        set_current_map("map");
+    };
+    auto& radio_shift = m_map_group->add<GUI::RadioButton>("Shift");
+    radio_shift.set_name("shift_map");
+    radio_shift.on_checked = [&](bool) {
+        set_current_map("shift_map");
+    };
+    auto& radio_altgr = m_map_group->add<GUI::RadioButton>("AltGr");
+    radio_altgr.set_name("altgr_map");
+    radio_altgr.on_checked = [&](bool) {
+        set_current_map("altgr_map");
+    };
+    auto& radio_alt = m_map_group->add<GUI::RadioButton>("Alt");
+    radio_alt.set_name("alt_map");
+    radio_alt.on_checked = [&](bool) {
+        set_current_map("alt_map");
+    };
+
+    bottom_widget.layout()->add_spacer();
+
+    auto& ok_button = bottom_widget.add<GUI::Button>();
+    ok_button.set_text("Save");
+    ok_button.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill);
+    ok_button.set_preferred_size(80, 0);
+    ok_button.on_click = [this](auto) {
+        save();
+    };
+}
+
+void KeyboardMapperWidget::load_from_file(String file_name)
+{
+    auto result = Keyboard::CharacterMapFile::load_from_file(file_name);
+    if (!result.has_value()) {
+        ASSERT_NOT_REACHED();
+    }
+
+    m_file_name = file_name;
+    m_character_map = result.value();
+    set_current_map("map");
+
+    for (Widget* widget : m_map_group->child_widgets()) {
+        auto radio_button = (GUI::RadioButton*)widget;
+        radio_button->set_checked(radio_button->name() == "map");
+    }
+
+    update_window_title();
+}
+
+void KeyboardMapperWidget::save()
+{
+    save_to_file(m_file_name);
+}
+
+void KeyboardMapperWidget::save_to_file(const StringView& file_name)
+{
+    JsonObject map_json;
+
+    auto add_array = [&](String name, char* values) {
+        JsonArray items;
+        for (int i = 0; i < 90; i++) {
+            AK::StringBuilder sb;
+            sb.append(values[i]);
+
+            JsonValue val(sb.to_string());
+            items.append(move(val));
+        }
+        map_json.set(name, move(items));
+    };
+
+    add_array("map", m_character_map.map);
+    add_array("shift_map", m_character_map.shift_map);
+    add_array("alt_map", m_character_map.alt_map);
+    add_array("altgr_map", m_character_map.altgr_map);
+
+    // Write to file.
+    String file_content = map_json.to_string();
+
+    auto file = Core::File::construct(file_name);
+    file->open(Core::IODevice::WriteOnly);
+    if (!file->is_open()) {
+        StringBuilder sb;
+        sb.append("Failed to open ");
+        sb.append(file_name);
+        sb.append(" for write. Error: ");
+        sb.append(file->error_string());
+
+        GUI::MessageBox::show(sb.to_string(), "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window());
+        return;
+    }
+
+    bool result = file->write(file_content);
+    if (!result) {
+        int error_number = errno;
+        StringBuilder sb;
+        sb.append("Unable to save file. Error: ");
+        sb.append(strerror(error_number));
+
+        GUI::MessageBox::show(sb.to_string(), "Error", GUI::MessageBox::Type::Error, GUI::MessageBox::InputType::OK, window());
+        return;
+    }
+
+    m_modified = false;
+    m_file_name = file_name;
+    update_window_title();
+}
+
+void KeyboardMapperWidget::keydown_event(GUI::KeyEvent& event)
+{
+    for (int i = 0; i < KEY_COUNT; i++) {
+        auto& tmp_button = m_keys.at(i);
+        tmp_button->set_pressed(keys[i].scancode == event.scancode());
+        tmp_button->update();
+    }
+}
+
+void KeyboardMapperWidget::keyup_event(GUI::KeyEvent& event)
+{
+    for (int i = 0; i < KEY_COUNT; i++) {
+        if (keys[i].scancode == event.scancode()) {
+            auto& tmp_button = m_keys.at(i);
+            tmp_button->set_pressed(false);
+            tmp_button->update();
+            break;
+        }
+    }
+}
+
+void KeyboardMapperWidget::set_current_map(const String current_map)
+{
+    m_current_map_name = current_map;
+    char* map;
+
+    if (m_current_map_name == "map") {
+        map = m_character_map.map;
+    } else if (m_current_map_name == "shift_map") {
+        map = m_character_map.shift_map;
+    } else if (m_current_map_name == "alt_map") {
+        map = m_character_map.alt_map;
+    } else if (m_current_map_name == "altgr_map") {
+        map = m_character_map.altgr_map;
+    } else {
+        ASSERT_NOT_REACHED();
+    }
+
+    for (unsigned k = 0; k < KEY_COUNT; k++) {
+        auto index = keys[k].map_index;
+        if (index == 0)
+            continue;
+
+        AK::StringBuilder sb;
+        sb.append(map[index]);
+
+        m_keys.at(k)->set_text(sb.to_string());
+    }
+
+    this->update();
+}
+
+void KeyboardMapperWidget::update_window_title()
+{
+    StringBuilder sb;
+    sb.append(m_file_name);
+    if (m_modified)
+        sb.append(" (*)");
+    sb.append(" - KeyboardMapper");
+
+    window()->set_title(sb.to_string());
+}

+ 60 - 0
Applications/KeyboardMapper/KeyboardMapperWidget.h

@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#pragma once
+
+#include "KeyButton.h"
+#include <LibGUI/Button.h>
+#include <LibKeyboard/CharacterMapData.h>
+
+class KeyboardMapperWidget : public GUI::Widget {
+    C_OBJECT(KeyboardMapperWidget)
+
+public:
+    KeyboardMapperWidget();
+    virtual ~KeyboardMapperWidget() override;
+
+    void create_frame();
+    void load_from_file(const String);
+    void save();
+    void save_to_file(const StringView&);
+
+protected:
+    virtual void keydown_event(GUI::KeyEvent&) override;
+    virtual void keyup_event(GUI::KeyEvent&) override;
+
+    void set_current_map(const String);
+    void update_window_title();
+
+private:
+    Vector<KeyButton*> m_keys;
+    RefPtr<GUI::Widget> m_map_group;
+
+    String m_file_name;
+    Keyboard::CharacterMapData m_character_map;
+    String m_current_map_name;
+    bool m_modified { false };
+};

+ 114 - 0
Applications/KeyboardMapper/main.cpp

@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2020, Hüseyin Aslıtürk <asliturk@hotmail.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ *    list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "KeyboardMapperWidget.h"
+#include <LibCore/ArgsParser.h>
+#include <LibGUI/AboutDialog.h>
+#include <LibGUI/Action.h>
+#include <LibGUI/Application.h>
+#include <LibGUI/FilePicker.h>
+#include <LibGUI/Icon.h>
+#include <LibGUI/Menu.h>
+#include <LibGUI/MenuBar.h>
+
+int main(int argc, char** argv)
+{
+    const char* path = nullptr;
+    Core::ArgsParser args_parser;
+    args_parser.add_positional_argument(path, "Keyboard character mapping file.", "file", Core::ArgsParser::Required::No);
+    args_parser.parse(argc, argv);
+
+    GUI::Application app(argc, argv);
+
+    auto app_icon = GUI::Icon::default_icon("app-keyboard-mapper");
+
+    auto window = GUI::Window::construct();
+    window->set_title("KeyboardMapper");
+    window->set_icon(app_icon.bitmap_for_size(16));
+    window->set_main_widget<KeyboardMapperWidget>();
+    window->resize(775, 315);
+    window->move_to(50, 50);
+    window->set_resizable(false);
+    window->show();
+
+    auto keyboard_mapper_widget = (KeyboardMapperWidget*)window->main_widget();
+    if (path != nullptr) {
+        keyboard_mapper_widget->load_from_file(path);
+    } else {
+        keyboard_mapper_widget->load_from_file("/res/keymaps/en.json");
+    }
+
+    // Actions
+    auto open_action = GUI::CommonActions::make_open_action(
+        [&](auto&) {
+            Optional<String> path = GUI::FilePicker::get_open_filepath("Open");
+            if (path.has_value()) {
+                keyboard_mapper_widget->load_from_file(path.value());
+            }
+        });
+
+    auto save_action = GUI::CommonActions::make_save_action(
+        [&](auto&) {
+            keyboard_mapper_widget->save();
+        });
+
+    auto save_as_action = GUI::Action::create("Save as...", { Mod_Ctrl | Mod_Shift, Key_S }, Gfx::Bitmap::load_from_file("/res/icons/16x16/save.png"),
+        [&](auto&) {
+            String m_name = "Unnamed";
+            Optional<String> save_path = GUI::FilePicker::get_save_filepath(m_name, "json");
+            if (!save_path.has_value())
+                return;
+
+            keyboard_mapper_widget->save_to_file(save_path.value());
+        });
+
+    auto quit_action = GUI::CommonActions::make_quit_action(
+        [&](auto&) {
+            app.quit();
+        });
+
+    auto about_action = GUI::Action::create("About",
+        [&](auto&) {
+            GUI::AboutDialog::show("KeyboardMapper", app_icon.bitmap_for_size(32), window);
+        });
+
+    // Menu
+    auto menubar = GUI::MenuBar::construct();
+
+    auto& app_menu = menubar->add_menu("KeyboardMapper");
+    app_menu.add_action(open_action);
+    app_menu.add_action(save_action);
+    app_menu.add_action(save_as_action);
+    app_menu.add_separator();
+    app_menu.add_action(quit_action);
+
+    auto& help_menu = menubar->add_menu("Help");
+    help_menu.add_action(about_action);
+
+    app.set_menubar(move(menubar));
+
+    return app.exec();
+}