JeVois  1.20
JeVois Smart Embedded Machine Vision Toolkit
Share this page:
GUIeditor.C
Go to the documentation of this file.
1 // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2 //
3 // JeVois Smart Embedded Machine Vision Toolkit - Copyright (C) 2022 by Laurent Itti, the University of Southern
4 // California (USC), and iLab at USC. See http://iLab.usc.edu and http://jevois.org for information about this project.
5 //
6 // This file is part of the JeVois Smart Embedded Machine Vision Toolkit. This program is free software; you can
7 // redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software
8 // Foundation, version 2. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
9 // without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
10 // License for more details. You should have received a copy of the GNU General Public License along with this program;
11 // if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
12 //
13 // Contact information: Laurent Itti - 3641 Watt Way, HNB-07A - Los Angeles, CA 90089-2520 - USA.
14 // Tel: +1 213 740 3527 - itti@pollux.usc.edu - http://iLab.usc.edu - http://jevois.org
15 // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
16 /*! \file */
17 
18 // This is only available on JeVoisPro
19 #ifdef JEVOIS_PRO
20 
21 #include <jevois/GPU/GUIeditor.H>
22 #include <jevois/GPU/GUIhelper.H>
23 #include <jevois/Core/Module.H>
24 #include <jevois/Core/Engine.H>
25 #include <imgui-filebrowser/imfilebrowser.h>
26 #include <fstream>
27 
28 #include <jevois/Debug/Log.H>
29 
30 // ##############################################################################################################
31 jevois::GUIeditor::GUIeditor(GUIhelper * helper, std::string const & imguiid,
32  std::vector<jevois::EditorItem> && fixeditems, std::string const & scanpath,
33  std::string const & prefix, std::set<std::string> && extensions) :
34  TextEditor(), itsHelper(helper), itsId(imguiid), itsItems(fixeditems), itsNumFixedItems(fixeditems.size()),
35  itsScanPath(scanpath), itsPrefix(prefix), itsExtensions(extensions),
36  itsBrowser(new ImGui::FileBrowser(ImGuiFileBrowserFlags_EnterNewFilename | ImGuiFileBrowserFlags_CreateNewDir))
37 {
38  TextEditor::SetSaveCallback([this]() { saveFile(); } ); // to enable Ctrl-S saving
39 
40  itsBrowser->SetTitle("Select a file to open or create...");
41  itsBrowser->SetPwd(JEVOIS_SHARE_PATH);
42  //itsBrowser->SetTypeFilters({ ".cfg", ".py", ".txt", ".C", ".H" });
43 }
44 
45 // ##############################################################################################################
47 { }
48 
49 // ##############################################################################################################
51 {
52  // If we have an open file that is not one of our fixed items, we want to keep that one open:
53  bool keep_current = false; EditorItem current_item;
54  if (itsCurrentItem >= int(itsNumFixedItems)) { current_item = itsItems[itsCurrentItem]; keep_current = true; }
55 
56  // Remove all dynamic files, keep the fixed ones:
57  itsItems.resize(itsNumFixedItems);
58 
59  // Do we have a CMakeLists for the current module?
60  if (itsItems[0].filename == "*")
61  {
62  jevois::VideoMapping const & vm = itsHelper->engine()->getCurrentVideoMapping();
63  if (std::filesystem::exists(vm.cmakepath()))
64  itsItems.emplace_back(EditorItem {"#", "Module's CMakeLists.txt", EditorSaveAction::Compile } );
65  }
66 
67  // Rescan recursively:
68  for (auto const & dent : std::filesystem::recursive_directory_iterator(itsScanPath))
69  {
70  if (dent.is_regular_file())
71  {
72  std::filesystem::path const path = dent.path();
73 
74  // Check that the extension is one we want:
75  if (itsExtensions.find(path.extension()) == itsExtensions.end()) continue;
76 
77  // Create an entry:
78  itsItems.emplace_back(EditorItem { path, itsPrefix + path.string(), EditorSaveAction::Reload });
79  }
80  }
81 
82  // Keep the current item?
83  if (keep_current) itsItems.emplace_back(std::move(current_item));
84 
85  // Add an entry for file browser / creation:
86  itsItems.emplace_back(EditorItem { "**", "Browse / Create file...", EditorSaveAction::Reload });
87 
88  // Update index of current file:
89  bool not_found = true;
90 
91  for (int i = 0; auto const & item : itsItems)
92  if (item.filename == itsFilename)
93  {
94  itsCurrentItem = i;
95  itsNewItem = i;
96  not_found = false;
97  break;
98  }
99  else ++i;
100 
101  // If the currently open file is not in our list anymore, load file 0:
102  if (not_found)
103  {
104  itsNewItem = 0;
105  itsWantLoad = true;
106  // If we have some edits, we will ask to save, and then to possibly reload the module (or reboot, etc). Prevent
107  // asking to reload:
108  itsOverrideReloadModule = IsEdited();
109  }
110 }
111 
112 // ##############################################################################################################
114 {
115  // Create combo entries for imgui:
116  char const * items[itsItems.size()];
117  for (int i = 0; EditorItem const & c : itsItems) items[i++] = c.displayname.c_str();
118 
119  // Check if the user is trying to select a different file:
120  if (ImGui::Combo(("##"+itsId+"editorcombo").c_str(), &itsNewItem, items, itsItems.size())) itsWantLoad = true;
121 
122  // Want to load a new file? check if we need to save the current one first:
123  if (itsWantLoad && itsWantAction == false)
124  {
125  if (IsEdited())
126  {
127  static int discard_edits_default = 0;
128  int ret = itsHelper->modal("Discard edits?", "File was edited. Discard all edits? This cannot be undone.",
129  &discard_edits_default, "Discard", "Save");
130  switch (ret)
131  {
132  case 1: itsWantLoad = false; itsOkToLoad = true; break; // Discard selected
133  case 2: saveFile(); /* itsWantAction = false; */ break; // save selected
134  default: break; // Need to wait
135  }
136  }
137  else
138  {
139  itsWantLoad = false;
140  itsOkToLoad = true;
141  }
142  }
143 
144  // Need to execute an action after a save?
145  if (itsWantAction)
146  {
147  switch (itsItems[itsCurrentItem].action)
148  {
149  // --------------------------------------------------
150  case jevois::EditorSaveAction::None:
151  itsWantAction = false;
152  break;
153 
154  // --------------------------------------------------
155  case jevois::EditorSaveAction::Reload:
156  {
157  // Skip if override requested by refresh(), typically because we loaded a new module but a config file from the
158  // old module was open so we asked to save, now we don't want to ask to reload again:
159  if (itsOverrideReloadModule)
160  {
161  itsOverrideReloadModule = false;
162  itsWantAction = false;
163  itsOkToLoad = itsWantLoad;
164  break;
165  }
166 
167  // Ask whether to reload the module now or later:
168  static int reload_default = 0;
169  int ret = itsHelper->modal("Reload Module?", "Reload Machine Vision Module for changes to take effect?",
170  &reload_default, "Reload", "Later");
171  switch (ret)
172  {
173  case 1: // Reload selected
174  itsHelper->engine()->requestSetFormat(-1);
175  itsWantAction = false;
176  itsOkToLoad = itsWantLoad;
177  break;
178 
179  case 2: // Later selected: we don't want action anymore
180  itsWantAction = false;
181  itsOkToLoad = itsWantLoad;
182  break;
183 
184  default: break; // need to wait
185  }
186  }
187  break;
188 
189  // --------------------------------------------------
190  case jevois::EditorSaveAction::Reboot:
191  {
192  int ret = itsHelper->modal("Restart?", "Restart JeVois-Pro for changes to take effect?",
193  nullptr, "Restart", "Later");
194  switch (ret)
195  {
196  case 1: // Reboot selected
197  itsHelper->engine()->reboot();
198  itsWantAction = false;
199  break;
200 
201  case 2: // Later selected: we don't want action anymore
202  itsWantAction = false;
203  break;
204 
205  default: break; // Need to wait
206  }
207  }
208  break;
209 
210  // --------------------------------------------------
211  case jevois::EditorSaveAction::RefreshMappings:
212  {
213  itsHelper->engine()->reloadVideoMappings();
214  itsWantAction = false;
215  }
216  break;
217 
218  // --------------------------------------------------
219  case jevois::EditorSaveAction::Compile:
220  {
221  // Ask whether to compile the module now or later:
222  static int compile_default = 0;
223  int ret = itsHelper->modal("Compile Module?", "Compile Machine Vision Module for changes to take effect?",
224  &compile_default, "Compile", "Later");
225  switch (ret)
226  {
227  case 1: // Compile selected
228  itsHelper->startCompilation();
229  itsWantAction = false;
230  itsOkToLoad = itsWantLoad;
231  break;
232 
233  case 2: // Later selected: we don't want action anymore
234  itsWantAction = false;
235  itsOkToLoad = itsWantLoad;
236  break;
237 
238  default: break; // need to wait
239  }
240  }
241  break;
242  }
243  }
244 
245  // Ready to load a new file?
246  if (itsOkToLoad)
247  {
248  // Do we want to browse or create a new file?
249  if (itsItems[itsNewItem].filename == "**")
250  {
251  ImGui::PushStyleColor(ImGuiCol_PopupBg, 0xf0ffe0e0);
252 
253  if (itsBrowser->IsOpened() == false)
254  {
255  itsBrowser->Open();
256  itsBrowser->Display();
257  }
258  else
259  {
260  itsBrowser->Display();
261 
262  if (itsBrowser->HasSelected())
263  {
264  std::filesystem::path const fn = itsBrowser->GetSelected();
265 
266  if (std::filesystem::exists(fn))
267  loadFileInternal(fn, "Could not load " + fn.string()); // load with error and read-only on fail
268  else
269  loadFileInternal(fn, ""); // load with no error and read-write on fail (create new file)
270 
271  itsBrowser->Close();
272  itsBrowser->Display();
273  }
274 
275  // Clicking "Cancel" in the browser just closes the popup:
276  if (itsBrowser->IsOpened() == false)
277  {
278  itsOkToLoad = false; // Record that we don't want to load anymore:
279  itsNewItem = itsCurrentItem; // Snap back the combo selector to the current item
280  itsBrowser->Close();
281  itsBrowser->Display();
282  }
283  }
284  ImGui::PopStyleColor();
285  }
286  else
287  {
288  // Load the file for itsNewItem:
289  itsCurrentItem = itsNewItem;
290  loadFileInternal(itsItems[itsCurrentItem].filename, "");
291  }
292  }
293 
294  // Add a pop-up menu for editor actions:
295  bool const ro = IsReadOnly();
296  ImGui::SameLine();
297  if (ImGui::Button("...")) ImGui::OpenPopup("editor_actions");
298  if (ImGui::BeginPopup("editor_actions"))
299  {
300  constexpr int ok = ImGuiSelectableFlags_None;
301  constexpr int disa = ImGuiSelectableFlags_Disabled;
302 
303  if (ImGui::Selectable("Save [Ctrl-S]", false, !ro && IsEdited() ? ok : disa)) saveFile();
304 
305  ImGui::Separator();
306 
307  if (ImGui::Selectable("Undo [Ctrl-Z]", false, !ro && CanUndo() ? ok : disa)) Undo();
308  if (ImGui::Selectable("Redo [Ctrl-Y]", false, !ro && CanRedo() ? ok : disa)) Redo();
309 
310  ImGui::Separator();
311 
312  if (ImGui::Selectable("Copy [Ctrl-C]", false, HasSelection() ? ok : disa)) Copy();
313  if (ImGui::Selectable("Cut [Ctrl-X]", false, !ro && HasSelection() ? ok : disa)) Cut();
314  if (ImGui::Selectable("Delete [Del]", false, !ro && HasSelection() ? ok : disa)) Delete();
315  if (ImGui::Selectable("Paste [Ctrl-V]", false, !ro && ImGui::GetClipboardText()!=nullptr ? ok : disa)) Paste();
316 
317  ImGui::Separator();
318 
319  ImGui::Selectable("More shortcuts...", false, disa);
320  if (ImGui::IsItemHovered())
321  ImGui::SetTooltip("[Ctrl-A] Select all\n"
322  "[PgUp/PgDn] Move one page up/down\n"
323  "[Home] Move to start of line\n"
324  "[End] Move to end of line\n"
325  "[Ctrl-Home] Move to start of file\n"
326  "[Ctrl-End] Move to end of file\n"
327  "[Ctrl-Left/Right] Move left/right one word\n"
328  "[Ins] Toggle overwrite mode\n"
329  "[Alt-Bksp] Undo (same as [Ctrl-Z])\n"
330  "[Ctrl-Ins] Copy (same as [Ctrl-C])\n"
331  "[Shift-Ins] Paste (same as [Ctrl-V])\n"
332  "[Shift-Del] Cut (same as [Ctrl-X])\n"
333  "[Shift-Cursor] Select while moving cursor (up, down, left, right, home, end)\n"
334  "[Mouse-Drag] Select with mouse\n"
335  );
336 
337  ImGui::EndPopup();
338  }
339 
340  // Draw a save button if we are read/write:
341  if (ro == false)
342  {
343  ImGui::SameLine();
344  ImGui::TextUnformatted(" "); ImGui::SameLine();
345  if (ImGui::Button("Save")) saveFile();
346  }
347 
348  ImGui::Separator();
349 
350  // Render the editor in a child window so it can scroll correctly:
351  auto cpos = GetCursorPosition();
352 
353  ImGui::Text("%6d/%-6d %6d lines | %s | %s | %s", cpos.mLine + 1, cpos.mColumn + 1, GetTotalLines(),
354  IsOverwrite() ? "Ovr" : "Ins",
355  IsEdited() ? "*" : " ",
356  GetLanguageDefinition().mName.c_str());
357 
358  Render("JeVois-Pro Editor");
359 }
360 
361 // ##############################################################################################################
362 void jevois::GUIeditor::loadFile(std::filesystem::path const & fn)
363 {
364  // Loading will happen in the main loop. Here we just create a new item:
365  for (int i = 0; EditorItem const & item : itsItems)
366  if (item.filename == fn) { itsNewItem = i; itsWantLoad = true; return; } else ++i;
367 
368  // Not already in our list of items, create a new one. Add an entry for our new file. If it's compilable, then action
369  // on save should be to compile, otherwise it should be to reload the module:
370  EditorSaveAction action = EditorSaveAction::Reload;
371  if (fn.filename() == "CMakeLists.txt")
372  action = EditorSaveAction::Compile;
373  else if (fn.filename() == "jevoispro-fan.service")
374  action = EditorSaveAction::Reboot;
375  else
376  {
377  std::string const ext = fn.extension().string();
378  if (ext == ".C" || ext == ".H" || ext == ".cpp" || ext == ".hpp" || ext == ".c" || ext == ".h")
379  action = EditorSaveAction::Compile;
380  }
381  itsItems.emplace_back(EditorItem { fn, "File " + fn.string(), action });
382 
383  itsNewItem = itsItems.size() - 1;
384  itsWantLoad = true;
385 }
386 
387 // ##############################################################################################################
388 void jevois::GUIeditor::loadFileInternal(std::filesystem::path const & fpath, std::string const & failt)
389 {
390  std::filesystem::path fn = fpath; std::string failtxt = failt; bool special_path = false;
391 
392  if (fpath == "*")
393  {
394  // If filename is "*", replace by the module's source code name:
395  jevois::VideoMapping const & vm = itsHelper->engine()->getCurrentVideoMapping();
396  fn = vm.srcpath();
397  failtxt = "Could not open Module's source code";
398  special_path = true;
399  }
400  else if (fpath == "#")
401  {
402  // If filename is "#", replace by the module's CMakeLists.txt:
403  jevois::VideoMapping const & vm = itsHelper->engine()->getCurrentVideoMapping();
404  fn = vm.cmakepath();
405  failtxt = "Could not open Module's CMakeLists.txt";
406  special_path = true;
407  }
408  else if (fpath.is_relative())
409  {
410  // If path is relative, make it within the module's path (if any):
411  auto m = itsHelper->engine()->module();
412  if (m) fn = m->absolutePath(fpath);
413  special_path = true;
414  }
415 
416  if (fn != fpath) LINFO("Loading " << fn << " ... [" << fpath << ']'); else LINFO("Loading " << fn << " ...");
417 
418  bool got_it = false;
419  EditorSaveAction action = EditorSaveAction::Reload;
420 
421  std::ifstream t(fn);
422  if (t.good())
423  {
424  // Load the whole file and set it as our text:
425  std::string str((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
426  SetText(str);
427 
428  // Is this a known file in our pull-down list? Otherwise we need to create a new item:
429  for (int i = 0; EditorItem const & item : itsItems)
430  if (item.filename == fpath) { itsCurrentItem = i; got_it = true; break; } else ++i;
431 
432  // Set the language, read-only, and possibly action according to file extension. C++/C source files are editable if
433  // there is a CMakeLists.txt in the same directory (e.g., newly created or cloned module, excludes jevoisbase
434  // modules), and then a compilation action will be triggered on save:
435  if (fn.filename() == "CMakeLists.txt")
436  {
437  SetLanguageDefinition(TextEditor::LanguageDefinition::CMake());
438  SetReadOnly(false);
439  action = EditorSaveAction::Compile;
440  }
441  else
442  {
443  std::filesystem::path const ext = fn.extension();
444  std::filesystem::path cmak = fn; cmak.remove_filename(); cmak /= "CMakeLists.txt";
445  bool has_cmake = std::filesystem::exists(cmak);
446 
447  if (ext == ".py")
448  {
449  SetLanguageDefinition(TextEditor::LanguageDefinition::Python());
450  SetReadOnly(false);
451  }
452  else if (ext == ".C" || ext == ".H" || ext == ".cpp" || ext == ".hpp")
453  {
454  SetLanguageDefinition(TextEditor::LanguageDefinition::CPlusPlus());
455  SetReadOnly(! has_cmake);
456  action = EditorSaveAction::Compile;
457  }
458  else if ( ext == ".c" || ext == ".h")
459  {
460  SetLanguageDefinition(TextEditor::LanguageDefinition::C());
461  SetReadOnly(! has_cmake);
462  action = EditorSaveAction::Compile;
463  }
464  else
465  {
466  // .cfg, .yaml, etc
467  SetLanguageDefinition(TextEditor::LanguageDefinition::JeVoisCfg());
468  SetReadOnly(false);
469  }
470  }
471  }
472  else
473  {
474  // Show the fail text:
475  SetText(failtxt);
476  if (failtxt.empty()) { LINFO("File " << fn << " not found -- CREATING NEW"); SetReadOnly(false); }
477  else { LINFO("File " << fn << " not found."); SetReadOnly(true); }
478  }
479 
480  // If this is a new file, add an item to our pull-down list:
481  if (got_it == false && special_path == false)
482  {
483  itsItems.emplace_back(EditorItem { fn, "File " + fn.string(), action });
484  itsCurrentItem = itsItems.size() - 1;
485  }
486  else if (fpath == "*") itsItems[itsCurrentItem].action = action; // Force compile action if needed on module's src
487 
488  // Remember the filename, for saveFile():
489  itsFilename = fn;
490  itsNewItem = itsCurrentItem;
491  itsWantLoad = false;
492  itsOkToLoad = false;
493 }
494 
495 // ##############################################################################################################
497 {
498  LINFO("Saving " << itsFilename << " ...");
499  std::ofstream os(itsFilename);
500  if (os.is_open() == false) { itsHelper->reportError("Cannot write " + itsFilename.string()); return; }
501 
502  std::string const txt = GetText();
503  os << txt;
504 
505  // Mark as un-edited:
506  SetEdited(false);
507 
508  // Delete modinfo.html if any, GUIhelper will re-compute it when the info tab is selected:
509  std::filesystem::path mi = itsFilename.parent_path() / "modinfo.html";
510  if (std::filesystem::exists(mi)) std::filesystem::remove(mi);
511 
512  // After a save, execute any required action like reload module, reboot, recompile, etc:
513  itsWantAction = true;
514 }
515 
516 #endif // JEVOIS_PRO
Module.H
jevois::GUIeditor::saveFile
void saveFile()
Save a file.
Definition: GUIeditor.C:496
jevois::GUIeditor::EditorSaveAction
EditorSaveAction
Helper enum for actions to execute after saving a config file.
Definition: GUIeditor.H:34
jevois::GUIhelper
Helper class to assist modules in creating graphical and GUI elements.
Definition: GUIhelper.H:128
jevois::GUIeditor::draw
void draw()
Draw the editor into ImGui.
Definition: GUIeditor.C:113
Engine.H
Log.H
jevois::GUIeditor::refresh
void refresh()
Refresh list of files.
Definition: GUIeditor.C:50
jevois::EditorItem
Helper class to represent a GUIeditor file in a pull-down menu.
Definition: GUIeditor.H:37
jevois::GUIeditor::GUIeditor
GUIeditor(GUIhelper *helper, std::string const &imguiid, std::vector< EditorItem > &&fixeditems, std::string const &scanpath, std::string const &prefix, std::set< std::string > &&extensions)
Constructor.
Definition: GUIeditor.C:31
jevois::GUIeditor::~GUIeditor
virtual ~GUIeditor()
Destructor.
Definition: GUIeditor.C:46
GUIeditor.H
ImGui
Definition: GUIeditor.H:27
LINFO
#define LINFO(msg)
Convenience macro for users to print out console or syslog messages, INFO level.
Definition: Log.H:194
GUIhelper.H
jevois::GUIeditor::loadFile
void loadFile(std::filesystem::path const &fn)
Load a file and set it as the current file.
Definition: GUIeditor.C:362