GCC Code Coverage Report


Directory: ./
File: lib/geogram_gfx/gui/console.cpp
Date: 2026-09-07 02:37:58
Exec Total Coverage
Lines: 0 223 0.0%
Functions: 0 14 0.0%
Branches: 0 207 0.0%

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