feat: add keyboard layout support
This commit is contained in:
@@ -352,7 +352,8 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
||||
fm->pathbar_cursor = 0;
|
||||
} else if (key.scancode == 0x4F) {
|
||||
fm->pathbar_cursor = fm->pathbar_len;
|
||||
} else if (key.ascii >= 32 && key.ascii < 127 && fm->pathbar_len < 254) {
|
||||
} else if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127 && fm->pathbar_len < 254) {
|
||||
for (int i = fm->pathbar_len; i > fm->pathbar_cursor; i--)
|
||||
fm->pathbar_buf[i] = fm->pathbar_buf[i - 1];
|
||||
fm->pathbar_buf[fm->pathbar_cursor] = key.ascii;
|
||||
@@ -399,7 +400,8 @@ void filemanager_on_key(Window* win, const montauk::abi::KeyEvent& key) {
|
||||
} else if (key.scancode == 0x4F) {
|
||||
// End
|
||||
fm->rename_cursor = fm->rename_len;
|
||||
} else if (key.ascii >= 32 && key.ascii < 127) {
|
||||
} else if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127) {
|
||||
// Printable character (reject / and other FS-unsafe chars)
|
||||
if (key.ascii != '/' && key.ascii != '\\' && fm->rename_len < 62) {
|
||||
for (int i = fm->rename_len; i > fm->rename_cursor; i--)
|
||||
|
||||
@@ -7,6 +7,27 @@
|
||||
#include "desktop_internal.hpp"
|
||||
#include <montauk/user.h>
|
||||
|
||||
bool gui::desktop_refresh_keyboard_layouts(DesktopState* ds, bool force) {
|
||||
if (!ds) return false;
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
if (!force && now - ds->keyboard_last_poll < 1000) return false;
|
||||
|
||||
// Only the selection can change while we run: the layout tables are
|
||||
// read-only OS data, so there is no reason to re-parse them every second.
|
||||
int prev_active = ds->keyboard.active;
|
||||
bool prev_enabled[montauk::keyboard::MAX_LAYOUTS];
|
||||
for (int i = 0; i < montauk::keyboard::MAX_LAYOUTS; i++)
|
||||
prev_enabled[i] = ds->keyboard.enabled[i];
|
||||
|
||||
montauk::keyboard::refresh_selection(&ds->keyboard, ds->current_user);
|
||||
|
||||
bool changed = ds->keyboard.active != prev_active;
|
||||
for (int i = 0; !changed && i < ds->keyboard.registry.count; i++)
|
||||
changed = ds->keyboard.enabled[i] != prev_enabled[i];
|
||||
ds->keyboard_last_poll = now;
|
||||
return changed;
|
||||
}
|
||||
|
||||
static bool try_unlock(DesktopState* ds) {
|
||||
ds->lock_show_error = false;
|
||||
|
||||
@@ -85,7 +106,8 @@ static void handle_lock_keyboard(DesktopState* ds, const montauk::abi::KeyEvent&
|
||||
}
|
||||
|
||||
// Printable characters
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F) {
|
||||
if ((unsigned char)key.ascii >= 0x20
|
||||
&& (unsigned char)key.ascii != 0x7F) {
|
||||
if (ds->lock_password_len < 63) {
|
||||
ds->lock_password[ds->lock_password_len] = key.ascii;
|
||||
ds->lock_password_len++;
|
||||
@@ -538,6 +560,22 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keyboard layout indicator: cycle through the layouts enabled in the
|
||||
// Keyboard Settings applet and persist the new active choice.
|
||||
if (ds->keyboard_layout_rect.w > 0
|
||||
&& ds->keyboard_layout_rect.contains(mx, my)) {
|
||||
ds->keyboard.active = montauk::keyboard::next_enabled(
|
||||
ds->keyboard, ds->keyboard.active);
|
||||
montauk::keyboard::save_user(ds->current_user, ds->keyboard);
|
||||
ds->keyboard_last_poll = montauk::get_milliseconds();
|
||||
ds->app_menu_open = false;
|
||||
ds->net_popup_open = false;
|
||||
ds->wifi_popup_open = false;
|
||||
ds->vol_popup_open = false;
|
||||
gui::mtk::context_menu_close(ds->ctx_menu);
|
||||
return;
|
||||
}
|
||||
|
||||
// Volume icon
|
||||
if (ds->vol_icon_rect.w > 0 && ds->vol_icon_rect.contains(mx, my)) {
|
||||
ds->vol_popup_open = !ds->vol_popup_open;
|
||||
@@ -801,8 +839,12 @@ void gui::desktop_handle_mouse(DesktopState* ds) {
|
||||
}
|
||||
|
||||
void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent& key) {
|
||||
montauk::abi::KeyEvent translated_key = key;
|
||||
montauk::keyboard::translate(ds->keyboard, &translated_key);
|
||||
const montauk::abi::KeyEvent& routed_key = translated_key;
|
||||
|
||||
if (ds->screen_locked) {
|
||||
handle_lock_keyboard(ds, key);
|
||||
handle_lock_keyboard(ds, routed_key);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -816,7 +858,7 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent
|
||||
montauk::abi::WinEvent ev;
|
||||
montauk::memset(&ev, 0, sizeof(ev));
|
||||
ev.type = 0; // key
|
||||
ev.key = key;
|
||||
ev.key = routed_key;
|
||||
montauk::win_sendevent(win->ext_win_id, &ev);
|
||||
ds->launcher_open = false;
|
||||
ds->app_menu_open = false;
|
||||
@@ -824,37 +866,37 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent
|
||||
return;
|
||||
}
|
||||
|
||||
if (desktop_handle_launcher_keyboard(ds, key)) {
|
||||
if (desktop_handle_launcher_keyboard(ds, routed_key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Global shortcuts (only on key press)
|
||||
if (key.pressed && key.ctrl && key.alt) {
|
||||
if (key.ascii == 'l' || key.ascii == 'L') {
|
||||
if (routed_key.pressed && routed_key.ctrl && routed_key.alt) {
|
||||
if (routed_key.ascii == 'l' || routed_key.ascii == 'L') {
|
||||
desktop_lock_screen(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 't' || key.ascii == 'T') {
|
||||
if (routed_key.ascii == 't' || routed_key.ascii == 'T') {
|
||||
open_terminal(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'f' || key.ascii == 'F') {
|
||||
if (routed_key.ascii == 'f' || routed_key.ascii == 'F') {
|
||||
open_filemanager(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'c' || key.ascii == 'C') {
|
||||
if (routed_key.ascii == 'c' || routed_key.ascii == 'C') {
|
||||
open_calculator(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'e' || key.ascii == 'E') {
|
||||
if (routed_key.ascii == 'e' || routed_key.ascii == 'E') {
|
||||
open_texteditor(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'w' || key.ascii == 'W') {
|
||||
if (routed_key.ascii == 'w' || routed_key.ascii == 'W') {
|
||||
open_wordprocessor(ds);
|
||||
return;
|
||||
}
|
||||
if (key.ascii == 'k' || key.ascii == 'K') {
|
||||
if (routed_key.ascii == 'k' || routed_key.ascii == 'K') {
|
||||
open_klog(ds);
|
||||
return;
|
||||
}
|
||||
@@ -868,10 +910,10 @@ void gui::desktop_handle_keyboard(DesktopState* ds, const montauk::abi::KeyEvent
|
||||
montauk::abi::WinEvent ev;
|
||||
montauk::memset(&ev, 0, sizeof(ev));
|
||||
ev.type = 0; // key
|
||||
ev.key = key;
|
||||
ev.key = routed_key;
|
||||
montauk::win_sendevent(win->ext_win_id, &ev);
|
||||
} else if (win->on_key) {
|
||||
win->on_key(win, key);
|
||||
win->on_key(win, routed_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -494,7 +494,8 @@ bool desktop_handle_launcher_keyboard(DesktopState* ds, const montauk::abi::KeyE
|
||||
return true;
|
||||
}
|
||||
|
||||
if (key.ascii >= 0x20 && key.ascii < 0x7F && ds->launcher_query_len < 63) {
|
||||
if ((unsigned char)key.ascii >= 0x20
|
||||
&& (unsigned char)key.ascii != 0x7F && ds->launcher_query_len < 63) {
|
||||
ds->launcher_query[ds->launcher_query_len++] = key.ascii;
|
||||
ds->launcher_query[ds->launcher_query_len] = '\0';
|
||||
desktop_update_launcher_results(ds);
|
||||
|
||||
@@ -405,6 +405,10 @@ void gui::desktop_init(DesktopState* ds) {
|
||||
ds->vol_output = montauk::audio_get_output(-1);
|
||||
ds->vol_bt_status = montauk::audio_bt_status(-1);
|
||||
|
||||
ds->keyboard = montauk::keyboard::load_user(ds->current_user);
|
||||
ds->keyboard_layout_rect = {0, 0, 0, 0};
|
||||
ds->keyboard_last_poll = montauk::get_milliseconds();
|
||||
|
||||
ds->closing_ext_count = 0;
|
||||
|
||||
ds->screen_locked = false;
|
||||
@@ -813,6 +817,7 @@ void gui::desktop_run(DesktopState* ds) {
|
||||
}
|
||||
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
sceneChanged |= desktop_refresh_keyboard_layouts(ds, false);
|
||||
sceneChanged |= desktop_refresh_panel_state(ds, now);
|
||||
|
||||
// Pick up any wallpaper buffer produced by a background loader.
|
||||
|
||||
@@ -108,8 +108,34 @@ void gui::desktop_draw_panel(DesktopState* ds) {
|
||||
int date_x = clock_x - date_w - 10;
|
||||
draw_text(fb, date_x, clock_y, date_str, colors::PANEL_TEXT);
|
||||
|
||||
// Volume icon (to the left of the date)
|
||||
int vol_icon_x = date_x - 16 - 12;
|
||||
// Only render switcher if more than one keyboard layout is installed
|
||||
int enabled_layouts = 0;
|
||||
for (int i = 0; i < ds->keyboard.registry.count; i++)
|
||||
if (ds->keyboard.enabled[i]) enabled_layouts++;
|
||||
|
||||
// Whatever text ends up leftmost is what the volume icon spaces off.
|
||||
int vol_anchor_x = date_x;
|
||||
|
||||
if (enabled_layouts > 1) {
|
||||
const char* layout_short = "en";
|
||||
if (ds->keyboard.active >= 0
|
||||
&& ds->keyboard.active < ds->keyboard.registry.count) {
|
||||
layout_short =
|
||||
ds->keyboard.registry.items[ds->keyboard.active].short_name;
|
||||
}
|
||||
int layout_tw = text_width(layout_short);
|
||||
int layout_x = date_x - layout_tw - 10;
|
||||
// Padded evenly past the glyphs so the click target is comfortable.
|
||||
ds->keyboard_layout_rect = {layout_x - 5, 4, layout_tw + 10, 24};
|
||||
draw_text(fb, layout_x, clock_y, layout_short, colors::PANEL_TEXT);
|
||||
vol_anchor_x = layout_x;
|
||||
} else {
|
||||
ds->keyboard_layout_rect = {0, 0, 0, 0};
|
||||
}
|
||||
|
||||
// Volume icon keeps its 12px icon-to-text gap whether it sits next to the
|
||||
// layout tag or, with one layout, the date.
|
||||
int vol_icon_x = vol_anchor_x - 12 - 16;
|
||||
int vol_icon_y = (PANEL_HEIGHT - 16) / 2;
|
||||
ds->vol_icon_rect = {vol_icon_x, vol_icon_y, 16, 16};
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Makefile for the Keyboard settings applet on MontaukOS
|
||||
|
||||
MAKEFLAGS += -rR
|
||||
.SUFFIXES:
|
||||
|
||||
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-montauk-
|
||||
ifeq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||
CXX = $(error x86_64-montauk toolchain not found. Run ./toolchain/build-montauk-toolchain.sh first)
|
||||
else
|
||||
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||
endif
|
||||
|
||||
PROG_INC := ../../include
|
||||
LINK_LD := ../../link.ld
|
||||
BINDIR := ../../bin
|
||||
OBJDIR := obj
|
||||
LIBDIR := ../../lib
|
||||
|
||||
CXXFLAGS := -std=gnu++20 -g -O2 -pipe -Wall -Wextra -Wno-unused-parameter \
|
||||
-Wno-unused-function -ffreestanding -fno-stack-protector -fno-stack-check \
|
||||
-fno-rtti -fno-exceptions -ffunction-sections -fdata-sections -msse -msse2 \
|
||||
-MMD -MP -I $(PROG_INC) -isystem $(PROG_INC)/libc
|
||||
LDFLAGS := -nostdlib -Wl,--gc-sections -T $(LINK_LD)
|
||||
|
||||
SRCS := main.cpp stb_truetype_impl.cpp
|
||||
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||
TARGET := $(BINDIR)/apps/keyboard/keyboard.elf
|
||||
LIBS := $(LIBDIR)/libc/liblibc.a
|
||||
|
||||
.PHONY: all clean
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
|
||||
mkdir -p $(BINDIR)/apps/keyboard
|
||||
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||
|
||||
$(OBJDIR)/%.o: %.cpp Makefile
|
||||
mkdir -p $(dir $@)
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
-include $(OBJS:.o=.d)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJDIR) $(TARGET)
|
||||
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* MontaukOS Keyboard settings applet
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/keyboard.h>
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/mtk.hpp>
|
||||
#include <gui/mtk/settings.hpp>
|
||||
#include <gui/standalone.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <stdio.h>
|
||||
}
|
||||
|
||||
using namespace gui;
|
||||
|
||||
static constexpr int WIN_W = 620;
|
||||
static constexpr int WIN_H = 460;
|
||||
static constexpr int TAB_H = 36;
|
||||
static constexpr int FOOTER_H = 44;
|
||||
static constexpr int PAD = 18;
|
||||
static constexpr int GAP = 12;
|
||||
static constexpr int BUTTON_W = 88;
|
||||
static constexpr int ROW_H = 36;
|
||||
|
||||
static constexpr int MAP_ROW_H = 24;
|
||||
|
||||
enum Tab { TAB_LAYOUTS = 0, TAB_KEYMAP, TAB_COUNT };
|
||||
static const char* const kTabLabels[TAB_COUNT] = {"Layouts", "Key Map"};
|
||||
static Tab g_tab = TAB_LAYOUTS;
|
||||
|
||||
static WsWindow g_win;
|
||||
static montauk::keyboard::State g_state;
|
||||
static montauk::keyboard::State g_saved;
|
||||
static char g_username[32] = {};
|
||||
static int g_mouse_x = -1;
|
||||
static int g_mouse_y = -1;
|
||||
static char g_status[96] = {};
|
||||
static uint64_t g_status_time = 0;
|
||||
static Color g_accent = colors::ACCENT;
|
||||
|
||||
static mtk::Theme app_theme() { return mtk::make_theme(g_accent); }
|
||||
|
||||
// ==== Geometry ====
|
||||
|
||||
static Rect tab_bar_rect() {
|
||||
return {0, 0, g_win.width, TAB_H};
|
||||
}
|
||||
|
||||
static Rect footer_rect() {
|
||||
return {0, g_win.height - FOOTER_H, g_win.width, FOOTER_H};
|
||||
}
|
||||
|
||||
static int heading_y() {
|
||||
return TAB_H + 20;
|
||||
}
|
||||
|
||||
static int list_separator_y() {
|
||||
return heading_y() + system_font_height() + 20;
|
||||
}
|
||||
|
||||
static int list_header_y() {
|
||||
return list_separator_y() + 16;
|
||||
}
|
||||
|
||||
static int list_y() {
|
||||
return list_header_y() + system_font_height() + 8;
|
||||
}
|
||||
|
||||
static Rect row_rect(int index) {
|
||||
return {PAD, list_y() + index * ROW_H, g_win.width - PAD * 2, ROW_H};
|
||||
}
|
||||
|
||||
// The checkbox owns only its indicator; the rest of the row activates the
|
||||
// layout, matching how the Display applet's mode rows select on row click.
|
||||
static Rect row_checkbox_rect(int index) {
|
||||
Rect row = row_rect(index);
|
||||
return {row.x + 8, row.y, row.w - 16, row.h};
|
||||
}
|
||||
|
||||
static Rect row_check_hit(int index) {
|
||||
Rect box = mtk::checkbox_indicator_rect(row_checkbox_rect(index));
|
||||
return {box.x - 6, box.y - 6, box.w + 12, box.h + 12};
|
||||
}
|
||||
|
||||
// Rows the key map can show without running into the footer.
|
||||
static int map_visible_rows() {
|
||||
int avail = footer_rect().y - 10 - list_y();
|
||||
int rows = avail / MAP_ROW_H;
|
||||
return rows < 0 ? 0 : rows;
|
||||
}
|
||||
|
||||
static Rect apply_button() {
|
||||
Rect foot = footer_rect();
|
||||
return {foot.x + foot.w - PAD - BUTTON_W, foot.y + 7, BUTTON_W, 30};
|
||||
}
|
||||
|
||||
static Rect revert_button() {
|
||||
Rect apply = apply_button();
|
||||
return {apply.x - GAP - BUTTON_W, apply.y, BUTTON_W, apply.h};
|
||||
}
|
||||
|
||||
// ==== State ====
|
||||
|
||||
static bool state_equal(const montauk::keyboard::State& a,
|
||||
const montauk::keyboard::State& b) {
|
||||
if (a.active != b.active || a.registry.count != b.registry.count) return false;
|
||||
for (int i = 0; i < a.registry.count; i++)
|
||||
if (a.enabled[i] != b.enabled[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool dirty() { return !state_equal(g_state, g_saved); }
|
||||
|
||||
static void set_status(const char* text) {
|
||||
montauk::strncpy(g_status, text, sizeof(g_status));
|
||||
g_status_time = montauk::get_milliseconds();
|
||||
}
|
||||
|
||||
static bool status_visible() {
|
||||
return g_status[0] && montauk::get_milliseconds() - g_status_time < 4000;
|
||||
}
|
||||
|
||||
static int enabled_count() {
|
||||
int count = 0;
|
||||
for (int i = 0; i < g_state.registry.count; i++)
|
||||
if (g_state.enabled[i]) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
static mtk::WidgetState button_state(const Rect& rect, bool enabled = true) {
|
||||
return mtk::widget_state(false, rect.contains(g_mouse_x, g_mouse_y), enabled);
|
||||
}
|
||||
|
||||
// ==== Rendering ====
|
||||
|
||||
static void draw_layouts_tab(Canvas& c, const mtk::Theme& theme) {
|
||||
c.text(PAD, heading_y(),
|
||||
"Enabled layouts appear in the panel switcher.", theme.text_subtle);
|
||||
|
||||
mtk::draw_separator(c, PAD, list_separator_y(),
|
||||
g_win.width - PAD * 2, theme);
|
||||
|
||||
c.text(PAD, list_header_y(), "AVAILABLE LAYOUTS", theme.text_muted);
|
||||
const char* hint = "Click a layout to make it active";
|
||||
c.text(g_win.width - PAD - text_width(hint), list_header_y(),
|
||||
hint, theme.text_muted);
|
||||
|
||||
for (int i = 0; i < g_state.registry.count; i++) {
|
||||
Rect row = row_rect(i);
|
||||
bool active = g_state.active == i;
|
||||
if (active)
|
||||
c.fill_rounded_rect(row.x, row.y + 2, row.w, row.h - 4, 4,
|
||||
theme.accent_soft);
|
||||
|
||||
mtk::draw_checkbox(c, row_checkbox_rect(i),
|
||||
g_state.registry.items[i].name,
|
||||
mtk::check_state(g_state.enabled[i]), theme, true,
|
||||
row_check_hit(i).contains(g_mouse_x, g_mouse_y));
|
||||
|
||||
// Layout tag ("en", "no") after the name, then the active marker.
|
||||
const char* tag = g_state.registry.items[i].short_name;
|
||||
int text_y = row.y + (row.h - system_font_height()) / 2;
|
||||
int name_w = text_width(g_state.registry.items[i].name);
|
||||
c.text(row.x + 8 + 16 + theme.gap_sm + name_w + theme.gap_sm, text_y,
|
||||
tag, theme.text_muted);
|
||||
|
||||
if (active) {
|
||||
const char* state_text = "Active";
|
||||
c.text(row.x + row.w - 10 - text_width(state_text), text_y,
|
||||
state_text, theme.text_subtle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Key Map tab: what the active layout actually remaps. Rows are identified by
|
||||
// the character the key produces unshifted, so no copy of the US scancode
|
||||
// table is needed here to name the physical keys.
|
||||
static void draw_char_cell(Canvas& c, int x, int y, uint8_t byte,
|
||||
const mtk::Theme& theme) {
|
||||
char text[2] = {(char)byte, '\0'};
|
||||
if (byte == 0) {
|
||||
c.text(x, y, "-", theme.text_muted);
|
||||
} else {
|
||||
c.text(x, y, text, theme.text);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_keymap_tab(Canvas& c, const mtk::Theme& theme) {
|
||||
const montauk::keyboard::Layout& layout =
|
||||
g_state.registry.items[g_state.active];
|
||||
|
||||
char heading[96];
|
||||
snprintf(heading, sizeof(heading), "Key map for %s", layout.name);
|
||||
c.text(PAD, heading_y(), heading, theme.text_subtle);
|
||||
mtk::draw_separator(c, PAD, list_separator_y(),
|
||||
g_win.width - PAD * 2, theme);
|
||||
|
||||
if (layout.key_count == 0) {
|
||||
c.text(PAD, list_header_y(),
|
||||
"This layout uses the built-in key map. No keys are remapped.",
|
||||
theme.text_muted);
|
||||
return;
|
||||
}
|
||||
|
||||
int group_w = (g_win.width - PAD * 2 - GAP) / 2;
|
||||
int per_group = (layout.key_count + 1) / 2;
|
||||
int shown = per_group;
|
||||
if (shown > map_visible_rows()) shown = map_visible_rows();
|
||||
const int col[3] = {0, 74, 148};
|
||||
|
||||
for (int group = 0; group < 2; group++) {
|
||||
int gx = PAD + group * (group_w + GAP);
|
||||
c.text(gx + col[0], list_header_y(), "NORMAL", theme.text_muted);
|
||||
c.text(gx + col[1], list_header_y(), "SHIFT", theme.text_muted);
|
||||
c.text(gx + col[2], list_header_y(), "ALTGR", theme.text_muted);
|
||||
|
||||
for (int row = 0; row < shown; row++) {
|
||||
int index = group * per_group + row;
|
||||
if (index >= layout.key_count) break;
|
||||
const montauk::keyboard::KeyMap& key = layout.keys[index];
|
||||
int y = list_y() + row * MAP_ROW_H;
|
||||
draw_char_cell(c, gx + col[0], y, key.base, theme);
|
||||
draw_char_cell(c, gx + col[1], y, key.shift, theme);
|
||||
draw_char_cell(c, gx + col[2], y, key.altgr, theme);
|
||||
}
|
||||
}
|
||||
|
||||
if (shown < per_group) {
|
||||
char note[64];
|
||||
snprintf(note, sizeof(note), "%d more keys not shown",
|
||||
layout.key_count - shown * 2);
|
||||
c.text(PAD, list_y() + shown * MAP_ROW_H + 4, note, theme.text_muted);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_footer(Canvas& c, const mtk::Theme& theme) {
|
||||
Rect foot = footer_rect();
|
||||
c.fill_rect(foot.x, foot.y, foot.w, foot.h, theme.surface);
|
||||
mtk::draw_separator(c, 0, foot.y, g_win.width, theme);
|
||||
|
||||
const char* message = status_visible()
|
||||
? g_status
|
||||
: (dirty() ? "Unsaved keyboard changes" : "Keyboard settings ready");
|
||||
c.text(PAD, foot.y + (foot.h - system_font_height()) / 2,
|
||||
message, dirty() ? theme.text : theme.text_subtle);
|
||||
|
||||
if (g_tab == TAB_LAYOUTS) {
|
||||
mtk::draw_button(c, revert_button(), "Revert", mtk::BUTTON_SECONDARY,
|
||||
button_state(revert_button(), dirty()), theme);
|
||||
mtk::draw_button(c, apply_button(), "Apply", mtk::BUTTON_PRIMARY,
|
||||
button_state(apply_button(), dirty()), theme);
|
||||
}
|
||||
}
|
||||
|
||||
static void render() {
|
||||
mtk::StandaloneHost host(&g_win);
|
||||
Canvas c = host.canvas();
|
||||
mtk::Theme theme = app_theme();
|
||||
c.fill(theme.window_bg);
|
||||
mtk::draw_tab_bar(c, tab_bar_rect(), kTabLabels, TAB_COUNT, (int)g_tab, theme);
|
||||
if (g_tab == TAB_LAYOUTS) draw_layouts_tab(c, theme);
|
||||
else draw_keymap_tab(c, theme);
|
||||
draw_footer(c, theme);
|
||||
host.present();
|
||||
}
|
||||
|
||||
// ==== Input ====
|
||||
|
||||
static void toggle_enabled(int index) {
|
||||
if (g_state.enabled[index] && enabled_count() == 1) {
|
||||
set_status("At least one layout must remain enabled");
|
||||
return;
|
||||
}
|
||||
g_state.enabled[index] = !g_state.enabled[index];
|
||||
if (!g_state.enabled[g_state.active])
|
||||
g_state.active = montauk::keyboard::next_enabled(g_state, g_state.active);
|
||||
}
|
||||
|
||||
static bool handle_mouse(const montauk::abi::WinEvent& ev) {
|
||||
g_mouse_x = ev.mouse.x;
|
||||
g_mouse_y = ev.mouse.y;
|
||||
bool pressed = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
||||
if (!pressed) return true;
|
||||
|
||||
int tab = mtk::hit_tab_bar(tab_bar_rect(), TAB_COUNT, g_mouse_x, g_mouse_y);
|
||||
if (tab >= 0) {
|
||||
g_tab = (Tab)tab;
|
||||
return true;
|
||||
}
|
||||
if (g_tab != TAB_LAYOUTS) return true;
|
||||
|
||||
for (int i = 0; i < g_state.registry.count; i++) {
|
||||
if (row_check_hit(i).contains(g_mouse_x, g_mouse_y)) {
|
||||
toggle_enabled(i);
|
||||
return true;
|
||||
}
|
||||
if (row_rect(i).contains(g_mouse_x, g_mouse_y)) {
|
||||
if (g_state.enabled[i]) {
|
||||
g_state.active = i;
|
||||
} else {
|
||||
set_status("Enable a layout before making it active");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dirty()) return true;
|
||||
if (revert_button().contains(g_mouse_x, g_mouse_y)) {
|
||||
g_state = g_saved;
|
||||
set_status("Changes reverted");
|
||||
} else if (apply_button().contains(g_mouse_x, g_mouse_y)) {
|
||||
if (montauk::keyboard::save_user(g_username, g_state)) {
|
||||
g_saved = g_state;
|
||||
set_status("Keyboard layouts applied");
|
||||
} else {
|
||||
set_status("Could not save keyboard settings");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
if (!fonts::init()) montauk::exit(1);
|
||||
if (montauk::getuser(g_username, sizeof(g_username)) <= 0)
|
||||
montauk::strcpy(g_username, "default");
|
||||
g_accent = mtk::load_system_accent();
|
||||
g_state = montauk::keyboard::load_user(g_username);
|
||||
g_saved = g_state;
|
||||
|
||||
if (!g_win.create("Keyboard", WIN_W, WIN_H)) montauk::exit(1);
|
||||
render();
|
||||
while (g_win.id >= 0 && !g_win.closed) {
|
||||
montauk::abi::WinEvent ev;
|
||||
int result = g_win.poll(&ev);
|
||||
if (result < 0) break;
|
||||
if (result == 0) {
|
||||
if (status_visible()) render();
|
||||
montauk::sleep_ms(16);
|
||||
continue;
|
||||
}
|
||||
if (ev.type == 3) break;
|
||||
if (ev.type == 1) handle_mouse(ev);
|
||||
else if (ev.type == 0 && ev.key.pressed && ev.key.scancode == 0x01)
|
||||
g_win.closed = true;
|
||||
render();
|
||||
}
|
||||
g_win.destroy();
|
||||
montauk::exit(0);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
[app]
|
||||
id = "keyboard"
|
||||
name = "Keyboard"
|
||||
binary = "keyboard.elf"
|
||||
icon = "preferences-keyboard.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
visible = false
|
||||
|
||||
[desktop]
|
||||
section = "settings"
|
||||
admin_only = false
|
||||
@@ -0,0 +1,2 @@
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include <gui/stb_truetype.h>
|
||||
@@ -96,6 +96,10 @@ extern "C" void _start() {
|
||||
while (montauk::is_key_available()) {
|
||||
montauk::abi::KeyEvent key;
|
||||
montauk::getkey(&key);
|
||||
// Deliberately untranslated: the login screen is US-English only
|
||||
// until it grows its own layout switcher. There is no user yet, so
|
||||
// there is no per-user layout to apply, and silently picking one
|
||||
// would leave no way to type a password in a different layout.
|
||||
handle_key(ls, key);
|
||||
key_changed = true;
|
||||
}
|
||||
|
||||
@@ -415,8 +415,9 @@ static char ascii_lower(char c) {
|
||||
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
|
||||
}
|
||||
|
||||
static bool is_printable_ascii(char c) {
|
||||
return c >= 32 && c < 127;
|
||||
static bool is_printable_byte(char c) {
|
||||
unsigned char value = (unsigned char)c;
|
||||
return value >= 32 && value != 127;
|
||||
}
|
||||
|
||||
static bool str_contains_case_insensitive(const char* text, const char* needle) {
|
||||
@@ -1448,7 +1449,7 @@ static bool handle_key(const montauk::abi::KeyEvent& key, bool& quit) {
|
||||
else g.filter_active = false;
|
||||
return true;
|
||||
}
|
||||
if (!key.ctrl && !key.alt && is_printable_ascii(key.ascii)) {
|
||||
if (!key.ctrl && !key.alt && is_printable_byte(key.ascii)) {
|
||||
append_filter_char(key.ascii);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -246,7 +246,8 @@ extern "C" void _start() {
|
||||
if (g_pathbar_cursor > 0) g_pathbar_cursor--;
|
||||
} else if (key.scancode == 0x4D) {
|
||||
if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++;
|
||||
} else if (key.ascii >= 32 && key.ascii < 127 && g_pathbar_len < 254) {
|
||||
} else if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127 && g_pathbar_len < 254) {
|
||||
for (int i = g_pathbar_len; i > g_pathbar_cursor; i--)
|
||||
g_pathbar_text[i] = g_pathbar_text[i - 1];
|
||||
g_pathbar_text[g_pathbar_cursor] = key.ascii;
|
||||
|
||||
@@ -426,7 +426,8 @@ extern "C" void _start() {
|
||||
if (g_pathbar_cursor > 0) g_pathbar_cursor--;
|
||||
} else if (key.scancode == 0x4D) {
|
||||
if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++;
|
||||
} else if (key.ascii >= 32 && key.ascii < 127 && g_pathbar_len < 254) {
|
||||
} else if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127 && g_pathbar_len < 254) {
|
||||
for (int i = g_pathbar_len; i > g_pathbar_cursor; i--)
|
||||
g_pathbar_text[i] = g_pathbar_text[i - 1];
|
||||
g_pathbar_text[g_pathbar_cursor] = key.ascii;
|
||||
@@ -669,7 +670,8 @@ extern "C" void _start() {
|
||||
redraw = true;
|
||||
}
|
||||
// Printable character: start editing or insert
|
||||
else if (key.ascii >= 32 && key.ascii < 127 && !key.ctrl) {
|
||||
else if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127 && !key.ctrl) {
|
||||
if (!g_editing) {
|
||||
clear_selection();
|
||||
g_editing = true;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/keyboard.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/canvas.hpp>
|
||||
#include <gui/draw.hpp>
|
||||
@@ -392,6 +393,14 @@ static void term_handle_mouse(const montauk::abi::WinEvent& ev) {
|
||||
}
|
||||
|
||||
// Keyboard handling shared by both run modes.
|
||||
// User whose keyboard layout selection console mode follows.
|
||||
static char g_layout_user[32] = "default";
|
||||
|
||||
static void init_layout_user() {
|
||||
if (montauk::getuser(g_layout_user, sizeof(g_layout_user)) <= 0)
|
||||
montauk::strcpy(g_layout_user, "default");
|
||||
}
|
||||
|
||||
static void term_handle_key_core(const montauk::abi::KeyEvent& key,
|
||||
int width, int height) {
|
||||
if (g_tabs.tab_count <= 0) return;
|
||||
@@ -586,6 +595,8 @@ static void run_console() {
|
||||
if (cols < 1) cols = 1;
|
||||
if (rows < 1) rows = 1;
|
||||
|
||||
init_layout_user();
|
||||
|
||||
g_tabs.tabs[0] = term_create_tab(cols, rows);
|
||||
if (!g_tabs.tabs[0])
|
||||
montauk::exit(1);
|
||||
@@ -610,6 +621,9 @@ static void run_console() {
|
||||
while (montauk::is_key_available()) {
|
||||
montauk::abi::KeyEvent key;
|
||||
montauk::getkey(&key);
|
||||
// Console mode reads the kernel buffer directly, so it does not
|
||||
// get the desktop's layout translation the windowed path does.
|
||||
montauk::keyboard::translate_direct(&key, g_layout_user);
|
||||
term_handle_key_core(key, sw, sh);
|
||||
redraw = true;
|
||||
if (g_should_exit) break;
|
||||
|
||||
@@ -1063,7 +1063,8 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) {
|
||||
}
|
||||
if (key.scancode == 0x4B) { if (g_pathbar_cursor > 0) g_pathbar_cursor--; return true; }
|
||||
if (key.scancode == 0x4D) { if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++; return true; }
|
||||
if (key.ascii >= 32 && key.ascii < 127 && g_pathbar_len < 254) {
|
||||
if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127 && g_pathbar_len < 254) {
|
||||
for (int i = g_pathbar_len; i > g_pathbar_cursor; i--)
|
||||
g_pathbar_text[i] = g_pathbar_text[i - 1];
|
||||
g_pathbar_text[g_pathbar_cursor] = key.ascii;
|
||||
@@ -1214,7 +1215,7 @@ static bool handle_key(montauk::abi::KeyEvent& key, int win_id) {
|
||||
}
|
||||
|
||||
// Printable
|
||||
if (key.ascii >= 32 && key.ascii < 127) {
|
||||
if ((unsigned char)key.ascii >= 32 && (unsigned char)key.ascii != 127) {
|
||||
if (g_has_selection) delete_selection();
|
||||
insert_char(key.ascii);
|
||||
return true;
|
||||
|
||||
@@ -302,7 +302,7 @@ static void sort_countries() {
|
||||
}
|
||||
|
||||
static void load_timezone_data() {
|
||||
g_data = montauk::config::load("timezonedata");
|
||||
g_data = montauk::data::load("timezones");
|
||||
g_data_loaded = true;
|
||||
g_country_count = 0;
|
||||
|
||||
@@ -798,7 +798,7 @@ static void render() {
|
||||
if (g_tab == TAB_TIME_ZONES) {
|
||||
if (g_country_count <= 0) {
|
||||
canvas.text(PAD, TAB_H + 24,
|
||||
"No time zone data was found in 0:/config/timezonedata.toml",
|
||||
"No time zone data was found in 0:/os/data/timezones.toml",
|
||||
theme.danger);
|
||||
} else {
|
||||
Rect country = country_list_rect();
|
||||
|
||||
@@ -628,7 +628,8 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
|
||||
if (wp->pathbar_cursor < wp->pathbar_len) wp->pathbar_cursor++;
|
||||
return;
|
||||
}
|
||||
if (key.ascii >= 32 && key.ascii < 127 && wp->pathbar_len < 254) {
|
||||
if ((unsigned char)key.ascii >= 32
|
||||
&& (unsigned char)key.ascii != 127 && wp->pathbar_len < 254) {
|
||||
for (int i = wp->pathbar_len; i > wp->pathbar_cursor; i--)
|
||||
wp->pathbar_text[i] = wp->pathbar_text[i - 1];
|
||||
wp->pathbar_text[wp->pathbar_cursor] = key.ascii;
|
||||
@@ -885,7 +886,7 @@ void wp_handle_key(const montauk::abi::KeyEvent& key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.ascii >= 32 && key.ascii < 127) {
|
||||
if ((unsigned char)key.ascii >= 32 && (unsigned char)key.ascii != 127) {
|
||||
if (wp->has_selection) wp_delete_selection(wp);
|
||||
wp_insert_char(wp, key.ascii);
|
||||
wp_history_checkpoint(wp);
|
||||
|
||||
Reference in New Issue
Block a user