From b4ca5e1912c09c55ac66477effb43fa868cb9a8c Mon Sep 17 00:00:00 2001 From: Daniel Dada Date: Wed, 17 Dec 2025 11:45:46 +0300 Subject: [PATCH] feat: autocomplete in template editor --- src/core/version.hpp | 2 +- src/gui/color_scheme_editor.cpp | 84 +++-- src/gui/color_table_renderer.cpp | 126 ++++++-- src/gui/color_table_renderer.hpp | 5 + src/gui/template_editor.cpp | 509 ++++++++++++++++++++++++++++--- src/gui/template_editor.hpp | 20 ++ 6 files changed, 665 insertions(+), 81 deletions(-) diff --git a/src/core/version.hpp b/src/core/version.hpp index 8485c30..c59757a 100644 --- a/src/core/version.hpp +++ b/src/core/version.hpp @@ -6,7 +6,7 @@ namespace clrsync::core { -const std::string GIT_SEMVER = "0.1.4+git.g2813a8b"; +const std::string GIT_SEMVER = "0.1.4+git.g899a5d5"; const std::string version_string(); } // namespace clrsync::core diff --git a/src/gui/color_scheme_editor.cpp b/src/gui/color_scheme_editor.cpp index 0003650..3c31068 100644 --- a/src/gui/color_scheme_editor.cpp +++ b/src/gui/color_scheme_editor.cpp @@ -65,11 +65,15 @@ void color_scheme_editor::render_controls() const auto ¤t = m_controller.current_palette(); const auto &palettes = m_controller.palettes(); - const float avail_width = ImGui::GetContentRegionAvail().x; + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(6, 8)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 5)); - ImGui::Text("Color Scheme:"); + // Palette selector + ImGui::AlignTextToFramePadding(); + ImGui::Text("Palette:"); ImGui::SameLine(); - ImGui::SetNextItemWidth(std::min(200.0f, avail_width * 0.3f)); + + ImGui::SetNextItemWidth(200.0f); if (ImGui::BeginCombo("##scheme", current.name().c_str())) { for (const auto &name : palettes | std::views::keys) @@ -85,35 +89,51 @@ void color_scheme_editor::render_controls() } ImGui::EndCombo(); } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Select a color palette to edit"); ImGui::SameLine(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8); + // Action buttons static char new_palette_name_buf[128] = ""; - if (ImGui::Button("New")) + if (ImGui::Button(" + New ")) { new_palette_name_buf[0] = 0; ImGui::OpenPopup("New Palette"); } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Create a new palette"); if (ImGui::BeginPopupModal("New Palette", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("New palette name:"); - ImGui::InputText("##new_palette_input", new_palette_name_buf, - IM_ARRAYSIZE(new_palette_name_buf)); + ImGui::Text("Enter a name for the new palette:"); + ImGui::Spacing(); + + ImGui::SetNextItemWidth(250); + ImGui::InputTextWithHint("##new_palette_input", "Palette name...", + new_palette_name_buf, IM_ARRAYSIZE(new_palette_name_buf)); + ImGui::Spacing(); ImGui::Separator(); + ImGui::Spacing(); + bool can_create = strlen(new_palette_name_buf) > 0; + + if (!can_create) + ImGui::BeginDisabled(); + if (ImGui::Button("Create", ImVec2(120, 0))) { - if (strlen(new_palette_name_buf) > 0) - { - m_controller.create_palette(new_palette_name_buf); - m_controller.select_palette(new_palette_name_buf); - apply_themes(); - new_palette_name_buf[0] = 0; - } + m_controller.create_palette(new_palette_name_buf); + m_controller.select_palette(new_palette_name_buf); + apply_themes(); + new_palette_name_buf[0] = 0; ImGui::CloseCurrentPopup(); } + + if (!can_create) + ImGui::EndDisabled(); ImGui::SameLine(); @@ -127,19 +147,23 @@ void color_scheme_editor::render_controls() } ImGui::SameLine(); - if (ImGui::Button("Save")) + if (ImGui::Button(" Save ")) { m_controller.save_current_palette(); } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Save current palette to file"); ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.6f, 0.1f, 0.1f, 1.0f)); - if (ImGui::Button("Delete")) + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f)); + if (ImGui::Button(" Delete ")) { m_show_delete_confirmation = true; } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Delete current palette"); ImGui::PopStyleColor(3); if (m_show_delete_confirmation) @@ -150,16 +174,24 @@ void color_scheme_editor::render_controls() if (ImGui::BeginPopupModal("Delete Palette?", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("Are you sure you want to delete '%s'?", current.name().c_str()); + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.4f, 1.0f), + "Are you sure you want to delete '%s'?", current.name().c_str()); ImGui::Text("This action cannot be undone."); + + ImGui::Spacing(); ImGui::Separator(); + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f)); if (ImGui::Button("Delete", ImVec2(120, 0))) { m_controller.delete_current_palette(); apply_themes(); ImGui::CloseCurrentPopup(); } + ImGui::PopStyleColor(2); + ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(120, 0))) { @@ -169,8 +201,18 @@ void color_scheme_editor::render_controls() } ImGui::SameLine(); - if (ImGui::Button("Apply")) + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 16); + + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.5f, 0.7f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.3f, 0.6f, 0.8f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.15f, 0.4f, 0.6f, 1.0f)); + if (ImGui::Button(" Apply Theme ")) { m_controller.apply_current_theme(); } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Apply current palette to all enabled templates"); + ImGui::PopStyleColor(3); + + ImGui::PopStyleVar(2); } diff --git a/src/gui/color_table_renderer.cpp b/src/gui/color_table_renderer.cpp index 44f0913..35d336c 100644 --- a/src/gui/color_table_renderer.cpp +++ b/src/gui/color_table_renderer.cpp @@ -1,24 +1,53 @@ #include "color_table_renderer.hpp" #include "imgui.h" +#include +#include #include +bool color_table_renderer::matches_filter(const std::string& name) const +{ + if (m_filter_text[0] == '\0') + return true; + + std::string filter_lower = m_filter_text; + std::string name_lower = name; + + std::transform(filter_lower.begin(), filter_lower.end(), filter_lower.begin(), + [](unsigned char c) { return std::tolower(c); }); + std::transform(name_lower.begin(), name_lower.end(), name_lower.begin(), + [](unsigned char c) { return std::tolower(c); }); + + return name_lower.find(filter_lower) != std::string::npos; +} + void color_table_renderer::render_color_row(const std::string &name, const clrsync::core::palette& current, palette_controller& controller, const OnColorChangedCallback& on_changed) { + if (!matches_filter(name)) + return; + const clrsync::core::color &col = current.get_color(name); ImGui::TableNextRow(); ImGui::TableSetColumnIndex(0); const float key_col_width = ImGui::GetContentRegionAvail().x; + + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.7f, 0.85f, 1.0f, 1.0f)); const bool copied = ImGui::Selectable(name.c_str(), false, 0, ImVec2(key_col_width, 0.0f)); + ImGui::PopStyleColor(); + if (ImGui::IsItemHovered()) + { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::SetTooltip("Click to copy: {%s.hex}", name.c_str()); + } if (copied) { - ImGui::SetClipboardText(name.c_str()); + std::string template_var = "{" + name + ".hex}"; + ImGui::SetClipboardText(template_var.c_str()); } ImGui::TableSetColumnIndex(1); @@ -54,7 +83,7 @@ void color_table_renderer::render_color_row(const std::string &name, ImGui::SetNextItemWidth(-FLT_MIN); if (ImGui::ColorEdit4(("##color_" + name).c_str(), c, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | - ImGuiColorEditFlags_AlphaBar)) + ImGuiColorEditFlags_AlphaBar | ImGuiColorEditFlags_AlphaPreviewHalf)) { uint32_t r = (uint32_t)(c[0] * 255.0f); uint32_t g = (uint32_t)(c[1] * 255.0f); @@ -76,48 +105,101 @@ void color_table_renderer::render(const clrsync::core::palette& current, { if (current.colors().empty()) { - ImGui::Text("No palette loaded"); + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.4f, 1.0f), "No palette loaded"); return; } - ImGui::Text("Color Variables"); - ImGui::Separator(); - - auto draw_table = [&](const char *title, const std::vector &keys) { - ImGui::TextUnformatted(title); - - if (ImGui::BeginTable(title, 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8, 6)); + + ImGui::Text("Filter:"); + ImGui::SameLine(); + ImGui::SetNextItemWidth(200); + bool filter_changed = ImGui::InputTextWithHint("##color_filter", "Search colors...", + m_filter_text, sizeof(m_filter_text)); + + if (m_filter_text[0] != '\0') + { + ImGui::SameLine(); + if (ImGui::SmallButton("X")) { - ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 160.0f); - ImGui::TableSetupColumn("HEX", ImGuiTableColumnFlags_WidthFixed, 90.0f); - ImGui::TableSetupColumn("Preview", ImGuiTableColumnFlags_WidthStretch); - ImGui::TableHeadersRow(); + m_filter_text[0] = '\0'; + filter_changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Clear filter"); + } + + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted("Click on a color name to copy its template variable"); + ImGui::TextUnformatted("Example: clicking 'background' copies {background.hex}"); + ImGui::EndTooltip(); + } + + ImGui::PopStyleVar(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); - for (auto *k : keys) - render_color_row(k, current, controller, on_changed); + auto draw_table = [&](const char *title, const char* id, const std::vector &keys) { + bool has_matches = false; + for (auto *k : keys) + { + if (matches_filter(k)) + { + has_matches = true; + break; + } + } + + if (!has_matches) + return; + + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.9f, 0.9f, 0.5f, 1.0f)); + bool header_open = ImGui::TreeNodeEx(title, ImGuiTreeNodeFlags_DefaultOpen | ImGuiTreeNodeFlags_SpanAvailWidth); + ImGui::PopStyleColor(); + + if (header_open) + { + if (ImGui::BeginTable(id, 3, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + { + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 160.0f); + ImGui::TableSetupColumn("HEX", ImGuiTableColumnFlags_WidthFixed, 95.0f); + ImGui::TableSetupColumn("Color", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); - ImGui::EndTable(); + for (auto *k : keys) + render_color_row(k, current, controller, on_changed); + + ImGui::EndTable(); + } + ImGui::TreePop(); } ImGui::Spacing(); }; - draw_table("General UI", {"background", "on_background", "surface", "on_surface", + draw_table("General UI", "##general_ui", {"background", "on_background", "surface", "on_surface", "surface_variant", "on_surface_variant", "foreground", "cursor", "accent"}); - draw_table("Borders", {"border_focused", "border"}); + draw_table("Borders", "##borders", {"border_focused", "border"}); - draw_table("Semantic Colors", {"success", "info", "warning", "error", + draw_table("Semantic Colors", "##semantic", {"success", "info", "warning", "error", "on_success", "on_info", "on_warning", "on_error"}); - draw_table("Editor", {"editor_background", "editor_command", "editor_comment", + draw_table("Editor", "##editor", {"editor_background", "editor_command", "editor_comment", "editor_disabled", "editor_emphasis", "editor_error", "editor_inactive", "editor_line_number", "editor_link", "editor_main", "editor_selected", "editor_selection_inactive", "editor_string", "editor_success", "editor_warning"}); - draw_table("Terminal (Base16)", {"base00", "base01", "base02", "base03", + draw_table("Terminal (Base16)", "##terminal", {"base00", "base01", "base02", "base03", "base04", "base05", "base06", "base07", "base08", "base09", "base0A", "base0B", "base0C", "base0D", "base0E", "base0F"}); diff --git a/src/gui/color_table_renderer.hpp b/src/gui/color_table_renderer.hpp index 6f6c144..e518db6 100644 --- a/src/gui/color_table_renderer.hpp +++ b/src/gui/color_table_renderer.hpp @@ -20,6 +20,11 @@ private: const clrsync::core::palette& palette, palette_controller& controller, const OnColorChangedCallback& on_changed); + + bool matches_filter(const std::string& name) const; + + char m_filter_text[128] = {0}; + bool m_show_only_modified{false}; }; #endif // CLRSYNC_GUI_COLOR_TABLE_RENDERER_HPP diff --git a/src/gui/template_editor.cpp b/src/gui/template_editor.cpp index 06aeee6..5876992 100644 --- a/src/gui/template_editor.cpp +++ b/src/gui/template_editor.cpp @@ -1,14 +1,33 @@ #include "template_editor.hpp" #include "core/config/config.hpp" #include "core/theme/theme_template.hpp" +#include "core/palette/color_keys.hpp" #include "core/utils.hpp" #include "imgui.h" +#include #include #include #include +namespace { + const std::vector COLOR_FORMATS = { + "hex", "hex_stripped", "hexa", "hexa_stripped", + "r", "g", "b", "a", + "rgb", "rgba", + "h", "s", "l", + "hsl", "hsla" + }; +} + template_editor::template_editor() : m_template_name("new_template") { + m_autocomplete_bg_color = ImVec4(0.12f, 0.12f, 0.15f, 0.98f); + m_autocomplete_border_color = ImVec4(0.4f, 0.4f, 0.45f, 1.0f); + m_autocomplete_selected_color = ImVec4(0.25f, 0.45f, 0.75f, 0.9f); + m_autocomplete_text_color = ImVec4(0.85f, 0.85f, 0.9f, 1.0f); + m_autocomplete_selected_text_color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + m_autocomplete_dim_text_color = ImVec4(0.6f, 0.6f, 0.7f, 1.0f); + TextEditor::LanguageDefinition lang; lang.mName = "Template"; @@ -96,6 +115,258 @@ void template_editor::apply_current_palette(const clrsync::core::palette &pal) get_color_u32("border_focused", "border"); m_editor.SetPalette(palette); + + // Update autocomplete colors from palette + auto convert_to_imvec4 = [&](const std::string &key, const std::string &fallback = "") -> ImVec4 { + auto it = colors.find(key); + if (it == colors.end() && !fallback.empty()) + { + it = colors.find(fallback); + } + + if (it != colors.end()) + { + const auto &col = it->second; + const uint32_t hex = col.hex(); + const float r = ((hex >> 24) & 0xFF) / 255.0f; + const float g = ((hex >> 16) & 0xFF) / 255.0f; + const float b = ((hex >> 8) & 0xFF) / 255.0f; + const float a = (hex & 0xFF) / 255.0f; + return ImVec4(r, g, b, a); + } + return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + }; + + m_autocomplete_bg_color = convert_to_imvec4("editor_background", "background"); + m_autocomplete_bg_color.w = 0.98f; + m_autocomplete_border_color = convert_to_imvec4("border", "editor_inactive"); + m_autocomplete_selected_color = convert_to_imvec4("editor_selected", "surface_variant"); + m_autocomplete_text_color = convert_to_imvec4("editor_main", "foreground"); + m_autocomplete_selected_text_color = convert_to_imvec4("foreground", "editor_main"); + m_autocomplete_dim_text_color = convert_to_imvec4("editor_comment", "editor_inactive"); +} + +void template_editor::update_autocomplete_suggestions() +{ + m_autocomplete_suggestions.clear(); + + auto cursor = m_editor.GetCursorPosition(); + std::string line = m_editor.GetCurrentLineText(); + int col = cursor.mColumn; + + // Check if inside '{' + int brace_pos = -1; + for (int i = col - 1; i >= 0; --i) + { + if (i < (int)line.length()) + { + if (line[i] == '{') + { + brace_pos = i; + break; + } + else if (line[i] == '}' || line[i] == ' ' || line[i] == '\t') + { + break; + } + } + } + + if (brace_pos < 0) + { + m_show_autocomplete = false; + m_autocomplete_dismissed = false; + return; + } + + if (m_autocomplete_dismissed) + { + bool should_reset_dismissal = false; + + if (cursor.mLine != m_dismiss_position.mLine || + brace_pos != m_dismiss_brace_pos || + abs(cursor.mColumn - m_dismiss_position.mColumn) > 3) + { + should_reset_dismissal = true; + } + + if (should_reset_dismissal) + { + m_autocomplete_dismissed = false; + } + else + { + m_show_autocomplete = false; + return; + } + } + + m_autocomplete_prefix = line.substr(brace_pos + 1, col - brace_pos - 1); + m_autocomplete_start_pos = TextEditor::Coordinates(cursor.mLine, brace_pos + 1); + + size_t dot_pos = m_autocomplete_prefix.find('.'); + + if (dot_pos != std::string::npos) + { + std::string color_key = m_autocomplete_prefix.substr(0, dot_pos); + std::string format_prefix = m_autocomplete_prefix.substr(dot_pos + 1); + + bool valid_key = false; + for (size_t i = 0; i < clrsync::core::NUM_COLOR_KEYS; ++i) + { + if (clrsync::core::COLOR_KEYS[i] == color_key) + { + valid_key = true; + break; + } + } + + if (valid_key) + { + for (const auto &fmt : COLOR_FORMATS) + { + if (format_prefix.empty() || + fmt.find(format_prefix) == 0 || + fmt.find(format_prefix) != std::string::npos) + { + m_autocomplete_suggestions.push_back(color_key + "." + fmt); + } + } + } + } + else + { + for (size_t i = 0; i < clrsync::core::NUM_COLOR_KEYS; ++i) + { + std::string key = clrsync::core::COLOR_KEYS[i]; + if (m_autocomplete_prefix.empty() || + key.find(m_autocomplete_prefix) == 0 || + key.find(m_autocomplete_prefix) != std::string::npos) + { + m_autocomplete_suggestions.push_back(key); + } + } + } + + std::sort(m_autocomplete_suggestions.begin(), m_autocomplete_suggestions.end(), + [this](const std::string &a, const std::string &b) { + bool a_prefix = a.find(m_autocomplete_prefix) == 0; + bool b_prefix = b.find(m_autocomplete_prefix) == 0; + if (a_prefix != b_prefix) + return a_prefix; + return a < b; + }); + + m_show_autocomplete = !m_autocomplete_suggestions.empty(); + if (m_show_autocomplete && m_autocomplete_selected >= (int)m_autocomplete_suggestions.size()) + { + m_autocomplete_selected = 0; + } +} + +void template_editor::render_autocomplete(const ImVec2& editor_pos) +{ + if (!m_show_autocomplete || m_autocomplete_suggestions.empty()) + return; + + float line_height = ImGui::GetTextLineHeightWithSpacing(); + float char_width = ImGui::GetFontSize() * 0.5f; + auto cursor = m_editor.GetCursorPosition(); + + const float line_number_width = 50.0f; + + ImVec2 popup_pos; + popup_pos.x = editor_pos.x + line_number_width + (m_autocomplete_start_pos.mColumn * char_width); + popup_pos.y = editor_pos.y + ((cursor.mLine + 1) * line_height); + + ImGui::SetNextWindowPos(popup_pos, ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(300, 0)); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_AlwaysAutoResize; + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6, 6)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 2)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, m_autocomplete_bg_color); + ImGui::PushStyleColor(ImGuiCol_Border, m_autocomplete_border_color); + + if (ImGui::Begin("##autocomplete", nullptr, flags)) + { + ImGui::PushStyleColor(ImGuiCol_Text, m_autocomplete_dim_text_color); + if (m_autocomplete_prefix.find('.') != std::string::npos) + ImGui::Text("Formats"); + else + ImGui::Text("Color Keys"); + ImGui::PopStyleColor(); + ImGui::Separator(); + + int max_items = std::min((int)m_autocomplete_suggestions.size(), 8); + + for (int i = 0; i < max_items; ++i) + { + const auto &suggestion = m_autocomplete_suggestions[i]; + bool is_selected = (i == m_autocomplete_selected); + + if (is_selected) + { + ImVec4 selected_hover = m_autocomplete_selected_color; + selected_hover.w = std::min(selected_hover.w + 0.1f, 1.0f); + ImGui::PushStyleColor(ImGuiCol_Header, m_autocomplete_selected_color); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, selected_hover); + ImGui::PushStyleColor(ImGuiCol_Text, m_autocomplete_selected_text_color); + } + else + { + ImVec4 normal_bg = m_autocomplete_bg_color; + normal_bg.w = 0.5f; + ImVec4 hover_bg = m_autocomplete_selected_color; + hover_bg.w = 0.3f; + ImGui::PushStyleColor(ImGuiCol_Header, normal_bg); + ImGui::PushStyleColor(ImGuiCol_HeaderHovered, hover_bg); + ImGui::PushStyleColor(ImGuiCol_Text, m_autocomplete_text_color); + } + + std::string display_text = " " + suggestion; + + if (ImGui::Selectable(display_text.c_str(), is_selected, + ImGuiSelectableFlags_None, ImVec2(0, 0))) + { + auto start = m_autocomplete_start_pos; + auto end = m_editor.GetCursorPosition(); + m_editor.SetSelection(start, end); + m_editor.Delete(); + m_editor.InsertText(suggestion + "}"); + m_show_autocomplete = false; + m_autocomplete_dismissed = false; + } + + ImGui::PopStyleColor(3); + + if (is_selected && ImGui::IsWindowAppearing()) + { + ImGui::SetScrollHereY(); + } + } + + if (m_autocomplete_suggestions.size() > 8) + { + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, m_autocomplete_dim_text_color); + ImGui::Text(" +%d more", (int)m_autocomplete_suggestions.size() - 8); + ImGui::PopStyleColor(); + } + + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, m_autocomplete_dim_text_color); + ImGui::Text(" Tab/Enter: accept | Esc: dismiss"); + ImGui::PopStyleColor(); + } + ImGui::End(); + + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(3); } void template_editor::render() @@ -158,10 +429,14 @@ void template_editor::render() void template_editor::render_controls() { - if (ImGui::Button("New Template")) + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 8)); + + if (ImGui::Button(" + New ")) { new_template(); } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Create a new template"); if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) && ImGui::IsKeyDown(ImGuiKey_LeftCtrl) && ImGui::IsKeyPressed(ImGuiKey_S)) @@ -170,28 +445,63 @@ void template_editor::render_controls() } ImGui::SameLine(); - if (ImGui::Button("Save")) + if (ImGui::Button(" Save ")) { save_template(); } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Save template (Ctrl+S)"); if (m_is_editing_existing) { ImGui::SameLine(); - ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.6f, 0.1f, 0.1f, 1.0f)); - if (ImGui::Button("Delete")) + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.7f, 0.2f, 0.2f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.5f, 0.1f, 0.1f, 1.0f)); + if (ImGui::Button(" Delete ")) { delete_template(); } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Delete this template"); ImGui::PopStyleColor(3); } ImGui::SameLine(); - ImGui::Text("Template Name:"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(150.0f); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 10); + + bool enabled_changed = false; + if (m_enabled) + { + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.2f, 0.5f, 0.2f, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.3f, 0.6f, 0.3f, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.4f, 0.9f, 0.4f, 1.0f)); + } + else + { + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.4f, 0.2f, 0.2f, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, ImVec4(0.5f, 0.3f, 0.3f, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.9f, 0.4f, 0.4f, 1.0f)); + } + + enabled_changed = ImGui::Checkbox("Enabled", &m_enabled); + ImGui::PopStyleColor(3); + + if (enabled_changed && m_is_editing_existing) + { + m_template_controller.set_template_enabled(m_template_name, m_enabled); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Enable/disable this template for theme application"); + + ImGui::PopStyleVar(); + + ImGui::Spacing(); + + ImGui::AlignTextToFramePadding(); + ImGui::Text("Name:"); + ImGui::SameLine(80); + ImGui::SetNextItemWidth(180.0f); char name_buf[256] = {0}; snprintf(name_buf, sizeof(name_buf), "%s", m_template_name.c_str()); if (ImGui::InputText("##template_name", name_buf, sizeof(name_buf))) @@ -202,22 +512,17 @@ void template_editor::render_controls() m_validation_error = ""; } } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Unique name for this template"); - ImGui::SameLine(); - if (ImGui::Checkbox("Enabled", &m_enabled)) - { - if (m_is_editing_existing) - { - m_template_controller.set_template_enabled(m_template_name, m_enabled); - } - } - - ImGui::Text("Input Path:"); - ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + ImGui::Text("Input:"); + ImGui::SameLine(80); ImGui::SetNextItemWidth(-FLT_MIN); char input_path_buf[512] = {0}; snprintf(input_path_buf, sizeof(input_path_buf), "%s", m_input_path.c_str()); - if (ImGui::InputText("##input_path", input_path_buf, sizeof(input_path_buf))) + if (ImGui::InputTextWithHint("##input_path", "Path to template file...", + input_path_buf, sizeof(input_path_buf))) { m_input_path = input_path_buf; if (!m_input_path.empty()) @@ -229,13 +534,17 @@ void template_editor::render_controls() m_template_controller.set_template_input_path(m_template_name, m_input_path); } } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Path where the template source file is stored"); - ImGui::Text("Output Path:"); - ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + ImGui::Text("Output:"); + ImGui::SameLine(80); ImGui::SetNextItemWidth(-FLT_MIN); char path_buf[512] = {0}; snprintf(path_buf, sizeof(path_buf), "%s", m_output_path.c_str()); - if (ImGui::InputText("##output_path", path_buf, sizeof(path_buf))) + if (ImGui::InputTextWithHint("##output_path", "Path for generated config...", + path_buf, sizeof(path_buf))) { m_output_path = path_buf; if (!m_output_path.empty()) @@ -247,13 +556,17 @@ void template_editor::render_controls() m_template_controller.set_template_output_path(m_template_name, m_output_path); } } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Path where the processed config will be written"); - ImGui::Text("Reload Command:"); - ImGui::SameLine(); + ImGui::AlignTextToFramePadding(); + ImGui::Text("Reload:"); + ImGui::SameLine(80); ImGui::SetNextItemWidth(-FLT_MIN); char reload_buf[512] = {0}; snprintf(reload_buf, sizeof(reload_buf), "%s", m_reload_command.c_str()); - if (ImGui::InputText("##reload_cmd", reload_buf, sizeof(reload_buf))) + if (ImGui::InputTextWithHint("##reload_cmd", "Command to reload app (optional)...", + reload_buf, sizeof(reload_buf))) { m_reload_command = reload_buf; if (m_is_editing_existing) @@ -261,10 +574,13 @@ void template_editor::render_controls() m_template_controller.set_template_reload_command(m_template_name, m_reload_command); } } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Shell command to run after applying theme (e.g., 'pkill -USR1 kitty')"); if (!m_validation_error.empty()) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f)); ImGui::TextWrapped("%s", m_validation_error.c_str()); ImGui::PopStyleColor(); } @@ -272,15 +588,17 @@ void template_editor::render_controls() void template_editor::render_editor() { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 4)); + if (!m_is_editing_existing) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.8f, 0.4f, 1.0f)); - ImGui::Text("New Template"); + ImGui::Text(" New Template"); ImGui::PopStyleColor(); } else { - ImGui::Text("%s", m_template_name.c_str()); + ImGui::Text(" %s", m_template_name.c_str()); auto trim_right = [](const std::string &s) -> std::string { size_t end = s.find_last_not_of("\r\n"); return (end == std::string::npos) ? "" : s.substr(0, end + 1); @@ -294,39 +612,157 @@ void template_editor::render_editor() { ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.6f, 0.2f, 1.0f)); - ImGui::Text("●"); + ImGui::Text("(unsaved)"); ImGui::PopStyleColor(); } } - + + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 30); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 25.0f); + ImGui::TextUnformatted("Template Syntax:"); + ImGui::Separator(); + ImGui::TextUnformatted("Use {color_key.format} for color variables"); + ImGui::Spacing(); + ImGui::TextUnformatted("Color Keys: background, foreground, accent, etc."); + ImGui::TextUnformatted("Formats: hex, rgb, rgba, r, g, b, hsl, hsla, etc."); + ImGui::Spacing(); + ImGui::TextUnformatted("Examples:"); + ImGui::BulletText("{background.hex} -> #1E1E1E"); + ImGui::BulletText("{accent.rgb} -> rgb(14,99,156)"); + ImGui::BulletText("{foreground.r} -> 204"); + ImGui::Spacing(); + ImGui::TextUnformatted("Tip: Type '{' to trigger autocomplete!"); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + ImGui::PopStyleVar(); ImGui::Separator(); + bool consume_keys = false; + + if (m_show_autocomplete && ImGui::IsKeyPressed(ImGuiKey_Escape, false)) + { + m_show_autocomplete = false; + m_autocomplete_dismissed = true; + + m_dismiss_position = m_editor.GetCursorPosition(); + + std::string line = m_editor.GetCurrentLineText(); + m_dismiss_brace_pos = -1; + for (int i = m_dismiss_position.mColumn - 1; i >= 0; --i) + { + if (i < (int)line.length() && line[i] == '{') + { + m_dismiss_brace_pos = i; + break; + } + } + + consume_keys = true; + } + else if (m_show_autocomplete && !m_autocomplete_suggestions.empty()) + { + int max_visible = std::min((int)m_autocomplete_suggestions.size(), 8); + + if (ImGui::IsKeyPressed(ImGuiKey_DownArrow, false)) + { + m_autocomplete_selected = (m_autocomplete_selected + 1) % max_visible; + consume_keys = true; + } + else if (ImGui::IsKeyPressed(ImGuiKey_UpArrow, false)) + { + m_autocomplete_selected = (m_autocomplete_selected - 1 + max_visible) % max_visible; + consume_keys = true; + } + else if (ImGui::IsKeyPressed(ImGuiKey_Tab, false) || + ImGui::IsKeyPressed(ImGuiKey_Enter, false)) + { + auto start = m_autocomplete_start_pos; + auto end = m_editor.GetCursorPosition(); + m_editor.SetSelection(start, end); + m_editor.Delete(); + m_editor.InsertText(m_autocomplete_suggestions[m_autocomplete_selected] + "}"); + m_show_autocomplete = false; + m_autocomplete_dismissed = false; + consume_keys = true; + } + } + + if (consume_keys) + { + m_editor.SetHandleKeyboardInputs(false); + } + + ImVec2 editor_pos = ImGui::GetCursorScreenPos(); + m_editor.Render("##TemplateEditor", ImVec2(0, 0), true); + + if (consume_keys) + { + m_editor.SetHandleKeyboardInputs(true); + } + + update_autocomplete_suggestions(); + render_autocomplete(editor_pos); } void template_editor::render_template_list() { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(8, 6)); + ImGui::Text("Templates"); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 20); + ImGui::TextDisabled("(%d)", (int)m_template_controller.templates().size()); ImGui::Separator(); if (!m_is_editing_existing) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.8f, 0.4f, 1.0f)); - ImGui::Selectable("* New Template *", true); - ImGui::PopStyleColor(); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.9f, 0.4f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0.2f, 0.5f, 0.2f, 0.5f)); + ImGui::Selectable("+ New Template", true); + ImGui::PopStyleColor(2); ImGui::Separator(); } const auto &templates = m_template_controller.templates(); - for (const auto &key : templates | std::views::keys) + for (const auto &[key, tmpl] : templates) { const bool selected = (m_template_name == key && m_is_editing_existing); + + if (!tmpl.enabled()) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f)); + } + if (ImGui::Selectable(key.c_str(), selected)) { load_template(key); } + + if (!tmpl.enabled()) + { + ImGui::PopStyleColor(); + } + + if (ImGui::IsItemHovered()) + { + ImGui::BeginTooltip(); + ImGui::Text("Template: %s", key.c_str()); + ImGui::Separator(); + ImGui::Text("Status: %s", tmpl.enabled() ? "Enabled" : "Disabled"); + if (!tmpl.output_path().empty()) + ImGui::Text("Output: %s", tmpl.output_path().c_str()); + ImGui::EndTooltip(); + } } + + ImGui::PopStyleVar(); } bool template_editor::is_valid_path(const std::string &path) @@ -423,7 +859,6 @@ void template_editor::save_template() std::filesystem::path template_file = clrsync::core::normalize_path(trimmed_input_path); - // Ensure the parent directory exists auto parent_dir = template_file.parent_path(); if (!parent_dir.empty() && !std::filesystem::exists(parent_dir)) { diff --git a/src/gui/template_editor.hpp b/src/gui/template_editor.hpp index eca7f13..e8d5c26 100644 --- a/src/gui/template_editor.hpp +++ b/src/gui/template_editor.hpp @@ -4,7 +4,9 @@ #include "template_controller.hpp" #include #include "color_text_edit/TextEditor.h" +#include "imgui.h" #include +#include class template_editor { @@ -17,6 +19,8 @@ private: void render_controls(); void render_editor(); void render_template_list(); + void render_autocomplete(const ImVec2& editor_pos); + void update_autocomplete_suggestions(); void save_template(); void load_template(const std::string &name); @@ -40,6 +44,22 @@ private: bool m_enabled{true}; bool m_is_editing_existing{false}; bool m_show_delete_confirmation{false}; + + bool m_show_autocomplete{false}; + bool m_autocomplete_dismissed{false}; + TextEditor::Coordinates m_dismiss_position; + int m_dismiss_brace_pos{-1}; + std::vector m_autocomplete_suggestions; + int m_autocomplete_selected{0}; + std::string m_autocomplete_prefix; + TextEditor::Coordinates m_autocomplete_start_pos; + + ImVec4 m_autocomplete_bg_color; + ImVec4 m_autocomplete_border_color; + ImVec4 m_autocomplete_selected_color; + ImVec4 m_autocomplete_text_color; + ImVec4 m_autocomplete_selected_text_color; + ImVec4 m_autocomplete_dim_text_color; }; #endif // CLRSYNC_GUI_TEMPLATE_EDITOR_HPP \ No newline at end of file