diff --git a/apps/OpenSpace/ext/sgct b/apps/OpenSpace/ext/sgct index 314b23bb66..4e1b1fdab9 160000 --- a/apps/OpenSpace/ext/sgct +++ b/apps/OpenSpace/ext/sgct @@ -1 +1 @@ -Subproject commit 314b23bb66a9c106e6109fc49d403173e79893fd +Subproject commit 4e1b1fdab9adadb224fbfb24e0ce7fb551560d1f diff --git a/data/assets/scene/solarsystem/sssb/amor_asteroid.asset b/data/assets/scene/solarsystem/sssb/amor_asteroid.asset index 5ab657307f..6ec7b3ba70 100644 --- a/data/assets/scene/solarsystem/sssb/amor_asteroid.asset +++ b/data/assets/scene/solarsystem/sssb/amor_asteroid.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_amor_asteroid.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 4, Color = { 1.0, 1.0, 1.0 }, TrailFade = 11, diff --git a/data/assets/scene/solarsystem/sssb/apollo_asteroid.asset b/data/assets/scene/solarsystem/sssb/apollo_asteroid.asset index 22624d2e33..2e16b38c75 100644 --- a/data/assets/scene/solarsystem/sssb/apollo_asteroid.asset +++ b/data/assets/scene/solarsystem/sssb/apollo_asteroid.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_apollo_asteroid.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 6, Color = { 0.7, 0.7, 1.0 }, TrailFade = 10, diff --git a/data/assets/scene/solarsystem/sssb/aten_asteroid.asset b/data/assets/scene/solarsystem/sssb/aten_asteroid.asset index 0e22950cd7..360f62e842 100644 --- a/data/assets/scene/solarsystem/sssb/aten_asteroid.asset +++ b/data/assets/scene/solarsystem/sssb/aten_asteroid.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_aten_asteroid.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 2, Color = { 0.15, 0.15, 1.0 }, TrailFade = 18, diff --git a/data/assets/scene/solarsystem/sssb/atira_asteroid.asset b/data/assets/scene/solarsystem/sssb/atira_asteroid.asset index 4c681e9100..63ee7e1c31 100644 --- a/data/assets/scene/solarsystem/sssb/atira_asteroid.asset +++ b/data/assets/scene/solarsystem/sssb/atira_asteroid.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_atira_asteroid.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 2, Color = { 0.5, 0.8, 1.0 }, TrailFade = 25, diff --git a/data/assets/scene/solarsystem/sssb/centaur_asteroid.asset b/data/assets/scene/solarsystem/sssb/centaur_asteroid.asset index 1f5a7a8e8f..5d5be3b5b1 100644 --- a/data/assets/scene/solarsystem/sssb/centaur_asteroid.asset +++ b/data/assets/scene/solarsystem/sssb/centaur_asteroid.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_centaur_asteroid.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 6, Color = { 0.94, 0.96, 0.94 }, TrailFade = 18, diff --git a/data/assets/scene/solarsystem/sssb/chiron-type_comet.asset b/data/assets/scene/solarsystem/sssb/chiron-type_comet.asset index b16a2eaa08..0d1a23096d 100644 --- a/data/assets/scene/solarsystem/sssb/chiron-type_comet.asset +++ b/data/assets/scene/solarsystem/sssb/chiron-type_comet.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_chiron-type_comet.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 10, Color = { 0.15, 0.1, 1.0 }, TrailFade = 25, diff --git a/data/assets/scene/solarsystem/sssb/encke-type_comet.asset b/data/assets/scene/solarsystem/sssb/encke-type_comet.asset index 584e4389d2..389613a446 100644 --- a/data/assets/scene/solarsystem/sssb/encke-type_comet.asset +++ b/data/assets/scene/solarsystem/sssb/encke-type_comet.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_encke-type_comet.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 2, Color = { 0.8, 0.34, 1.0 }, TrailFade = 23, diff --git a/data/assets/scene/solarsystem/sssb/halley-type_comet.asset b/data/assets/scene/solarsystem/sssb/halley-type_comet.asset index d32fdb162b..ec74bef49b 100644 --- a/data/assets/scene/solarsystem/sssb/halley-type_comet.asset +++ b/data/assets/scene/solarsystem/sssb/halley-type_comet.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_halley-type_comet.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 9, Color = { 0.66, 0.66, 0.66 }, TrailFade = 18, diff --git a/data/assets/scene/solarsystem/sssb/inner_main_belt_asteroid.asset b/data/assets/scene/solarsystem/sssb/inner_main_belt_asteroid.asset index 939251108a..aad14262aa 100644 --- a/data/assets/scene/solarsystem/sssb/inner_main_belt_asteroid.asset +++ b/data/assets/scene/solarsystem/sssb/inner_main_belt_asteroid.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_inner_main_belt_asteroid.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 1, Color = { 1.0, 1.0, 0.0 }, TrailFade = 0.5, diff --git a/data/assets/scene/solarsystem/sssb/jupiter-family_comet.asset b/data/assets/scene/solarsystem/sssb/jupiter-family_comet.asset index d41134268a..f9c9a81f1a 100644 --- a/data/assets/scene/solarsystem/sssb/jupiter-family_comet.asset +++ b/data/assets/scene/solarsystem/sssb/jupiter-family_comet.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_jupiter-family_comet.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 10, Color = { 0.2, 0.8, 0.2 }, TrailFade = 28, diff --git a/data/assets/scene/solarsystem/sssb/jupiter_trojan_asteroid.asset b/data/assets/scene/solarsystem/sssb/jupiter_trojan_asteroid.asset index 56697005a4..65b021874a 100644 --- a/data/assets/scene/solarsystem/sssb/jupiter_trojan_asteroid.asset +++ b/data/assets/scene/solarsystem/sssb/jupiter_trojan_asteroid.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_jupiter_trojan_asteroid.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 1, Color = { 0.5, 0.8, 0.5 }, TrailFade = 5, diff --git a/data/assets/scene/solarsystem/sssb/main_belt_asteroid.asset b/data/assets/scene/solarsystem/sssb/main_belt_asteroid.asset index 48876f918b..ffe3aa2b8f 100644 --- a/data/assets/scene/solarsystem/sssb/main_belt_asteroid.asset +++ b/data/assets/scene/solarsystem/sssb/main_belt_asteroid.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_main_belt_asteroid.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 1, Color = { 0.0, 0.5, 0.0 }, TrailFade = 0.1, diff --git a/data/assets/scene/solarsystem/sssb/mars-crossing_asteroid.asset b/data/assets/scene/solarsystem/sssb/mars-crossing_asteroid.asset index 6e60ac5f21..779b82dcc3 100644 --- a/data/assets/scene/solarsystem/sssb/mars-crossing_asteroid.asset +++ b/data/assets/scene/solarsystem/sssb/mars-crossing_asteroid.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_mars-crossing_asteroid.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 1, Color = { 0.814, 0.305, 0.22 }, TrailFade = 13, diff --git a/data/assets/scene/solarsystem/sssb/mpc.asset b/data/assets/scene/solarsystem/sssb/mpc.asset index 1c20a51f00..8a7626f317 100644 --- a/data/assets/scene/solarsystem/sssb/mpc.asset +++ b/data/assets/scene/solarsystem/sssb/mpc.asset @@ -15,9 +15,8 @@ local Object = { Parent = transforms.SunEclipJ2000.Identifier, Renderable = { Type = "RenderableOrbitalKepler", - Path = mpcorb, + Path = mpcorb .. "MPCORB.DAT", Format = "MPC", - Segments = 200, SegmentQuality = 4, Color = { 0.8, 0.15, 0.2 }, TrailFade = 11, diff --git a/data/assets/scene/solarsystem/sssb/outer_main_belt_asteroid.asset b/data/assets/scene/solarsystem/sssb/outer_main_belt_asteroid.asset index cc082e8074..4800019512 100644 --- a/data/assets/scene/solarsystem/sssb/outer_main_belt_asteroid.asset +++ b/data/assets/scene/solarsystem/sssb/outer_main_belt_asteroid.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_outer_main_belt_asteroid.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 1, Color = { 0.4, 0.4, 1.0 }, TrailFade = 2, diff --git a/data/assets/scene/solarsystem/sssb/pha.asset b/data/assets/scene/solarsystem/sssb/pha.asset index 60b3505f49..09cbd8ee92 100644 --- a/data/assets/scene/solarsystem/sssb/pha.asset +++ b/data/assets/scene/solarsystem/sssb/pha.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_pha.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 3, Color = { 0.98, 0.09, 0.06 }, TrailFade = 17, diff --git a/data/assets/scene/solarsystem/sssb/transneptunian_object_asteroid.asset b/data/assets/scene/solarsystem/sssb/transneptunian_object_asteroid.asset index 3c39ebbd1c..43cd13839e 100644 --- a/data/assets/scene/solarsystem/sssb/transneptunian_object_asteroid.asset +++ b/data/assets/scene/solarsystem/sssb/transneptunian_object_asteroid.asset @@ -17,7 +17,6 @@ local Object = { Type = "RenderableOrbitalKepler", Path = sssb .. "sssb_data_transneptunian_object_asteroid.csv", Format = "SBDB", - Segments = 200, SegmentQuality = 8, Color = { 0.56, 0.64, 0.95 }, TrailFade = 10, diff --git a/include/openspace/rendering/dashboardtextitem.h b/include/openspace/rendering/dashboardtextitem.h index 2375963453..f5b10b0dd7 100644 --- a/include/openspace/rendering/dashboardtextitem.h +++ b/include/openspace/rendering/dashboardtextitem.h @@ -39,8 +39,7 @@ namespace documentation { struct Documentation; } class DashboardTextItem : public DashboardItem { public: - explicit DashboardTextItem(const ghoul::Dictionary& dictionary, float fontSize = 10.f, - const std::string& fontName = "Mono"); + explicit DashboardTextItem(const ghoul::Dictionary& dictionary); void render(glm::vec2& penPosition) override; diff --git a/include/openspace/rendering/luaconsole.h b/include/openspace/rendering/luaconsole.h index c545f68a4b..03095dc516 100644 --- a/include/openspace/rendering/luaconsole.h +++ b/include/openspace/rendering/luaconsole.h @@ -34,6 +34,7 @@ #include #include #include +#include #include #include #include @@ -63,6 +64,17 @@ public: private: void parallelConnectionChanged(const ParallelConnection::Status& status); void addToCommand(const std::string& c); + void registerKeyHandlers(); + void registerKeyHandler(Key key, KeyModifier modifier, std::function callback); + + // Helper functions for tab autocomplete + void autoCompleteCommand(); + size_t detectContext(std::string_view command); + bool gatherPathSuggestions(size_t contextStart); + void gatherFunctionSuggestions(size_t contextStart); + void filterSuggestions(); + void cycleSuggestion(); + void applySuggestion(); properties::BoolProperty _isVisible; properties::BoolProperty _shouldBeSynchronized; @@ -79,12 +91,29 @@ private: std::vector _commandsHistory; size_t _activeCommand = 0; std::vector _commands; + // Map of registered keybinds and their corresponding callbacks + std::map> _keyHandlers; - struct { - int lastIndex; - bool hasInitialValue; - std::string initialValue; - } _autoCompleteInfo; + enum class Context { + None = 0, + Function, + Path + }; + + struct AutoCompleteState { + AutoCompleteState(); + + Context context; // Assumed context we are currently in based on + bool isDataDirty; // Flag indicating if we need to update the suggestion data + std::string input; // Part of the command that we're intrested in + std::vector suggestions; // All suggestions found so far + int currentIndex; // Current suggestion index + std::string suggestion; // Current suggestion to show + bool cycleReverse; // Whether we should cycle suggestions forward or backwards + size_t insertPosition; // Where to insert the suggestion in the command + }; + + AutoCompleteState _autoCompleteState; float _currentHeight = 0.f; float _targetHeight = 0.f; diff --git a/include/openspace/util/spicemanager.h b/include/openspace/util/spicemanager.h index 3128e5c4c4..e7c71f30ef 100644 --- a/include/openspace/util/spicemanager.h +++ b/include/openspace/util/spicemanager.h @@ -613,6 +613,9 @@ public: std::string dateFromEphemerisTime(double ephemerisTime, const char* format); + void dateFromEphemerisTime(double ephemerisTime, char* outBuf, int bufferSize, + const std::string& format = "YYYY MON DDTHR:MN:SC.### ::RND") const; + /** * Returns the \p position of a \p target body relative to an \p observer in a * specific \p referenceFrame, optionally corrected for \p lightTime (planetary diff --git a/include/openspace/util/time.h b/include/openspace/util/time.h index e6fe1b581c..c2a4fd651f 100644 --- a/include/openspace/util/time.h +++ b/include/openspace/util/time.h @@ -136,6 +136,13 @@ public: */ std::string_view UTC() const; + /** + * Returns the current time as a formatted date string. The date string can be + * formatted using the SPICE picture parameters as described in + * https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/timout_c.html + */ + std::string_view string(const std::string& format) const; + /** * Returns the current time as a ISO 8601 formatted, i.e YYYY-MM-DDThh:mm:ssZ. * diff --git a/modules/base/dashboard/dashboarditemdate.cpp b/modules/base/dashboard/dashboarditemdate.cpp index 36e1541faa..5f4287629d 100644 --- a/modules/base/dashboard/dashboarditemdate.cpp +++ b/modules/base/dashboard/dashboarditemdate.cpp @@ -79,7 +79,7 @@ documentation::Documentation DashboardItemDate::Documentation() { } DashboardItemDate::DashboardItemDate(const ghoul::Dictionary& dictionary) - : DashboardTextItem(dictionary, 15.f) + : DashboardTextItem(dictionary) , _formatString(FormatStringInfo, "Date: {}") , _timeFormat(TimeFormatInfo, "YYYY MON DD HR:MN:SC.### UTC ::RND") { diff --git a/modules/base/dashboard/dashboarditemelapsedtime.cpp b/modules/base/dashboard/dashboarditemelapsedtime.cpp index f04285ddb3..7595594b06 100644 --- a/modules/base/dashboard/dashboarditemelapsedtime.cpp +++ b/modules/base/dashboard/dashboarditemelapsedtime.cpp @@ -111,7 +111,7 @@ documentation::Documentation DashboardItemElapsedTime::Documentation() { } DashboardItemElapsedTime::DashboardItemElapsedTime(const ghoul::Dictionary& dictionary) - : DashboardTextItem(dictionary, 15.f) + : DashboardTextItem(dictionary) , _formatString(FormatStringInfo, "Elapsed time: {}") , _referenceTime(ReferenceTimeInfo) , _simplifyTime(SimplifyTimeInfo, true) diff --git a/modules/base/dashboard/dashboarditemmission.cpp b/modules/base/dashboard/dashboarditemmission.cpp index 61d9133e48..571773cb23 100644 --- a/modules/base/dashboard/dashboarditemmission.cpp +++ b/modules/base/dashboard/dashboarditemmission.cpp @@ -69,7 +69,7 @@ documentation::Documentation DashboardItemMission::Documentation() { } DashboardItemMission::DashboardItemMission(const ghoul::Dictionary& dictionary) - : DashboardTextItem(dictionary, 15.f) + : DashboardTextItem(dictionary) {} void DashboardItemMission::update() {} diff --git a/src/engine/openspaceengine_lua.inl b/src/engine/openspaceengine_lua.inl index 1713edf81c..10acdbfae4 100644 --- a/src/engine/openspaceengine_lua.inl +++ b/src/engine/openspaceengine_lua.inl @@ -103,7 +103,8 @@ namespace { // Downloads a file from Lua interpreter [[codegen::luawrap]] void downloadFile(std::string url, std::string savePath, - bool waitForCompletion = false) + bool waitForCompletion = false, + bool overrideExistingFile = true) { using namespace openspace; @@ -112,7 +113,7 @@ namespace { global::downloadManager->downloadFile( url, savePath, - DownloadManager::OverrideFile::Yes, + DownloadManager::OverrideFile(overrideExistingFile), DownloadManager::FailOnError::Yes, 5 ); diff --git a/src/rendering/dashboardtextitem.cpp b/src/rendering/dashboardtextitem.cpp index 288a5001ab..c2eee6549a 100644 --- a/src/rendering/dashboardtextitem.cpp +++ b/src/rendering/dashboardtextitem.cpp @@ -64,11 +64,10 @@ documentation::Documentation DashboardTextItem::Documentation() { return codegen::doc("dashboardtextitem"); } -DashboardTextItem::DashboardTextItem(const ghoul::Dictionary& dictionary, float fontSize, - const std::string& fontName) +DashboardTextItem::DashboardTextItem(const ghoul::Dictionary& dictionary) : DashboardItem(dictionary) - , _fontName(FontNameInfo, fontName) - , _fontSize(FontSizeInfo, fontSize, 6.f, 144.f, 1.f) + , _fontName(FontNameInfo, "Mono") + , _fontSize(FontSizeInfo, 10.f, 6.f, 144.f, 1.f) { const Parameters p = codegen::bake(dictionary); diff --git a/src/rendering/luaconsole.cpp b/src/rendering/luaconsole.cpp index cbd945bdf9..a9896d847a 100644 --- a/src/rendering/luaconsole.cpp +++ b/src/rendering/luaconsole.cpp @@ -45,6 +45,9 @@ namespace { constexpr std::string_view HistoryFile = "ConsoleHistory"; + constexpr std::string_view JumpCharacters = ".,'/()\\"; + constexpr std::string_view PathStartIdentifier = "\"'["; + constexpr std::string_view PathEndIdentifier = "\"']"; constexpr int NoAutoComplete = -1; @@ -65,10 +68,6 @@ namespace { // Determines at which speed the console opens. constexpr float ConsoleOpenSpeed = 2.5; - // The number of characters to display after the cursor - // when horizontal scrolling is required. - constexpr int NVisibleCharsAfterCursor = 5; - constexpr openspace::properties::Property::PropertyInfo VisibleInfo = { "IsVisible", "Is visible", @@ -158,6 +157,18 @@ namespace { namespace openspace { + +LuaConsole::AutoCompleteState::AutoCompleteState() + : context{ Context::None } + , isDataDirty{ true } + , input{ "" } + , suggestions{ } + , currentIndex{ NoAutoComplete } + , suggestion{ "" } + , cycleReverse{ false } + , insertPosition{ 0 } +{} + LuaConsole::LuaConsole() : properties::PropertyOwner({ "LuaConsole", "Lua Console" }) , _isVisible(VisibleInfo, false) @@ -182,7 +193,7 @@ LuaConsole::LuaConsole() glm::vec4(1.f) ) , _historyLength(HistoryLengthInfo, 13, 0, 100) - , _autoCompleteInfo({NoAutoComplete, false, ""}) + , _autoCompleteState{} { addProperty(_isVisible); addProperty(_shouldBeSynchronized); @@ -262,6 +273,8 @@ void LuaConsole::initialize() { parallelConnectionChanged(status); } ); + + registerKeyHandlers(); } void LuaConsole::deinitialize() { @@ -335,6 +348,7 @@ bool LuaConsole::keyboardCallback(Key key, KeyModifier modifier, KeyAction actio _isVisible = false; _commands.back() = ""; _inputPosition = 0; + _autoCompleteState = AutoCompleteState(); } } else { @@ -348,253 +362,19 @@ bool LuaConsole::keyboardCallback(Key key, KeyModifier modifier, KeyAction actio return false; } - if (key == Key::Escape) { - _isVisible = false; - return true; + KeyWithModifier keyCombination = KeyWithModifier(key, modifier); + + // Call the registered function for the key combination pressed + auto it = _keyHandlers.find(keyCombination); + if (it != _keyHandlers.end()) { + it->second(); } - // Paste from clipboard - if (modifierControl && (key == Key::V || key == Key::Y)) { - addToCommand(sanitizeInput(ghoul::clipboardText())); - return true; - } - - // Copy to clipboard - if (modifierControl && key == Key::C) { - ghoul::setClipboardText(_commands.at(_activeCommand)); - return true; - } - - // Cut to clipboard - if (modifierControl && key == Key::X) { - ghoul::setClipboardText(_commands.at(_activeCommand)); - _commands.at(_activeCommand).clear(); - _inputPosition = 0; - } - - // Cut part after cursor to clipboard ("Kill") - if (modifierControl && key == Key::K) { - auto here = _commands.at(_activeCommand).begin() + _inputPosition; - auto end = _commands.at(_activeCommand).end(); - ghoul::setClipboardText(std::string(here, end)); - _commands.at(_activeCommand).erase(here, end); - } - - // Go to the previous character - if (key == Key::Left || (modifierControl && key == Key::B)) { - if (_inputPosition > 0) { - --_inputPosition; - } - return true; - } - - // Go to the next character - if (key == Key::Right || (modifierControl && key == Key::F)) { - _inputPosition = std::min( - _inputPosition + 1, - _commands.at(_activeCommand).length() - ); - return true; - } - - // Go to the previous '.' character - if (modifierControl && key == Key::Left) { - std::string current = _commands.at(_activeCommand); - std::reverse(current.begin(), current.end()); - auto it = current.find('.', current.size() - (_inputPosition - 1)); - if (it != std::string::npos) { - _inputPosition = current.size() - it; - } - else { - _inputPosition = 0; - } - } - - // Go to the next '.' character - if (modifierControl && key == Key::Right) { - auto it = _commands.at(_activeCommand).find('.', _inputPosition); - if (it != std::string::npos) { - _inputPosition = it + 1; - } - else { - _inputPosition = _commands.at(_activeCommand).size(); - } - } - - // Go to previous command - if (key == Key::Up) { - if (_activeCommand > 0) { - --_activeCommand; - } - _inputPosition = _commands.at(_activeCommand).length(); - return true; - } - - // Go to next command (the last is empty) - if (key == Key::Down) { - if (_activeCommand < _commands.size() - 1) { - ++_activeCommand; - } - _inputPosition = _commands.at(_activeCommand).length(); - return true; - } - - // Remove character before _inputPosition - if (key == Key::BackSpace) { - if (_inputPosition > 0) { - _commands.at(_activeCommand).erase(_inputPosition - 1, 1); - --_inputPosition; - } - return true; - } - - // Remove character after _inputPosition - if (key == Key::Delete) { - if (_inputPosition <= _commands.at(_activeCommand).size()) { - _commands.at(_activeCommand).erase(_inputPosition, 1); - } - return true; - } - - // Go to the beginning of command string - if (key == Key::Home || (modifierControl && key == Key::A)) { - _inputPosition = 0; - return true; - } - - // Go to the end of command string - if (key == Key::End || (modifierControl && key == Key::E)) { - _inputPosition = _commands.at(_activeCommand).size(); - return true; - } - - if (key == Key::Enter || key == Key::KeypadEnter) { - const std::string cmd = _commands.at(_activeCommand); - if (!cmd.empty()) { - using Script = scripting::ScriptEngine::Script; - global::scriptEngine->queueScript({ - .code = cmd, - .synchronized = Script::ShouldBeSynchronized(_shouldBeSynchronized), - .sendToRemote = Script::ShouldSendToRemote(_shouldSendToRemote) - }); - - // Only add the current command to the history if it hasn't been - // executed before. We don't want two of the same commands in a row - if (_commandsHistory.empty() || (cmd != _commandsHistory.back())) { - _commandsHistory.push_back(_commands.at(_activeCommand)); - } - } - - // Some clean up after the execution of the command - _commands = _commandsHistory; - _commands.emplace_back(""); - _activeCommand = _commands.size() - 1; - _inputPosition = 0; - return true; - } - - if (key == Key::Tab) { - // We get a list of all the available commands and initially find the first - // command that starts with how much we typed sofar. We store the index so - // that in subsequent "tab" presses, we will discard previous commands. This - // implements the 'hop-over' behavior. As soon as another key is pressed, - // everything is set back to normal - - // If the shift key is pressed, we decrement the current index so that we will - // find the value before the one that was previously found - if (_autoCompleteInfo.lastIndex != NoAutoComplete && modifierShift) { - _autoCompleteInfo.lastIndex -= 2; - } - std::vector allCommands = global::scriptEngine->allLuaFunctions(); - std::sort(allCommands.begin(), allCommands.end()); - - const std::string currentCommand = _commands.at(_activeCommand); - - // Check if it is the first time the tab has been pressed. If so, we need to - // store the already entered command so that we can later start the search - // from there. We will overwrite the 'currentCommand' thus making the storage - // necessary - if (!_autoCompleteInfo.hasInitialValue) { - _autoCompleteInfo.initialValue = currentCommand; - _autoCompleteInfo.hasInitialValue = true; - } - - for (int i = 0; i < static_cast(allCommands.size()); i++) { - const std::string& command = allCommands[i]; - - // Check if the command has enough length (we don't want crashes here) - // Then check if the iterator-command's start is equal to what we want - // then check if we need to skip the first found values as the user has - // pressed TAB repeatedly - const size_t fullLength = _autoCompleteInfo.initialValue.length(); - const bool correctLength = command.length() >= fullLength; - - const std::string commandLowerCase = ghoul::toLowerCase(command); - - const std::string initialValueLowerCase = ghoul::toLowerCase( - _autoCompleteInfo.initialValue - ); - - const bool correctCommand = - commandLowerCase.substr(0, fullLength) == initialValueLowerCase; - - if (correctLength && correctCommand && (i > _autoCompleteInfo.lastIndex)) { - // We found our index, so store it - _autoCompleteInfo.lastIndex = i; - - // We only want to auto-complete until the next separator "." - const size_t pos = command.find('.', fullLength); - if (pos == std::string::npos) { - // If we don't find a separator, we autocomplete until the end - // Set the found command as active command - _commands.at(_activeCommand) = command + "();"; - // Set the cursor position to be between the brackets - _inputPosition = _commands.at(_activeCommand).size() - 2; - } - else { - // If we find a separator, we autocomplete until and including the - // separator unless the autocompletion would be the same that we - // already have (the case if there are multiple commands in the - // same group - const std::string subCommand = command.substr(0, pos + 1); - if (subCommand == _commands.at(_activeCommand)) { - continue; - } - else { - _commands.at(_activeCommand) = command.substr(0, pos + 1); - _inputPosition = _commands.at(_activeCommand).length(); - // We only want to remove the autocomplete info if we just - // entered the 'default' openspace namespace - if (command.substr(0, pos + 1) == "openspace.") { - _autoCompleteInfo = { - .lastIndex = NoAutoComplete, - .hasInitialValue = false, - .initialValue = "" - }; - } - } - } - - break; - } - } - return true; - } - else { - // If any other key is pressed, we want to remove our previous findings - // The special case for Shift is necessary as we want to allow Shift+TAB - if (!modifierShift) { - _autoCompleteInfo = { - .lastIndex = NoAutoComplete, - .hasInitialValue = false, - .initialValue = "" - }; - } - } - - // We want to ignore the function keys as they don't translate to text anyway - if (key >= Key::F1 && key <= Key::F25) { - return false; + // If any other key is pressed, we want to remove our previous findings + // The special case for Shift is necessary as we want to allow Shift+TAB + const bool isShiftModifierOnly = (key == Key::LeftShift || key == Key::RightShift); + if (key != Key::Tab && !isShiftModifierOnly) { + _autoCompleteState = AutoCompleteState(); } // Do not consume modifier keys @@ -716,80 +496,111 @@ void LuaConsole::render() { ); // Render the current command - std::string currentCommand = _commands.at(_activeCommand); - // We chop off the beginning and end of the string until it fits on the screen (with - // a margin) this should be replaced as soon as the mono-spaced fonts work properly. - // Right now, every third character is a bit wider than the others + std::string currentCommand = _commands[_activeCommand]; - size_t nChoppedCharsBeginning = 0; - size_t nChoppedCharsEnd = 0; + // Render the command and suggestions with line breaks where required, + 2 accounts + // for the '> ' characters in the beginning of the command + const size_t totalCommandSize = 2 + currentCommand.size() + + _autoCompleteState.suggestion.size(); + // Scalefactor 0.925f chosen arbitraily to fit characters on screen with some margin + const size_t nCharactersPerRow = std::max( + static_cast(1), + static_cast(res.x * 0.925f / static_cast(_font->glyph('m')->width)) + ); + size_t nCommandRows = static_cast( + std::ceil(static_cast(totalCommandSize) / nCharactersPerRow) + ); + // We're intrested in the zero based index when computing the input position + // If the characters fit on one line we should not add any extra rows + nCommandRows = nCommandRows > 1 ? nCommandRows - 1 : 0; - const size_t inputPositionFromEnd = currentCommand.size() - _inputPosition; - while (true) { - using namespace ghoul::fontrendering; + // The command is split in 3 parts to render the suggestion in a different color: + // the part before the suggestion, the suggestion, and the part after the suggestion + std::string beforeSuggestion = currentCommand; + std::string afterSuggestion = ""; - // Compute the current width of the string and console prefix. - const float currentWidth = - _font->boundingBox("> " + currentCommand).x + inputLocation.x; + if (_autoCompleteState.insertPosition != 0) { + beforeSuggestion = currentCommand.substr(0, _autoCompleteState.insertPosition); + afterSuggestion = currentCommand.substr(_autoCompleteState.insertPosition); + } - // Compute the overflow in pixels - const float overflow = currentWidth - res.x * 0.995f; - if (res.x == 1 || overflow <= 0.f) { - // The resolution might be equal 1 pixel if the windows was minimized - break; + // We pad the strings with empty spaces so that each part is rendered in their correct + // positions, even if linebreaks are added + // Pad suggestion before and after with ' ' + const std::string suggestion = std::string(beforeSuggestion.size() + 2, ' ') + + _autoCompleteState.suggestion + std::string(afterSuggestion.size(), ' '); + // Pad first part at the end with ' ' + beforeSuggestion.insert( + beforeSuggestion.end(), + totalCommandSize - beforeSuggestion.size(), + ' ' + ); + // Pad second part in the beginning with ' ' + afterSuggestion.insert( + afterSuggestion.begin(), + totalCommandSize - afterSuggestion.size(), + ' ' + ); + + // Adds newline character to command whenever it reaches the max width of the window + auto linebreakCommand = [nCharactersPerRow](const std::string& s) { + const bool requiresSplitting = s.size() > nCharactersPerRow; + + if (!requiresSplitting) { + return s; } - // Since the overflow is positive, at least one character needs to be removed. - const size_t nCharsOverflow = static_cast(std::min( - std::max(1.f, overflow / _font->glyph('m')->width), - static_cast(currentCommand.size()) - )); + std::string result; + result.reserve(s.size() + s.size() / nCharactersPerRow); - // Do not hide the cursor and `NVisibleCharsAfterCursor` characters in the end. - const size_t maxAdditionalCharsToChopEnd = std::max( - 0, - static_cast(inputPositionFromEnd) - (NVisibleCharsAfterCursor + 1) - - static_cast(nChoppedCharsEnd) - ); + size_t count = 0; + for (char c : s) { + result += c; + count++; - // Do not hide the cursor in the beginning. - const size_t maxAdditionalCharsToChopBeginning = std::max( - 0, - static_cast(_inputPosition) - 1 - - static_cast(nChoppedCharsBeginning) - ); + if (c == '\n') { + count = 0; + continue; + } - // Prioritize chopping in the end of the string. - const size_t nCharsToChopEnd = std::min( - nCharsOverflow, - maxAdditionalCharsToChopEnd - ); - const size_t nCharsToChopBeginning = std::min( - nCharsOverflow - nCharsToChopEnd, - maxAdditionalCharsToChopBeginning - ); + if (count >= nCharactersPerRow) { + result += '\n'; + count = 0; + } + } - nChoppedCharsBeginning += nCharsToChopBeginning; - nChoppedCharsEnd += nCharsToChopEnd; + return result; + }; - const size_t displayLength = - _commands.at(_activeCommand).size() - - nChoppedCharsBeginning - nChoppedCharsEnd; - - currentCommand = _commands.at(_activeCommand).substr( - nChoppedCharsBeginning, - displayLength - ); - } + // Offset the command depending on how many rows we require for a nicer look + inputLocation.y += nCommandRows * EntryFontSize * 1.25f * dpiScaling.y; RenderFont( *_font, inputLocation, - "> " + currentCommand, + linebreakCommand("> " + beforeSuggestion), + _entryTextColor + ); + + // Render suggestion + RenderFont( + *_font, + inputLocation, + linebreakCommand(suggestion), + glm::vec4(0.7f, 0.7f, 0.7f, 1.f) + ); + + RenderFont( + *_font, + inputLocation, + linebreakCommand(afterSuggestion), _entryTextColor, ghoul::fontrendering::CrDirection::Down ); + // Move the marker to the correct row if there are multiple + const size_t markerStartRow = nCommandRows - (_inputPosition + 2) / nCharactersPerRow; + inputLocation.y += markerStartRow * EntryFontSize * 1.25f * dpiScaling.y; // Just offset the ^ marker slightly for a nicer look inputLocation.y += 3 * dpiScaling.y; @@ -797,7 +608,7 @@ void LuaConsole::render() { RenderFont( *_font, inputLocation, - (std::string(_inputPosition - nChoppedCharsBeginning + 2, ' ') + "^"), + (std::string((_inputPosition + 2) % nCharactersPerRow, ' ') + "^"), _entryTextColor ); @@ -808,12 +619,16 @@ void LuaConsole::render() { // @CPP: Replace with array_view std::vector commandSubset; - if (_commandsHistory.size() < static_cast(_historyLength)) { + if ((_commandsHistory.size() + nCommandRows) < static_cast(_historyLength)) { commandSubset = _commandsHistory; } else { + // Historic lines are reduced by the number of rows the command is occupying + const size_t historyOffset = std::min( + nCommandRows, static_cast(_historyLength) + ); commandSubset = std::vector( - _commandsHistory.end() - _historyLength, + _commandsHistory.end() - _historyLength + historyOffset, _commandsHistory.end() ); } @@ -886,7 +701,7 @@ void LuaConsole::setCommandInputButton(Key key) { void LuaConsole::addToCommand(const std::string& c) { const size_t length = c.length(); - _commands.at(_activeCommand).insert(_inputPosition, c); + _commands[_activeCommand].insert(_inputPosition, c); _inputPosition += length; } @@ -894,4 +709,584 @@ void LuaConsole::parallelConnectionChanged(const ParallelConnection::Status& sta _shouldSendToRemote = (status == ParallelConnection::Status::Host); } +void LuaConsole::registerKeyHandler(Key key, KeyModifier modifier, + std::function callback) +{ + _keyHandlers[{ key, modifier }] = std::move(callback); +} + +void LuaConsole::registerKeyHandlers() { + registerKeyHandler( + Key::Escape, + KeyModifier::None, + [this]() { _isVisible = false; } + ); + + // Paste from clipboard + registerKeyHandler(Key::V, KeyModifier::Control, [this]() { + addToCommand(sanitizeInput(ghoul::clipboardText())); + }); + + // Paste from clipboard + registerKeyHandler(Key::Y, KeyModifier::Control, [this]() { + addToCommand(sanitizeInput(ghoul::clipboardText())); + }); + + // Copy to clipboard + registerKeyHandler(Key::C, KeyModifier::Control, [this]() { + ghoul::setClipboardText(_commands[_activeCommand]); + }); + + // Cut to clipboard + registerKeyHandler( + Key::X, + KeyModifier::Control, + [this]() { + ghoul::setClipboardText(_commands[_activeCommand]); + _commands[_activeCommand].clear(); + _inputPosition = 0; + } + ); + + // Cut part after cursor to clipboard ("Kill") + registerKeyHandler( + Key::K, + KeyModifier::Control, + [this]() { + auto here = _commands[_activeCommand].begin() + _inputPosition; + auto end = _commands[_activeCommand].end(); + ghoul::setClipboardText(std::string(here, end)); + _commands[_activeCommand].erase(here, end); + } + ); + + // Go to the previous JumpCharacter character + registerKeyHandler( + Key::Left, + KeyModifier::Control, + [this]() { + std::string current = _commands[_activeCommand]; + std::reverse(current.begin(), current.end()); + const size_t start = current.size() - (_inputPosition - 1); + const size_t jumpCharPos = current.find_first_of(JumpCharacters, start); + if (jumpCharPos != std::string::npos) { + _inputPosition = current.size() - jumpCharPos; + } + else { + _inputPosition = 0; + } + } + ); + + // Go to the next JumpCharacter character + registerKeyHandler( + Key::Right, + KeyModifier::Control, + [this]() { + const std::string current = _commands[_activeCommand]; + const size_t jumpCharPos = current.find_first_of( + JumpCharacters, + _inputPosition + 1 + ); + if (jumpCharPos != std::string::npos) { + _inputPosition = jumpCharPos; + } + else { + _inputPosition = current.size(); + } + } + ); + + // Go to the previous character + registerKeyHandler( + Key::Left, + KeyModifier::None, + [this]() { + if (_inputPosition > 0) { + _inputPosition--; + } + } + ); + + // Go to the previous character + registerKeyHandler( + Key::B, + KeyModifier::Control, + [this]() { + if (_inputPosition > 0) { + _inputPosition--; + } + } + ); + + // Go to the next character + registerKeyHandler( + Key::Right, + KeyModifier::None, + [this]() { + if (!_autoCompleteState.suggestion.empty()) { + applySuggestion(); + return; + } + + _inputPosition = std::min( + _inputPosition + 1, + _commands[_activeCommand].length() + ); + } + ); + + // Go to the next character + registerKeyHandler( + Key::F, + KeyModifier::Control, + [this]() { + _inputPosition = std::min( + _inputPosition + 1, + _commands[_activeCommand].length() + ); + } + ); + + // Go to previous command + registerKeyHandler( + Key::Up, + KeyModifier::None, + [this]() { + if (_activeCommand > 0) { + _activeCommand--; + } + _inputPosition = _commands[_activeCommand].length(); + } + ); + + // Go to next command (the last is empty) + registerKeyHandler( + Key::Down, + KeyModifier::None, + [this]() { + if (_activeCommand < _commands.size() - 1) { + _activeCommand++; + } + _inputPosition = _commands[_activeCommand].length(); + } + ); + + // Remove character before _inputPosition + registerKeyHandler( + Key::BackSpace, + KeyModifier::None, + [this]() { + if (_inputPosition > 0) { + _commands[_activeCommand].erase(_inputPosition - 1, 1); + _inputPosition--; + } + } + ); + + // Remove characters before _inputPosition until the previous JumpCharacter. + registerKeyHandler( + Key::BackSpace, + KeyModifier::Control, + [this]() { + if (_inputPosition == 0) { + return; + } + + std::string& command = _commands[_activeCommand]; + + // If the previous character is a JumpCharacter, remove just that one. This + // behavior results in abc.de -> abc. -> abc -> 'empty string' + if (JumpCharacters.find(command[_inputPosition - 1]) != std::string::npos) { + command.erase(_inputPosition - 1, 1); + _inputPosition--; + return; + } + + // Find the position of the last JumpCharacter before _inputPosition + size_t start = 0; + for (size_t i = _inputPosition; i > 0; i--) { + if (JumpCharacters.find(command[i - 1]) != std::string::npos) { + start = i; + break; + } + } + + size_t count = _inputPosition - start; + command.erase(start, count); + _inputPosition -= count; + } + ); + + // Remove character after _inputPosition + registerKeyHandler( + Key::Delete, + KeyModifier::None, + [this]() { + if (_inputPosition <= _commands[_activeCommand].size()) { + _commands[_activeCommand].erase(_inputPosition, 1); + } + } + ); + + // Remove characters after _inputPosition until the ne JumpCharacter + registerKeyHandler( + Key::Delete, + KeyModifier::Control, + [this]() { + std::string& command = _commands[_activeCommand]; + if (_inputPosition >= command.size()) { + return; + } + + // If the next character after _inputPosition is a JumpCharacter, delete just that + if (JumpCharacters.find(command[_inputPosition]) != std::string::npos) { + command.erase(_inputPosition, 1); + return; + } + + // Find the position of the next Jumpcharacter after _inputPosition + size_t next = command.find_first_of(JumpCharacters, _inputPosition); + size_t end = next != std::string::npos ? next : command.size(); + command.erase(_inputPosition, end - _inputPosition); + } + ); + + // Go to the beginning of command string + registerKeyHandler( + Key::Home, + KeyModifier::None, + [this]() { + _inputPosition = 0; + } + ); + + // Go to the beginning of command string + registerKeyHandler( + Key::A, + KeyModifier::Control, + [this]() { + _inputPosition = 0; + } + ); + + // Go to end of command string + registerKeyHandler( + Key::End, + KeyModifier::None, + [this]() { + _inputPosition = _commands[_activeCommand].size(); + } + ); + + // Go to end of command string + registerKeyHandler( + Key::E, + KeyModifier::Control, + [this]() { + _inputPosition = _commands[_activeCommand].size(); + } + ); + + + auto executeCommand = [this]() { + if (!_autoCompleteState.suggestion.empty()) { + applySuggestion(); + return; + } + + const std::string cmd = _commands[_activeCommand]; + if (!cmd.empty()) { + using Script = scripting::ScriptEngine::Script; + global::scriptEngine->queueScript({ + .code = cmd, + .synchronized = Script::ShouldBeSynchronized(_shouldBeSynchronized), + .sendToRemote = Script::ShouldSendToRemote(_shouldSendToRemote) + }); + + // Only add the current command to the history if it hasn't been + // executed before. We don't want two of the same commands in a row + if (_commandsHistory.empty() || (cmd != _commandsHistory.back())) { + _commandsHistory.push_back(_commands[_activeCommand]); + } + } + + // Some clean up after the execution of the command + _commands = _commandsHistory; + _commands.emplace_back(""); + _activeCommand = _commands.size() - 1; + _inputPosition = 0; + }; + + registerKeyHandler(Key::Enter, KeyModifier::None, executeCommand); + registerKeyHandler(Key::KeypadEnter, KeyModifier::None, executeCommand); + + registerKeyHandler(Key::Tab, KeyModifier::None, [this]() { + _autoCompleteState.cycleReverse = false; + autoCompleteCommand(); + }); + + registerKeyHandler(Key::Tab, KeyModifier::Shift, [this]() { + _autoCompleteState.cycleReverse = true; + autoCompleteCommand(); + }); +} + +void LuaConsole::autoCompleteCommand() { + // We get a list of all the available commands or paths and initially find the + // first match that starts with how much we typed so far. We store the index so + // that in subsequent "tab" presses, we will discard previous matches. This + // implements the 'hop-over' behavior. As soon as another key is pressed, + // everything is set back to normal + + const std::string currentCommand = _commands[_activeCommand]; + // Determine if we are currently in a function or path context + if (_autoCompleteState.isDataDirty) { + const size_t contextStart = detectContext(currentCommand); + + switch (_autoCompleteState.context) { + case Context::Path: { + const bool hasSugestions = gatherPathSuggestions(contextStart); + if (!hasSugestions) { + return; + } + break; + } + case Context::Function: + gatherFunctionSuggestions(contextStart); + break; + default: + throw ghoul::RuntimeError("Unhandled context"); + } + + filterSuggestions(); + _autoCompleteState.isDataDirty = false; + } + cycleSuggestion(); +} + +size_t LuaConsole::detectContext(std::string_view command) { + // Find the path starting point which can start with either " ' or [ + size_t pathStartIndex = 0; + for (size_t i = _inputPosition; i > 0; i--) { + if (PathStartIdentifier.find(command[i - 1]) != std::string::npos) { + pathStartIndex = i; + break; + } + } + + // @TODO (anden88, 2025-08-08): Detect functions in a smarter way that allows nested + // function calls. The following example currently does not work. + // If the user typed e.g., "openspace.printInfo(open", we will not be able to + // autocomplete the last openspace. since the first instance we find is at the + // beginning, resulting in the fragment being wrongly assumed as "printInfo(open" + size_t functionStartIndex = command.rfind("openspace."); + + if (functionStartIndex == std::string::npos) { + functionStartIndex = 0; + } + + const bool isPathContext = pathStartIndex > functionStartIndex; + + _autoCompleteState.context = isPathContext ? Context::Path : Context::Function; + return isPathContext ? pathStartIndex : functionStartIndex; +} + +bool LuaConsole::gatherPathSuggestions(size_t contextStart) { + const std::string currentCommand = _commands[_activeCommand]; + // Find the end of the path + const std::string possiblePath = currentCommand.substr(contextStart); + // Find the last ' " ] if any exists, which marks the end of the path string. + // Otherwise the rest of the string is assumed to be part of the path + const size_t pathEnd = possiblePath.find_last_of(PathEndIdentifier); + const std::string userTypedPath = currentCommand.substr(contextStart, pathEnd); + + const std::filesystem::path path = std::filesystem::path(userTypedPath); + + std::filesystem::path dirToSearch; + + if (std::filesystem::exists(path) && std::filesystem::is_directory(path)) { + // User typed a full valid directory - show its contents + dirToSearch = path; + } + else { + // Not a valid dir - check the parent + std::filesystem::path parent = path.parent_path(); + if (std::filesystem::exists(parent) && std::filesystem::is_directory(parent)) { + dirToSearch = parent; + } + else { + // Neither path nor parent is valid, cancel autocomplete + return false; + } + } + + // Get the entries in directory + std::vector suggestions = + ghoul::filesystem::walkDirectory( + dirToSearch, + ghoul::filesystem::Recursive::No, + ghoul::filesystem::Sorted::Yes + ); + + auto containsNonAscii = [](const std::filesystem::path& p) { + const std::u8string s = p.generic_u8string(); + for (auto it = s.rbegin(); it != s.rend(); it++) { + if (static_cast(*it) > 0x7F) { + return true; + } + } + return false; + }; + + std::vector entries; + for (const std::filesystem::path& entry : suggestions) { + // Filter paths that contain non-ASCII characters + if (containsNonAscii(entry)) { + continue; + } + + entries.push_back(entry.string()); + } + + _autoCompleteState.suggestions = std::move(entries); + _autoCompleteState.input = userTypedPath; + + if (pathEnd != std::string::npos) { + // There is something after the path so we want to insert inbetween + _autoCompleteState.insertPosition = contextStart + pathEnd; + } + else { + // There is nothing after the path so we render normally at the end + _autoCompleteState.insertPosition = currentCommand.size(); + } + + return true; +} + +void LuaConsole::gatherFunctionSuggestions(size_t contextStart) { + const std::string currentCommand = _commands[_activeCommand]; + // Get a list of all the available commands + std::vector allCommands = global::scriptEngine->allLuaFunctions(); + std::sort(allCommands.begin(), allCommands.end()); + + _autoCompleteState.suggestions = std::move(allCommands); + + std::string possibleFunction = currentCommand.substr(contextStart); + // Find the nearest parenthesis if any exists, which marks the end of the + // function. Otherwise the rest of the string is assumed to be part of + // the function + size_t functionEnd = possibleFunction.find_first_of("()"); + _autoCompleteState.input = possibleFunction.substr(0, functionEnd); + + if (functionEnd != std::string::npos) { + // There is something after the function so we want to insert inbetween + _autoCompleteState.insertPosition = contextStart + functionEnd; + } + else { + // There is nothing after the path so we render normally at the end + _autoCompleteState.insertPosition = currentCommand.size(); + } +} + +void LuaConsole::filterSuggestions() { + auto normalize = [this](const std::string& s) { + std::string out = s; + + if (_autoCompleteState.context == Context::Function) { + out = ghoul::toLowerCase(out); + } + +#ifdef WIN32 + // On Windows, file paths are generally case-insensitive. For example, + // "C:/User/Desktop/Foo" refers to the same location as "c:/user/desktop/foo" + // Normalize paths to lowercase so they are treated equivalently + if (_autoCompleteState.context == Context::Path) { + out = ghoul::toLowerCase(sanitizeInput(out)); + } +#endif // WIN32 + + return out; + }; + + std::vector results; + results.reserve(_autoCompleteState.suggestions.size()); + + std::string input = normalize(_autoCompleteState.input); + + for (const std::string& suggestion : _autoCompleteState.suggestions) { + std::string suggestionNormalized = normalize(suggestion); + std::string result = sanitizeInput(suggestion); + + if (_autoCompleteState.context == Context::Function) { + // We're only interested in autocomplete until the next separator "." + const size_t offset = input.size(); + const size_t pos = suggestionNormalized.find('.', offset); + + if (pos != std::string::npos) { + result = result.substr(0, pos + 1); // include the "." + } + } + + if (suggestionNormalized.starts_with(input)) { + results.push_back(result); + } + } + + results.shrink_to_fit(); + + // We're only interested in unique matches, and want them sorted alphabetically + std::sort(results.begin(), results.end()); + results.erase(std::unique(results.begin(), results.end()), results.end()); + + _autoCompleteState.suggestions = std::move(results); +} + +void LuaConsole::cycleSuggestion() { + if (_autoCompleteState.suggestions.empty()) { + return; + } + + const int size = static_cast(_autoCompleteState.suggestions.size()); + const int dir = _autoCompleteState.cycleReverse ? -1 : 1; + + // First time cycling: pick start depending on direction + if (_autoCompleteState.currentIndex == NoAutoComplete) { + _autoCompleteState.currentIndex = _autoCompleteState.cycleReverse ? size - 1 : 0; + } + else { + // Wrap around on either start or end edges + _autoCompleteState.currentIndex = + (_autoCompleteState.currentIndex + dir + size) % size; + } + + const std::string& suggestion = + _autoCompleteState.suggestions[_autoCompleteState.currentIndex]; + // Show only the characters not yet written + _autoCompleteState.suggestion = suggestion.substr(_autoCompleteState.input.size()); +} + +void LuaConsole::applySuggestion() { + std::string& currentCommand = _commands[_activeCommand]; + currentCommand.insert( + _autoCompleteState.insertPosition, + _autoCompleteState.suggestion + ); + // Set cursor to the end of the command + _inputPosition = _autoCompleteState.insertPosition + + _autoCompleteState.suggestion.size(); + + if (_autoCompleteState.context == Context::Function && + !_autoCompleteState.suggestion.ends_with('.')) + { + // We're in a leaf function => add parantheses + currentCommand.insert(_inputPosition, "()"); + // Set the cursor position to be between the brackets + _inputPosition++; + } + + // Clear suggestion + _autoCompleteState = AutoCompleteState(); +} + } // namespace openspace diff --git a/src/util/spicemanager.cpp b/src/util/spicemanager.cpp index b53fa40b9c..c8f1d3c3e4 100644 --- a/src/util/spicemanager.cpp +++ b/src/util/spicemanager.cpp @@ -637,10 +637,26 @@ std::string SpiceManager::dateFromEphemerisTime(double ephemerisTime, const char et2utc_c(ephemerisTime, "C", SecondsPrecision, BufferSize, Buffer.data()); } - return std::string(Buffer.data()); } +void SpiceManager::dateFromEphemerisTime(double ephemerisTime, char* outBuf, + int bufferSize, const std::string& format) const +{ + timout_c(ephemerisTime, format.c_str(), bufferSize, outBuf); + if (failed_c()) { + throwSpiceError(std::format( + "Error converting ephemeris time '{}' to date with format '{}'", + ephemerisTime, format + )); + } + if (outBuf[0] == '*') { + // The conversion failed and we need to use et2utc + constexpr int SecondsPrecision = 3; + et2utc_c(ephemerisTime, "C", SecondsPrecision, bufferSize, outBuf); + } +} + glm::dvec3 SpiceManager::targetPosition(const std::string& target, const std::string& observer, const std::string& referenceFrame, diff --git a/src/util/time.cpp b/src/util/time.cpp index 5ae23792e6..3dcf06894f 100644 --- a/src/util/time.cpp +++ b/src/util/time.cpp @@ -121,6 +121,16 @@ std::string_view Time::UTC() const { return std::string_view(b); } +std::string_view Time::string(const std::string& format) const { + char* b = reinterpret_cast( + global::memoryManager->TemporaryMemory.allocate(128) + ); + std::memset(b, 0, 128); + + SpiceManager::ref().dateFromEphemerisTime(_time, b, 128, format.c_str()); + return std::string_view(b); +} + std::string_view Time::ISO8601() const { ZoneScoped; diff --git a/src/util/time_lua.inl b/src/util/time_lua.inl index 2dfb3a4daa..f8a6a00904 100644 --- a/src/util/time_lua.inl +++ b/src/util/time_lua.inl @@ -349,13 +349,18 @@ namespace { /** - * Returns the current time as an date string of the form - * (YYYY MON DDTHR:MN:SC.### ::RND) as returned by SPICE. + * Returns the current time as an date string. The format of the returned string can be + * adjusted by providing the format picture. The default picture that is used will be + * (YYYY MON DDTHR:MN:SC.### ::RND). See + * https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/timout_c.html + * for documentation on how the format string can be formatted * * \return The current time, in the format used by SPICE (YYYY MON DDTHR:MN:SC.### ::RND) */ -[[codegen::luawrap("SPICE")]] std::string currentTimeSpice() { - return std::string(openspace::global::timeManager->time().UTC()); +[[codegen::luawrap("SPICE")]] std::string currentTimeSpice( + std::string format = "YYYY MON DDTHR:MN:SC.### ::RND") +{ + return std::string(openspace::global::timeManager->time().string(format)); } /** diff --git a/visualtests/example/screenspacerenderable/screenspacedebugplane/debugplane.ostest b/visualtests/example/screenspacerenderable/screenspacedebugplane/debugplane.ostest index 70b0cbc674..aeb8ba22b5 100644 --- a/visualtests/example/screenspacerenderable/screenspacedebugplane/debugplane.ostest +++ b/visualtests/example/screenspacerenderable/screenspacedebugplane/debugplane.ostest @@ -1,6 +1,10 @@ { "profile": "empty", "commands": [ + { + "type": "asset", + "value": "examples/screenspacerenderable/screenspaceimagelocal/imagelocal.asset" + }, { "type": "asset", "value": "examples/screenspacerenderable/screenspacedebugplane/debugplane.asset" @@ -33,31 +37,8 @@ { "type": "property", "value": { - "property": "RenderEngine.ShowLog", - "value": true - } - }, - { - "type": "script", - "value": "openspace.printInfo('abcdefghijklmnopqrstuvwxyz')" - }, - { - "type": "script", - "value": "openspace.printInfo('ABCDEFGHIJKLMNOPQRSTUVWXYZ')" - }, - { - "type": "script", - "value": "openspace.printInfo('0123456789:-;!@#$%^&*()_+')" - }, - { - "type": "wait", - "value": 5 - }, - { - "type": "property", - "value": { - "property": "RenderEngine.ShowLog", - "value": false + "property": "ScreenSpace.ScreenSpaceDebugPlane_Example.Texture", + "value": 2 } }, {