diff --git a/.vs/ImGuiColorTextEdit/FileContentIndex/95dccef0-3376-43c0-8feb-7ce36e0f2bad.vsidx b/.vs/ImGuiColorTextEdit/FileContentIndex/95dccef0-3376-43c0-8feb-7ce36e0f2bad.vsidx new file mode 100644 index 00000000..edf70c3b Binary files /dev/null and b/.vs/ImGuiColorTextEdit/FileContentIndex/95dccef0-3376-43c0-8feb-7ce36e0f2bad.vsidx differ diff --git a/.vs/ImGuiColorTextEdit/FileContentIndex/read.lock b/.vs/ImGuiColorTextEdit/FileContentIndex/read.lock new file mode 100644 index 00000000..e69de29b diff --git a/.vs/ImGuiColorTextEdit/v17/.wsuo b/.vs/ImGuiColorTextEdit/v17/.wsuo new file mode 100644 index 00000000..a1770a33 Binary files /dev/null and b/.vs/ImGuiColorTextEdit/v17/.wsuo differ diff --git a/.vs/ImGuiColorTextEdit/v17/Browse.VC.db b/.vs/ImGuiColorTextEdit/v17/Browse.VC.db new file mode 100644 index 00000000..3ceddf32 Binary files /dev/null and b/.vs/ImGuiColorTextEdit/v17/Browse.VC.db differ diff --git a/.vs/ImGuiColorTextEdit/v17/ipch/AutoPCH/f23918067be76bd/TEXTEDITORDEMO.ipch b/.vs/ImGuiColorTextEdit/v17/ipch/AutoPCH/f23918067be76bd/TEXTEDITORDEMO.ipch new file mode 100644 index 00000000..1d1d5839 Binary files /dev/null and b/.vs/ImGuiColorTextEdit/v17/ipch/AutoPCH/f23918067be76bd/TEXTEDITORDEMO.ipch differ diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json new file mode 100644 index 00000000..0cf5ea50 --- /dev/null +++ b/.vs/ProjectSettings.json @@ -0,0 +1,3 @@ +{ + "CurrentProjectSetting": "No Configurations" +} \ No newline at end of file diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 00000000..43f7138e --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,7 @@ +{ + "ExpandedNodes": [ + "" + ], + "SelectedNode": "\\TextEditor.h", + "PreviewInSolutionExplorer": false +} \ No newline at end of file diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 00000000..457373cc Binary files /dev/null and b/.vs/slnx.sqlite differ diff --git a/README.md b/README.md index 3efc95f0..eeef8da4 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,43 @@ Syntax highlighting text editor for ImGui ![Screenshot](https://github.com/BalazsJako/ImGuiColorTextEdit/wiki/ImGuiTextEdit.png "Screenshot") -Demo project: https://github.com/BalazsJako/ColorTextEditorDemo -This started as my attempt to write a relatively simple widget which provides text editing functionality with syntax highlighting. Now there are other contributors who provide valuable additions. +To use in your own projects you really only need these files +ImGuiDebugPanel.cpp +LanguageDefinitions.cpp +TextEditor.cpp +TextEditor.h +TextEditorDemo.cpp +UnitTests.cpp + + + + +To run the demo + +//clone imgui + +// copy the ImGuiColorTextEdit folder into your desired example_... folder +// add .\ImGuiColorTextEdit to c++ include directory of the appropriate example... project (remember both debug and release) + +// include TextEditor.h and the following definitions into example...\main.cpp +#include "TextEditor.h" +int TextEditorDemo(); + + +// add a call to the demo into example...\main.cpp + +// 4. Show a textEditor frame in a simple window. +TextEditorDemo(); + + +It should look like this: +![Screenshot](TextEditorDemo.png "Demo") + + +This started as BalazsJakos attempt to write a relatively simple widget which provides text editing functionality with syntax highlighting. Now there are other contributors who provide valuable additions. +The project was dead for a very long time, but has now reforked and revitalised! + While it relies on Omar Cornut's https://github.com/ocornut/imgui, it does not follow the "pure" one widget - one function approach. Since the editor has to maintain a relatively complex and large internal state, it did not seem to be practical to try and enforce fully immediate mode. It stores its internal state in an object instance which is reused across frames. diff --git a/TextEditor.cpp b/TextEditor.cpp index cabac2ff..b4675814 100644 --- a/TextEditor.cpp +++ b/TextEditor.cpp @@ -344,11 +344,11 @@ TextEditor::Coordinates TextEditor::ScreenPosToCoordinates(const ImVec2& aPositi { auto& line = mLines.at(lineNo); - int columnIndex = 0; + //int columnIndex = 0; std::string cumulatedString = ""; - float columnWidth = 0.0f; + //float columnWidth = 0.0f; float columnX = 0.0f; - int delta = 0; + //int delta = 0; // First we find the hovered column coord. for (size_t columnIndex = 0; columnIndex < line.size(); ++columnIndex) @@ -1204,10 +1204,12 @@ void TextEditor::Render(bool aParentIsFocused) } // Draw line number (right aligned) - snprintf(buf, 16, "%d ", lineNo + 1); + if (mDrawLineNumbers) { + snprintf(buf, 16, "%d ", lineNo + 1); - auto lineNoWidth = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr).x; - drawList->AddText(ImVec2(lineStartScreenPos.x + mTextStart - lineNoWidth, lineStartScreenPos.y), mPalette[(int)PaletteIndex::LineNumber], buf); + auto lineNoWidth = ImGui::GetFont()->CalcTextSizeA(ImGui::GetFontSize(), FLT_MAX, -1.0f, buf, nullptr, nullptr).x; + drawList->AddText(ImVec2(lineStartScreenPos.x + mTextStart - lineNoWidth, lineStartScreenPos.y), mPalette[(int)PaletteIndex::LineNumber], buf); + } std::vector cursorCoordsInThisLine; for (int c = 0; c <= mState.mCurrentCursor; c++) diff --git a/TextEditor.h b/TextEditor.h index 8006e4c1..ebf9eca8 100644 --- a/TextEditor.h +++ b/TextEditor.h @@ -340,6 +340,25 @@ class IMGUI_API TextEditor void ImGuiDebugPanel(const std::string& panelName = "Debug"); void UnitTests(); + + inline bool needsInit() { return mNeedsInit; } + int Initialize(); + + inline void setLastSaveTime(uint64_t lastSaveTime = 0) { mLastSaveTime = lastSaveTime; } + inline uint64_t getLastSaveTime() { return mLastSaveTime; } + inline void setAutoSaveInterval(uint64_t autoSaveInterval = 0) { mAutoSaveInterval = autoSaveInterval; } + inline uint64_t getAutoSaveInterval() { return mAutoSaveInterval; } + + inline void SetFilename(const std::string& aFilename) { mFilename = aFilename; } + inline std::string GetFilename() const { return mFilename; } + + void MoveTo(const Coordinates& aPosition); + void SetFind(const std::string& aFindStr) { mFindStr = aFindStr; } + void Find(); + + inline void drawLineNumbers(bool dln = true) { mDrawLineNumbers = dln; } + + private: typedef std::vector> RegexList; @@ -492,4 +511,37 @@ class IMGUI_API TextEditor uint64_t mStartTime; float mLastClick; + + + + bool mDrawLineNumbers = true; + + uint64_t mLastSaveTime = 0; + uint64_t mAutoSaveInterval = 5; + std::string mFilename; + std::string mFindStr; + + bool mNeedsInit = true; + }; + + + + +enum textEditorFlags +{ + textEditorFlags_None = 0, + textEditorFlags_NoStatusBar = 1 << 1, // Disable the status bar + textEditorFlags_StatusBarTop = 1 << 2, // Status bar on Top (else at bottom) + textEditorFlags_NoMenuBar = 1 << 3, // disable the menu-bar + textEditorFlags_MenuBarOutsideFrame = 1 << 4, // disable the menu-bar + textEditorFlags_ReadOnly = 1 << 5, + textEditorFlags_ReadOnly_OnceOnly = 1 << 6, + textEditorFlags_NoOpen = 1 << 7, // disable open + textEditorFlags_NoSave = 1 << 8, // disable save + textEditorFlags_AutoSave = 1 << 9, + textEditorFlags_NoQuit = 1 << 10, // disable quit + textEditorFlags_NoLineNumbers = 1 << 11, // don't display line numbers + + textEditorFlags_NoDecoration = textEditorFlags_NoStatusBar | textEditorFlags_NoMenuBar | textEditorFlags_NoLineNumbers, +}; \ No newline at end of file diff --git a/TextEditorDemo.cpp b/TextEditorDemo.cpp new file mode 100644 index 00000000..1ddf0029 --- /dev/null +++ b/TextEditorDemo.cpp @@ -0,0 +1,405 @@ + + +/* +To run the demo + +//clone imgui + +// copy the ImGuiColorTextEdit folder into your desired example_... folder +// add .\ImGuiColorTextEdit to c++ include directory of the appropriate example... project (remember both debug and release) + +// include TextEditor.h and the following definitions into example...\main.cpp +#include "TextEditor.h" +void TextEditorDemo(); + + +// add a call to the demo into example...\main.cpp + +// 4. Show a textEditor frame in a simple window. +TextEditorDemo(); +*/ + + + + + + + +#include "TextEditor.h" + + +// required for load/save +#include +#include +#include // required for autosave + + + + +#define RGB2FLOAT(r,g,b) (float)r/255.0f, (float)g/255.0f, (float)b/255.0f +#define RGB2IM32(RGB,A) (ImU32)(RGB | A << 24) + +// Use these when an ImU32 is expected: In this format: RGB2IM32(RGB_lavenderblush, 255) +#define RGB_ghostwhite 248 | 248 <<8 | 255 <<16 + +// Use these when an ImVec4 is expected: In this format: ImVec4(CF_firebrick4, 1.00f) +#define CF_red RGB2FLOAT(255, 0, 0) + + +// helpers +int timeNow() { + return std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); +} +std::string lower(std::string s) { + for (int i = 0; i < s.size(); i++) + s[i] = (char)std::tolower(s[i]); + return s; +} + +void TextEditor::MoveTo(const Coordinates& aPosition) +{ + SetCursorPosition(aPosition); + mState.mCursors[mState.mCurrentCursor].mCursorPosition = aPosition; + EnsureCursorVisible(); +} + + +void TextEditor::Find() +{ + std::string curText = lower(GetText()); + std::string findStr = lower(mFindStr); + static std::string oldFindStr = ""; + + // if findStr has changed then reinitialise + static int startLoc = 0; + if (oldFindStr != findStr) + startLoc = 0; + + int foundLoc = curText.find(findStr, startLoc); + + //if it's not on the first round and we've got to the end then wrap around to the beginning again + if (startLoc > 0 && foundLoc == std::string::npos) + foundLoc = curText.find(findStr); + + // if nothing has been found then don't proceed + if (foundLoc == std::string::npos) + return; + + oldFindStr = findStr; + + // editor uses a line/column location system to position the cursor or selection, so + // count how many '\n' characters occurred before the point findStr has been found + // can't use GetLineMaxColumn here because it miscounts tab ('\t') characters + startLoc = foundLoc + 1; + int retCnt = 0; + int lineStart = 0; + for (int i = 0; i < foundLoc; i++) { + if (curText[i] == '\n') { + retCnt++; + lineStart = i; + } + } + if (foundLoc > 0) + lineStart++; // the count starts just after the previous '\n' so add 1 + + // move to the selection + Coordinates tecStart = { retCnt, (int)(foundLoc - lineStart) }; + Coordinates tecEnd = { retCnt, (int)(foundLoc - lineStart + findStr.size()) }; + SetSelection(tecStart, tecEnd, SelectionMode::Normal); + + // make as much of the selection visible as possible + MoveTo(tecEnd); + MoveTo(tecStart); +} + + + +int TextEditor::Initialize() { + + mNeedsInit = false; + + static LanguageDefinition lang; + + this->SetLanguageDefinition(lang); + this->SetPalette(TextEditor::GetLightPalette()); + + return 0; +} + + +std::string readFile(std::string fileToLoad) { + + std::ifstream inFile(fileToLoad, std::ios::in); + std::string data; + inFile >> data; + return data; + +} + +int loadEditorFile(TextEditor& editor, std::string fileName) { + + editor.SetText(readFile(fileName)); + editor.SetFilename(fileName); + return 0; +} + + +int saveEditorFile(TextEditor& editor, std::string saveAsFileName = "") { + + if (saveAsFileName == "") + saveAsFileName = editor.GetFilename(); + + if (editor.GetFilename() == "") + return 1; + + std::stringstream oFile(editor.GetText()); + std::cout << "Saving " << saveAsFileName << std::endl; + + editor.setLastSaveTime(timeNow()); + + return 0; +} + + +void textEditorMenuBar(TextEditor& editor, bool& show, textEditorFlags Flags) { +// if (ImGui::BeginMenuBar()) +// { + + if ((Flags & (textEditorFlags_NoOpen | textEditorFlags_NoSave | textEditorFlags_NoQuit)) == 0) { + if (ImGui::BeginMenu("File")) + { + if ((Flags & textEditorFlags_NoOpen) == 0) { + if (ImGui::MenuItem("Open TextEditorDemo.cpp")) + loadEditorFile(editor, "TextEditorDemo.cpp"); + } + + if ((Flags & textEditorFlags_NoSave) == 0) { + if (editor.IsReadOnly()) + ImGui::BeginDisabled(); + if (ImGui::MenuItem("Save")) + saveEditorFile(editor); + if (ImGui::MenuItem("SaveAs newname.tmp")) + saveEditorFile(editor, "newname.tmp"); + if (editor.IsReadOnly()) + ImGui::EndDisabled(); + } + + if ((Flags & textEditorFlags_NoQuit) == 0) { + if (ImGui::MenuItem("Quit", "Alt-F4")) + show = false; + } + ImGui::EndMenu(); + } + } + + if (ImGui::BeginMenu("Edit")) + { + if ((Flags & textEditorFlags_ReadOnly) != 0) + ImGui::BeginDisabled(); + bool ro = editor.IsReadOnly(); + if (ImGui::MenuItem("Read-only mode", nullptr, &ro)) + editor.SetReadOnly(ro); + if ((Flags & textEditorFlags_ReadOnly) != 0) + ImGui::EndDisabled(); + + ImGui::Separator(); + + if (ImGui::MenuItem("Undo", "ALT-Backspace", nullptr, !ro && editor.CanUndo())) + editor.Undo(); + if (ImGui::MenuItem("Redo", "Ctrl-Y", nullptr, !ro && editor.CanRedo())) + editor.Redo(); + + ImGui::Separator(); + + if (ImGui::MenuItem("Copy", "Ctrl-C", nullptr, editor.HasSelection())) + editor.Copy(); + if (ImGui::MenuItem("Cut", "Ctrl-X", nullptr, !ro && editor.HasSelection())) + editor.Cut(); + if (ImGui::MenuItem("Delete", "Del", nullptr, !ro && editor.HasSelection())) + editor.Delete(); + if (ImGui::MenuItem("Paste", "Ctrl-V", nullptr, !ro)) + editor.Paste(); + + ImGui::Separator(); + + if (ImGui::MenuItem("Select all", nullptr, nullptr)) + editor.SetSelection(TextEditor::Coordinates(), TextEditor::Coordinates(editor.GetTotalLines(), 0)); + + ImGui::Separator(); + + static char findChr[64] = ""; + if (ImGui::InputText("Find (F3)", findChr, IM_ARRAYSIZE(findChr))) //ImGuiInputTextFlags_EnterReturnsTrue + editor.SetFind(findChr); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Enter the (case insensitive) string to search for\n then press F3 to repeatedly search for it"); + ImGui::EndTooltip(); + } + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("View")) + { + if (ImGui::MenuItem("Dark palette")) + editor.SetPalette(TextEditor::GetDarkPalette()); + if (ImGui::MenuItem("Light palette")) + editor.SetPalette(TextEditor::GetLightPalette()); + if (ImGui::MenuItem("Retro blue palette")) + editor.SetPalette(TextEditor::GetRetroBluePalette()); + ImGui::EndMenu(); + } +// } +} + +void textEditorStatusBar(TextEditor& editor) { + auto cpos = editor.GetCursorPosition(); + ImGui::Text("%6d/%-6d %6d lines | %s | %s | %s | %s", cpos.mLine + 1, cpos.mColumn + 1, editor.GetTotalLines(), + editor.IsOverwrite() ? "Ovr" : "Ins", + editor.CanUndo() ? "*" : " ", + editor.GetLanguageDefinitionName(), + editor.GetFilename().c_str() + ); +} + +void textEditorHeaderStuff(TextEditor& editor, textEditorFlags Flags, std::string fileToEdit = "") { + + if (editor.needsInit()) + editor.Initialize(); // sets up the highlighter definitions - you can also find more by searching TextEditor::LanguageDefinition + + static std::string curLoadedFile; + + if (curLoadedFile != fileToEdit) { + // If there's already a file loaded which has been changed, then save it before loading the next + if (curLoadedFile != "" && !editor.IsReadOnly()) // this doesn't work: and editor.IsTextChanged()) + saveEditorFile(editor); + curLoadedFile = fileToEdit; + loadEditorFile(editor, fileToEdit); + if (((Flags & textEditorFlags_ReadOnly) != 0) || ((Flags & textEditorFlags_ReadOnly_OnceOnly) != 0)) + editor.SetReadOnly(true); + } + if ((Flags & textEditorFlags_ReadOnly) != 0) + editor.SetReadOnly(true); + +} + +int drawTextEditorFrame(TextEditor& editor, bool& show, const char* frameName, ImVec2 frameSize, textEditorFlags Flags, std::string fileToEdit) { + + static int saved = 0; + float renderSizeY = frameSize.y; + + textEditorHeaderStuff(editor, Flags, fileToEdit); + + ImGuiWindowFlags childFlags = ImGuiWindowFlags_None; + if ((Flags & textEditorFlags_NoMenuBar) == 0 && (Flags & textEditorFlags_MenuBarOutsideFrame) == 0) + childFlags = ImGuiWindowFlags_MenuBar; + ImGui::BeginChild(frameName, frameSize, false, childFlags); + { + + if ((Flags & textEditorFlags_NoMenuBar) == 0 && (Flags & textEditorFlags_MenuBarOutsideFrame) == 0) { + textEditorMenuBar(editor, show, Flags); + //renderSizeY -= 3 * 20; + } + + + if ((Flags & textEditorFlags_AutoSave) != 0 && saved != 0 && editor.IsTextChanged()) + ImGui::TextColored(ImVec4(CF_red, 1.00f), "WARNING: AUTOSAVE FAILURE"); + + + if ((Flags & textEditorFlags_NoStatusBar) == 0) { + renderSizeY -= 20; + if ((Flags & textEditorFlags_StatusBarTop) != 0) + textEditorStatusBar(editor); + } + + editor.Render("TextEditor", false, { frameSize.x, renderSizeY }, false); + + if ((Flags & textEditorFlags_NoStatusBar) == 0 && (Flags & textEditorFlags_StatusBarTop) == 0) + textEditorStatusBar(editor); + + } + ImGui::EndChild(); + + + if ((Flags & textEditorFlags_AutoSave) != 0) { + if (editor.IsTextChanged()) { + if (editor.getLastSaveTime() < timeNow() - editor.getAutoSaveInterval()) { + saved = saveEditorFile(editor); + } + } + } + + + return 0; +} + + + + + +// if you want to create your own version then just declare the required function +//int drawTextEditorFrame(TextEditor& editor, bool& show, const char* frameName = "##Text Editor Frame", ImVec2 frameSize = { -1, -1 }, textEditorFlags Flags = textEditorFlags_None, std::string fileToEdit = ""); +//void textEditorMenuBar(TextEditor& editor, bool& show, textEditorFlags Flags = textEditorFlags_None); +//void textEditorStatusBar(TextEditor& editor); + +void TextEditorDemo() { + + static bool showTextEditorFrame = true; + if (showTextEditorFrame) + { + + static TextEditor editor; + textEditorFlags teFlags = textEditorFlags_None; + static bool menuInFrame = true; + + + ImGuiWindowFlags childFlags = ImGuiWindowFlags_None; + if (!menuInFrame) + childFlags = ImGuiWindowFlags_MenuBar; + if (ImGui::Begin("Text Editor Frame", &showTextEditorFrame, childFlags)) + { + + ImGui::Checkbox("Menu in TextEditor Frame", &menuInFrame); + if (!menuInFrame) { + teFlags = (textEditorFlags)(teFlags | textEditorFlags_MenuBarOutsideFrame); + textEditorMenuBar(editor, showTextEditorFrame, teFlags); + } + + static bool statusBarAtTop = true; + ImGui::Checkbox("Status Bar at Top", &statusBarAtTop); + if (statusBarAtTop) + teFlags = (textEditorFlags)(teFlags | textEditorFlags_StatusBarTop); + + static bool statusBarInFrame = true; + ImGui::Checkbox("Status Bar in Text Editor Frame", &statusBarInFrame); + if (!statusBarInFrame) + teFlags = (textEditorFlags)(teFlags | textEditorFlags_NoStatusBar); + + static bool drawLineNumbers = true; + if (ImGui::Checkbox("Number Lines", &drawLineNumbers)) + editor.drawLineNumbers(drawLineNumbers); + + + if (!statusBarInFrame && statusBarAtTop) + textEditorStatusBar(editor); + + // static bool showDecorations = true; + // ImGui::Checkbox("Show Decorations", &showDecorations); // this will override the others + // if (!showDecorations) + // teFlags = textEditorFlags_NoDecoration; + + ImGui::Text("The TextEditor frame begins below here!"); + ImGui::Separator(); + drawTextEditorFrame(editor, showTextEditorFrame, "##Text Editor Frame", { -1, 500 }, teFlags, ""); + + + ImGui::Separator(); + ImGui::Text("The TextEditor frame ends above here!"); + + if (!statusBarInFrame && !statusBarAtTop) + textEditorStatusBar(editor); + + } + ImGui::End(); + } + +} \ No newline at end of file diff --git a/TextEditorDemo.png b/TextEditorDemo.png new file mode 100644 index 00000000..830647fb Binary files /dev/null and b/TextEditorDemo.png differ