Introduce QtFrontend with renderer, input handler, and theming support.

- Added `QtFrontend`, `QtRenderer`, and `QtInputHandler` for Qt-based UI rendering and input handling.
- Implemented support for theming, font customization, and palette overrides in GUITheme.
- Renamed and refactored ImGui-specific components (e.g., `GUIRenderer` -> `ImGuiRenderer`).
- Added cross-frontend integration for commands and visual font picker.
This commit is contained in:
2025-12-04 21:33:55 -08:00
parent f5a4625652
commit ee2c9939d7
22 changed files with 2972 additions and 726 deletions

View File

@@ -9,6 +9,7 @@ set(KTE_VERSION "1.3.9")
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.")
set(KTE_USE_QT OFF CACHE BOOL "Build the QT frontend instead of ImGui.")
set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
@@ -101,14 +102,30 @@ set(FONT_SOURCES
fonts/FontRegistry.cc
)
set(GUI_SOURCES
${FONT_SOURCES}
GUIConfig.cc
GUIRenderer.cc
GUIInputHandler.cc
GUIFrontend.cc
)
if (BUILD_GUI)
set(GUI_SOURCES
GUIConfig.cc
)
if (KTE_USE_QT)
find_package(Qt6 COMPONENTS Widgets REQUIRED)
set(GUI_SOURCES
${GUI_SOURCES}
QtFrontend.cc
QtInputHandler.cc
QtRenderer.cc
)
# Expose preprocessor switch so sources can exclude ImGui-specific code
add_compile_definitions(KTE_USE_QT)
else ()
set(GUI_SOURCES
${GUI_SOURCES}
${FONT_SOURCES}
ImGuiFrontend.cc
ImGuiInputHandler.cc
ImGuiRenderer.cc
)
endif ()
endif ()
set(COMMON_SOURCES
GapBuffer.cc
@@ -222,14 +239,29 @@ set(COMMON_HEADERS
${SYNTAX_HEADERS}
)
set(GUI_HEADERS
${THEME_HEADERS}
${FONT_HEADERS}
GUIConfig.h
GUIRenderer.h
GUIInputHandler.h
GUIFrontend.h
)
if (BUILD_GUI)
set(GUI_HEADERS
GUIConfig.h
)
if (KTE_USE_QT)
set(GUI_HEADERS
${GUI_HEADERS}
QtFrontend.h
QtInputHandler.h
QtRenderer.h
)
else ()
set(GUI_HEADERS
${GUI_HEADERS}
${THEME_HEADERS}
${FONT_HEADERS}
ImGuiFrontend.h
ImGuiInputHandler.h
ImGuiRenderer.h
)
endif ()
endif ()
# kte (terminal-first) executable
add_executable(kte
@@ -319,10 +351,17 @@ if (${BUILD_GUI})
)
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
if (KTE_USE_QT)
target_compile_definitions(kge PRIVATE KTE_USE_QT=1)
endif ()
if (KTE_UNDO_DEBUG)
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
endif ()
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
if (KTE_USE_QT)
target_link_libraries(kge ${CURSES_LIBRARIES} Qt6::Widgets)
else ()
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
endif ()
# On macOS, build kge as a proper .app bundle
if (APPLE)

View File

@@ -18,11 +18,31 @@
#include "syntax/HighlighterEngine.h"
#include "syntax/CppHighlighter.h"
#ifdef KTE_BUILD_GUI
#include "GUITheme.h"
#include "fonts/FontRegistry.h"
#include "imgui.h"
# include "GUITheme.h"
# if !defined(KTE_USE_QT)
# include "fonts/FontRegistry.h"
# include "imgui.h"
# endif
# if defined(KTE_USE_QT)
# include <QFontDatabase>
# include <QStringList>
# endif
#endif
// Define cross-frontend theme change flags declared in GUITheme.h
namespace kte {
bool gThemeChangePending = false;
std::string gThemeChangeRequest;
// Qt font change globals
bool gFontChangePending = false;
std::string gFontFamilyRequest;
float gFontSizeRequest = 0.0f;
std::string gCurrentFontFamily;
float gCurrentFontSize = 0.0f;
// Request Qt visual font dialog
bool gFontDialogRequested = false;
}
// Keep buffer viewport offsets so that the cursor stays within the visible
// window based on the editor's current dimensions. The bottom row is reserved
@@ -104,24 +124,24 @@ static bool
is_mutating_command(CommandId id)
{
switch (id) {
case CommandId::InsertText:
case CommandId::Newline:
case CommandId::Backspace:
case CommandId::DeleteChar:
case CommandId::KillToEOL:
case CommandId::KillLine:
case CommandId::Yank:
case CommandId::DeleteWordPrev:
case CommandId::DeleteWordNext:
case CommandId::IndentRegion:
case CommandId::UnindentRegion:
case CommandId::ReflowParagraph:
case CommandId::KillRegion:
case CommandId::Undo:
case CommandId::Redo:
return true;
default:
return false;
case CommandId::InsertText:
case CommandId::Newline:
case CommandId::Backspace:
case CommandId::DeleteChar:
case CommandId::KillToEOL:
case CommandId::KillLine:
case CommandId::Yank:
case CommandId::DeleteWordPrev:
case CommandId::DeleteWordNext:
case CommandId::IndentRegion:
case CommandId::UnindentRegion:
case CommandId::ReflowParagraph:
case CommandId::KillRegion:
case CommandId::Undo:
case CommandId::Redo:
return true;
default:
return false;
}
}
@@ -914,8 +934,8 @@ cmd_set_option(CommandContext &ctx)
}
// GUI theme cycling commands (available in GUI build; show message otherwise)
#ifdef KTE_BUILD_GUI
// GUI theme cycling commands (available in GUI build; ImGui-only for now)
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
static bool
cmd_theme_next(CommandContext &ctx)
{
@@ -951,7 +971,7 @@ cmd_theme_prev(CommandContext &ctx)
// Theme set by name command
#ifdef KTE_BUILD_GUI
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
static bool
cmd_theme_set_by_name(const CommandContext &ctx)
{
@@ -995,15 +1015,41 @@ cmd_theme_set_by_name(const CommandContext &ctx)
static bool
cmd_theme_set_by_name(CommandContext &ctx)
{
# if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT)
// Qt GUI build: schedule theme change for frontend
std::string name = ctx.arg;
// trim spaces
auto ltrim = [](std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
};
auto rtrim = [](std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), s.end());
};
ltrim(name);
rtrim(name);
if (name.empty()) {
ctx.editor.SetStatus("theme: provide a name (e.g., nord, solarized-dark, gruvbox-light, eink)");
return true;
}
kte::gThemeChangeRequest = name;
kte::gThemeChangePending = true;
ctx.editor.SetStatus(std::string("Theme requested: ") + name);
return true;
# else
(void) ctx;
// No-op in terminal build
return true;
# endif
}
#endif
// Font set by name (GUI)
#ifdef KTE_BUILD_GUI
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
static bool
cmd_font_set_by_name(const CommandContext &ctx)
{
@@ -1056,14 +1102,38 @@ cmd_font_set_by_name(const CommandContext &ctx)
static bool
cmd_font_set_by_name(CommandContext &ctx)
{
(void) ctx;
// Qt build: queue font family change
std::string name = ctx.arg;
// trim
auto ltrim = [](std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
};
auto rtrim = [](std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), s.end());
};
ltrim(name);
rtrim(name);
if (name.empty()) {
// Show current font when no argument provided
std::string cur = kte::gCurrentFontFamily.empty() ? std::string("default") : kte::gCurrentFontFamily;
ctx.editor.SetStatus(std::string("Current font: ") + cur);
return true;
}
kte::gFontFamilyRequest = name;
// Keep size if not specified by user; signal change
kte::gFontChangePending = true;
ctx.editor.SetStatus(std::string("Font requested: ") + name);
return true;
}
#endif
// Font size set (GUI)
#ifdef KTE_BUILD_GUI
// Font size set (GUI, ImGui-only for now)
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
static bool
cmd_font_set_size(const CommandContext &ctx)
{
@@ -1122,14 +1192,45 @@ cmd_font_set_size(const CommandContext &ctx)
static bool
cmd_font_set_size(CommandContext &ctx)
{
(void) ctx;
// Qt build: parse size and queue change
std::string a = ctx.arg;
auto ltrim = [](std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
};
auto rtrim = [](std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), s.end());
};
ltrim(a);
rtrim(a);
if (a.empty()) {
float cur = (kte::gCurrentFontSize > 0.0f) ? kte::gCurrentFontSize : 18.0f;
ctx.editor.SetStatus(std::string("Current font size: ") + std::to_string((int) std::round(cur)));
return true;
}
char *endp = nullptr;
float size = strtof(a.c_str(), &endp);
if (endp == a.c_str() || !std::isfinite(size)) {
ctx.editor.SetStatus("font-size: expected number");
return true;
}
if (size < 6.0f)
size = 6.0f;
if (size > 96.0f)
size = 96.0f;
kte::gFontSizeRequest = size;
kte::gFontChangePending = true;
ctx.editor.SetStatus(std::string("Font size requested: ") + std::to_string((int) std::round(size)));
return true;
}
#endif
// Background set command (GUI)
#ifdef KTE_BUILD_GUI
// Background set command (GUI, ImGui-only for now)
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
static bool
cmd_background_set(const CommandContext &ctx)
{
@@ -1298,6 +1399,20 @@ cmd_visual_file_picker_toggle(const CommandContext &ctx)
}
// GUI: request visual font picker (Qt frontend will consume flag)
static bool
cmd_visual_font_picker_toggle(const CommandContext &ctx)
{
#ifdef KTE_BUILD_GUI
kte::gFontDialogRequested = true;
ctx.editor.SetStatus("Font chooser");
#else
ctx.editor.SetStatus("Font chooser not available in terminal");
#endif
return true;
}
static bool
cmd_jump_to_line_start(const CommandContext &ctx)
{
@@ -1599,7 +1714,8 @@ cmd_insert_text(CommandContext &ctx)
std::string argprefix = text.substr(sp + 1);
// Only special-case argument completion for certain commands
if (cmd == "theme") {
#ifdef KTE_BUILD_GUI
#if defined(KTE_BUILD_GUI)
# if !defined(KTE_USE_QT)
std::vector<std::string> cands;
const auto &reg = kte::ThemeRegistry();
for (const auto &t: reg) {
@@ -1607,6 +1723,67 @@ cmd_insert_text(CommandContext &ctx)
if (argprefix.empty() || n.rfind(argprefix, 0) == 0)
cands.push_back(n);
}
# else
// Qt: offer known theme names handled by ApplyQtThemeByName
static const char *qt_themes[] = {
"nord",
"solarized-dark",
"solarized-light",
"gruvbox-dark",
"gruvbox-light",
"eink"
};
std::vector<std::string> cands;
for (const char *t: qt_themes) {
std::string n(t);
if (argprefix.empty() || n.rfind(argprefix, 0) == 0)
cands.push_back(n);
}
# endif
if (cands.empty()) {
// no change
} else if (cands.size() == 1) {
ctx.editor.SetPromptText(cmd + std::string(" ") + cands[0]);
} else {
std::string lcp = cands[0];
for (size_t i = 1; i < cands.size(); ++i) {
const std::string &s = cands[i];
size_t j = 0;
while (j < lcp.size() && j < s.size() && lcp[j] == s[j])
++j;
lcp.resize(j);
if (lcp.empty())
break;
}
if (!lcp.empty() && lcp != argprefix)
ctx.editor.SetPromptText(cmd + std::string(" ") + lcp);
}
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
return true;
#else
(void) argprefix; // no completion in non-GUI build
#endif
}
if (cmd == "font") {
#if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT)
// Complete against installed font families (case-insensitive prefix)
std::vector<std::string> cands;
QStringList fams = QFontDatabase::families();
std::string apfx_lower = argprefix;
std::transform(apfx_lower.begin(), apfx_lower.end(), apfx_lower.begin(),
[](unsigned char c) {
return (char) std::tolower(c);
});
for (const auto &fam: fams) {
std::string n = fam.toStdString();
std::string nlower = n;
std::transform(nlower.begin(), nlower.end(), nlower.begin(),
[](unsigned char c) {
return (char) std::tolower(c);
});
if (apfx_lower.empty() || nlower.rfind(apfx_lower, 0) == 0)
cands.push_back(n);
}
if (cands.empty()) {
// no change
} else if (cands.size() == 1) {
@@ -1628,7 +1805,7 @@ cmd_insert_text(CommandContext &ctx)
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
return true;
#else
(void) argprefix; // no completion in non-GUI build
(void) argprefix;
#endif
}
// default: no special arg completion
@@ -3770,11 +3947,11 @@ cmd_reflow_paragraph(CommandContext &ctx)
if (!buf)
return false;
ensure_at_least_one_line(*buf);
auto &rows = buf->Rows();
std::size_t y = buf->Cury();
auto &rows = buf->Rows();
std::size_t y = buf->Cury();
// Treat a universal-argument count of 1 as "no width specified".
// Editor::UArgGet() returns 1 when no explicit count was provided.
int width = ctx.count > 1 ? ctx.count : 72;
int width = ctx.count > 1 ? ctx.count : 72;
std::size_t para_start = y;
while (para_start > 0 && !rows[para_start - 1].empty())
--para_start;
@@ -4201,9 +4378,9 @@ InstallDefaultCommands()
CommandRegistry::Register(
{CommandId::UnindentRegion, "unindent-region", "Unindent region", cmd_unindent_region});
CommandRegistry::Register({
CommandId::ReflowParagraph, "reflow-paragraph",
"Reflow paragraph to column width", cmd_reflow_paragraph
});
CommandId::ReflowParagraph, "reflow-paragraph",
"Reflow paragraph to column width", cmd_reflow_paragraph
});
// Read-only
CommandRegistry::Register({
CommandId::ToggleReadOnly, "toggle-read-only", "Toggle buffer read-only", cmd_toggle_read_only
@@ -4249,6 +4426,10 @@ InstallDefaultCommands()
CommandId::VisualFilePickerToggle, "file-picker-toggle", "Toggle visual file picker",
cmd_visual_file_picker_toggle, false, false
});
CommandRegistry::Register({
CommandId::VisualFontPickerToggle, "font-picker-toggle", "Show visual font picker",
cmd_visual_font_picker_toggle, false, false
});
// Working directory
CommandRegistry::Register({
CommandId::ShowWorkingDirectory, "show-working-directory", "Show current working directory",

View File

@@ -27,6 +27,8 @@ enum class CommandId {
SearchReplace, // begin search & replace (two-step prompt)
OpenFileStart, // begin open-file prompt
VisualFilePickerToggle,
// GUI-only: toggle/show a visual font selector dialog
VisualFontPickerToggle,
// Buffers
BufferSwitchStart, // begin buffer switch prompt
BufferClose,

View File

@@ -1,599 +0,0 @@
#include <cstdio>
#include <algorithm>
#include <ncurses.h>
#include <SDL.h>
#include <imgui.h>
#include "GUIInputHandler.h"
#include "KKeymap.h"
#include "Editor.h"
static bool
map_key(const SDL_Keycode key,
const SDL_Keymod mod,
bool &k_prefix,
bool &esc_meta,
bool &k_ctrl_pending,
Editor *ed,
MappedInput &out,
bool &suppress_textinput_once)
{
// Ctrl handling
const bool is_ctrl = (mod & KMOD_CTRL) != 0;
const bool is_alt = (mod & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
// If previous key was ESC, interpret this as Meta via ESC keymap.
// Prefer KEYDOWN when we can derive a printable ASCII; otherwise defer to TEXTINPUT.
if (esc_meta) {
int ascii_key = 0;
if (key == SDLK_BACKSPACE) {
// ESC BACKSPACE: map to DeleteWordPrev using ncurses KEY_BACKSPACE constant
ascii_key = KEY_BACKSPACE;
} else if (key >= SDLK_a && key <= SDLK_z) {
ascii_key = static_cast<int>('a' + (key - SDLK_a));
} else if (key == SDLK_COMMA) {
ascii_key = '<';
} else if (key == SDLK_PERIOD) {
ascii_key = '>';
} else if (key == SDLK_LESS) {
ascii_key = '<';
} else if (key == SDLK_GREATER) {
ascii_key = '>';
}
if (ascii_key != 0) {
esc_meta = false; // consume if we can decide on KEYDOWN
ascii_key = KLowerAscii(ascii_key);
CommandId id;
if (KLookupEscCommand(ascii_key, id)) {
out = {true, id, "", 0};
return true;
}
// Known printable but unmapped ESC sequence: report invalid
out = {true, CommandId::UnknownEscCommand, "", 0};
return true;
}
// No usable ASCII from KEYDOWN → keep esc_meta set and let TEXTINPUT handle it
out.hasCommand = false;
return true;
}
// Movement and basic keys
// These keys exit k-prefix mode if active (user pressed C-k then a special key).
switch (key) {
case SDLK_LEFT:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::MoveLeft, "", 0};
return true;
case SDLK_RIGHT:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::MoveRight, "", 0};
return true;
case SDLK_UP:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::MoveUp, "", 0};
return true;
case SDLK_DOWN:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::MoveDown, "", 0};
return true;
case SDLK_HOME:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::MoveHome, "", 0};
return true;
case SDLK_END:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::MoveEnd, "", 0};
return true;
case SDLK_PAGEUP:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::PageUp, "", 0};
return true;
case SDLK_PAGEDOWN:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::PageDown, "", 0};
return true;
case SDLK_DELETE:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::DeleteChar, "", 0};
return true;
case SDLK_BACKSPACE:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::Backspace, "", 0};
return true;
case SDLK_TAB:
// Insert a literal tab character when not interpreting a k-prefix suffix.
// If k-prefix is active, let the k-prefix handler below consume the key
// (so Tab doesn't leave k-prefix stuck).
if (!k_prefix) {
out = {true, CommandId::InsertText, std::string("\t"), 0};
return true;
}
break; // fall through so k-prefix handler can process
case SDLK_RETURN:
case SDLK_KP_ENTER:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::Newline, "", 0};
return true;
case SDLK_ESCAPE:
k_prefix = false;
k_ctrl_pending = false;
esc_meta = true; // next key will be treated as Meta
out.hasCommand = false; // no immediate command for bare ESC in GUI
return true;
default:
break;
}
// If we are in k-prefix, interpret the very next key via the C-k keymap immediately.
if (k_prefix) {
esc_meta = false;
// Normalize to ASCII; preserve case for letters using Shift
int ascii_key = 0;
if (key >= SDLK_a && key <= SDLK_z) {
ascii_key = static_cast<int>('a' + (key - SDLK_a));
if (mod & KMOD_SHIFT)
ascii_key = ascii_key - 'a' + 'A';
} else if (key == SDLK_COMMA) {
ascii_key = '<';
} else if (key == SDLK_PERIOD) {
ascii_key = '>';
} else if (key == SDLK_LESS) {
ascii_key = '<';
} else if (key == SDLK_GREATER) {
ascii_key = '>';
} else if (key >= SDLK_SPACE && key <= SDLK_z) {
ascii_key = static_cast<int>(key);
}
bool ctrl2 = (mod & KMOD_CTRL) != 0;
// If user typed a literal 'C' (or '^') as a control qualifier, keep k-prefix active
if (ascii_key == 'C' || ascii_key == 'c' || ascii_key == '^') {
k_ctrl_pending = true;
// Keep waiting for the next suffix; show status and suppress ensuing TEXTINPUT
if (ed)
ed->SetStatus("C-k C _");
suppress_textinput_once = true;
out.hasCommand = false;
return true;
}
// Otherwise, consume the k-prefix now for the actual suffix
k_prefix = false;
if (ascii_key != 0) {
int lower = KLowerAscii(ascii_key);
bool ctrl_suffix_supported = (lower == 'd' || lower == 'x' || lower == 'q');
bool pass_ctrl = (ctrl2 || k_ctrl_pending) && ctrl_suffix_supported;
k_ctrl_pending = false;
CommandId id;
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
// Diagnostics for u/U
if (lower == 'u') {
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
? static_cast<char>(ascii_key)
: '?';
std::fprintf(stderr,
"[kge] k-prefix suffix: sym=%d mods=0x%x ascii=%d '%c' ctrl2=%d pass_ctrl=%d mapped=%d id=%d\n",
static_cast<int>(key), static_cast<unsigned int>(mod), ascii_key, disp,
ctrl2 ? 1 : 0, pass_ctrl ? 1 : 0, mapped ? 1 : 0,
mapped ? static_cast<int>(id) : -1);
std::fflush(stderr);
}
if (mapped) {
out = {true, id, "", 0};
if (ed)
ed->SetStatus(""); // clear "C-k _" hint after suffix
return true;
}
int shown = KLowerAscii(ascii_key);
char c = (shown >= 0x20 && shown <= 0x7e) ? static_cast<char>(shown) : '?';
std::string arg(1, c);
out = {true, CommandId::UnknownKCommand, arg, 0};
if (ed)
ed->SetStatus(""); // clear hint; handler will set unknown status
return true;
}
// Non-printable/unmappable key as k-suffix (e.g., F-keys): report unknown and exit k-mode
out = {true, CommandId::UnknownKCommand, std::string("?"), 0};
if (ed)
ed->SetStatus("");
return true;
}
if (is_ctrl) {
// Universal argument: C-u
if (key == SDLK_u) {
if (ed)
ed->UArgStart();
out.hasCommand = false;
return true;
}
// Cancel universal arg on C-g as well (it maps to Refresh via ctrl map)
if (key == SDLK_g) {
if (ed)
ed->UArgClear();
// Also cancel any pending k-prefix qualifier
k_ctrl_pending = false;
k_prefix = false; // treat as cancel of prefix
}
if (key == SDLK_k || key == SDLK_KP_EQUALS) {
k_prefix = true;
out = {true, CommandId::KPrefix, "", 0};
return true;
}
// Map other control chords via shared keymap
if (key >= SDLK_a && key <= SDLK_z) {
int ascii_key = static_cast<int>('a' + (key - SDLK_a));
CommandId id;
if (KLookupCtrlCommand(ascii_key, id)) {
out = {true, id, "", 0};
return true;
}
}
}
// Alt/Meta bindings (ESC f/b equivalent)
if (is_alt) {
int ascii_key = 0;
if (key == SDLK_BACKSPACE) {
// Alt BACKSPACE: map to DeleteWordPrev using ncurses KEY_BACKSPACE constant
ascii_key = KEY_BACKSPACE;
} else if (key >= SDLK_a && key <= SDLK_z) {
ascii_key = static_cast<int>('a' + (key - SDLK_a));
} else if (key == SDLK_COMMA) {
ascii_key = '<';
} else if (key == SDLK_PERIOD) {
ascii_key = '>';
} else if (key == SDLK_LESS) {
ascii_key = '<';
} else if (key == SDLK_GREATER) {
ascii_key = '>';
}
if (ascii_key != 0) {
CommandId id;
if (KLookupEscCommand(ascii_key, id)) {
out = {true, id, "", 0};
return true;
}
}
}
// If collecting universal argument, allow digits on KEYDOWN path too
if (ed && ed->UArg() != 0) {
if ((key >= SDLK_0 && key <= SDLK_9) && !(mod & KMOD_SHIFT)) {
int d = static_cast<int>(key - SDLK_0);
ed->UArgDigit(d);
out.hasCommand = false;
// We consumed a digit on KEYDOWN; SDL will often also emit TEXTINPUT for it.
// Request suppression of the very next TEXTINPUT to avoid double-counting.
suppress_textinput_once = true;
return true;
}
}
// k_prefix handled earlier
return false;
}
bool
GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
{
MappedInput mi;
bool produced = false;
switch (e.type) {
case SDL_MOUSEWHEEL: {
// Let ImGui handle mouse wheel when it wants to capture the mouse
// (e.g., when hovering the editor child window with scrollbars).
// This enables native vertical and horizontal scrolling behavior in GUI.
if (ImGui::GetIO().WantCaptureMouse)
return false;
// Otherwise, fallback to mapping vertical wheel to editor scroll commands.
int dy = e.wheel.y;
#ifdef SDL_MOUSEWHEEL_FLIPPED
if (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED)
dy = -dy;
#endif
if (dy != 0) {
int repeat = dy > 0 ? dy : -dy;
CommandId id = dy > 0 ? CommandId::ScrollUp : CommandId::ScrollDown;
std::lock_guard<std::mutex> lk(mu_);
for (int i = 0; i < repeat; ++i) {
q_.push(MappedInput{true, id, std::string(), 0});
}
return true; // consumed
}
return false;
}
case SDL_KEYDOWN: {
// Remember state before mapping; used for TEXTINPUT suppression heuristics
const bool was_k_prefix = k_prefix_;
const bool was_esc_meta = esc_meta_;
SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod);
const SDL_Keycode key = e.key.keysym.sym;
// Handle Paste: Ctrl+V (Windows/Linux) or Cmd+V (macOS)
// Note: SDL defines letter keycodes in lowercase only (e.g., SDLK_v). Shift does not change keycode.
if ((mods & (KMOD_CTRL | KMOD_GUI)) && (key == SDLK_v)) {
char *clip = SDL_GetClipboardText();
if (clip) {
std::string text(clip);
SDL_free(clip);
// Split on '\n' and enqueue as InsertText/Newline commands
std::lock_guard<std::mutex> lk(mu_);
std::size_t start = 0;
while (start <= text.size()) {
std::size_t pos = text.find('\n', start);
std::string_view segment;
bool has_nl = (pos != std::string::npos);
if (has_nl) {
segment = std::string_view(text).substr(start, pos - start);
} else {
segment = std::string_view(text).substr(start);
}
if (!segment.empty()) {
MappedInput ins{
true, CommandId::InsertText, std::string(segment), 0
};
q_.push(ins);
}
if (has_nl) {
MappedInput nl{true, CommandId::Newline, std::string(), 0};
q_.push(nl);
start = pos + 1;
} else {
break;
}
}
// Suppress the corresponding TEXTINPUT that may follow
suppress_text_input_once_ = true;
return true; // consumed
}
}
{
bool suppress_req = false;
produced = map_key(key, mods,
k_prefix_, esc_meta_,
k_ctrl_pending_,
ed_,
mi,
suppress_req);
if (suppress_req) {
// Prevent the corresponding TEXTINPUT from delivering the same digit again
suppress_text_input_once_ = true;
}
}
// Note: Do NOT suppress SDL_TEXTINPUT after inserting a TAB. Most platforms
// do not emit TEXTINPUT for Tab, and suppressing here would incorrectly
// eat the next character typed if no TEXTINPUT follows the Tab press.
// If we just consumed a universal-argument digit or '-' on KEYDOWN and emitted UArgStatus,
// suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
// Additional suppression handled above when KEYDOWN consumed a uarg digit
// Suppress the immediate following SDL_TEXTINPUT when a printable KEYDOWN was used as a
// k-prefix suffix or Meta (Alt/ESC) chord, regardless of whether a command was produced.
const bool is_alt = (mods & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
const bool is_printable_letter = (key >= SDLK_SPACE && key <= SDLK_z);
const bool is_non_text_key =
key == SDLK_TAB || key == SDLK_RETURN || key == SDLK_KP_ENTER ||
key == SDLK_BACKSPACE || key == SDLK_DELETE || key == SDLK_ESCAPE ||
key == SDLK_LEFT || key == SDLK_RIGHT || key == SDLK_UP || key == SDLK_DOWN ||
key == SDLK_HOME || key == SDLK_END || key == SDLK_PAGEUP || key == SDLK_PAGEDOWN;
bool should_suppress = false;
if (!is_non_text_key) {
// Any k-prefix suffix that is printable should suppress TEXTINPUT, even if no
// command mapped (we report unknown via status instead of inserting text).
if (was_k_prefix && is_printable_letter) {
should_suppress = true;
}
// Alt/Meta + letter can also generate TEXTINPUT on some platforms
const bool is_meta_symbol = (
key == SDLK_COMMA || key == SDLK_PERIOD || key == SDLK_LESS || key ==
SDLK_GREATER);
if (is_alt && ((key >= SDLK_a && key <= SDLK_z) || is_meta_symbol)) {
should_suppress = true;
}
// ESC-as-meta followed by printable
if (was_esc_meta && (is_printable_letter || is_meta_symbol)) {
should_suppress = true;
}
}
if (should_suppress) {
suppress_text_input_once_ = true;
}
break;
}
case SDL_TEXTINPUT: {
// If the previous KEYDOWN requested suppression of this TEXTINPUT (e.g.,
// we already handled a uarg digit/minus or a k-prefix printable), do it
// immediately before any other handling to avoid duplicates.
if (suppress_text_input_once_) {
suppress_text_input_once_ = false; // consume suppression
produced = true; // consumed event
break;
}
// If editor universal argument is active, consume digit TEXTINPUT
if (ed_ &&ed_
->
UArg() != 0
)
{
const char *txt = e.text.text;
if (txt && *txt) {
unsigned char c0 = static_cast<unsigned char>(txt[0]);
if (c0 >= '0' && c0 <= '9') {
int d = c0 - '0';
ed_->UArgDigit(d);
produced = true; // consumed to update status
break;
}
}
// Non-digit ends collection; allow processing normally below
}
// If we are still in k-prefix and KEYDOWN path didn't handle the suffix,
// use TEXTINPUT's actual character (handles Shifted letters on macOS) to map the k-command.
if (k_prefix_) {
k_prefix_ = false;
esc_meta_ = false;
const char *txt = e.text.text;
if (txt && *txt) {
unsigned char c0 = static_cast<unsigned char>(txt[0]);
int ascii_key = 0;
if (c0 < 0x80) {
ascii_key = static_cast<int>(c0);
}
if (ascii_key != 0) {
// Qualifier via TEXTINPUT: 'C' or '^'
if (ascii_key == 'C' || ascii_key == 'c' || ascii_key == '^') {
k_ctrl_pending_ = true;
if (ed_)
ed_->SetStatus("C-k C _");
// Keep k-prefix active; do not emit a command
k_prefix_ = true;
produced = true;
break;
}
// Map via k-prefix table; do not pass Ctrl for TEXTINPUT case
CommandId id;
bool pass_ctrl = k_ctrl_pending_;
k_ctrl_pending_ = false;
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
// Diagnostics: log any k-prefix TEXTINPUT suffix mapping
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
? static_cast<char>(ascii_key)
: '?';
std::fprintf(stderr,
"[kge] k-prefix TEXTINPUT suffix: ascii=%d '%c' mapped=%d id=%d\n",
ascii_key, disp, mapped ? 1 : 0,
mapped ? static_cast<int>(id) : -1);
std::fflush(stderr);
if (mapped) {
mi = {true, id, "", 0};
if (ed_)
ed_->SetStatus(""); // clear "C-k _" hint after suffix
produced = true;
break; // handled; do not insert text
} else {
// Unknown k-command via TEXTINPUT path
int shown = KLowerAscii(ascii_key);
char c = (shown >= 0x20 && shown <= 0x7e)
? static_cast<char>(shown)
: '?';
std::string arg(1, c);
mi = {true, CommandId::UnknownKCommand, arg, 0};
if (ed_)
ed_->SetStatus("");
produced = true;
break;
}
}
}
// If no usable ASCII was found, still report an unknown k-command and exit k-mode
mi = {true, CommandId::UnknownKCommand, std::string("?"), 0};
if (ed_)
ed_->SetStatus("");
produced = true;
break;
}
// (suppression is handled at the top of this case)
// Handle ESC-as-meta fallback on TEXTINPUT: some platforms emit only TEXTINPUT
// for the printable part after ESC. If esc_meta_ is set, translate first char.
if (esc_meta_) {
esc_meta_ = false; // consume meta prefix
const char *txt = e.text.text;
if (txt && *txt) {
// Parse first UTF-8 codepoint (we care only about common ASCII cases)
unsigned char c0 = static_cast<unsigned char>(txt[0]);
// Map a few common symbols/letters used in our ESC map
int ascii_key = 0;
if (c0 < 0x80) {
// ASCII path
ascii_key = static_cast<int>(c0);
ascii_key = KLowerAscii(ascii_key);
} else {
// Basic handling for macOS Option combos that might produce ≤/≥
// Compare the UTF-8 prefix for these two symbols
std::string s(txt);
if (s.rfind("\xE2\x89\xA4", 0) == 0) {
// U+2264 '≤'
ascii_key = '<';
} else if (s.rfind("\xE2\x89\xA5", 0) == 0) {
// U+2265 '≥'
ascii_key = '>';
}
}
if (ascii_key != 0) {
CommandId id;
if (KLookupEscCommand(ascii_key, id)) {
mi = {true, id, "", 0};
produced = true;
break;
}
}
}
// If we get here, unmapped ESC sequence via TEXTINPUT: report invalid
mi = {true, CommandId::UnknownEscCommand, "", 0};
produced = true;
break;
}
if (!k_prefix_ && e.text.text[0] != '\0') {
// Ensure InsertText never carries a newline; those must originate from KEYDOWN
std::string text(e.text.text);
// Strip any CR/LF that might slip through from certain platforms/IME behaviors
text.erase(std::remove(text.begin(), text.end(), '\n'), text.end());
text.erase(std::remove(text.begin(), text.end(), '\r'), text.end());
if (!text.empty()) {
mi.hasCommand = true;
mi.id = CommandId::InsertText;
mi.arg = std::move(text);
mi.count = 0;
produced = true;
} else {
// Nothing to insert after filtering; consume the event
produced = true;
}
} else {
produced = true; // consumed while k-prefix is active
}
break;
}
default:
break;
}
if (produced && mi.hasCommand) {
std::lock_guard<std::mutex> lk(mu_);
q_.push(mi);
}
return produced;
}
bool
GUIInputHandler::Poll(MappedInput &out)
{
std::lock_guard<std::mutex> lk(mu_);
if (q_.empty())
return false;
out = q_.front();
q_.pop();
return true;
}

View File

@@ -1,14 +0,0 @@
/*
* GUIRenderer - ImGui-based renderer for GUI mode
*/
#pragma once
#include "Renderer.h"
class GUIRenderer final : public Renderer {
public:
GUIRenderer() = default;
~GUIRenderer() override = default;
void Draw(Editor &ed) override;
};

View File

@@ -1,11 +1,307 @@
// GUITheme.h — ImGui theming helpers and background mode
// GUITheme.h — theming helpers and background mode
#pragma once
#include <cstddef>
#include <string>
#include <algorithm>
#include <cctype>
#include "Highlight.h"
// Cross-frontend theme change request hook: declared here, defined in Command.cc
namespace kte {
extern bool gThemeChangePending;
extern std::string gThemeChangeRequest; // raw user-provided name
// Qt GUI: cross-frontend font change hooks and current font state
extern bool gFontChangePending;
extern std::string gFontFamilyRequest; // requested family (case-insensitive)
extern float gFontSizeRequest; // <= 0 means keep size
extern std::string gCurrentFontFamily; // last applied family (Qt)
extern float gCurrentFontSize; // last applied size (Qt)
// Qt GUI: request to show a visual font dialog (set by command handler)
extern bool gFontDialogRequested;
}
#if defined(KTE_USE_QT)
// Qt build: avoid hard dependency on ImGui headers/types.
// Provide a lightweight color vector matching ImVec4 fields used by renderers.
struct KteColor {
float x{0}, y{0}, z{0}, w{1};
};
static inline KteColor
RGBA(unsigned int rgb, float a = 1.0f)
{
const float r = static_cast<float>((rgb >> 16) & 0xFF) / 255.0f;
const float g = static_cast<float>((rgb >> 8) & 0xFF) / 255.0f;
const float b = static_cast<float>(rgb & 0xFF) / 255.0f;
return {r, g, b, a};
}
namespace kte {
// Background mode selection for light/dark palettes
enum class BackgroundMode { Light, Dark };
// Global background mode; default to Dark to match prior defaults
static inline auto gBackgroundMode = BackgroundMode::Dark;
static inline void
SetBackgroundMode(const BackgroundMode m)
{
gBackgroundMode = m;
}
static inline BackgroundMode
GetBackgroundMode()
{
return gBackgroundMode;
}
// Minimal GUI palette for Qt builds. This mirrors the defaults used in the ImGui
// frontend (Nord-ish) and switches for light/dark background mode.
struct Palette {
KteColor bg; // editor background
KteColor fg; // default foreground text
KteColor sel_bg; // selection background
KteColor cur_bg; // cursor cell background
KteColor status_bg; // status bar background
KteColor status_fg; // status bar foreground
};
// Optional theme override (Qt): when set, GetPalette() will return this instead
// of the generic light/dark defaults. This allows honoring theme names in kge.ini.
static inline bool gPaletteOverride = false;
static inline Palette gOverridePalette{};
static inline std::string gOverrideThemeName = ""; // lowercased name
static inline Palette
GetPalette()
{
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
if (gPaletteOverride) {
return gOverridePalette;
}
if (dark) {
return Palette{
/*bg*/ RGBA(0x1C1C1E),
/*fg*/ RGBA(0xDCDCDC),
/*sel_bg*/ RGBA(0xC8C800, 0.35f),
/*cur_bg*/ RGBA(0xC8C8FF, 0.50f),
/*status_bg*/ RGBA(0x28282C),
/*status_fg*/ RGBA(0xB4B48C)
};
} else {
// Light palette tuned for readability
return Palette{
/*bg*/ RGBA(0xFBFBFC),
/*fg*/ RGBA(0x30343A),
/*sel_bg*/ RGBA(0x268BD2, 0.22f),
/*cur_bg*/ RGBA(0x000000, 0.15f),
/*status_bg*/ RGBA(0xE6E8EA),
/*status_fg*/ RGBA(0x50555A)
};
}
}
// A few named palettes to provide visible differences between themes in Qt.
// These are approximate and palette-based (no widget style changes like ImGuiStyle).
static inline Palette
NordDark()
{
return {
/*bg*/RGBA(0x2E3440), /*fg*/RGBA(0xD8DEE9), /*sel_bg*/RGBA(0x88C0D0, 0.25f),
/*cur_bg*/RGBA(0x81A1C1, 0.35f), /*status_bg*/RGBA(0x3B4252), /*status_fg*/RGBA(0xE5E9F0)
};
}
static inline Palette
NordLight()
{
return {
/*bg*/RGBA(0xECEFF4), /*fg*/RGBA(0x2E3440), /*sel_bg*/RGBA(0x5E81AC, 0.22f),
/*cur_bg*/RGBA(0x000000, 0.12f), /*status_bg*/RGBA(0xE5E9F0), /*status_fg*/RGBA(0x4C566A)
};
}
static inline Palette
SolarizedDark()
{
return {
/*bg*/RGBA(0x002b36), /*fg*/RGBA(0x93a1a1), /*sel_bg*/RGBA(0x586e75, 0.40f),
/*cur_bg*/RGBA(0x657b83, 0.35f), /*status_bg*/RGBA(0x073642), /*status_fg*/RGBA(0xeee8d5)
};
}
static inline Palette
SolarizedLight()
{
return {
/*bg*/RGBA(0xfdf6e3), /*fg*/RGBA(0x586e75), /*sel_bg*/RGBA(0x268bd2, 0.25f),
/*cur_bg*/RGBA(0x000000, 0.10f), /*status_bg*/RGBA(0xeee8d5), /*status_fg*/RGBA(0x657b83)
};
}
static inline Palette
GruvboxDark()
{
return {
/*bg*/RGBA(0x282828), /*fg*/RGBA(0xebdbb2), /*sel_bg*/RGBA(0xd79921, 0.35f),
/*cur_bg*/RGBA(0x458588, 0.40f), /*status_bg*/RGBA(0x3c3836), /*status_fg*/RGBA(0xd5c4a1)
};
}
static inline Palette
GruvboxLight()
{
return {
/*bg*/RGBA(0xfbf1c7), /*fg*/RGBA(0x3c3836), /*sel_bg*/RGBA(0x076678, 0.22f),
/*cur_bg*/RGBA(0x000000, 0.10f), /*status_bg*/RGBA(0xebdbb2), /*status_fg*/RGBA(0x504945)
};
}
static inline Palette
EInk()
{
return {
/*bg*/RGBA(0xffffff), /*fg*/RGBA(0x000000), /*sel_bg*/RGBA(0x000000, 0.10f),
/*cur_bg*/RGBA(0x000000, 0.12f), /*status_bg*/RGBA(0x000000), /*status_fg*/RGBA(0xffffff)
};
}
// Apply a Qt theme by name. Returns true on success. Name matching is case-insensitive and
// supports common aliases (e.g., "solarized-light" or "solarized light"). If the name conveys
// a background (light/dark), BackgroundMode is updated to keep SyntaxInk consistent.
static inline bool
ApplyQtThemeByName(std::string name)
{
// normalize
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) {
return (char) std::tolower(c);
});
auto has = [&](const std::string &s) {
return name.find(s) != std::string::npos;
};
if (name.empty() || name == "default" || name == "nord") {
// Choose variant by current background mode
if (GetBackgroundMode() == BackgroundMode::Dark) {
gOverridePalette = NordDark();
} else {
gOverridePalette = NordLight();
}
gPaletteOverride = true;
gOverrideThemeName = "nord";
return true;
}
if (has("solarized")) {
if (has("light")) {
SetBackgroundMode(BackgroundMode::Light);
gOverridePalette = SolarizedLight();
} else if (has("dark")) {
SetBackgroundMode(BackgroundMode::Dark);
gOverridePalette = SolarizedDark();
} else {
// pick from current background
gOverridePalette = (GetBackgroundMode() == BackgroundMode::Dark)
? SolarizedDark()
: SolarizedLight();
}
gPaletteOverride = true;
gOverrideThemeName = "solarized";
return true;
}
if (has("gruvbox")) {
if (has("light")) {
SetBackgroundMode(BackgroundMode::Light);
gOverridePalette = GruvboxLight();
} else if (has("dark")) {
SetBackgroundMode(BackgroundMode::Dark);
gOverridePalette = GruvboxDark();
} else {
gOverridePalette = (GetBackgroundMode() == BackgroundMode::Dark)
? GruvboxDark()
: GruvboxLight();
}
gPaletteOverride = true;
gOverrideThemeName = "gruvbox";
return true;
}
if (has("eink") || has("e-ink") || has("paper")) {
SetBackgroundMode(BackgroundMode::Light);
gOverridePalette = EInk();
gPaletteOverride = true;
gOverrideThemeName = "eink";
return true;
}
// Unknown -> clear override so default light/dark applies; return false.
gPaletteOverride = false;
gOverrideThemeName.clear();
return false;
}
// Minimal SyntaxInk mapping for Qt builds, returning KteColor
[[maybe_unused]] static KteColor
SyntaxInk(const TokenKind k)
{
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
const KteColor def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
switch (k) {
case TokenKind::Keyword:
return dark ? RGBA(0x81A1C1) : RGBA(0x5E81AC);
case TokenKind::Type:
return dark ? RGBA(0x8FBCBB) : RGBA(0x4C566A);
case TokenKind::String:
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
case TokenKind::Char:
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
case TokenKind::Comment:
return dark ? RGBA(0x616E88) : RGBA(0x7A869A);
case TokenKind::Number:
return dark ? RGBA(0xEBCB8B) : RGBA(0xB58900);
case TokenKind::Preproc:
return dark ? RGBA(0xD08770) : RGBA(0xAF3A03);
case TokenKind::Constant:
return dark ? RGBA(0xB48EAD) : RGBA(0x7B4B7F);
case TokenKind::Function:
return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4);
case TokenKind::Operator:
return dark ? RGBA(0x2E3440) : RGBA(0x2E3440);
case TokenKind::Punctuation:
return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
case TokenKind::Identifier:
return def;
case TokenKind::Whitespace:
return def;
case TokenKind::Error:
return dark ? RGBA(0xBF616A) : RGBA(0xCC0000);
case TokenKind::Default: default:
return def;
}
}
} // namespace kte
#else
#include <imgui.h>
#include <vector>
#include <memory>
#include <string>
#include <cstddef>
#include <algorithm>
#include <cctype>
@@ -644,4 +940,6 @@ SyntaxInk(const TokenKind k)
return def;
}
}
} // namespace kte
} // namespace kte
#endif // KTE_USE_QT

View File

@@ -11,7 +11,7 @@
#include <backends/imgui_impl_opengl3.h>
#include <backends/imgui_impl_sdl2.h>
#include "GUIFrontend.h"
#include "ImGuiFrontend.h"
#include "Command.h"
#include "Editor.h"
#include "GUIConfig.h"
@@ -224,17 +224,17 @@ GUIFrontend::Step(Editor &ed, bool &running)
while (SDL_PollEvent(&e)) {
ImGui_ImplSDL2_ProcessEvent(&e);
switch (e.type) {
case SDL_QUIT:
running = false;
break;
case SDL_WINDOWEVENT:
if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
width_ = e.window.data1;
height_ = e.window.data2;
}
break;
default:
break;
case SDL_QUIT:
running = false;
break;
case SDL_WINDOWEVENT:
if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
width_ = e.window.data1;
height_ = e.window.data2;
}
break;
default:
break;
}
// Map input to commands
input_.ProcessSDLEvent(e);

View File

@@ -1,11 +1,11 @@
/*
* GUIFrontend - couples GUIInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
* GUIFrontend - couples ImGuiInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
*/
#pragma once
#include "Frontend.h"
#include "GUIConfig.h"
#include "GUIInputHandler.h"
#include "GUIRenderer.h"
#include "ImGuiInputHandler.h"
#include "ImGuiRenderer.h"
struct SDL_Window;
@@ -27,8 +27,8 @@ private:
static bool LoadGuiFont_(const char *path, float size_px);
GUIConfig config_{};
GUIInputHandler input_{};
GUIRenderer renderer_{};
ImGuiInputHandler input_{};
ImGuiRenderer renderer_{};
SDL_Window *window_ = nullptr;
SDL_GLContext gl_ctx_ = nullptr;
int width_ = 1280;

601
ImGuiInputHandler.cc Normal file
View File

@@ -0,0 +1,601 @@
#include <cstdio>
#include <algorithm>
#include <ncurses.h>
#include <SDL.h>
#include <imgui.h>
#include "ImGuiInputHandler.h"
#include "KKeymap.h"
#include "Editor.h"
static bool
map_key(const SDL_Keycode key,
const SDL_Keymod mod,
bool &k_prefix,
bool &esc_meta,
bool &k_ctrl_pending,
Editor *ed,
MappedInput &out,
bool &suppress_textinput_once)
{
// Ctrl handling
const bool is_ctrl = (mod & KMOD_CTRL) != 0;
const bool is_alt = (mod & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
// If previous key was ESC, interpret this as Meta via ESC keymap.
// Prefer KEYDOWN when we can derive a printable ASCII; otherwise defer to TEXTINPUT.
if (esc_meta) {
int ascii_key = 0;
if (key == SDLK_BACKSPACE) {
// ESC BACKSPACE: map to DeleteWordPrev using ncurses KEY_BACKSPACE constant
ascii_key = KEY_BACKSPACE;
} else if (key >= SDLK_a && key <= SDLK_z) {
ascii_key = static_cast<int>('a' + (key - SDLK_a));
} else if (key == SDLK_COMMA) {
ascii_key = '<';
} else if (key == SDLK_PERIOD) {
ascii_key = '>';
} else if (key == SDLK_LESS) {
ascii_key = '<';
} else if (key == SDLK_GREATER) {
ascii_key = '>';
}
if (ascii_key != 0) {
esc_meta = false; // consume if we can decide on KEYDOWN
ascii_key = KLowerAscii(ascii_key);
CommandId id;
if (KLookupEscCommand(ascii_key, id)) {
out = {true, id, "", 0};
return true;
}
// Known printable but unmapped ESC sequence: report invalid
out = {true, CommandId::UnknownEscCommand, "", 0};
return true;
}
// No usable ASCII from KEYDOWN → keep esc_meta set and let TEXTINPUT handle it
out.hasCommand = false;
return true;
}
// Movement and basic keys
// These keys exit k-prefix mode if active (user pressed C-k then a special key).
switch (key) {
case SDLK_LEFT:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::MoveLeft, "", 0};
return true;
case SDLK_RIGHT:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::MoveRight, "", 0};
return true;
case SDLK_UP:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::MoveUp, "", 0};
return true;
case SDLK_DOWN:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::MoveDown, "", 0};
return true;
case SDLK_HOME:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::MoveHome, "", 0};
return true;
case SDLK_END:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::MoveEnd, "", 0};
return true;
case SDLK_PAGEUP:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::PageUp, "", 0};
return true;
case SDLK_PAGEDOWN:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::PageDown, "", 0};
return true;
case SDLK_DELETE:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::DeleteChar, "", 0};
return true;
case SDLK_BACKSPACE:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::Backspace, "", 0};
return true;
case SDLK_TAB:
// Insert a literal tab character when not interpreting a k-prefix suffix.
// If k-prefix is active, let the k-prefix handler below consume the key
// (so Tab doesn't leave k-prefix stuck).
if (!k_prefix) {
out = {true, CommandId::InsertText, std::string("\t"), 0};
return true;
}
break; // fall through so k-prefix handler can process
case SDLK_RETURN:
case SDLK_KP_ENTER:
k_prefix = false;
k_ctrl_pending = false;
out = {true, CommandId::Newline, "", 0};
return true;
case SDLK_ESCAPE:
k_prefix = false;
k_ctrl_pending = false;
esc_meta = true; // next key will be treated as Meta
out.hasCommand = false; // no immediate command for bare ESC in GUI
return true;
default:
break;
}
// If we are in k-prefix, interpret the very next key via the C-k keymap immediately.
if (k_prefix) {
esc_meta = false;
// Normalize to ASCII; preserve case for letters using Shift
int ascii_key = 0;
if (key >= SDLK_a && key <= SDLK_z) {
ascii_key = static_cast<int>('a' + (key - SDLK_a));
if (mod & KMOD_SHIFT)
ascii_key = ascii_key - 'a' + 'A';
} else if (key == SDLK_COMMA) {
ascii_key = '<';
} else if (key == SDLK_PERIOD) {
ascii_key = '>';
} else if (key == SDLK_LESS) {
ascii_key = '<';
} else if (key == SDLK_GREATER) {
ascii_key = '>';
} else if (key >= SDLK_SPACE && key <= SDLK_z) {
ascii_key = static_cast<int>(key);
}
bool ctrl2 = (mod & KMOD_CTRL) != 0;
// If user typed a literal 'C' (or '^') as a control qualifier, keep k-prefix active
if (ascii_key == 'C' || ascii_key == 'c' || ascii_key == '^') {
k_ctrl_pending = true;
// Keep waiting for the next suffix; show status and suppress ensuing TEXTINPUT
if (ed)
ed->SetStatus("C-k C _");
suppress_textinput_once = true;
out.hasCommand = false;
return true;
}
// Otherwise, consume the k-prefix now for the actual suffix
k_prefix = false;
if (ascii_key != 0) {
int lower = KLowerAscii(ascii_key);
bool ctrl_suffix_supported = (lower == 'd' || lower == 'x' || lower == 'q');
bool pass_ctrl = (ctrl2 || k_ctrl_pending) && ctrl_suffix_supported;
k_ctrl_pending = false;
CommandId id;
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
// Diagnostics for u/U
if (lower == 'u') {
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
? static_cast<char>(ascii_key)
: '?';
std::fprintf(stderr,
"[kge] k-prefix suffix: sym=%d mods=0x%x ascii=%d '%c' ctrl2=%d pass_ctrl=%d mapped=%d id=%d\n",
static_cast<int>(key), static_cast<unsigned int>(mod), ascii_key, disp,
ctrl2 ? 1 : 0, pass_ctrl ? 1 : 0, mapped ? 1 : 0,
mapped ? static_cast<int>(id) : -1);
std::fflush(stderr);
}
if (mapped) {
out = {true, id, "", 0};
if (ed)
ed->SetStatus(""); // clear "C-k _" hint after suffix
return true;
}
int shown = KLowerAscii(ascii_key);
char c = (shown >= 0x20 && shown <= 0x7e) ? static_cast<char>(shown) : '?';
std::string arg(1, c);
out = {true, CommandId::UnknownKCommand, arg, 0};
if (ed)
ed->SetStatus(""); // clear hint; handler will set unknown status
return true;
}
// Non-printable/unmappable key as k-suffix (e.g., F-keys): report unknown and exit k-mode
out = {true, CommandId::UnknownKCommand, std::string("?"), 0};
if (ed)
ed->SetStatus("");
return true;
}
if (is_ctrl) {
// Universal argument: C-u
if (key == SDLK_u) {
if (ed)
ed->UArgStart();
out.hasCommand = false;
return true;
}
// Cancel universal arg on C-g as well (it maps to Refresh via ctrl map)
if (key == SDLK_g) {
if (ed)
ed->UArgClear();
// Also cancel any pending k-prefix qualifier
k_ctrl_pending = false;
k_prefix = false; // treat as cancel of prefix
}
if (key == SDLK_k || key == SDLK_KP_EQUALS) {
k_prefix = true;
out = {true, CommandId::KPrefix, "", 0};
return true;
}
// Map other control chords via shared keymap
if (key >= SDLK_a && key <= SDLK_z) {
int ascii_key = static_cast<int>('a' + (key - SDLK_a));
CommandId id;
if (KLookupCtrlCommand(ascii_key, id)) {
out = {true, id, "", 0};
return true;
}
}
}
// Alt/Meta bindings (ESC f/b equivalent)
if (is_alt) {
int ascii_key = 0;
if (key == SDLK_BACKSPACE) {
// Alt BACKSPACE: map to DeleteWordPrev using ncurses KEY_BACKSPACE constant
ascii_key = KEY_BACKSPACE;
} else if (key >= SDLK_a && key <= SDLK_z) {
ascii_key = static_cast<int>('a' + (key - SDLK_a));
} else if (key == SDLK_COMMA) {
ascii_key = '<';
} else if (key == SDLK_PERIOD) {
ascii_key = '>';
} else if (key == SDLK_LESS) {
ascii_key = '<';
} else if (key == SDLK_GREATER) {
ascii_key = '>';
}
if (ascii_key != 0) {
CommandId id;
if (KLookupEscCommand(ascii_key, id)) {
out = {true, id, "", 0};
return true;
}
}
}
// If collecting universal argument, allow digits on KEYDOWN path too
if (ed && ed->UArg() != 0) {
if ((key >= SDLK_0 && key <= SDLK_9) && !(mod & KMOD_SHIFT)) {
int d = static_cast<int>(key - SDLK_0);
ed->UArgDigit(d);
out.hasCommand = false;
// We consumed a digit on KEYDOWN; SDL will often also emit TEXTINPUT for it.
// Request suppression of the very next TEXTINPUT to avoid double-counting.
suppress_textinput_once = true;
return true;
}
}
// k_prefix handled earlier
return false;
}
bool
ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
{
MappedInput mi;
bool produced = false;
switch (e.type) {
case SDL_MOUSEWHEEL: {
// Let ImGui handle mouse wheel when it wants to capture the mouse
// (e.g., when hovering the editor child window with scrollbars).
// This enables native vertical and horizontal scrolling behavior in GUI.
if (ImGui::GetIO().WantCaptureMouse)
return false;
// Otherwise, fallback to mapping vertical wheel to editor scroll commands.
int dy = e.wheel.y;
#ifdef SDL_MOUSEWHEEL_FLIPPED
if (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED)
dy = -dy;
#endif
if (dy != 0) {
int repeat = dy > 0 ? dy : -dy;
CommandId id = dy > 0 ? CommandId::ScrollUp : CommandId::ScrollDown;
std::lock_guard<std::mutex> lk(mu_);
for (int i = 0; i < repeat; ++i) {
q_.push(MappedInput{true, id, std::string(), 0});
}
return true; // consumed
}
return false;
}
case SDL_KEYDOWN: {
// Remember state before mapping; used for TEXTINPUT suppression heuristics
const bool was_k_prefix = k_prefix_;
const bool was_esc_meta = esc_meta_;
SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod);
const SDL_Keycode key = e.key.keysym.sym;
// Handle Paste: Ctrl+V (Windows/Linux) or Cmd+V (macOS)
// Note: SDL defines letter keycodes in lowercase only (e.g., SDLK_v). Shift does not change keycode.
if ((mods & (KMOD_CTRL | KMOD_GUI)) && (key == SDLK_v)) {
char *clip = SDL_GetClipboardText();
if (clip) {
std::string text(clip);
SDL_free(clip);
// Split on '\n' and enqueue as InsertText/Newline commands
std::lock_guard<std::mutex> lk(mu_);
std::size_t start = 0;
while (start <= text.size()) {
std::size_t pos = text.find('\n', start);
std::string_view segment;
bool has_nl = (pos != std::string::npos);
if (has_nl) {
segment = std::string_view(text).substr(start, pos - start);
} else {
segment = std::string_view(text).substr(start);
}
if (!segment.empty()) {
MappedInput ins{
true, CommandId::InsertText, std::string(segment), 0
};
q_.push(ins);
}
if (has_nl) {
MappedInput nl{true, CommandId::Newline, std::string(), 0};
q_.push(nl);
start = pos + 1;
} else {
break;
}
}
// Suppress the corresponding TEXTINPUT that may follow
suppress_text_input_once_ = true;
return true; // consumed
}
}
{
bool suppress_req = false;
produced = map_key(key, mods,
k_prefix_, esc_meta_,
k_ctrl_pending_,
ed_,
mi,
suppress_req);
if (suppress_req) {
// Prevent the corresponding TEXTINPUT from delivering the same digit again
suppress_text_input_once_ = true;
}
}
// Note: Do NOT suppress SDL_TEXTINPUT after inserting a TAB. Most platforms
// do not emit TEXTINPUT for Tab, and suppressing here would incorrectly
// eat the next character typed if no TEXTINPUT follows the Tab press.
// If we just consumed a universal-argument digit or '-' on KEYDOWN and emitted UArgStatus,
// suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
// Additional suppression handled above when KEYDOWN consumed a uarg digit
// Suppress the immediate following SDL_TEXTINPUT when a printable KEYDOWN was used as a
// k-prefix suffix or Meta (Alt/ESC) chord, regardless of whether a command was produced.
const bool is_alt = (mods & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
const bool is_printable_letter = (key >= SDLK_SPACE && key <= SDLK_z);
const bool is_non_text_key =
key == SDLK_TAB || key == SDLK_RETURN || key == SDLK_KP_ENTER ||
key == SDLK_BACKSPACE || key == SDLK_DELETE || key == SDLK_ESCAPE ||
key == SDLK_LEFT || key == SDLK_RIGHT || key == SDLK_UP || key == SDLK_DOWN ||
key == SDLK_HOME || key == SDLK_END || key == SDLK_PAGEUP || key == SDLK_PAGEDOWN;
bool should_suppress = false;
if (!is_non_text_key) {
// Any k-prefix suffix that is printable should suppress TEXTINPUT, even if no
// command mapped (we report unknown via status instead of inserting text).
if (was_k_prefix && is_printable_letter) {
should_suppress = true;
}
// Alt/Meta + letter can also generate TEXTINPUT on some platforms
const bool is_meta_symbol = (
key == SDLK_COMMA || key == SDLK_PERIOD || key == SDLK_LESS || key ==
SDLK_GREATER);
if (is_alt && ((key >= SDLK_a && key <= SDLK_z) || is_meta_symbol)) {
should_suppress = true;
}
// ESC-as-meta followed by printable
if (was_esc_meta && (is_printable_letter || is_meta_symbol)) {
should_suppress = true;
}
}
if (should_suppress) {
suppress_text_input_once_ = true;
}
break;
}
case SDL_TEXTINPUT: {
// If the previous KEYDOWN requested suppression of this TEXTINPUT (e.g.,
// we already handled a uarg digit/minus or a k-prefix printable), do it
// immediately before any other handling to avoid duplicates.
if (suppress_text_input_once_) {
suppress_text_input_once_ = false; // consume suppression
produced = true; // consumed event
break;
}
// If editor universal argument is active, consume digit TEXTINPUT
if (ed_ &&ed_
->
UArg() != 0
)
{
const char *txt = e.text.text;
if (txt && *txt) {
unsigned char c0 = static_cast<unsigned char>(txt[0]);
if (c0 >= '0' && c0 <= '9') {
int d = c0 - '0';
ed_->UArgDigit(d);
produced = true; // consumed to update status
break;
}
}
// Non-digit ends collection; allow processing normally below
}
// If we are still in k-prefix and KEYDOWN path didn't handle the suffix,
// use TEXTINPUT's actual character (handles Shifted letters on macOS) to map the k-command.
if (k_prefix_) {
k_prefix_ = false;
esc_meta_ = false;
const char *txt = e.text.text;
if (txt && *txt) {
unsigned char c0 = static_cast<unsigned char>(txt[0]);
int ascii_key = 0;
if (c0 < 0x80) {
ascii_key = static_cast<int>(c0);
}
if (ascii_key != 0) {
// Qualifier via TEXTINPUT: 'C' or '^'
if (ascii_key == 'C' || ascii_key == 'c' || ascii_key == '^') {
k_ctrl_pending_ = true;
if (ed_)
ed_->SetStatus("C-k C _");
// Keep k-prefix active; do not emit a command
k_prefix_ = true;
produced = true;
break;
}
// Map via k-prefix table; do not pass Ctrl for TEXTINPUT case
CommandId id;
bool pass_ctrl = k_ctrl_pending_;
k_ctrl_pending_ = false;
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
// Diagnostics: log any k-prefix TEXTINPUT suffix mapping
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
? static_cast<char>(ascii_key)
: '?';
std::fprintf(stderr,
"[kge] k-prefix TEXTINPUT suffix: ascii=%d '%c' mapped=%d id=%d\n",
ascii_key, disp, mapped ? 1 : 0,
mapped ? static_cast<int>(id) : -1);
std::fflush(stderr);
if (mapped) {
mi = {true, id, "", 0};
if (ed_)
ed_->SetStatus(""); // clear "C-k _" hint after suffix
produced = true;
break; // handled; do not insert text
} else {
// Unknown k-command via TEXTINPUT path
int shown = KLowerAscii(ascii_key);
char c = (shown >= 0x20 && shown <= 0x7e)
? static_cast<char>(shown)
: '?';
std::string arg(1, c);
mi = {true, CommandId::UnknownKCommand, arg, 0};
if (ed_)
ed_->SetStatus("");
produced = true;
break;
}
}
}
// If no usable ASCII was found, still report an unknown k-command and exit k-mode
mi = {true, CommandId::UnknownKCommand, std::string("?"), 0};
if (ed_)
ed_->SetStatus("");
produced = true;
break;
}
// (suppression is handled at the top of this case)
// Handle ESC-as-meta fallback on TEXTINPUT: some platforms emit only TEXTINPUT
// for the printable part after ESC. If esc_meta_ is set, translate first char.
if (esc_meta_) {
esc_meta_ = false; // consume meta prefix
const char *txt = e.text.text;
if (txt && *txt) {
// Parse first UTF-8 codepoint (we care only about common ASCII cases)
unsigned char c0 = static_cast<unsigned char>(txt[0]);
// Map a few common symbols/letters used in our ESC map
int ascii_key = 0;
if (c0 < 0x80) {
// ASCII path
ascii_key = static_cast<int>(c0);
ascii_key = KLowerAscii(ascii_key);
} else {
// Basic handling for macOS Option combos that might produce ≤/≥
// Compare the UTF-8 prefix for these two symbols
std::string s(txt);
if (s.rfind("\xE2\x89\xA4", 0) == 0) {
// U+2264 '≤'
ascii_key = '<';
} else if (s.rfind("\xE2\x89\xA5", 0) == 0) {
// U+2265 '≥'
ascii_key = '>';
}
}
if (ascii_key != 0) {
CommandId id;
if (KLookupEscCommand(ascii_key, id)) {
mi = {true, id, "", 0};
produced = true;
break;
}
}
}
// If we get here, unmapped ESC sequence via TEXTINPUT: report invalid
mi = {true, CommandId::UnknownEscCommand, "", 0};
produced = true;
break;
}
if (!k_prefix_ && e.text.text[0] != '\0') {
// Ensure InsertText never carries a newline; those must originate from KEYDOWN
std::string text(e.text.text);
// Strip any CR/LF that might slip through from certain platforms/IME behaviors
text.erase(std::remove(text.begin(), text.end(), '\n'), text.end());
text.erase(std::remove(text.begin(), text.end(), '\r'), text.end());
if (!text.empty()) {
mi.hasCommand = true;
mi.id = CommandId::InsertText;
mi.arg = std::move(text);
mi.count = 0;
produced = true;
} else {
// Nothing to insert after filtering; consume the event
produced = true;
}
} else {
produced = true; // consumed while k-prefix is active
}
break;
}
default:
break;
}
if (produced && mi.hasCommand) {
std::lock_guard<std::mutex> lk(mu_);
q_.push(mi);
}
return produced;
}
bool
ImGuiInputHandler::Poll(MappedInput &out)
{
std::lock_guard<std::mutex> lk(mu_);
if (q_.empty())
return false;
out = q_.front();
q_.pop();
return true;
}

View File

@@ -1,5 +1,5 @@
/*
* GUIInputHandler - ImGui/SDL2-based input mapping for GUI mode
* ImGuiInputHandler - ImGui/SDL2-based input mapping for GUI mode
*/
#pragma once
#include <mutex>
@@ -10,11 +10,11 @@
union SDL_Event; // fwd decl to avoid including SDL here (SDL defines SDL_Event as a union)
class GUIInputHandler final : public InputHandler {
class ImGuiInputHandler final : public InputHandler {
public:
GUIInputHandler() = default;
ImGuiInputHandler() = default;
~GUIInputHandler() override = default;
~ImGuiInputHandler() override = default;
void Attach(Editor *ed) override

View File

@@ -9,7 +9,7 @@
#include <imgui.h>
#include <regex>
#include "GUIRenderer.h"
#include "ImGuiRenderer.h"
#include "Highlight.h"
#include "GUITheme.h"
#include "Buffer.h"
@@ -30,7 +30,7 @@
void
GUIRenderer::Draw(Editor &ed)
ImGuiRenderer::Draw(Editor &ed)
{
// Make the editor window occupy the entire GUI container/viewport
ImGuiViewport *vp = ImGui::GetMainViewport();
@@ -461,10 +461,10 @@ GUIRenderer::Draw(Editor &ed)
float cursor_px = 0.0f;
if (rx_viewport > 0 && coloffs_now < expanded.size()) {
std::size_t start = coloffs_now;
std::size_t end = std::min(expanded.size(), start + rx_viewport);
std::size_t end = std::min(expanded.size(), start + rx_viewport);
// Measure substring width in pixels
ImVec2 sz = ImGui::CalcTextSize(expanded.c_str() + start,
expanded.c_str() + end);
expanded.c_str() + end);
cursor_px = sz.x;
}
ImVec2 p0 = ImVec2(line_pos.x + cursor_px, line_pos.y);

14
ImGuiRenderer.h Normal file
View File

@@ -0,0 +1,14 @@
/*
* ImGuiRenderer - ImGui-based renderer for GUI mode
*/
#pragma once
#include "Renderer.h"
class ImGuiRenderer final : public Renderer {
public:
ImGuiRenderer() = default;
~ImGuiRenderer() override = default;
void Draw(Editor &ed) override;
};

988
QtFrontend.cc Normal file
View File

@@ -0,0 +1,988 @@
#include "QtFrontend.h"
#include <QApplication>
#include <QWidget>
#include <QKeyEvent>
#include <QTimer>
#include <QScreen>
#include <QFont>
#include <QFontMetrics>
#include <QFontDatabase>
#include <QFileDialog>
#include <QFontDialog>
#include <QPainter>
#include <QPaintEvent>
#include <QWheelEvent>
#include <regex>
#include "Editor.h"
#include "Command.h"
#include "Buffer.h"
#include "GUITheme.h"
#include "Highlight.h"
namespace {
class MainWindow : public QWidget {
public:
explicit MainWindow(class QtInputHandler &ih, QWidget *parent = nullptr)
: QWidget(parent), input_(ih)
{
// Match ImGui window title format
setWindowTitle(QStringLiteral("kge - kyle's graphical editor ")
+ QStringLiteral(KTE_VERSION_STR));
resize(1280, 800);
setFocusPolicy(Qt::StrongFocus);
}
bool WasClosed() const
{
return closed_;
}
void SetEditor(Editor *ed)
{
ed_ = ed;
}
void SetFontFamilyAndSize(QString family, int px)
{
if (family.isEmpty())
family = QStringLiteral("Brass Mono");
if (px <= 0)
px = 18;
font_family_ = std::move(family);
font_px_ = px;
update();
}
protected:
void keyPressEvent(QKeyEvent *event) override
{
// Route to editor keymap; if handled, accept and stop propagation so
// Qt doesn't trigger any default widget shortcuts.
if (input_.ProcessKeyEvent(*event)) {
event->accept();
return;
}
QWidget::keyPressEvent(event);
}
void paintEvent(QPaintEvent *event) override
{
Q_UNUSED(event);
QPainter p(this);
p.setRenderHint(QPainter::TextAntialiasing, true);
// Colors from GUITheme palette (Qt branch)
auto to_qcolor = [](const KteColor &c) -> QColor {
int r = int(std::round(c.x * 255.0f));
int g = int(std::round(c.y * 255.0f));
int b = int(std::round(c.z * 255.0f));
int a = int(std::round(c.w * 255.0f));
return QColor(r, g, b, a);
};
const auto pal = kte::GetPalette();
const QColor bg = to_qcolor(pal.bg);
const QColor fg = to_qcolor(pal.fg);
const QColor sel_bg = to_qcolor(pal.sel_bg);
const QColor cur_bg = to_qcolor(pal.cur_bg);
const QColor status_bg = to_qcolor(pal.status_bg);
const QColor status_fg = to_qcolor(pal.status_fg);
// Background
p.fillRect(rect(), bg);
// Font/metrics (configured or defaults)
QFont f(font_family_, font_px_);
p.setFont(f);
QFontMetrics fm(f);
const int line_h = fm.height();
const int ch_w = std::max(1, fm.horizontalAdvance(QStringLiteral(" ")));
// Layout metrics
const int pad_l = 8;
const int pad_t = 6;
const int pad_r = 8;
const int pad_b = 6;
const int status_h = line_h + 6; // status bar height
// Content area (text viewport)
const QRect content_rect(pad_l,
pad_t,
width() - pad_l - pad_r,
height() - pad_t - pad_b - status_h);
// Text viewport occupies all content area (no extra title row)
QRect viewport(content_rect.x(), content_rect.y(), content_rect.width(), content_rect.height());
// Draw buffer contents
if (ed_ && viewport.height() > 0 && viewport.width() > 0) {
const Buffer *buf = ed_->CurrentBuffer();
if (buf) {
const auto &lines = buf->Rows();
const std::size_t nrows = lines.size();
const std::size_t rowoffs = buf->Rowoffs();
const std::size_t coloffs = buf->Coloffs();
const std::size_t cy = buf->Cury();
const std::size_t cx = buf->Curx();
// Visible line count
const int max_lines = (line_h > 0) ? (viewport.height() / line_h) : 0;
const std::size_t last_row = std::min<std::size_t>(
nrows, rowoffs + std::max(0, max_lines));
// Tab width: follow ImGuiRenderer default of 4
const std::size_t tabw = 4;
// Prepare painter clip to viewport
p.save();
p.setClipRect(viewport);
// Iterate visible lines
for (std::size_t i = rowoffs, vis_idx = 0; i < last_row; ++i, ++vis_idx) {
const auto &line = static_cast<const std::string &>(lines[i]);
const int y = viewport.y() + static_cast<int>(vis_idx) * line_h;
const int baseline = y + fm.ascent();
// Helper: convert src col -> rx with tab expansion
auto src_to_rx_line = [&](std::size_t src_col) -> std::size_t {
std::size_t rx = 0;
for (std::size_t k = 0; k < src_col && k < line.size(); ++k) {
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
}
return rx;
};
// Search-match background highlights first (under text)
if (ed_->SearchActive() && !ed_->SearchQuery().empty()) {
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
// Compute ranges per line (source indices)
if (ed_->PromptActive() &&
(ed_->CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
ed_->CurrentPromptKind() ==
Editor::PromptKind::RegexReplaceFind)) {
try {
std::regex rx(ed_->SearchQuery());
for (auto it = std::sregex_iterator(
line.begin(), line.end(), rx);
it != std::sregex_iterator(); ++it) {
const auto &m = *it;
std::size_t sx = static_cast<std::size_t>(m.
position());
std::size_t ex =
sx + static_cast<std::size_t>(m.
length());
hl_src_ranges.emplace_back(sx, ex);
}
} catch (const std::regex_error &) {
// Invalid regex: ignore, status line already shows errors
}
} else {
const std::string &q = ed_->SearchQuery();
if (!q.empty()) {
std::size_t pos = 0;
while ((pos = line.find(q, pos)) != std::string::npos) {
hl_src_ranges.emplace_back(pos, pos + q.size());
pos += q.size();
}
}
}
if (!hl_src_ranges.empty()) {
const bool has_current =
ed_->SearchMatchLen() > 0 && ed_->SearchMatchY() == i;
const std::size_t cur_x = has_current ? ed_->SearchMatchX() : 0;
const std::size_t cur_end = has_current
? (ed_->SearchMatchX() + ed_->SearchMatchLen())
: 0;
for (const auto &rg: hl_src_ranges) {
std::size_t sx = rg.first, ex = rg.second;
std::size_t rx_s = src_to_rx_line(sx);
std::size_t rx_e = src_to_rx_line(ex);
if (rx_e <= coloffs)
continue; // fully left of view
int vx0 = viewport.x() + static_cast<int>((
(rx_s > coloffs ? rx_s - coloffs : 0)
* ch_w));
int vx1 = viewport.x() + static_cast<int>((
(rx_e - coloffs) * ch_w));
QRect r(vx0, y, std::max(0, vx1 - vx0), line_h);
if (r.width() <= 0)
continue;
bool is_current =
has_current && sx == cur_x && ex == cur_end;
QColor col = is_current
? QColor(255, 220, 120, 140)
: QColor(200, 200, 0, 90);
p.fillRect(r, col);
}
}
}
// Selection background (if active on this line)
if (buf->MarkSet() && (
i == buf->MarkCury() || i == cy || (
i > std::min(buf->MarkCury(), cy) && i < std::max(
buf->MarkCury(), cy)))) {
std::size_t sx = 0, ex = 0;
if (buf->MarkCury() == i && cy == i) {
sx = std::min(buf->MarkCurx(), cx);
ex = std::max(buf->MarkCurx(), cx);
} else if (i == buf->MarkCury()) {
sx = buf->MarkCurx();
ex = line.size();
} else if (i == cy) {
sx = 0;
ex = cx;
} else {
sx = 0;
ex = line.size();
}
std::size_t rx_s = src_to_rx_line(sx);
std::size_t rx_e = src_to_rx_line(ex);
if (rx_e > coloffs) {
int vx0 = viewport.x() + static_cast<int>((rx_s > coloffs
? rx_s - coloffs
: 0) * ch_w);
int vx1 = viewport.x() + static_cast<int>(
(rx_e - coloffs) * ch_w);
QRect sel_r(vx0, y, std::max(0, vx1 - vx0), line_h);
if (sel_r.width() > 0)
p.fillRect(sel_r, sel_bg);
}
}
// Build expanded line (tabs -> spaces) for drawing
std::string expanded;
expanded.reserve(line.size() + 8);
std::size_t rx_acc = 0;
for (char c: line) {
if (c == '\t') {
std::size_t adv = (tabw - (rx_acc % tabw));
expanded.append(adv, ' ');
rx_acc += adv;
} else {
expanded.push_back(c);
rx_acc += 1;
}
}
// Syntax highlighting spans or plain text
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
HasHighlighter()) {
kte::LineHighlight lh = buf->Highlighter()->GetLine(
*buf, static_cast<int>(i), buf->Version());
struct SSpan {
std::size_t s;
std::size_t e;
kte::TokenKind k;
};
std::vector<SSpan> spans;
spans.reserve(lh.spans.size());
const std::size_t line_len = line.size();
for (const auto &sp: lh.spans) {
int s_raw = sp.col_start;
int e_raw = sp.col_end;
if (e_raw < s_raw)
std::swap(e_raw, s_raw);
std::size_t s = static_cast<std::size_t>(std::max(
0, std::min(s_raw, (int) line_len)));
std::size_t e = static_cast<std::size_t>(std::max(
(int) s, std::min(e_raw, (int) line_len)));
if (s < e)
spans.push_back({s, e, sp.kind});
}
std::sort(spans.begin(), spans.end(),
[](const SSpan &a, const SSpan &b) {
return a.s < b.s;
});
auto colorFor = [](kte::TokenKind k) -> QColor {
// GUITheme provides colors via ImGui vector; avoid direct dependency types
const auto v = kte::SyntaxInk(k);
return QColor(int(v.x * 255.0f), int(v.y * 255.0f),
int(v.z * 255.0f), int(v.w * 255.0f));
};
// Helper to convert src col to expanded rx
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
std::size_t rx = 0;
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
}
return rx;
};
if (spans.empty()) {
// No highlight spans: draw the whole (visible) expanded line in default fg
if (coloffs < expanded.size()) {
const char *start =
expanded.c_str() + static_cast<int>(coloffs);
p.setPen(fg);
p.drawText(viewport.x(), baseline,
QString::fromUtf8(start));
}
} else {
// Draw colored spans
for (const auto &sp: spans) {
std::size_t rx_s = src_to_rx_full(sp.s);
std::size_t rx_e = src_to_rx_full(sp.e);
if (rx_e <= coloffs)
continue; // left of viewport
std::size_t draw_start = (rx_s > coloffs)
? rx_s
: coloffs;
std::size_t draw_end = std::min<std::size_t>(
rx_e, expanded.size());
if (draw_end <= draw_start)
continue;
std::size_t screen_x = draw_start - coloffs;
int px = viewport.x() + int(screen_x * ch_w);
int len = int(draw_end - draw_start);
p.setPen(colorFor(sp.k));
p.drawText(px, baseline,
QString::fromUtf8(
expanded.c_str() + draw_start, len));
}
}
} else {
// Draw expanded text clipped by coloffs
if (static_cast<std::size_t>(coloffs) < expanded.size()) {
const char *start =
expanded.c_str() + static_cast<int>(coloffs);
p.setPen(fg);
p.drawText(viewport.x(), baseline, QString::fromUtf8(start));
}
}
// Cursor indicator on current line
if (i == cy) {
std::size_t rx_cur = src_to_rx_line(cx);
if (rx_cur >= coloffs) {
// Compute exact pixel x by measuring expanded substring [coloffs, rx_cur)
std::size_t start = std::min<std::size_t>(
coloffs, expanded.size());
std::size_t end = std::min<
std::size_t>(rx_cur, expanded.size());
int px_advance = 0;
if (end > start) {
const QString sub = QString::fromUtf8(
expanded.c_str() + start,
static_cast<int>(end - start));
px_advance = fm.horizontalAdvance(sub);
}
int x0 = viewport.x() + px_advance;
QRect r(x0, y, ch_w, line_h);
p.fillRect(r, cur_bg);
}
}
}
p.restore();
}
}
// Status bar
const int bar_y = height() - status_h;
QRect status_rect(0, bar_y, width(), status_h);
p.fillRect(status_rect, status_bg);
p.setPen(status_fg);
if (ed_) {
const int pad = 6;
const int left_x = status_rect.x() + pad;
const int right_x_max = status_rect.x() + status_rect.width() - pad;
const int baseline_y = bar_y + (status_h + fm.ascent() - fm.descent()) / 2;
// If a prompt is active, mirror ImGui/TUI: show only the prompt across the bar
if (ed_->PromptActive()) {
std::string label = ed_->PromptLabel();
std::string text = ed_->PromptText();
// Map $HOME to ~ for path prompts (Open/Save/Chdir)
auto kind = ed_->CurrentPromptKind();
if (kind == Editor::PromptKind::OpenFile ||
kind == Editor::PromptKind::SaveAs ||
kind == Editor::PromptKind::Chdir) {
const char *home_c = std::getenv("HOME");
if (home_c && *home_c) {
std::string home(home_c);
if (text.rfind(home, 0) == 0) {
std::string rest = text.substr(home.size());
if (rest.empty())
text = "~";
else if (!rest.empty() && (rest[0] == '/' || rest[0] == '\\'))
text = std::string("~") + rest;
}
}
}
std::string prefix;
if (kind == Editor::PromptKind::Command)
prefix = ": ";
else if (!label.empty())
prefix = label + ": ";
// Compose text and elide per behavior:
const int max_w = status_rect.width() - 2 * pad;
QString qprefix = QString::fromStdString(prefix);
QString qtext = QString::fromStdString(text);
int avail_w = std::max(0, max_w - fm.horizontalAdvance(qprefix));
Qt::TextElideMode mode = Qt::ElideRight;
if (kind == Editor::PromptKind::OpenFile ||
kind == Editor::PromptKind::SaveAs ||
kind == Editor::PromptKind::Chdir) {
mode = Qt::ElideLeft;
}
QString shown = fm.elidedText(qtext, mode, avail_w);
p.drawText(left_x, baseline_y, qprefix + shown);
} else {
// Build left segment: app/version, buffer idx/total, filename [+dirty], line count
QString left;
left += QStringLiteral("kge ");
left += QStringLiteral(KTE_VERSION_STR);
const Buffer *buf = ed_->CurrentBuffer();
if (buf) {
// buffer index/total
std::size_t total = ed_->BufferCount();
if (total > 0) {
std::size_t idx1 = ed_->CurrentBufferIndex() + 1; // 1-based
left += QStringLiteral(" [");
left += QString::number(static_cast<qlonglong>(idx1));
left += QStringLiteral("/");
left += QString::number(static_cast<qlonglong>(total));
left += QStringLiteral("] ");
} else {
left += QStringLiteral(" ");
}
// buffer display name
std::string disp;
try {
disp = ed_->DisplayNameFor(*buf);
} catch (...) {
disp = buf->Filename();
}
if (disp.empty())
disp = "[No Name]";
left += QString::fromStdString(disp);
if (buf->Dirty())
left += QStringLiteral(" *");
// total lines suffix " <n>L"
unsigned long lcount = static_cast<unsigned long>(buf->Rows().size());
left += QStringLiteral(" ");
left += QString::number(static_cast<qlonglong>(lcount));
left += QStringLiteral("L");
}
// Build right segment: cursor and mark
QString right;
if (buf) {
int row1 = static_cast<int>(buf->Cury()) + 1;
int col1 = static_cast<int>(buf->Curx()) + 1;
bool have_mark = buf->MarkSet();
int mrow1 = have_mark ? static_cast<int>(buf->MarkCury()) + 1 : 0;
int mcol1 = have_mark ? static_cast<int>(buf->MarkCurx()) + 1 : 0;
if (have_mark)
right = QString("%1,%2 | M: %3,%4").arg(row1).arg(col1).arg(mrow1).arg(
mcol1);
else
right = QString("%1,%2 | M: not set").arg(row1).arg(col1);
}
// Middle message: status text
QString mid = QString::fromStdString(ed_->Status());
// Measure and layout
int left_w = fm.horizontalAdvance(left);
int right_w = fm.horizontalAdvance(right);
int lx = left_x;
int rx = std::max(left_x, right_x_max - right_w);
// If overlap, elide left to make space for right
if (lx + left_w + pad > rx) {
int max_left_w = std::max(0, rx - lx - pad);
left = fm.elidedText(left, Qt::ElideRight, max_left_w);
left_w = fm.horizontalAdvance(left);
}
// Draw left and right
p.drawText(lx, baseline_y, left);
if (!right.isEmpty())
p.drawText(rx, baseline_y, right);
// Middle message clipped between end of left and start of right
int mid_left = lx + left_w + pad;
int mid_right = std::max(mid_left, rx - pad);
int mid_w = std::max(0, mid_right - mid_left);
if (mid_w > 0 && !mid.isEmpty()) {
QString mid_show = fm.elidedText(mid, Qt::ElideRight, mid_w);
p.save();
p.setClipRect(QRect(mid_left, bar_y, mid_w, status_h));
p.drawText(mid_left, baseline_y, mid_show);
p.restore();
}
}
}
}
void resizeEvent(QResizeEvent *event) override
{
QWidget::resizeEvent(event);
if (!ed_)
return;
// Update editor dimensions based on new size
QFont f(font_family_, font_px_);
QFontMetrics fm(f);
const int line_h = std::max(12, fm.height());
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral(" ")));
const int pad_l = 8, pad_r = 8, pad_t = 6, pad_b = 6;
const int status_h = line_h + 6;
const int avail_w = std::max(0, width() - pad_l - pad_r);
const int avail_h = std::max(0, height() - pad_t - pad_b - status_h);
std::size_t rows = std::max<std::size_t>(1, (avail_h / line_h));
std::size_t cols = std::max<std::size_t>(1, (avail_w / ch_w));
ed_->SetDimensions(rows, cols);
}
void wheelEvent(QWheelEvent *event) override
{
if (!ed_) {
QWidget::wheelEvent(event);
return;
}
Buffer *buf = ed_->CurrentBuffer();
if (!buf) {
QWidget::wheelEvent(event);
return;
}
// Recompute metrics to map pixel deltas to rows/cols
QFont f(font_family_, font_px_);
QFontMetrics fm(f);
const int line_h = std::max(12, fm.height());
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral(" ")));
// Determine scroll intent: use pixelDelta when available (trackpads), otherwise angleDelta
QPoint pixel = event->pixelDelta();
QPoint angle = event->angleDelta();
double v_lines_delta = 0.0;
double h_cols_delta = 0.0;
// Horizontal scroll with Shift or explicit horizontal delta
bool horiz_mode = (event->modifiers() & Qt::ShiftModifier) || (!pixel.isNull() && pixel.x() != 0) || (
!angle.isNull() && angle.x() != 0);
if (!pixel.isNull()) {
// Trackpad smooth scrolling (pixels)
v_lines_delta = -static_cast<double>(pixel.y()) / std::max(1, line_h);
h_cols_delta = -static_cast<double>(pixel.x()) / std::max(1, ch_w);
} else if (!angle.isNull()) {
// Mouse wheel: 120 units per notch; map one notch to 3 lines similar to ImGui UX
v_lines_delta = -static_cast<double>(angle.y()) / 120.0 * 3.0;
// For horizontal wheels, each notch scrolls 8 columns
h_cols_delta = -static_cast<double>(angle.x()) / 120.0 * 8.0;
}
// Accumulate fractional deltas across events
v_scroll_accum_ += v_lines_delta;
h_scroll_accum_ += h_cols_delta;
int d_rows = 0;
int d_cols = 0;
if (std::fabs(v_scroll_accum_) >= 1.0 && (!horiz_mode || std::fabs(v_scroll_accum_) > std::fabs(
h_scroll_accum_))) {
d_rows = static_cast<int>(v_scroll_accum_);
v_scroll_accum_ -= d_rows;
}
if (std::fabs(h_scroll_accum_) >= 1.0 && (horiz_mode || std::fabs(h_scroll_accum_) >= std::fabs(
v_scroll_accum_))) {
d_cols = static_cast<int>(h_scroll_accum_);
h_scroll_accum_ -= d_cols;
}
if (d_rows != 0 || d_cols != 0) {
std::size_t new_rowoffs = buf->Rowoffs();
std::size_t new_coloffs = buf->Coloffs();
// Clamp vertical between 0 and last row (leaving at least one visible line)
if (d_rows != 0) {
long nr = static_cast<long>(new_rowoffs) + d_rows;
if (nr < 0)
nr = 0;
const auto nrows = static_cast<long>(buf->Rows().size());
if (nr > std::max(0L, nrows - 1))
nr = std::max(0L, nrows - 1);
new_rowoffs = static_cast<std::size_t>(nr);
}
if (d_cols != 0) {
long nc = static_cast<long>(new_coloffs) + d_cols;
if (nc < 0)
nc = 0;
new_coloffs = static_cast<std::size_t>(nc);
}
buf->SetOffsets(new_rowoffs, new_coloffs);
update();
event->accept();
return;
}
QWidget::wheelEvent(event);
}
void closeEvent(QCloseEvent *event) override
{
closed_ = true;
QWidget::closeEvent(event);
}
private:
QtInputHandler &input_;
bool closed_ = false;
Editor *ed_ = nullptr;
double v_scroll_accum_ = 0.0;
double h_scroll_accum_ = 0.0;
QString font_family_ = QStringLiteral("Brass Mono");
int font_px_ = 18;
};
} // namespace
bool
GUIFrontend::Init(Editor &ed)
{
int argc = 0;
char **argv = nullptr;
app_ = new QApplication(argc, argv);
window_ = new MainWindow(input_);
window_->show();
// Ensure the window becomes the active, focused window so it receives key events
window_->activateWindow();
window_->raise();
window_->setFocus(Qt::OtherFocusReason);
renderer_.Attach(window_);
input_.Attach(&ed);
if (auto *mw = dynamic_cast<MainWindow *>(window_))
mw->SetEditor(&ed);
// Load GUI configuration (kge.ini) and configure font for Qt
config_ = GUIConfig::Load();
// Apply background mode from config to match ImGui frontend behavior
if (config_.background == "light")
kte::SetBackgroundMode(kte::BackgroundMode::Light);
else
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
// Apply theme by name for Qt palette-based theming (maps to named palettes).
// If unknown, falls back to the generic light/dark palette.
(void) kte::ApplyQtThemeByName(config_.theme);
if (window_)
window_->update();
// Map GUIConfig font name to a system family (Qt uses installed fonts)
auto choose_family = [](const std::string &name) -> QString {
QString fam;
std::string n = name;
std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) {
return (char) std::tolower(c);
});
if (n.empty() || n == "default" || n == "brassmono" || n == "brassmonocode") {
fam = QStringLiteral("Brass Mono");
} else if (n == "jetbrains" || n == "jetbrains mono" || n == "jetbrains-mono") {
fam = QStringLiteral("JetBrains Mono");
} else if (n == "iosevka") {
fam = QStringLiteral("Iosevka");
} else if (n == "inconsolata" || n == "inconsolataex") {
fam = QStringLiteral("Inconsolata");
} else if (n == "space" || n == "spacemono" || n == "space mono") {
fam = QStringLiteral("Space Mono");
} else if (n == "go") {
fam = QStringLiteral("Go Mono");
} else if (n == "ibm" || n == "ibm plex mono" || n == "ibm-plex-mono") {
fam = QStringLiteral("IBM Plex Mono");
} else if (n == "fira" || n == "fira code" || n == "fira-code") {
fam = QStringLiteral("Fira Code");
} else if (!name.empty()) {
fam = QString::fromStdString(name);
}
// Validate availability; choose a fallback if needed
const auto families = QFontDatabase::families();
if (!fam.isEmpty() && families.contains(fam)) {
return fam;
}
// Preferred fallback chain on macOS; otherwise, try common monospace families
const QStringList fallbacks = {
QStringLiteral("Brass Mono"),
QStringLiteral("JetBrains Mono"),
QStringLiteral("SF Mono"),
QStringLiteral("Menlo"),
QStringLiteral("Monaco"),
QStringLiteral("Courier New"),
QStringLiteral("Courier"),
QStringLiteral("Monospace")
};
for (const auto &fb: fallbacks) {
if (families.contains(fb))
return fb;
}
// As a last resort, return the request (Qt will substitute)
return fam.isEmpty() ? QStringLiteral("Monospace") : fam;
};
QString family = choose_family(config_.font);
int px_size = (config_.font_size > 0.0f) ? (int) std::lround(config_.font_size) : 18;
if (auto *mw = dynamic_cast<MainWindow *>(window_)) {
mw->SetFontFamilyAndSize(family, px_size);
}
// Track current font in globals for command/status queries
kte::gCurrentFontFamily = family.toStdString();
kte::gCurrentFontSize = static_cast<float>(px_size);
// Set initial dimensions based on font metrics
QFont f(family, px_size);
QFontMetrics fm(f);
const int line_h = std::max(12, fm.height());
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral("M")));
const int w = window_->width();
const int h = window_->height();
const int pad = 16;
const int status_h = line_h + 4;
const int avail_w = std::max(0, w - 2 * pad);
const int avail_h = std::max(0, h - 2 * pad - status_h);
std::size_t rows = std::max<std::size_t>(1, (avail_h / line_h) + 1); // + status
std::size_t cols = std::max<std::size_t>(1, (avail_w / ch_w));
ed.SetDimensions(rows, cols);
return true;
}
void
GUIFrontend::Step(Editor &ed, bool &running)
{
// Pump Qt events
if (app_)
app_->processEvents();
// Drain input queue
for (;;) {
MappedInput mi;
if (!input_.Poll(mi))
break;
if (mi.hasCommand) {
Execute(ed, mi.id, mi.arg, mi.count);
}
}
if (ed.QuitRequested()) {
running = false;
}
// --- Visual File Picker (Qt): invoked via CommandId::VisualFilePickerToggle ---
if (ed.FilePickerVisible()) {
QString startDir;
if (!ed.FilePickerDir().empty()) {
startDir = QString::fromStdString(ed.FilePickerDir());
}
QFileDialog dlg(window_, QStringLiteral("Open File"), startDir);
dlg.setFileMode(QFileDialog::ExistingFile);
if (dlg.exec() == QDialog::Accepted) {
const QStringList files = dlg.selectedFiles();
if (!files.isEmpty()) {
const QString fp = files.front();
std::string err;
if (ed.OpenFile(fp.toStdString(), err)) {
ed.SetStatus(std::string("Opened: ") + fp.toStdString());
} else if (!err.empty()) {
ed.SetStatus(std::string("Open failed: ") + err);
} else {
ed.SetStatus("Open failed");
}
// Update picker dir for next time
QFileInfo info(fp);
ed.SetFilePickerDir(info.dir().absolutePath().toStdString());
}
}
// Close picker overlay regardless of outcome
ed.SetFilePickerVisible(false);
if (window_)
window_->update();
}
// Apply any queued theme change requests (from command handler)
if (kte::gThemeChangePending) {
if (!kte::gThemeChangeRequest.empty()) {
// Apply Qt palette theme by name; if unknown, keep current palette
(void) kte::ApplyQtThemeByName(kte::gThemeChangeRequest);
}
kte::gThemeChangePending = false;
kte::gThemeChangeRequest.clear();
if (window_)
window_->update();
}
// Visual font picker request (Qt only)
if (kte::gFontDialogRequested) {
// Seed initial font from current or default
QFont seed;
if (!kte::gCurrentFontFamily.empty()) {
seed = QFont(QString::fromStdString(kte::gCurrentFontFamily),
(int) std::lround(kte::gCurrentFontSize > 0 ? kte::gCurrentFontSize : 18));
} else {
seed = window_ ? window_->font() : QFont();
}
bool ok = false;
const QFont chosen = QFontDialog::getFont(&ok, seed, window_, QStringLiteral("Choose Editor Font"));
if (ok) {
// Queue font change via existing hooks
kte::gFontFamilyRequest = chosen.family().toStdString();
// Use pixel size if available, otherwise convert from point size approximately
int px = chosen.pixelSize();
if (px <= 0) {
// Approximate points to pixels (96 DPI assumption); Qt will rasterize appropriately
px = (int) std::lround(chosen.pointSizeF() * 96.0 / 72.0);
if (px <= 0)
px = 18;
}
kte::gFontSizeRequest = static_cast<float>(px);
kte::gFontChangePending = true;
}
kte::gFontDialogRequested = false;
if (window_)
window_->update();
}
// Apply any queued font change requests (Qt)
if (kte::gFontChangePending) {
// Derive target family
auto map_family = [](const std::string &name) -> QString {
std::string n = name;
std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) {
return (char) std::tolower(c);
});
QString fam;
if (n == "brass" || n == "brassmono" || n == "brass mono") {
fam = QStringLiteral("Brass Mono");
} else if (n == "jetbrains" || n == "jetbrains mono" || n == "jetbrains-mono") {
fam = QStringLiteral("JetBrains Mono");
} else if (n == "iosevka") {
fam = QStringLiteral("Iosevka");
} else if (n == "inconsolata" || n == "inconsolataex") {
fam = QStringLiteral("Inconsolata");
} else if (n == "space" || n == "spacemono" || n == "space mono") {
fam = QStringLiteral("Space Mono");
} else if (n == "go") {
fam = QStringLiteral("Go Mono");
} else if (n == "ibm" || n == "ibm plex mono" || n == "ibm-plex-mono") {
fam = QStringLiteral("IBM Plex Mono");
} else if (n == "fira" || n == "fira code" || n == "fira-code") {
fam = QStringLiteral("Fira Code");
} else if (!name.empty()) {
fam = QString::fromStdString(name);
}
// Validate availability; choose fallback if needed
const auto families = QFontDatabase::families();
if (!fam.isEmpty() && families.contains(fam)) {
return fam;
}
// Fallback chain
const QStringList fallbacks = {
QStringLiteral("Brass Mono"),
QStringLiteral("JetBrains Mono"),
QStringLiteral("SF Mono"),
QStringLiteral("Menlo"),
QStringLiteral("Monaco"),
QStringLiteral("Courier New"),
QStringLiteral("Courier"),
QStringLiteral("Monospace")
};
for (const auto &fb: fallbacks) {
if (families.contains(fb))
return fb;
}
return fam.isEmpty() ? QStringLiteral("Monospace") : fam;
};
QString target_family;
if (!kte::gFontFamilyRequest.empty()) {
target_family = map_family(kte::gFontFamilyRequest);
} else if (!kte::gCurrentFontFamily.empty()) {
target_family = QString::fromStdString(kte::gCurrentFontFamily);
}
int target_px = 0;
if (kte::gFontSizeRequest > 0.0f) {
target_px = (int) std::lround(kte::gFontSizeRequest);
} else if (kte::gCurrentFontSize > 0.0f) {
target_px = (int) std::lround(kte::gCurrentFontSize);
}
if (target_px <= 0)
target_px = 18;
if (target_family.isEmpty())
target_family = QStringLiteral("Monospace");
if (auto *mw = dynamic_cast<MainWindow *>(window_)) {
mw->SetFontFamilyAndSize(target_family, target_px);
}
// Update globals
kte::gCurrentFontFamily = target_family.toStdString();
kte::gCurrentFontSize = static_cast<float>(target_px);
// Reset requests
kte::gFontChangePending = false;
kte::gFontFamilyRequest.clear();
kte::gFontSizeRequest = 0.0f;
// Recompute editor dimensions to match new metrics
QFont f(target_family, target_px);
QFontMetrics fm(f);
const int line_h = std::max(12, fm.height());
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral("M")));
const int w = window_ ? window_->width() : 0;
const int h = window_ ? window_->height() : 0;
const int pad = 16;
const int status_h = line_h + 4;
const int avail_w = std::max(0, w - 2 * pad);
const int avail_h = std::max(0, h - 2 * pad - status_h);
std::size_t rows = std::max<std::size_t>(1, (avail_h / line_h) + 1); // + status
std::size_t cols = std::max<std::size_t>(1, (avail_w / ch_w));
ed.SetDimensions(rows, cols);
if (window_)
window_->update();
}
// Draw current frame (request repaint)
renderer_.Draw(ed);
// Detect window close
if (auto *mw = dynamic_cast<MainWindow *>(window_)) {
if (mw->WasClosed()) {
running = false;
}
}
}
void
GUIFrontend::Shutdown()
{
if (window_) {
window_->close();
delete window_;
window_ = nullptr;
}
if (app_) {
delete app_;
app_ = nullptr;
}
}

36
QtFrontend.h Normal file
View File

@@ -0,0 +1,36 @@
/*
* QtFrontend - couples QtInputHandler + QtRenderer and owns Qt lifecycle
*/
#pragma once
#include "Frontend.h"
#include "GUIConfig.h"
#include "QtInputHandler.h"
#include "QtRenderer.h"
class QApplication;
class QWidget;
// Keep the public class name GUIFrontend to match main.cc selection logic.
class GUIFrontend final : public Frontend {
public:
GUIFrontend() = default;
~GUIFrontend() override = default;
bool Init(Editor &ed) override;
void Step(Editor &ed, bool &running) override;
void Shutdown() override;
private:
GUIConfig config_{};
QtInputHandler input_{};
QtRenderer renderer_{};
QApplication *app_ = nullptr; // owned
QWidget *window_ = nullptr; // owned
int width_ = 1280;
int height_ = 800;
};

538
QtInputHandler.cc Normal file
View File

@@ -0,0 +1,538 @@
// Refactor: route all key processing through command subsystem, mirroring ImGuiInputHandler
#include "QtInputHandler.h"
#include <QKeyEvent>
#include <ncurses.h>
#include "Editor.h"
#include "KKeymap.h"
// Temporary verbose logging to debug macOS Qt key translation issues
// Default to off; enable by defining QT_IH_DEBUG=1 at compile time when needed.
#ifndef QT_IH_DEBUG
#define QT_IH_DEBUG 0
#endif
#if QT_IH_DEBUG
#include <cstdio>
static const char *
mods_str(Qt::KeyboardModifiers m)
{
static thread_local char buf[64];
buf[0] = '\0';
bool first = true;
auto add = [&](const char *s) {
if (!first)
std::snprintf(buf + std::strlen(buf), sizeof(buf) - std::strlen(buf), "|");
std::snprintf(buf + std::strlen(buf), sizeof(buf) - std::strlen(buf), "%s", s);
first = false;
};
if (m & Qt::ShiftModifier)
add("Shift");
if (m & Qt::ControlModifier)
add("Ctrl");
if (m & Qt::AltModifier)
add("Alt");
if (m & Qt::MetaModifier)
add("Meta");
if (first)
std::snprintf(buf, sizeof(buf), "none");
return buf;
}
#define LOGF(...) std::fprintf(stderr, __VA_ARGS__)
#else
#define LOGF(...) ((void)0)
#endif
static bool
IsPrintableQt(const QKeyEvent &e)
{
// Printable if it yields non-empty text and no Ctrl/Meta modifier
if (e.modifiers() & (Qt::ControlModifier | Qt::MetaModifier))
return false;
const QString t = e.text();
return !t.isEmpty() && !t.at(0).isNull();
}
static int
ToAsciiKey(const QKeyEvent &e)
{
const QString t = e.text();
if (!t.isEmpty()) {
const QChar c = t.at(0);
if (!c.isNull())
return KLowerAscii(c.unicode());
}
// When modifiers (like Control) are held, Qt::text() can be empty on macOS.
// Fall back to mapping common virtual keys to ASCII.
switch (e.key()) {
case Qt::Key_A:
return 'a';
case Qt::Key_B:
return 'b';
case Qt::Key_C:
return 'c';
case Qt::Key_D:
return 'd';
case Qt::Key_E:
return 'e';
case Qt::Key_F:
return 'f';
case Qt::Key_G:
return 'g';
case Qt::Key_H:
return 'h';
case Qt::Key_I:
return 'i';
case Qt::Key_J:
return 'j';
case Qt::Key_K:
return 'k';
case Qt::Key_L:
return 'l';
case Qt::Key_M:
return 'm';
case Qt::Key_N:
return 'n';
case Qt::Key_O:
return 'o';
case Qt::Key_P:
return 'p';
case Qt::Key_Q:
return 'q';
case Qt::Key_R:
return 'r';
case Qt::Key_S:
return 's';
case Qt::Key_T:
return 't';
case Qt::Key_U:
return 'u';
case Qt::Key_V:
return 'v';
case Qt::Key_W:
return 'w';
case Qt::Key_X:
return 'x';
case Qt::Key_Y:
return 'y';
case Qt::Key_Z:
return 'z';
case Qt::Key_0:
return '0';
case Qt::Key_1:
return '1';
case Qt::Key_2:
return '2';
case Qt::Key_3:
return '3';
case Qt::Key_4:
return '4';
case Qt::Key_5:
return '5';
case Qt::Key_6:
return '6';
case Qt::Key_7:
return '7';
case Qt::Key_8:
return '8';
case Qt::Key_9:
return '9';
case Qt::Key_Comma:
return ',';
case Qt::Key_Period:
return '.';
case Qt::Key_Semicolon:
return ';';
case Qt::Key_Apostrophe:
return '\'';
case Qt::Key_Minus:
return '-';
case Qt::Key_Equal:
return '=';
case Qt::Key_Slash:
return '/';
case Qt::Key_Backslash:
return '\\';
case Qt::Key_BracketLeft:
return '[';
case Qt::Key_BracketRight:
return ']';
case Qt::Key_QuoteLeft:
return '`';
case Qt::Key_Space:
return ' ';
default:
break;
}
return 0;
}
// Case-preserving ASCII derivation for k-prefix handling where we need to
// distinguish between 'C' and 'c'. Falls back to virtual-key mapping if
// event text is unavailable (common when Control/Meta held on macOS).
static int
ToAsciiKeyPreserveCase(const QKeyEvent &e)
{
const QString t = e.text();
if (!t.isEmpty()) {
const QChar c = t.at(0);
if (!c.isNull())
return c.unicode();
}
// Fall back to virtual key mapping (letters as uppercase A..Z)
switch (e.key()) {
case Qt::Key_A:
return 'A';
case Qt::Key_B:
return 'B';
case Qt::Key_C:
return 'C';
case Qt::Key_D:
return 'D';
case Qt::Key_E:
return 'E';
case Qt::Key_F:
return 'F';
case Qt::Key_G:
return 'G';
case Qt::Key_H:
return 'H';
case Qt::Key_I:
return 'I';
case Qt::Key_J:
return 'J';
case Qt::Key_K:
return 'K';
case Qt::Key_L:
return 'L';
case Qt::Key_M:
return 'M';
case Qt::Key_N:
return 'N';
case Qt::Key_O:
return 'O';
case Qt::Key_P:
return 'P';
case Qt::Key_Q:
return 'Q';
case Qt::Key_R:
return 'R';
case Qt::Key_S:
return 'S';
case Qt::Key_T:
return 'T';
case Qt::Key_U:
return 'U';
case Qt::Key_V:
return 'V';
case Qt::Key_W:
return 'W';
case Qt::Key_X:
return 'X';
case Qt::Key_Y:
return 'Y';
case Qt::Key_Z:
return 'Z';
case Qt::Key_Comma:
return ',';
case Qt::Key_Period:
return '.';
case Qt::Key_Semicolon:
return ';';
case Qt::Key_Apostrophe:
return '\'';
case Qt::Key_Minus:
return '-';
case Qt::Key_Equal:
return '=';
case Qt::Key_Slash:
return '/';
case Qt::Key_Backslash:
return '\\';
case Qt::Key_BracketLeft:
return '[';
case Qt::Key_BracketRight:
return ']';
case Qt::Key_QuoteLeft:
return '`';
case Qt::Key_Space:
return ' ';
default:
break;
}
return 0;
}
bool
QtInputHandler::ProcessKeyEvent(const QKeyEvent &e)
{
const Qt::KeyboardModifiers mods = e.modifiers();
LOGF("[QtIH] keyPress key=0x%X mods=%s text='%s' k_prefix=%d k_ctrl_pending=%d esc_meta=%d\n",
e.key(), mods_str(mods), e.text().toUtf8().constData(), (int)k_prefix_, (int)k_ctrl_pending_,
(int)esc_meta_);
// Control-chord detection: only treat the physical Control key as control-like.
// Do NOT include Meta (Command) here so that ⌘-letter shortcuts do not fall into
// the Ctrl map (prevents ⌘-T being mistaken for C-t).
const bool ctrl_like = (mods & Qt::ControlModifier);
// 1) Universal argument digits (when active), consume digits without enqueuing commands
if (ed_ &&ed_
->
UArg() != 0
)
{
if (!(mods & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier))) {
if (e.key() >= Qt::Key_0 && e.key() <= Qt::Key_9) {
int d = e.key() - Qt::Key_0;
ed_->UArgDigit(d);
// request status refresh
std::lock_guard<std::mutex> lk(mu_);
q_.push(MappedInput{true, CommandId::UArgStatus, std::string(), 0});
LOGF("[QtIH] UArg digit %d -> enqueue UArgStatus\n", d);
return true;
}
}
}
// 2) Enter k-prefix on C-k
if (ctrl_like && (e.key() == Qt::Key_K)) {
k_prefix_ = true;
k_ctrl_pending_ = false;
LOGF("[QtIH] Enter KPrefix\n");
std::lock_guard<std::mutex> lk(mu_);
q_.push(MappedInput{true, CommandId::KPrefix, std::string(), 0});
return true;
}
// 3) If currently in k-prefix, resolve next key via KLookupKCommand
if (k_prefix_) {
// ESC/meta prefix should not interfere with k-suffix resolution
esc_meta_ = false;
// Support literal 'C' (uppercase) or '^' to indicate the next key is Ctrl-qualified.
// Use case-preserving derivation so that 'c' (lowercase) can still be a valid suffix
// like C-k c (BufferClose).
int ascii_raw = ToAsciiKeyPreserveCase(e);
if (ascii_raw == 'C' || ascii_raw == '^') {
k_ctrl_pending_ = true;
if (ed_)
ed_->SetStatus("C-k C _");
LOGF("[QtIH] KPrefix: set k_ctrl_pending via '%c'\n", (ascii_raw == 'C') ? 'C' : '^');
return true; // consume, wait for next key
}
int ascii_key = (ascii_raw != 0) ? ascii_raw : ToAsciiKey(e);
int lower = KLowerAscii(ascii_key);
// Only pass a control suffix for specific supported keys (d/x/q),
// matching ImGui behavior so that holding Ctrl during the suffix
// doesn't break other mappings like C-k c (BufferClose).
bool ctrl_suffix_supported = (lower == 'd' || lower == 'x' || lower == 'q');
bool pass_ctrl = (ctrl_like || k_ctrl_pending_) && ctrl_suffix_supported;
k_ctrl_pending_ = false; // consume pending qualifier on any suffix
LOGF("[QtIH] KPrefix: ascii_key=%d lower=%d pass_ctrl=%d\n", ascii_key, lower, (int)pass_ctrl);
if (ascii_key != 0) {
CommandId id;
if (KLookupKCommand(ascii_key, pass_ctrl, id)) {
LOGF("[QtIH] KPrefix: mapped to command id=%d\n", (int)id);
std::lock_guard<std::mutex> lk(mu_);
q_.push(MappedInput{true, id, std::string(), 0});
} else {
// Unknown k-command: notify
std::string a;
a.push_back(static_cast<char>(ascii_key));
LOGF("[QtIH] KPrefix: unknown command for '%c'\n", (char)ascii_key);
std::lock_guard<std::mutex> lk(mu_);
q_.push(MappedInput{true, CommandId::UnknownKCommand, a, 0});
}
k_prefix_ = false;
return true;
}
// If not resolvable, consume and exit k-prefix
k_prefix_ = false;
LOGF("[QtIH] KPrefix: unresolved key; exiting prefix\n");
return true;
}
// 3.5) GUI shortcut: Command/Meta + T opens the visual font picker (Qt only).
// Require Meta present and Control NOT present so Ctrl-T never triggers this.
if ((mods & Qt::MetaModifier) && !(mods & Qt::ControlModifier) && e.key() == Qt::Key_T) {
LOGF("[QtIH] Meta/Super-T -> VisualFontPickerToggle\n");
std::lock_guard<std::mutex> lk(mu_);
q_.push(MappedInput{true, CommandId::VisualFontPickerToggle, std::string(), 0});
return true;
}
// 4) ESC as Meta prefix (set state). Alt/Meta chord handled below directly.
if (e.key() == Qt::Key_Escape) {
esc_meta_ = true;
LOGF("[QtIH] ESC: set esc_meta\n");
return true; // consumed
}
// 5) Alt/Meta bindings (ESC f/b equivalent). Handle either Alt/Meta or pending esc_meta_
// ESC/meta chords: on macOS, do NOT treat Meta as ESC; only Alt (Option) should trigger.
#if defined(__APPLE__)
if (esc_meta_ || (mods & Qt::AltModifier)) {
#else
if (esc_meta_ || (mods & (Qt::AltModifier | Qt::MetaModifier))) {
#endif
int ascii_key = 0;
if (e.key() == Qt::Key_Backspace) {
ascii_key = KEY_BACKSPACE;
} else if (e.key() >= Qt::Key_A && e.key() <= Qt::Key_Z) {
ascii_key = 'a' + (e.key() - Qt::Key_A);
} else if (e.key() == Qt::Key_Comma) {
ascii_key = '<';
} else if (e.key() == Qt::Key_Period) {
ascii_key = '>';
}
// If still unknown, try deriving from text (covers digits, punctuation, locale)
if (ascii_key == 0) {
ascii_key = ToAsciiKey(e);
}
esc_meta_ = false; // one-shot regardless
if (ascii_key != 0) {
ascii_key = KLowerAscii(ascii_key);
CommandId id;
if (KLookupEscCommand(ascii_key, id)) {
LOGF("[QtIH] ESC/Meta: mapped '%d' -> id=%d\n", ascii_key, (int)id);
std::lock_guard<std::mutex> lk(mu_);
q_.push(MappedInput{true, id, std::string(), 0});
return true;
} else {
// Report invalid ESC sequence just like ImGui path
LOGF("[QtIH] ESC/Meta: unknown command for ascii=%d\n", ascii_key);
std::lock_guard<std::mutex> lk(mu_);
q_.push(MappedInput{true, CommandId::UnknownEscCommand, std::string(), 0});
return true;
}
}
// Nothing derivable: consume (ESC prefix cleared) and do not insert text
return true;
}
// 6) Control-chord direct mappings (e.g., C-n/C-p/C-f/C-b...)
if (ctrl_like) {
// Universal argument handling: C-u starts collection; C-g cancels
if (e.key() == Qt::Key_U) {
if (ed_)
ed_->UArgStart();
LOGF("[QtIH] Ctrl-chord: start universal argument\n");
return true;
}
if (e.key() == Qt::Key_G) {
if (ed_)
ed_->UArgClear();
k_ctrl_pending_ = false;
k_prefix_ = false;
LOGF("[QtIH] Ctrl-chord: cancel universal argument and k-prefix via C-g\n");
// Fall through to map C-g to Refresh via ctrl map
}
if (e.key() >= Qt::Key_A && e.key() <= Qt::Key_Z) {
int ascii_key = 'a' + (e.key() - Qt::Key_A);
CommandId id;
if (KLookupCtrlCommand(ascii_key, id)) {
LOGF("[QtIH] Ctrl-chord: 'C-%c' -> id=%d\n", (char)ascii_key, (int)id);
std::lock_guard<std::mutex> lk(mu_);
q_.push(MappedInput{true, id, std::string(), 0});
return true;
}
}
// If no mapping, continue to allow other keys below
}
// 7) Special navigation/edit keys (match ImGui behavior)
{
CommandId id;
bool has = false;
switch (e.key()) {
case Qt::Key_Return:
case Qt::Key_Enter:
id = CommandId::Newline;
has = true;
break;
case Qt::Key_Backspace:
id = CommandId::Backspace;
has = true;
break;
case Qt::Key_Delete:
id = CommandId::DeleteChar;
has = true;
break;
case Qt::Key_Left:
id = CommandId::MoveLeft;
has = true;
break;
case Qt::Key_Right:
id = CommandId::MoveRight;
has = true;
break;
case Qt::Key_Up:
id = CommandId::MoveUp;
has = true;
break;
case Qt::Key_Down:
id = CommandId::MoveDown;
has = true;
break;
case Qt::Key_Home:
id = CommandId::MoveHome;
has = true;
break;
case Qt::Key_End:
id = CommandId::MoveEnd;
has = true;
break;
case Qt::Key_PageUp:
id = CommandId::PageUp;
has = true;
break;
case Qt::Key_PageDown:
id = CommandId::PageDown;
has = true;
break;
default:
break;
}
if (has) {
LOGF("[QtIH] Special key -> id=%d\n", (int)id);
std::lock_guard<std::mutex> lk(mu_);
q_.push(MappedInput{true, id, std::string(), 0});
return true;
}
}
// 8) Insert printable text
if (IsPrintableQt(e)) {
std::string s = e.text().toStdString();
if (!s.empty()) {
LOGF("[QtIH] InsertText '%s'\n", s.c_str());
std::lock_guard<std::mutex> lk(mu_);
q_.push(MappedInput{true, CommandId::InsertText, s, 0});
return true;
}
}
LOGF("[QtIH] Unhandled key\n");
return false;
}
bool
QtInputHandler::Poll(MappedInput &out)
{
std::lock_guard<std::mutex> lock(mu_);
if (q_.empty())
return false;
out = q_.front();
q_.pop();
return true;
}

40
QtInputHandler.h Normal file
View File

@@ -0,0 +1,40 @@
/*
* QtInputHandler - Qt-based input mapping for GUI mode
*/
#pragma once
#include <mutex>
#include <queue>
#include "InputHandler.h"
class QKeyEvent;
class QtInputHandler final : public InputHandler {
public:
QtInputHandler() = default;
~QtInputHandler() override = default;
void Attach(Editor *ed) override
{
ed_ = ed;
}
// Translate a Qt key event to editor command and enqueue if applicable.
// Returns true if it produced a mapped command or consumed input.
bool ProcessKeyEvent(const QKeyEvent &e);
bool Poll(MappedInput &out) override;
private:
std::mutex mu_;
std::queue<MappedInput> q_;
bool k_prefix_ = false;
bool k_ctrl_pending_ = false; // C-k C-… qualifier
bool esc_meta_ = false; // ESC-prefix for next key
bool suppress_text_input_once_ = false; // reserved (Qt sends text in keyPressEvent)
Editor *ed_ = nullptr;
};

76
QtRenderer.cc Normal file
View File

@@ -0,0 +1,76 @@
#include "QtRenderer.h"
#include <QWidget>
#include <QPainter>
#include <QPaintEvent>
#include <QFont>
#include <QFontMetrics>
#include "Editor.h"
namespace {
class EditorWidget : public QWidget {
public:
explicit EditorWidget(QWidget *parent = nullptr) : QWidget(parent)
{
setAttribute(Qt::WA_OpaquePaintEvent);
setFocusPolicy(Qt::StrongFocus);
}
void SetEditor(Editor *ed)
{
ed_ = ed;
}
protected:
void paintEvent(QPaintEvent *event) override
{
Q_UNUSED(event);
QPainter p(this);
// Background
const QColor bg(28, 28, 30);
p.fillRect(rect(), bg);
// Font and metrics
QFont f("JetBrains Mono", 13);
p.setFont(f);
QFontMetrics fm(f);
const int line_h = fm.height();
// Title
p.setPen(QColor(220, 220, 220));
p.drawText(8, fm.ascent() + 4, QStringLiteral("kte (Qt frontend)"));
// Status bar at bottom
const int bar_h = line_h + 6; // padding
const int bar_y = height() - bar_h;
QRect status_rect(0, bar_y, width(), bar_h);
p.fillRect(status_rect, QColor(40, 40, 44));
p.setPen(QColor(180, 180, 140));
if (ed_) {
const QString status = QString::fromStdString(ed_->Status());
// draw at baseline within the bar
const int baseline = bar_y + 3 + fm.ascent();
p.drawText(8, baseline, status);
}
}
private:
Editor *ed_ = nullptr;
};
} // namespace
void
QtRenderer::Draw(Editor &ed)
{
if (!widget_)
return;
// If our widget is an EditorWidget, pass the editor pointer for painting
if (auto *ew = dynamic_cast<EditorWidget *>(widget_)) {
ew->SetEditor(&ed);
}
// Request a repaint
widget_->update();
}

27
QtRenderer.h Normal file
View File

@@ -0,0 +1,27 @@
/*
* QtRenderer - minimal Qt-based renderer
*/
#pragma once
#include "Renderer.h"
class QWidget;
class QtRenderer final : public Renderer {
public:
QtRenderer() = default;
~QtRenderer() override = default;
void Attach(QWidget *widget)
{
widget_ = widget;
}
void Draw(Editor &ed) override;
private:
QWidget *widget_ = nullptr; // not owned
};

View File

@@ -9,6 +9,7 @@
installShellFiles,
graphical ? false,
graphical-qt ? false,
...
}:
let
@@ -34,10 +35,15 @@ stdenv.mkDerivation {
SDL2
libGL
xorg.libX11
]
++ lib.optionals graphical-qt [
qt5Full
qtcreator ## not sure if this is actually needed
];
cmakeFlags = [
"-DBUILD_GUI=${if graphical then "ON" else "OFF"}"
"-DBUILD_GUI=${if graphical or graphical-qt then "ON" else "OFF"}"
"-DKTE_USE_QT=${if graphical-qt then "ON" else "OFF"}"
"-DCMAKE_BUILD_TYPE=Debug"
];

View File

@@ -113,4 +113,12 @@ C++ projects than GTK (which is C-based, though `gtkmm` exists).
When the core calls `DrawText()`, `QtRenderer` should queue that
command or draw directly to the widget's paint buffer.
4. **Refactor `main.cc`** to instantiate `QApplication` instead of the
current manual loop.
current manual loop.
---
Note (2025-12): The Qt frontend defers all key processing to the
existing command subsystem and keymaps, mirroring the ImGui path. There
are no Qt-only keybindings; `QtInputHandler` translates Qt key events
into the shared keymap flow (C-k prefix, Ctrl chords, ESC/Meta,
universal-argument digits, printable insertion).

View File

@@ -15,6 +15,7 @@
full = kge;
kte = (pkgsFor system).callPackage ./default.nix { graphical = false; };
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; };
qt = (pkgsFor system).callPackage ./default.nix { graphical-qt = true; }
});
};
}

58
main.cc
View File

@@ -21,7 +21,11 @@
#include "TerminalFrontend.h"
#if defined(KTE_BUILD_GUI)
#include "GUIFrontend.h"
#if defined(KTE_USE_QT)
#include "QtFrontend.h"
#else
#include "ImGuiFrontend.h"
#endif
#endif
@@ -131,33 +135,33 @@ main(int argc, const char *argv[])
unsigned stress_seconds = 0;
while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "gthV", long_opts, &long_index)) != -1) {
switch (opt) {
case 'g':
req_gui = true;
break;
case 't':
req_term = true;
break;
case 'h':
show_help = true;
break;
case 'V':
show_version = true;
break;
case 1000: {
stress_seconds = 5; // default
if (optarg && *optarg) {
try {
unsigned v = static_cast<unsigned>(std::stoul(optarg));
if (v > 0 && v < 36000)
stress_seconds = v;
} catch (...) {}
}
break;
case 'g':
req_gui = true;
break;
case 't':
req_term = true;
break;
case 'h':
show_help = true;
break;
case 'V':
show_version = true;
break;
case 1000: {
stress_seconds = 5; // default
if (optarg && *optarg) {
try {
unsigned v = static_cast<unsigned>(std::stoul(optarg));
if (v > 0 && v < 36000)
stress_seconds = v;
} catch (...) {}
}
case '?':
default:
PrintUsage(argv[0]);
return 2;
break;
}
case '?':
default:
PrintUsage(argv[0]);
return 2;
}
}