| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * Copyright (c) 2000-2022 Inria | ||
| 3 | * All rights reserved. | ||
| 4 | * | ||
| 5 | * Redistribution and use in source and binary forms, with or without | ||
| 6 | * modification, are permitted provided that the following conditions are met: | ||
| 7 | * | ||
| 8 | * * Redistributions of source code must retain the above copyright notice, | ||
| 9 | * this list of conditions and the following disclaimer. | ||
| 10 | * * Redistributions in binary form must reproduce the above copyright notice, | ||
| 11 | * this list of conditions and the following disclaimer in the documentation | ||
| 12 | * and/or other materials provided with the distribution. | ||
| 13 | * * Neither the name of the ALICE Project-Team nor the names of its | ||
| 14 | * contributors may be used to endorse or promote products derived from this | ||
| 15 | * software without specific prior written permission. | ||
| 16 | * | ||
| 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
| 18 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
| 19 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | ||
| 20 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | ||
| 21 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||
| 22 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||
| 23 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | ||
| 24 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | ||
| 25 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||
| 26 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | ||
| 27 | * POSSIBILITY OF SUCH DAMAGE. | ||
| 28 | * | ||
| 29 | * Contact: Bruno Levy | ||
| 30 | * | ||
| 31 | * https://www.inria.fr/fr/bruno-levy | ||
| 32 | * | ||
| 33 | * Inria, | ||
| 34 | * Domaine de Voluceau, | ||
| 35 | * 78150 Le Chesnay - Rocquencourt | ||
| 36 | * FRANCE | ||
| 37 | * | ||
| 38 | */ | ||
| 39 | |||
| 40 | #include <geogram_gfx/gui/console.h> | ||
| 41 | #include <geogram_gfx/gui/application.h> | ||
| 42 | #include <geogram_gfx/imgui_ext/icon_font.h> | ||
| 43 | #include <geogram/basic/string.h> | ||
| 44 | #include <geogram/basic/command_line.h> | ||
| 45 | |||
| 46 | namespace GEO { | ||
| 47 | |||
| 48 | ✗ | Console::Console(bool* visible_flag) : | |
| 49 | ✗ | command_prompt_(true), | |
| 50 | ✗ | visible_flag_(visible_flag), | |
| 51 | ✗ | completion_callback_(nullptr), | |
| 52 | ✗ | history_callback_(nullptr), | |
| 53 | ✗ | history_index_(0), | |
| 54 | ✗ | max_history_index_(0) | |
| 55 | { | ||
| 56 | ✗ | input_buf_[0] = '\0'; | |
| 57 | ✗ | scroll_to_bottom_ = 0; | |
| 58 | ✗ | } | |
| 59 | |||
| 60 | ✗ | void Console::notify_error(const std::string& err) { | |
| 61 | geo_argused(err); | ||
| 62 | ✗ | return; | |
| 63 | } | ||
| 64 | |||
| 65 | ✗ | void Console::div(const std::string& value) { | |
| 66 | ✗ | this->printf("========== %s", value.c_str()); | |
| 67 | ✗ | } | |
| 68 | |||
| 69 | ✗ | void Console::out(const std::string& value) { | |
| 70 | ✗ | this->printf(" %s", value.c_str()); | |
| 71 | ✗ | } | |
| 72 | |||
| 73 | ✗ | void Console::warn(const std::string& value) { | |
| 74 | ✗ | this->printf("[W] %s", value.c_str()); | |
| 75 | ✗ | if(visible_flag_ != nullptr) { | |
| 76 | ✗ | *visible_flag_ = true; | |
| 77 | } | ||
| 78 | ✗ | } | |
| 79 | |||
| 80 | ✗ | void Console::err(const std::string& value) { | |
| 81 | ✗ | this->printf("[E] %s", value.c_str()); | |
| 82 | ✗ | if(visible_flag_ != nullptr) { | |
| 83 | ✗ | *visible_flag_ = true; | |
| 84 | } | ||
| 85 | ✗ | notify_error(value); | |
| 86 | ✗ | } | |
| 87 | |||
| 88 | ✗ | void Console::status(const std::string& value) { | |
| 89 | // Do not display error messages twice. | ||
| 90 | ✗ | if(String::string_starts_with(value, "Error:")) { | |
| 91 | return; | ||
| 92 | } | ||
| 93 | ✗ | this->printf("[status] %s", value.c_str()); | |
| 94 | } | ||
| 95 | |||
| 96 | ✗ | void Console::clear() { | |
| 97 | buf_.clear(); | ||
| 98 | line_offsets_.clear(); | ||
| 99 | ✗ | } | |
| 100 | |||
| 101 | ✗ | void Console::printf(const char* fmt, ...) { | |
| 102 | va_list args; | ||
| 103 | ✗ | va_start(args, fmt); | |
| 104 | ✗ | int old_size = buf_.size(); | |
| 105 | ✗ | buf_.appendfv(fmt, args); | |
| 106 | ✗ | va_end(args); | |
| 107 | ✗ | for (int new_size = buf_.size(); old_size < new_size; old_size++) { | |
| 108 | ✗ | if (buf_[old_size] == '\n') { | |
| 109 | ✗ | line_offsets_.push_back(old_size); | |
| 110 | } | ||
| 111 | } | ||
| 112 | // If I just use a boolean as in ImGuiDemo, then when printing to a | ||
| 113 | // hidden console then showing it again does not scroll to bottom | ||
| 114 | // (it seems that ImGui needs a couple of frames to 'figure out' | ||
| 115 | // what happened). | ||
| 116 | ✗ | scroll_to_bottom_ = 10; | |
| 117 | ✗ | update(); | |
| 118 | ✗ | } | |
| 119 | |||
| 120 | ✗ | void Console::update() { | |
| 121 | ✗ | if(Application::instance() != nullptr) { | |
| 122 | ✗ | Application::instance()->draw(); | |
| 123 | } | ||
| 124 | ✗ | } | |
| 125 | |||
| 126 | ✗ | static int TextEditCallbackStub(ImGuiInputTextCallbackData* data) { | |
| 127 | ✗ | Console* console = (Console*)data->UserData; | |
| 128 | ✗ | return console->TextEditCallback(data); | |
| 129 | } | ||
| 130 | |||
| 131 | |||
| 132 | ✗ | int Console::TextEditCallback(ImGuiInputTextCallbackData* data) { | |
| 133 | ✗ | switch (data->EventFlag) { | |
| 134 | ✗ | case ImGuiInputTextFlags_CallbackCompletion: { | |
| 135 | ✗ | if(completion_callback_ != nullptr) { | |
| 136 | static const char* | ||
| 137 | completer_word_break_characters = " .(){},+-*/="; | ||
| 138 | // Locate beginning of current word | ||
| 139 | ✗ | const char* word_end = data->Buf + data->CursorPos; | |
| 140 | const char* word_start = word_end; | ||
| 141 | ✗ | while (word_start > data->Buf) | |
| 142 | { | ||
| 143 | ✗ | const char c = word_start[-1]; | |
| 144 | ✗ | if(strchr(completer_word_break_characters,c) != nullptr) { | |
| 145 | break; | ||
| 146 | } | ||
| 147 | ✗ | word_start--; | |
| 148 | } | ||
| 149 | ✗ | index_t startw = index_t(word_start - data->Buf); | |
| 150 | ✗ | index_t endw = index_t(word_end - data->Buf); | |
| 151 | ✗ | std::string cmpword(word_start, size_t(word_end - word_start)); | |
| 152 | std::vector<std::string> matches; | ||
| 153 | ✗ | completion_callback_( | |
| 154 | ✗ | this, std::string(data->Buf), | |
| 155 | startw, endw, cmpword, matches | ||
| 156 | ); | ||
| 157 | ✗ | if(matches.size() == 0) { | |
| 158 | ✗ | this->printf("Completions: no match\n"); | |
| 159 | ✗ | } else if(matches.size() == 1) { | |
| 160 | // Single match. Delete the beginning of the word and | ||
| 161 | // replace it entirely so we've got nice casing | ||
| 162 | ✗ | data->DeleteChars( | |
| 163 | ✗ | (int)(word_start-data->Buf), | |
| 164 | (int)(word_end-word_start) | ||
| 165 | ); | ||
| 166 | ✗ | data->InsertChars(data->CursorPos, matches[0].c_str()); | |
| 167 | } else { | ||
| 168 | // Several matches, find longest common prefix | ||
| 169 | std::string longest_prefix; | ||
| 170 | size_t cur_char = 0; | ||
| 171 | bool finished = false; | ||
| 172 | ✗ | while(!finished) { | |
| 173 | char c = '\0'; | ||
| 174 | ✗ | for(size_t i=0; i<matches.size(); ++i) { | |
| 175 | if( | ||
| 176 | ✗ | cur_char >= matches[i].length() || | |
| 177 | ✗ | (i != 0 && matches[i][cur_char] != c) | |
| 178 | ) { | ||
| 179 | finished = true; | ||
| 180 | break; | ||
| 181 | } | ||
| 182 | ✗ | c = matches[i][cur_char]; | |
| 183 | } | ||
| 184 | ✗ | if(!finished) { | |
| 185 | ✗ | longest_prefix.push_back(c); | |
| 186 | } | ||
| 187 | ✗ | ++cur_char; | |
| 188 | } | ||
| 189 | // Replace edited text with longest prefix | ||
| 190 | ✗ | if(longest_prefix.length() != 0) { | |
| 191 | ✗ | data->DeleteChars( | |
| 192 | ✗ | (int)(word_start-data->Buf), | |
| 193 | (int)(word_end-word_start) | ||
| 194 | ); | ||
| 195 | ✗ | data->InsertChars( | |
| 196 | data->CursorPos, longest_prefix.c_str() | ||
| 197 | ); | ||
| 198 | } | ||
| 199 | ✗ | this->printf("Completions:\n"); | |
| 200 | ✗ | for(size_t i=0; i<matches.size(); ++i) { | |
| 201 | ✗ | this->printf( | |
| 202 | "[%d] ... %s\n", int(i), matches[i].c_str() | ||
| 203 | ); | ||
| 204 | } | ||
| 205 | } | ||
| 206 | ✗ | } | |
| 207 | } break; | ||
| 208 | ✗ | case ImGuiInputTextFlags_CallbackHistory: { | |
| 209 | ✗ | if(history_callback_ != nullptr) { | |
| 210 | std::string history_command; | ||
| 211 | // Call the callback first, to give it the opportunity to | ||
| 212 | // declare the history size. | ||
| 213 | ✗ | history_callback_(this, history_index_, history_command); | |
| 214 | ✗ | if(max_history_index_ > 0) { | |
| 215 | ✗ | int h = int(history_index_); | |
| 216 | ✗ | if(data->EventKey == ImGuiKey_UpArrow) { | |
| 217 | ✗ | --h; | |
| 218 | ✗ | if(h < 0) { | |
| 219 | ✗ | h = int(max_history_index_); | |
| 220 | } | ||
| 221 | ✗ | } else if(data->EventKey == ImGuiKey_DownArrow) { | |
| 222 | ✗ | ++h; | |
| 223 | ✗ | if(h > int(max_history_index_)) { | |
| 224 | h = 0; | ||
| 225 | } | ||
| 226 | } | ||
| 227 | { | ||
| 228 | ✗ | history_index_ = index_t(h); | |
| 229 | ✗ | if(history_index_ == max_history_index_) { | |
| 230 | history_command = ""; | ||
| 231 | } else { | ||
| 232 | ✗ | history_callback_( | |
| 233 | this, history_index_, history_command | ||
| 234 | ); | ||
| 235 | } | ||
| 236 | int newpos = std::min( | ||
| 237 | ✗ | data->BufSize-1, int(history_command.length()) | |
| 238 | ); | ||
| 239 | strncpy( | ||
| 240 | data->Buf, history_command.c_str(), size_t(newpos) | ||
| 241 | ); | ||
| 242 | ✗ | data->Buf[newpos] = '\0'; | |
| 243 | ✗ | data->CursorPos = newpos; | |
| 244 | ✗ | data->SelectionStart = newpos; | |
| 245 | ✗ | data->SelectionEnd = newpos; | |
| 246 | ✗ | data->BufTextLen = newpos; | |
| 247 | ✗ | data->BufDirty = true; | |
| 248 | } | ||
| 249 | } | ||
| 250 | } | ||
| 251 | } break; | ||
| 252 | } | ||
| 253 | ✗ | return 0; | |
| 254 | } | ||
| 255 | |||
| 256 | ✗ | bool Console::exec_command(const char* command) { | |
| 257 | geo_argused(command); | ||
| 258 | // Note: history_index_ and max_history_index_ are | ||
| 259 | // not managed here. They are managed by the callback. | ||
| 260 | ✗ | return false; | |
| 261 | } | ||
| 262 | |||
| 263 | ✗ | void Console::draw(bool* visible, bool with_window) { | |
| 264 | ✗ | if(!*visible) { | |
| 265 | return; | ||
| 266 | } | ||
| 267 | ✗ | if(with_window) { | |
| 268 | ✗ | ImGui::Begin("Console", visible); | |
| 269 | } | ||
| 270 | |||
| 271 | bool copy = false; | ||
| 272 | |||
| 273 | ✗ | bool phone_screen = CmdLine::get_arg_bool("gui:phone_screen"); | |
| 274 | bool toolbar_visible = phone_screen; | ||
| 275 | |||
| 276 | ✗ | if(toolbar_visible) { | |
| 277 | // Add a close button under Android | ||
| 278 | if(phone_screen) { | ||
| 279 | ✗ | if (ImGui::SimpleButton(icon_UTF8("window-close").c_str())) { | |
| 280 | ✗ | *visible = false; | |
| 281 | } | ||
| 282 | ✗ | ImGui::SameLine(); | |
| 283 | } | ||
| 284 | ✗ | if (ImGui::SimpleButton(icon_UTF8("eraser").c_str())) { | |
| 285 | ✗ | clear(); | |
| 286 | } | ||
| 287 | ✗ | ImGui::Tooltip("clear"); | |
| 288 | ✗ | ImGui::SameLine(); | |
| 289 | ✗ | copy = ImGui::SimpleButton(icon_UTF8("copy").c_str()); | |
| 290 | ✗ | ImGui::Tooltip("copy"); | |
| 291 | ✗ | ImGui::SameLine(); | |
| 292 | ✗ | filter_.Draw((icon_UTF8("filter")+" Filter").c_str(), -200.0f); | |
| 293 | ✗ | ImGui::Separator(); | |
| 294 | } | ||
| 295 | |||
| 296 | if(phone_screen) { | ||
| 297 | // Use smaller font if using phone in vertical mode. | ||
| 298 | if( | ||
| 299 | ✗ | Application::instance() != nullptr && | |
| 300 | Application::instance()->get_height() > | ||
| 301 | Application::instance()->get_width() | ||
| 302 | ) { | ||
| 303 | ✗ | ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[3]); | |
| 304 | } else { | ||
| 305 | ✗ | ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[1]); | |
| 306 | } | ||
| 307 | } else { | ||
| 308 | ✗ | ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[1]); | |
| 309 | } | ||
| 310 | |||
| 311 | ✗ | float scaling = Application::instance()->get_font_size(0) / 16.0f; | |
| 312 | |||
| 313 | ✗ | ImGui::BeginChild( | |
| 314 | "scrolling", | ||
| 315 | ✗ | (command_prompt_?ImVec2(0.0f,-20.0f*scaling):ImVec2(0.0f, 0.0f)), | |
| 316 | false, | ||
| 317 | ImGuiWindowFlags_HorizontalScrollbar | ||
| 318 | ); | ||
| 319 | |||
| 320 | ✗ | if (copy) { | |
| 321 | ✗ | ImGui::LogToClipboard(); | |
| 322 | } | ||
| 323 | |||
| 324 | { | ||
| 325 | const char* buf_begin = buf_.begin(); | ||
| 326 | const char* line = buf_begin; | ||
| 327 | ✗ | for (int line_no = 0; line != nullptr; line_no++) { | |
| 328 | const char* line_end = | ||
| 329 | ✗ | (line_no < line_offsets_.Size) ? | |
| 330 | ✗ | buf_begin + line_offsets_[line_no] : nullptr; | |
| 331 | |||
| 332 | ✗ | if (!filter_.IsActive() || | |
| 333 | ✗ | filter_.PassFilter(line, line_end) | |
| 334 | ) { | ||
| 335 | ✗ | bool is_error = ((line_end - line) >= 3 && | |
| 336 | ✗ | line[0] == '[' && | |
| 337 | ✗ | line[2] == ']' && | |
| 338 | ✗ | (line[1] == 'E' || line[1] == 'W') | |
| 339 | ); | ||
| 340 | if(is_error) { | ||
| 341 | ✗ | ImGui::PushStyleColor( | |
| 342 | ✗ | ImGuiCol_Text,ImVec4(1.0f,0.0f,0.0f,1.0f) | |
| 343 | ); | ||
| 344 | } | ||
| 345 | ✗ | ImGui::TextUnformatted(line, line_end); | |
| 346 | ✗ | if(is_error) { | |
| 347 | ✗ | ImGui::PopStyleColor(); | |
| 348 | } | ||
| 349 | } | ||
| 350 | ✗ | line = line_end && line_end[1] ? line_end + 1 : nullptr; | |
| 351 | } | ||
| 352 | } | ||
| 353 | // If I just use a boolean as in ImGuiDemo, then when printing to a | ||
| 354 | // hidden console then showing it again does not scroll to bottom | ||
| 355 | // (it seems that ImGui needs a couple of frames to 'figure out' | ||
| 356 | // what happened). | ||
| 357 | ✗ | if (scroll_to_bottom_ > 0) { | |
| 358 | ✗ | ImGui::SetScrollHereY(1.0); | |
| 359 | ✗ | --scroll_to_bottom_; | |
| 360 | } | ||
| 361 | ✗ | ImGui::EndChild(); | |
| 362 | ✗ | ImGui::PopFont(); | |
| 363 | |||
| 364 | ✗ | if(command_prompt_) { | |
| 365 | ✗ | ImGui::Text("%s",icon_UTF8("terminal").c_str()); | |
| 366 | ✗ | ImGui::SameLine(); | |
| 367 | ✗ | ImGui::PushItemWidth( | |
| 368 | ✗ | -20.0f -25.0f * float(Application::instance()->scaling()) | |
| 369 | ); | ||
| 370 | ✗ | ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[1]); | |
| 371 | ✗ | if(ImGui::InputText( | |
| 372 | ✗ | "##CommandInput", input_buf_, geo_imgui_string_length, | |
| 373 | ImGuiInputTextFlags_EnterReturnsTrue | | ||
| 374 | ImGuiInputTextFlags_CallbackCompletion | | ||
| 375 | ImGuiInputTextFlags_CallbackHistory, | ||
| 376 | &TextEditCallbackStub, (void*)this) | ||
| 377 | ) { | ||
| 378 | ✗ | char* input_end = input_buf_+strlen(input_buf_); | |
| 379 | ✗ | while (input_end > input_buf_ && input_end[-1] == ' ') { | |
| 380 | ✗ | input_end--; | |
| 381 | } | ||
| 382 | ✗ | *input_end = 0; | |
| 383 | ✗ | if (input_buf_[0]) { | |
| 384 | ✗ | exec_command(input_buf_); | |
| 385 | } | ||
| 386 | strcpy(input_buf_, ""); | ||
| 387 | } | ||
| 388 | ✗ | ImGui::PopItemWidth(); | |
| 389 | // Keeping auto focus on the input box | ||
| 390 | ✗ | if(ImGui::IsItemHovered()) { | |
| 391 | ✗ | ImGui::SetKeyboardFocusHere(-1); // Auto focus previous widget | |
| 392 | } | ||
| 393 | ✗ | ImGui::PopFont(); | |
| 394 | ✗ | ImGui::SameLine(); | |
| 395 | ✗ | if(ImGui::SimpleButton(icon_UTF8("window-close").c_str())) { | |
| 396 | ✗ | *visible = false; | |
| 397 | } | ||
| 398 | } | ||
| 399 | |||
| 400 | ✗ | if(with_window) { | |
| 401 | ✗ | ImGui::End(); | |
| 402 | } | ||
| 403 | } | ||
| 404 | } | ||
| 405 |