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:
@@ -9,6 +9,7 @@ set(KTE_VERSION "1.3.9")
|
|||||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||||
set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.")
|
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.")
|
set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
|
||||||
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
|
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")
|
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
|
||||||
@@ -101,14 +102,30 @@ set(FONT_SOURCES
|
|||||||
fonts/FontRegistry.cc
|
fonts/FontRegistry.cc
|
||||||
)
|
)
|
||||||
|
|
||||||
set(GUI_SOURCES
|
if (BUILD_GUI)
|
||||||
${FONT_SOURCES}
|
set(GUI_SOURCES
|
||||||
GUIConfig.cc
|
GUIConfig.cc
|
||||||
GUIRenderer.cc
|
)
|
||||||
GUIInputHandler.cc
|
if (KTE_USE_QT)
|
||||||
GUIFrontend.cc
|
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
|
set(COMMON_SOURCES
|
||||||
GapBuffer.cc
|
GapBuffer.cc
|
||||||
@@ -222,14 +239,29 @@ set(COMMON_HEADERS
|
|||||||
${SYNTAX_HEADERS}
|
${SYNTAX_HEADERS}
|
||||||
)
|
)
|
||||||
|
|
||||||
set(GUI_HEADERS
|
if (BUILD_GUI)
|
||||||
${THEME_HEADERS}
|
set(GUI_HEADERS
|
||||||
${FONT_HEADERS}
|
GUIConfig.h
|
||||||
GUIConfig.h
|
)
|
||||||
GUIRenderer.h
|
|
||||||
GUIInputHandler.h
|
if (KTE_USE_QT)
|
||||||
GUIFrontend.h
|
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
|
# kte (terminal-first) executable
|
||||||
add_executable(kte
|
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})
|
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)
|
if (KTE_UNDO_DEBUG)
|
||||||
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
|
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
|
||||||
endif ()
|
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
|
# On macOS, build kge as a proper .app bundle
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
|
|||||||
259
Command.cc
259
Command.cc
@@ -18,11 +18,31 @@
|
|||||||
#include "syntax/HighlighterEngine.h"
|
#include "syntax/HighlighterEngine.h"
|
||||||
#include "syntax/CppHighlighter.h"
|
#include "syntax/CppHighlighter.h"
|
||||||
#ifdef KTE_BUILD_GUI
|
#ifdef KTE_BUILD_GUI
|
||||||
#include "GUITheme.h"
|
# include "GUITheme.h"
|
||||||
#include "fonts/FontRegistry.h"
|
# if !defined(KTE_USE_QT)
|
||||||
#include "imgui.h"
|
# include "fonts/FontRegistry.h"
|
||||||
|
# include "imgui.h"
|
||||||
|
# endif
|
||||||
|
# if defined(KTE_USE_QT)
|
||||||
|
# include <QFontDatabase>
|
||||||
|
# include <QStringList>
|
||||||
|
# endif
|
||||||
#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
|
// 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
|
// window based on the editor's current dimensions. The bottom row is reserved
|
||||||
@@ -104,24 +124,24 @@ static bool
|
|||||||
is_mutating_command(CommandId id)
|
is_mutating_command(CommandId id)
|
||||||
{
|
{
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case CommandId::InsertText:
|
case CommandId::InsertText:
|
||||||
case CommandId::Newline:
|
case CommandId::Newline:
|
||||||
case CommandId::Backspace:
|
case CommandId::Backspace:
|
||||||
case CommandId::DeleteChar:
|
case CommandId::DeleteChar:
|
||||||
case CommandId::KillToEOL:
|
case CommandId::KillToEOL:
|
||||||
case CommandId::KillLine:
|
case CommandId::KillLine:
|
||||||
case CommandId::Yank:
|
case CommandId::Yank:
|
||||||
case CommandId::DeleteWordPrev:
|
case CommandId::DeleteWordPrev:
|
||||||
case CommandId::DeleteWordNext:
|
case CommandId::DeleteWordNext:
|
||||||
case CommandId::IndentRegion:
|
case CommandId::IndentRegion:
|
||||||
case CommandId::UnindentRegion:
|
case CommandId::UnindentRegion:
|
||||||
case CommandId::ReflowParagraph:
|
case CommandId::ReflowParagraph:
|
||||||
case CommandId::KillRegion:
|
case CommandId::KillRegion:
|
||||||
case CommandId::Undo:
|
case CommandId::Undo:
|
||||||
case CommandId::Redo:
|
case CommandId::Redo:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -914,8 +934,8 @@ cmd_set_option(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// GUI theme cycling commands (available in GUI build; show message otherwise)
|
// GUI theme cycling commands (available in GUI build; ImGui-only for now)
|
||||||
#ifdef KTE_BUILD_GUI
|
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
|
||||||
static bool
|
static bool
|
||||||
cmd_theme_next(CommandContext &ctx)
|
cmd_theme_next(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -951,7 +971,7 @@ cmd_theme_prev(CommandContext &ctx)
|
|||||||
|
|
||||||
|
|
||||||
// Theme set by name command
|
// Theme set by name command
|
||||||
#ifdef KTE_BUILD_GUI
|
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
|
||||||
static bool
|
static bool
|
||||||
cmd_theme_set_by_name(const CommandContext &ctx)
|
cmd_theme_set_by_name(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -995,15 +1015,41 @@ cmd_theme_set_by_name(const CommandContext &ctx)
|
|||||||
static bool
|
static bool
|
||||||
cmd_theme_set_by_name(CommandContext &ctx)
|
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;
|
(void) ctx;
|
||||||
// No-op in terminal build
|
// No-op in terminal build
|
||||||
return true;
|
return true;
|
||||||
|
# endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
// Font set by name (GUI)
|
// Font set by name (GUI)
|
||||||
#ifdef KTE_BUILD_GUI
|
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
|
||||||
static bool
|
static bool
|
||||||
cmd_font_set_by_name(const CommandContext &ctx)
|
cmd_font_set_by_name(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -1056,14 +1102,38 @@ cmd_font_set_by_name(const CommandContext &ctx)
|
|||||||
static bool
|
static bool
|
||||||
cmd_font_set_by_name(CommandContext &ctx)
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
// Font size set (GUI)
|
// Font size set (GUI, ImGui-only for now)
|
||||||
#ifdef KTE_BUILD_GUI
|
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
|
||||||
static bool
|
static bool
|
||||||
cmd_font_set_size(const CommandContext &ctx)
|
cmd_font_set_size(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -1122,14 +1192,45 @@ cmd_font_set_size(const CommandContext &ctx)
|
|||||||
static bool
|
static bool
|
||||||
cmd_font_set_size(CommandContext &ctx)
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
// Background set command (GUI)
|
// Background set command (GUI, ImGui-only for now)
|
||||||
#ifdef KTE_BUILD_GUI
|
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
|
||||||
static bool
|
static bool
|
||||||
cmd_background_set(const CommandContext &ctx)
|
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
|
static bool
|
||||||
cmd_jump_to_line_start(const CommandContext &ctx)
|
cmd_jump_to_line_start(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -1599,7 +1714,8 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
std::string argprefix = text.substr(sp + 1);
|
std::string argprefix = text.substr(sp + 1);
|
||||||
// Only special-case argument completion for certain commands
|
// Only special-case argument completion for certain commands
|
||||||
if (cmd == "theme") {
|
if (cmd == "theme") {
|
||||||
#ifdef KTE_BUILD_GUI
|
#if defined(KTE_BUILD_GUI)
|
||||||
|
# if !defined(KTE_USE_QT)
|
||||||
std::vector<std::string> cands;
|
std::vector<std::string> cands;
|
||||||
const auto ® = kte::ThemeRegistry();
|
const auto ® = kte::ThemeRegistry();
|
||||||
for (const auto &t: reg) {
|
for (const auto &t: reg) {
|
||||||
@@ -1607,6 +1723,67 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
if (argprefix.empty() || n.rfind(argprefix, 0) == 0)
|
if (argprefix.empty() || n.rfind(argprefix, 0) == 0)
|
||||||
cands.push_back(n);
|
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()) {
|
if (cands.empty()) {
|
||||||
// no change
|
// no change
|
||||||
} else if (cands.size() == 1) {
|
} else if (cands.size() == 1) {
|
||||||
@@ -1628,7 +1805,7 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
||||||
return true;
|
return true;
|
||||||
#else
|
#else
|
||||||
(void) argprefix; // no completion in non-GUI build
|
(void) argprefix;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
// default: no special arg completion
|
// default: no special arg completion
|
||||||
@@ -3770,11 +3947,11 @@ cmd_reflow_paragraph(CommandContext &ctx)
|
|||||||
if (!buf)
|
if (!buf)
|
||||||
return false;
|
return false;
|
||||||
ensure_at_least_one_line(*buf);
|
ensure_at_least_one_line(*buf);
|
||||||
auto &rows = buf->Rows();
|
auto &rows = buf->Rows();
|
||||||
std::size_t y = buf->Cury();
|
std::size_t y = buf->Cury();
|
||||||
// Treat a universal-argument count of 1 as "no width specified".
|
// Treat a universal-argument count of 1 as "no width specified".
|
||||||
// Editor::UArgGet() returns 1 when no explicit count was provided.
|
// 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;
|
std::size_t para_start = y;
|
||||||
while (para_start > 0 && !rows[para_start - 1].empty())
|
while (para_start > 0 && !rows[para_start - 1].empty())
|
||||||
--para_start;
|
--para_start;
|
||||||
@@ -4201,9 +4378,9 @@ InstallDefaultCommands()
|
|||||||
CommandRegistry::Register(
|
CommandRegistry::Register(
|
||||||
{CommandId::UnindentRegion, "unindent-region", "Unindent region", cmd_unindent_region});
|
{CommandId::UnindentRegion, "unindent-region", "Unindent region", cmd_unindent_region});
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::ReflowParagraph, "reflow-paragraph",
|
CommandId::ReflowParagraph, "reflow-paragraph",
|
||||||
"Reflow paragraph to column width", cmd_reflow_paragraph
|
"Reflow paragraph to column width", cmd_reflow_paragraph
|
||||||
});
|
});
|
||||||
// Read-only
|
// Read-only
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::ToggleReadOnly, "toggle-read-only", "Toggle buffer read-only", cmd_toggle_read_only
|
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",
|
CommandId::VisualFilePickerToggle, "file-picker-toggle", "Toggle visual file picker",
|
||||||
cmd_visual_file_picker_toggle, false, false
|
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
|
// Working directory
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::ShowWorkingDirectory, "show-working-directory", "Show current working directory",
|
CommandId::ShowWorkingDirectory, "show-working-directory", "Show current working directory",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ enum class CommandId {
|
|||||||
SearchReplace, // begin search & replace (two-step prompt)
|
SearchReplace, // begin search & replace (two-step prompt)
|
||||||
OpenFileStart, // begin open-file prompt
|
OpenFileStart, // begin open-file prompt
|
||||||
VisualFilePickerToggle,
|
VisualFilePickerToggle,
|
||||||
|
// GUI-only: toggle/show a visual font selector dialog
|
||||||
|
VisualFontPickerToggle,
|
||||||
// Buffers
|
// Buffers
|
||||||
BufferSwitchStart, // begin buffer switch prompt
|
BufferSwitchStart, // begin buffer switch prompt
|
||||||
BufferClose,
|
BufferClose,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
304
GUITheme.h
304
GUITheme.h
@@ -1,11 +1,307 @@
|
|||||||
// GUITheme.h — ImGui theming helpers and background mode
|
// GUITheme.h — theming helpers and background mode
|
||||||
#pragma once
|
#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 <imgui.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <cstddef>
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
|
||||||
@@ -644,4 +940,6 @@ SyntaxInk(const TokenKind k)
|
|||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace kte
|
} // namespace kte
|
||||||
|
|
||||||
|
#endif // KTE_USE_QT
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
#include <backends/imgui_impl_opengl3.h>
|
#include <backends/imgui_impl_opengl3.h>
|
||||||
#include <backends/imgui_impl_sdl2.h>
|
#include <backends/imgui_impl_sdl2.h>
|
||||||
|
|
||||||
#include "GUIFrontend.h"
|
#include "ImGuiFrontend.h"
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
#include "GUIConfig.h"
|
#include "GUIConfig.h"
|
||||||
@@ -224,17 +224,17 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
while (SDL_PollEvent(&e)) {
|
while (SDL_PollEvent(&e)) {
|
||||||
ImGui_ImplSDL2_ProcessEvent(&e);
|
ImGui_ImplSDL2_ProcessEvent(&e);
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case SDL_QUIT:
|
case SDL_QUIT:
|
||||||
running = false;
|
running = false;
|
||||||
break;
|
break;
|
||||||
case SDL_WINDOWEVENT:
|
case SDL_WINDOWEVENT:
|
||||||
if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||||
width_ = e.window.data1;
|
width_ = e.window.data1;
|
||||||
height_ = e.window.data2;
|
height_ = e.window.data2;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Map input to commands
|
// Map input to commands
|
||||||
input_.ProcessSDLEvent(e);
|
input_.ProcessSDLEvent(e);
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
* GUIFrontend - couples GUIInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
|
* GUIFrontend - couples ImGuiInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "Frontend.h"
|
#include "Frontend.h"
|
||||||
#include "GUIConfig.h"
|
#include "GUIConfig.h"
|
||||||
#include "GUIInputHandler.h"
|
#include "ImGuiInputHandler.h"
|
||||||
#include "GUIRenderer.h"
|
#include "ImGuiRenderer.h"
|
||||||
|
|
||||||
|
|
||||||
struct SDL_Window;
|
struct SDL_Window;
|
||||||
@@ -27,8 +27,8 @@ private:
|
|||||||
static bool LoadGuiFont_(const char *path, float size_px);
|
static bool LoadGuiFont_(const char *path, float size_px);
|
||||||
|
|
||||||
GUIConfig config_{};
|
GUIConfig config_{};
|
||||||
GUIInputHandler input_{};
|
ImGuiInputHandler input_{};
|
||||||
GUIRenderer renderer_{};
|
ImGuiRenderer renderer_{};
|
||||||
SDL_Window *window_ = nullptr;
|
SDL_Window *window_ = nullptr;
|
||||||
SDL_GLContext gl_ctx_ = nullptr;
|
SDL_GLContext gl_ctx_ = nullptr;
|
||||||
int width_ = 1280;
|
int width_ = 1280;
|
||||||
601
ImGuiInputHandler.cc
Normal file
601
ImGuiInputHandler.cc
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* GUIInputHandler - ImGui/SDL2-based input mapping for GUI mode
|
* ImGuiInputHandler - ImGui/SDL2-based input mapping for GUI mode
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -10,11 +10,11 @@
|
|||||||
|
|
||||||
union SDL_Event; // fwd decl to avoid including SDL here (SDL defines SDL_Event as a union)
|
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:
|
public:
|
||||||
GUIInputHandler() = default;
|
ImGuiInputHandler() = default;
|
||||||
|
|
||||||
~GUIInputHandler() override = default;
|
~ImGuiInputHandler() override = default;
|
||||||
|
|
||||||
|
|
||||||
void Attach(Editor *ed) override
|
void Attach(Editor *ed) override
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
#include "GUIRenderer.h"
|
#include "ImGuiRenderer.h"
|
||||||
#include "Highlight.h"
|
#include "Highlight.h"
|
||||||
#include "GUITheme.h"
|
#include "GUITheme.h"
|
||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
GUIRenderer::Draw(Editor &ed)
|
ImGuiRenderer::Draw(Editor &ed)
|
||||||
{
|
{
|
||||||
// Make the editor window occupy the entire GUI container/viewport
|
// Make the editor window occupy the entire GUI container/viewport
|
||||||
ImGuiViewport *vp = ImGui::GetMainViewport();
|
ImGuiViewport *vp = ImGui::GetMainViewport();
|
||||||
@@ -461,10 +461,10 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
float cursor_px = 0.0f;
|
float cursor_px = 0.0f;
|
||||||
if (rx_viewport > 0 && coloffs_now < expanded.size()) {
|
if (rx_viewport > 0 && coloffs_now < expanded.size()) {
|
||||||
std::size_t start = coloffs_now;
|
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
|
// Measure substring width in pixels
|
||||||
ImVec2 sz = ImGui::CalcTextSize(expanded.c_str() + start,
|
ImVec2 sz = ImGui::CalcTextSize(expanded.c_str() + start,
|
||||||
expanded.c_str() + end);
|
expanded.c_str() + end);
|
||||||
cursor_px = sz.x;
|
cursor_px = sz.x;
|
||||||
}
|
}
|
||||||
ImVec2 p0 = ImVec2(line_pos.x + cursor_px, line_pos.y);
|
ImVec2 p0 = ImVec2(line_pos.x + cursor_px, line_pos.y);
|
||||||
14
ImGuiRenderer.h
Normal file
14
ImGuiRenderer.h
Normal 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
988
QtFrontend.cc
Normal 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
36
QtFrontend.h
Normal 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
538
QtInputHandler.cc
Normal 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
40
QtInputHandler.h
Normal 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
76
QtRenderer.cc
Normal 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
27
QtRenderer.h
Normal 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
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
installShellFiles,
|
installShellFiles,
|
||||||
|
|
||||||
graphical ? false,
|
graphical ? false,
|
||||||
|
graphical-qt ? false,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -34,10 +35,15 @@ stdenv.mkDerivation {
|
|||||||
SDL2
|
SDL2
|
||||||
libGL
|
libGL
|
||||||
xorg.libX11
|
xorg.libX11
|
||||||
|
]
|
||||||
|
++ lib.optionals graphical-qt [
|
||||||
|
qt5Full
|
||||||
|
qtcreator ## not sure if this is actually needed
|
||||||
];
|
];
|
||||||
|
|
||||||
cmakeFlags = [
|
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"
|
"-DCMAKE_BUILD_TYPE=Debug"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -113,4 +113,12 @@ C++ projects than GTK (which is C-based, though `gtkmm` exists).
|
|||||||
When the core calls `DrawText()`, `QtRenderer` should queue that
|
When the core calls `DrawText()`, `QtRenderer` should queue that
|
||||||
command or draw directly to the widget's paint buffer.
|
command or draw directly to the widget's paint buffer.
|
||||||
4. **Refactor `main.cc`** to instantiate `QApplication` instead of the
|
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).
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
full = kge;
|
full = kge;
|
||||||
kte = (pkgsFor system).callPackage ./default.nix { graphical = false; };
|
kte = (pkgsFor system).callPackage ./default.nix { graphical = false; };
|
||||||
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; };
|
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; };
|
||||||
|
qt = (pkgsFor system).callPackage ./default.nix { graphical-qt = true; }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
58
main.cc
58
main.cc
@@ -21,7 +21,11 @@
|
|||||||
#include "TerminalFrontend.h"
|
#include "TerminalFrontend.h"
|
||||||
|
|
||||||
#if defined(KTE_BUILD_GUI)
|
#if defined(KTE_BUILD_GUI)
|
||||||
#include "GUIFrontend.h"
|
#if defined(KTE_USE_QT)
|
||||||
|
#include "QtFrontend.h"
|
||||||
|
#else
|
||||||
|
#include "ImGuiFrontend.h"
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
@@ -131,33 +135,33 @@ main(int argc, const char *argv[])
|
|||||||
unsigned stress_seconds = 0;
|
unsigned stress_seconds = 0;
|
||||||
while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "gthV", long_opts, &long_index)) != -1) {
|
while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "gthV", long_opts, &long_index)) != -1) {
|
||||||
switch (opt) {
|
switch (opt) {
|
||||||
case 'g':
|
case 'g':
|
||||||
req_gui = true;
|
req_gui = true;
|
||||||
break;
|
break;
|
||||||
case 't':
|
case 't':
|
||||||
req_term = true;
|
req_term = true;
|
||||||
break;
|
break;
|
||||||
case 'h':
|
case 'h':
|
||||||
show_help = true;
|
show_help = true;
|
||||||
break;
|
break;
|
||||||
case 'V':
|
case 'V':
|
||||||
show_version = true;
|
show_version = true;
|
||||||
break;
|
break;
|
||||||
case 1000: {
|
case 1000: {
|
||||||
stress_seconds = 5; // default
|
stress_seconds = 5; // default
|
||||||
if (optarg && *optarg) {
|
if (optarg && *optarg) {
|
||||||
try {
|
try {
|
||||||
unsigned v = static_cast<unsigned>(std::stoul(optarg));
|
unsigned v = static_cast<unsigned>(std::stoul(optarg));
|
||||||
if (v > 0 && v < 36000)
|
if (v > 0 && v < 36000)
|
||||||
stress_seconds = v;
|
stress_seconds = v;
|
||||||
} catch (...) {}
|
} catch (...) {}
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case '?':
|
break;
|
||||||
default:
|
}
|
||||||
PrintUsage(argv[0]);
|
case '?':
|
||||||
return 2;
|
default:
|
||||||
|
PrintUsage(argv[0]);
|
||||||
|
return 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user