#include "QtFrontend.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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( 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) { // Materialize the Buffer::Line into a std::string for // regex/iterator usage and general string ops. const std::string line = static_cast(lines[i]); const int y = viewport.y() + static_cast(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 > 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(m. position()); std::size_t ex = sx + static_cast(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(( (rx_s > coloffs ? rx_s - coloffs : 0) * ch_w)); int vx1 = viewport.x() + static_cast(( (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((rx_s > coloffs ? rx_s - coloffs : 0) * ch_w); int vx1 = viewport.x() + static_cast( (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(i), buf->Version()); struct SSpan { std::size_t s; std::size_t e; kte::TokenKind k; }; std::vector 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::max( 0, std::min(s_raw, (int) line_len))); std::size_t e = static_cast(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(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( 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(coloffs) < expanded.size()) { const char *start = expanded.c_str() + static_cast(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( 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(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(idx1)); left += QStringLiteral("/"); left += QString::number(static_cast(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 " L" unsigned long lcount = static_cast(buf->Rows().size()); left += QStringLiteral(" "); left += QString::number(static_cast(lcount)); left += QStringLiteral("L"); } // Build right segment: cursor and mark QString right; if (buf) { int row1 = static_cast(buf->Cury()) + 1; int col1 = static_cast(buf->Curx()) + 1; bool have_mark = buf->MarkSet(); int mrow1 = have_mark ? static_cast(buf->MarkCury()) + 1 : 0; int mcol1 = have_mark ? static_cast(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(1, (avail_h / line_h)); std::size_t cols = std::max(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(pixel.y()) / std::max(1, line_h); h_cols_delta = -static_cast(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(angle.y()) / 120.0 * 3.0; // For horizontal wheels, each notch scrolls 8 columns h_cols_delta = -static_cast(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(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(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(new_rowoffs) + d_rows; if (nr < 0) nr = 0; const auto nrows = static_cast(buf->Rows().size()); if (nr > std::max(0L, nrows - 1)) nr = std::max(0L, nrows - 1); new_rowoffs = static_cast(nr); } if (d_cols != 0) { long nc = static_cast(new_coloffs) + d_cols; if (nc < 0) nc = 0; new_coloffs = static_cast(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(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(window_)) { mw->SetFontFamilyAndSize(family, px_size); } // Track current font in globals for command/status queries kte::gCurrentFontFamily = family.toStdString(); kte::gCurrentFontSize = static_cast(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(1, (avail_h / line_h) + 1); // + status std::size_t cols = std::max(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(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(window_)) { mw->SetFontFamilyAndSize(target_family, target_px); } // Update globals kte::gCurrentFontFamily = target_family.toStdString(); kte::gCurrentFontSize = static_cast(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(1, (avail_h / line_h) + 1); // + status std::size_t cols = std::max(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(window_)) { if (mw->WasClosed()) { running = false; } } } void GUIFrontend::Shutdown() { if (window_) { window_->close(); delete window_; window_ = nullptr; } if (app_) { delete app_; app_ = nullptr; } }