HAL
python_editor.cpp
Go to the documentation of this file.
2 
3 #include "gui/action/action.h"
7 #include "gui/gui_globals.h"
15 #include "gui/splitter/splitter.h"
16 #include "gui/toolbar/toolbar.h"
17 #include "hal_core/utilities/log.h"
19 
20 #include <QAction>
21 #include <QDebug>
22 #include <QDesktopServices>
23 #include <QFileDialog>
24 #include <QFileInfo>
25 #include <QMenu>
26 #include <QShortcut>
27 #include <QTabBar>
28 #include <QTextDocumentFragment>
29 #include <QTextStream>
30 #include <QToolButton>
31 #include <QVBoxLayout>
32 #include <chrono>
33 #include <fstream>
36 #include <QDir>
37 #include "rapidjson/filereadstream.h"
38 
39 namespace hal
40 {
42  : ProjectSerializer("pythoneditor")
43  {
45  if (pm->get_project_status() == ProjectManager::ProjectStatus::NONE) return;
46 
47  mSaveDir = QString::fromStdString(pm->get_project_directory().get_filename("py").string());
48  QDir().mkpath(mSaveDir);
49 
50  std::string relname = pm->get_filename(m_name);
51  if (!relname.empty())
52  restoreTabs(pm->get_project_directory(), relname);
53  }
54 
56  std::string PythonSerializer::sControlFileName = "pythoneditor.json";
57 
58  std::string PythonSerializer::serialize(Netlist* netlist, const std::filesystem::path &savedir, bool isAutosave)
59  {
60  Q_UNUSED(netlist);
61  QDir pyDir(QString::fromStdString((savedir / sPythonRelDir.toStdString()).string()));
63 
64  return serialize_control(savedir,isAutosave);
65  }
66 
67  bool PythonSerializer::write_control_file(const std::filesystem::path& savedir, const std::vector<PythonEditorControlEntry>& tabinfo)
68  {
69  QDir workDir(QString::fromStdString(savedir.empty()
71  : savedir.string()));
72  QDir pyDir(workDir.absoluteFilePath(sPythonRelDir));
73  QString PythonEditorControlFile = workDir.absoluteFilePath(QString::fromStdString(sControlFileName));
74 
76 
77  doc["python_dir"] = sPythonRelDir.toStdString();
78  JsonWriteArray& tabArr = doc.add_array("tabs");
79 
80  for (const PythonEditorControlEntry& pece : tabinfo)
81  {
82  JsonWriteObject& tabObj = tabArr.add_object();
83  tabObj["tab"] = pece.tabInx;
84  if (pece.active)
85  tabObj["active"] = true;
86  if (!pece.restore.empty())
87  tabObj["restore"] = pece.restore;
88  tabObj["filename"] = pece.filename;
89  tabObj.close();
90  }
91 
92  tabArr.close();
93  return doc.serialize(PythonEditorControlFile.toStdString());
94  }
95 
96  std::string PythonSerializer::serialize_control(const std::filesystem::path& savedir, bool isAutosave)
97  {
98  QDir workDir(QString::fromStdString(savedir.empty()
100  : savedir.string()));
101  QDir pyDir(workDir.absoluteFilePath(sPythonRelDir));
102 
103  std::vector<PythonEditorControlEntry> tabinfo;
104 
106  if (!pedit) return std::string();
107 
108  QTabWidget* tabw = pedit->getTabWidget();
109  for (int tabInx=0; tabInx < tabw->count(); tabInx++)
110  {
111  PythonCodeEditor* pce = pedit->getPythonEditor(tabInx);
112  if (!pce) continue;
113 
114  PythonEditorControlEntry pece;
115  pece.tabInx = tabInx;
116  pece.active = (tabw->currentWidget() == pce);
117 
118  QString tabPath = pce->getRelFilename();
119  if (tabPath.isEmpty())
120  tabPath = pyDir.absoluteFilePath(pedit->unnamedFilename(tabInx));
121  else if (isAutosave)
122  {
123  pece.restore = tabPath.toStdString();
124  tabPath = pyDir.absoluteFilePath(pedit->autosaveFilename(tabInx));
125  }
126  QString pydirPrefix = pyDir.absolutePath() + "/";
127  QString tabFilename = tabPath.startsWith(pydirPrefix)
128  ? tabPath.mid(pydirPrefix.size())
129  : tabPath;
130  pece.filename = tabFilename.toStdString();
131  tabinfo.push_back(pece);
132  }
133 
134  if (!write_control_file(savedir,tabinfo))
135  {
136  log_warning("gui", "Failed to save python editor control file to project dir {}.", workDir.absolutePath().toStdString());
137  }
138  return sControlFileName;
139  }
140 
141  void PythonSerializer::deserialize(Netlist* netlist, const std::filesystem::path& loaddir)
142  {
143  Q_UNUSED(netlist);
144  std::string relname = ProjectManager::instance()->get_filename(m_name);
145  if (!relname.empty())
146  restoreTabs(loaddir, relname);
147  }
148 
149  void PythonSerializer::restoreTabs(const std::filesystem::path& loaddir, const std::string& jsonfile)
150  {
151  std::filesystem::path jsonpath(loaddir);
152  jsonpath.append(jsonfile);
153 
154  FILE* pytabFile = fopen(jsonpath.string().c_str(), "rb");
155  if (pytabFile == NULL)
156  {
157  log_error("GroupingSerializer::deserialize", "unable to open '{}'.", jsonpath.string());
158  return;
159  }
160 
161  PythonEditor* pedit = gContentManager->getPythonEditorWidget();
162 
163  char buffer[65536];
164  rapidjson::FileReadStream frs(pytabFile, buffer, sizeof(buffer));
165  rapidjson::Document document;
166  document.ParseStream<0, rapidjson::UTF8<>, rapidjson::FileReadStream>(frs);
167 
168  QDir pyDir(QString::fromStdString((loaddir / std::filesystem::path(document.HasMember("python_dir")?document["python_dir"].GetString():std::string("py"))).string()));
169 
170  bool restoreAutosave = false;
171 
172  int activeInx = -1;
173  if (document.HasMember("tabs"))
174  {
175  for (const rapidjson::Value& tabVal : document["tabs"].GetArray())
176  {
177  if (!tabVal.HasMember("tab") || !tabVal.HasMember("filename")) continue;
178  int tabInx = tabVal["tab"].GetUint();
179  QString tabFilename = QString::fromStdString(tabVal["filename"].GetString());
180  QString tabPath = QFileInfo(tabFilename).isRelative() ? pyDir.absoluteFilePath(tabFilename) : tabFilename;
181  if (tabFilename.startsWith(".autosave_tab") && tabVal.HasMember("restore"))
182  {
183  QString restorePath = QString::fromStdString(tabVal["restore"].GetString());
184  if (QFileInfo(restorePath).exists()) QFile::remove(restorePath);
185  QFile::copy(tabPath,restorePath);
186  tabPath = restorePath;
187  restoreAutosave = true;
188  }
189  pedit->tabLoadFile(tabInx, tabPath);
190  if (tabVal.HasMember("active"))
191  activeInx = tabInx;
192  }
193  }
194 
195  if (activeInx >= 0)
196  pedit->getTabWidget()->setCurrentIndex(activeInx);
197 
198  if (restoreAutosave)
199  {
200  std::filesystem::path workdir = ProjectManager::instance()->get_project_directory().get_canonical_path();
201  pedit->saveAllTabs(QString::fromStdString(workdir.string()),false);
202  pedit->saveControl();
203  }
204  }
205 
206 
208  : ContentWidget("Python Editor", parent), PythonContextSubscriber(), mSearchbar(new Searchbar(this)), mActionOpenFile(new Action(this)), mActionRun(new Action(this)),
209  mActionSave(new Action(this)), mActionSaveAs(new Action(this)), mActionToggleMinimap(new Action(this)), mActionNewFile(new Action(this)),
210  mFileWatcher(nullptr)
211  {
212  ensurePolished();
213  mNewFileCounter = 0;
214  mLastClickTime = 0;
215 
216  mTabWidget = new QTabWidget(this);
217  mTabWidget->setTabsClosable(true);
218  mTabWidget->setMovable(true);
219  // we need to grab mouse events from the tab bar
220  mTabWidget->tabBar()->installEventFilter(this);
221  mContentLayout->addWidget(mTabWidget);
223  mContentLayout->addWidget(mSearchbar);
224  mSearchbar->hide();
225  mSearchbar->setEmitTextWithFlags(false);
226 
227  mActionOpenFile->setIcon(gui_utility::getStyledSvgIcon(mOpenIconStyle, mOpenIconPath));
228  mActionSave->setIcon(gui_utility::getStyledSvgIcon(mSaveIconStyle, mSaveIconPath));
229  mActionSaveAs->setIcon(gui_utility::getStyledSvgIcon(mSaveAsIconStyle, mSaveAsIconPath));
230  mActionRun->setIcon(gui_utility::getStyledSvgIcon(mRunIconStyle, mRunIconPath));
231  mActionToggleMinimap->setIcon(gui_utility::getStyledSvgIcon(mToggleMinimapIconStyle, mToggleMinimapIconPath));
232  mActionNewFile->setIcon(gui_utility::getStyledSvgIcon(mNewFileIconStyle, mNewFileIconPath));
233  mSearchAction->setIcon(gui_utility::getStyledSvgIcon(mSearchIconStyle, mSearchIconPath));
234 
235  mActionOpenFile->setText("Open Script");
236  mActionSave->setText("Save");
237  mActionSaveAs->setText("Save as");
238  mActionRun->setText("Execute Script");
239  mActionNewFile->setText("New Script");
240  mActionToggleMinimap->setText("Toggle Minimap");
241  mSearchAction->setText("Search");
242 
244 
250  connect(mActionToggleMinimap, &Action::triggered, this, &PythonEditor::handleActionToggleMinimap);
252 
255 
257 
260 
261  mPathEditorMap = QMap<QString, PythonCodeEditor*>();
262 
263  mFileModifiedBar = new FileModifiedBar();
264  mFileModifiedBar->setHidden(true);
265  mContentLayout->addWidget(mFileModifiedBar);
269 
270  mFileWatcher = new QFileSystemWatcher(this);
273 
274  mSettingFontSize = new SettingsItemSpinbox("Font Size", "python/font_size", 11, "Python Editor", "Size of Font measured in pt");
275  mSettingFontSize->setRange(6, 48);
276 
277  mSettingLineNumbers = new SettingsItemCheckbox("Line Numbers", "python/line_numbers", true, "Python Editor", "Enables line numbers.");
278 
279  mSettingHighlight = new SettingsItemCheckbox("Highlight Current Lines", "python/highlight_current_line", true, "Python Editor", "The current line in the editor gets highlighted if enabled.");
280 
281  mSettingLineWrap = new SettingsItemCheckbox("Line Wrap", "python/line_wrap", false, "Python Editor", "Autowraps lines in the editor to prevent horizontal scroll bars.");
282 
283  mSettingMinimap = new SettingsItemCheckbox("Code Minimap", "python/minimap", false, "Python Editor", "Enable code minimap.");
284 
285  mSettingOpenFile = new SettingsItemKeybind(
286  "PyEditor Shortcut 'Open Python File'", "keybinds/python_open_file", QKeySequence("Ctrl+Shift+O"), "Keybindings: PyEditor", "Keybind for opening a python file in the Python Editor.");
287 
288  mSettingSaveFile = new SettingsItemKeybind(
289  "PyEditor Shortcut 'Save Python File'", "keybinds/python_save_file", QKeySequence("Ctrl+Shift+S"), "Keybindings: PyEditor", "Keybind for saving a python file in the Python Editor.");
290 
291  mSettingSaveFileAs = new SettingsItemKeybind("PyEditor Shortcut 'Save Python File As'",
292  "keybinds/python_save_file_as",
293  QKeySequence("Ctrl+Alt+S"),
294  "Keybindings: PyEditor",
295  "Keybind for saving a python file in the Python Editor 'as ...' in the Python Editor.");
296 
297  mSettingRunFile = new SettingsItemKeybind(
298  "PyEditor Shortcut 'Run Python File'", "keybinds/python_run_file", QKeySequence("Ctrl+R"), "Keybindings: PyEditor", "Keybind for executing a python file in the Python Editor.");
299 
300  mSettingCreateFile = new SettingsItemKeybind("PyEditor Shortcut 'Create New Python File'",
301  "keybinds/python_create_file",
302  QKeySequence("Ctrl+Shift+N"),
303  "Keybindings: PyEditor",
304  "Keybind for creating a new python file in the Python Editor.");
305 
307 
308  using namespace std::placeholders;
309  }
310 
311  bool PythonEditor::handleDeserializationFromHalFile(const std::filesystem::path& path, Netlist* netlist, rapidjson::Document& document)
312  {
313  UNUSED(path);
314 
315  QString netlist_name = QString::fromStdString(netlist->get_design_name());
316  // Replace all special characters from the netlist name to avoid file system problems
317  netlist_name.replace("/", "-");
318  netlist_name.replace("\\", "-");
319 
320  int cnt = 0;
321  if (document.HasMember("PythonEditor"))
322  {
323  auto root = document["PythonEditor"].GetObject();
324 
325  auto array = root["tabs"].GetArray();
326  for (auto it = array.Begin(); it != array.End(); ++it)
327  {
328  cnt++;
329  if (mTabWidget->count() < cnt)
330  {
332  }
333  auto val = it->GetObject();
334 
335  if (val.HasMember("path"))
336  {
337  QFileInfo original_path(val["path"].GetString());
338 
339  tabLoadFile(cnt - 1, original_path.filePath());
340  }
341  }
342  if (root.HasMember("selected_tab"))
343  {
344  mTabWidget->setCurrentIndex(root["selected_tab"].GetInt());
345  }
346  }
347 
348  return true;
349  }
350 
352  {
353  PythonCodeEditor* editor = dynamic_cast<PythonCodeEditor*>(mTabWidget->widget(index));
354  QString relFilename = editor->getRelFilename();
355  if (editor->document()->isModified())
356  {
357  QMessageBox::StandardButton ret = askSaveTab(index);
358 
359  if (ret == QMessageBox::Cancel)
360  return;
361 
362  // Remove existing snapshots
363  removeSnapshotFile(editor);
364 
365  // discard is not handled specially, we just treat the document
366  // as if it did not require saving and call discardTab on it
367 
368  if (ret == QMessageBox::Save)
369  {
370  if (relFilename.isEmpty())
371  {
372  bool suc = saveFile(false, QueryAlways, index);
373  if (!suc)
374  return;
375  }
376  else
377  saveFile(false, QueryIfEmpty, index);
378  }
379  this->discardTab(index);
380  }
381  else
382  {
383  this->discardTab(index);
384  }
385  saveControl();
386  }
387 
389  {
390  if (mTabWidget->currentWidget())
391  dynamic_cast<PythonCodeEditor*>(mTabWidget->currentWidget())->toggleMinimap();
392  }
393 
395  {
396  mLastClickTime = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
397  }
398 
400  {
401  if (std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count() - 100 < mLastClickTime)
402  {
403  PythonCodeEditor* currentEditor = dynamic_cast<PythonCodeEditor*>(mTabWidget->currentWidget());
404 
405  QString tab_name = mTabWidget->tabText(mTabWidget->indexOf(currentEditor));
406 
407  if (currentEditor)
408  gFileStatusManager->fileChanged(currentEditor->getUuid(), "Python tab: " + tab_name);
409 
410  if (!tab_name.endsWith("*"))
411  mTabWidget->setTabText(mTabWidget->indexOf(currentEditor), tab_name + "*");
412  }
413  }
414 
416  {
417  if (mTabWidget->count() > 0)
418  dynamic_cast<PythonCodeEditor*>(mTabWidget->currentWidget())->search(text, opts);
419 
420  if (mSearchbar->filterApplied())
421  mSearchAction->setIcon(gui_utility::getStyledSvgIcon(mSearchActiveIconStyle, mSearchIconPath));
422  else
423  mSearchAction->setIcon(gui_utility::getStyledSvgIcon(mSearchIconStyle, mSearchIconPath));
424  }
425 
427  {
428  Q_UNUSED(index)
429 
430  bool enable = mTabWidget->count() > 0;
431 
433 
434  QAction* entryBasedAction[] = {mActionSave, mActionSaveAs, mActionRun, mActionToggleMinimap, mSearchAction, nullptr};
435 
436  iconStyle << mSaveIconStyle << mSaveAsIconStyle << mRunIconStyle << mToggleMinimapIconStyle << mSearchIconStyle;
437  iconPath << mSaveIconPath << mSaveAsIconPath << mRunIconPath << mToggleMinimapIconPath << mSearchIconPath;
438 
439  for (int iacc = 0; entryBasedAction[iacc]; iacc++)
440  {
441  entryBasedAction[iacc]->setEnabled(enable);
442  entryBasedAction[iacc]->setIcon(gui_utility::getStyledSvgIcon(enable ? iconStyle.at(iacc) : disabledIconStyle(), iconPath.at(iacc)));
443  }
444 
445  if (!mTabWidget->currentWidget())
446  {
447  mSearchbar->hide();
448  return;
449  }
450 
451  PythonCodeEditor* currentEditor = dynamic_cast<PythonCodeEditor*>(mTabWidget->currentWidget());
452 
453  if (!mSearchbar->isHidden())
454  currentEditor->search(mSearchbar->getCurrentText());
455  else if (!currentEditor->extraSelections().isEmpty())
456  currentEditor->search("");
457 
458  if (currentEditor->isBaseFileModified() || (gPythonContext->pythonThread()))
459  mFileModifiedBar->setHidden(false);
460  else
461  mFileModifiedBar->setHidden(true);
462 
464  }
465 
467  {
468  }
469 
471  {
472  Toolbar->addAction(mActionNewFile);
473  Toolbar->addAction(mActionOpenFile);
474  Toolbar->addAction(mActionSave);
475  Toolbar->addAction(mActionSaveAs);
476  Toolbar->addAction(mActionRun);
477  Toolbar->addAction(mActionToggleMinimap);
479  }
480 
482  {
483  QShortcut* shortcutNewFile = new QShortcut(mSettingCreateFile->value().toString(), this);
484  QShortcut* shortcutOpenFile = new QShortcut(mSettingOpenFile->value().toString(), this);
485  QShortcut* shortcutSaveFile = new QShortcut(mSettingSaveFile->value().toString(), this);
486  QShortcut* shortcutSaveFileAs = new QShortcut(mSettingSaveFileAs->value().toString(), this);
487  QShortcut* shortcutRun = new QShortcut(mSettingRunFile->value().toString(), this);
489 
491  connect(shortcutNewFile, &QShortcut::activated, mActionNewFile, &QAction::trigger);
492  connect(shortcutOpenFile, &QShortcut::activated, mActionOpenFile, &QAction::trigger);
493  connect(shortcutSaveFile, &QShortcut::activated, mActionSave, &QAction::trigger);
494  connect(shortcutSaveFileAs, &QShortcut::activated, mActionSaveAs, &QAction::trigger);
495  connect(shortcutRun, &QShortcut::activated, mActionRun, &QAction::trigger);
496 
497  connect(mSettingCreateFile, &SettingsItemKeybind::keySequenceChanged, shortcutNewFile, &QShortcut::setKey);
498  connect(mSettingOpenFile, &SettingsItemKeybind::keySequenceChanged, shortcutOpenFile, &QShortcut::setKey);
499  connect(mSettingSaveFile, &SettingsItemKeybind::keySequenceChanged, shortcutSaveFile, &QShortcut::setKey);
500  connect(mSettingSaveFileAs, &SettingsItemKeybind::keySequenceChanged, shortcutSaveFileAs, &QShortcut::setKey);
501  connect(mSettingRunFile, &SettingsItemKeybind::keySequenceChanged, shortcutRun, &QShortcut::setKey);
502 
503  QList<QShortcut*> list;
504  list.append(shortcutNewFile);
505  list.append(shortcutOpenFile);
506  list.append(shortcutSaveFile);
507  list.append(shortcutSaveFileAs);
508  list.append(shortcutRun);
509  list.append(mSearchShortcut);
510 
511  return list;
512  }
513 
515  {
517  }
518 
519  void PythonEditor::handleError(const QString& output)
520  {
522  }
523 
525  {
526  }
527 
529  {
530  QString caption = "Open File";
531  QString filter = "Python Scripts(*.py)";
532 
533  // Non native dialogs does not work on macOS. Therefore do net set DontUseNativeDialog!
534  QStringList file_names = QFileDialog::getOpenFileNames(nullptr, caption, getDefaultPath(), filter);
535 
536  if (file_names.isEmpty())
537  {
538  return;
539  }
540 
541  for (auto fileName : file_names)
542  {
543  for (int i = 0; i < mTabWidget->count(); ++i)
544  {
545  auto editor = dynamic_cast<PythonCodeEditor*>(mTabWidget->widget(i));
546  if (editor->getAbsFilename() == fileName)
547  {
548  mTabWidget->setCurrentIndex(i);
549 
550  if (editor->document()->isModified())
551  {
552  if (QMessageBox::question(editor, "Script has unsaved changes", "Do you want to reload the file from disk? Unsaved changes are lost.", QMessageBox::Yes | QMessageBox::No)
553  == QMessageBox::Yes)
554  {
555  tabLoadFile(i, fileName);
556  }
557  }
558  return;
559  }
560  }
561 
563  tabLoadFile(mTabWidget->count() - 1, fileName);
564  }
565 
566  mDefaultPath = QFileInfo(file_names.last()).path();
567  }
568 
569  void PythonEditor::tabLoadFile(u32 index, QString fileName)
570  {
571  QFile pyFile(fileName);
572  if (!pyFile.open(QIODevice::ReadOnly))
573  return;
574 
575  QByteArray pyText = pyFile.readAll();
576  QFileInfo info(fileName);
577 
578  while (mTabWidget->count() <= (int)index)
580 
581  auto tab = dynamic_cast<PythonCodeEditor*>(mTabWidget->widget(index));
582 
583  tab->setPlainText(QString::fromUtf8(pyText));
584 
585  if (!info.fileName().startsWith(".unnamed_tab"))
586  {
587  tab->document()->setModified(false);
588  tab->setFilename(fileName);
589  mTabWidget->setTabText(mTabWidget->indexOf(tab), info.completeBaseName() + "." + info.completeSuffix());
590  mNewFileCounter--;
591 
592  mPathEditorMap.insert(fileName, tab);
593  mFileWatcher->addPath(fileName);
594 
595  gFileStatusManager->fileSaved(tab->getUuid());
596  }
597  }
598 
599  QString PythonEditor::getDefaultPath() const
600  {
601  if (!mDefaultPath.isEmpty()) return mDefaultPath;
603  if (pm->get_project_status() != ProjectManager::ProjectStatus::NONE)
604  return QString::fromStdString(pm->get_project_directory().get_filename("py").string());
605  return QDir::currentPath();
606  }
607 
609  {
610  return dynamic_cast<PythonCodeEditor*>(mTabWidget->widget(tabIndex));
611  }
612 
613  bool PythonEditor::saveFile(bool isAutosave, PythonEditor::QueryFilenamePolicy queryPolicy, int index)
614  {
615  QString title = "Save File";
616  QString filter = "Python Scripts(*.py)";
617 
618  QString selected_file_name;
619 
620  bool isUnnamed = false;
621 
622  if (index == -1)
623  {
624  index = mTabWidget->currentIndex();
625  }
626 
627  PythonCodeEditor* currentEditor = getPythonEditor(index);
628  if (!currentEditor)
629  return false;
630 
631  // currentFilename : unnamed -> empty autosave -> into autosave
632  QString currentFilename = currentEditor->getAbsFilename();
633  if (isAutosave && !currentFilename.isEmpty())
634  {
635  currentFilename = QDir(mGenericPath).absoluteFilePath(autosaveFilename(index));
636  }
637 
638  bool changeFileLocation = false;
639 
640  // evaluate query policy
641  if (queryPolicy == QueryAlways ||
642  ( queryPolicy == QueryIfEmpty && currentFilename.isEmpty()))
643  {
644  selected_file_name = QFileDialog::getSaveFileName(this, title, getDefaultPath(), filter, nullptr, QFileDialog::DontUseNativeDialog);
645  if (selected_file_name.isEmpty())
646  return false;
647 
648  if (!selected_file_name.endsWith(".py"))
649  selected_file_name.append(".py");
650 
651  changeFileLocation = true;
652  }
653  else if (queryPolicy == GenericName && currentFilename.isEmpty())
654  {
655  selected_file_name = QDir(mGenericPath).absoluteFilePath(unnamedFilename(index));
656  isUnnamed = true;
657  }
658  else
659  {
660  selected_file_name = currentFilename;
661  // Remove an existing snapshot
662  removeSnapshotFile(currentEditor);
663  }
664 
665  std::ofstream out(selected_file_name.toStdString(), std::ios::out);
666  if (!out.is_open())
667  {
668  log_error("gui", "could not open file path '{}' to serialize python script", selected_file_name.toStdString());
669  QMessageBox::warning(this, "Save Script Error", "Cannot save python script to\n<" + selected_file_name + ">");
670  return false;
671  }
672 
673  if (changeFileLocation)
674  {
675 
676  currentEditor->setFilename(selected_file_name);
677  mDefaultPath = QFileInfo(selected_file_name).path();
678 
679  // Remove an existing snapshot and update its location
680  removeSnapshotFile(currentEditor);
681  QString snapShotDirectory = getSnapshotDirectory(true);
682  if (!snapShotDirectory.isEmpty())
683  {
684  QString new_snapshot_path = snapShotDirectory + "/" + selected_file_name + ".py";
685  if(mTabToSnapshotPath.contains(currentEditor))
686  {
687  mTabToSnapshotPath[currentEditor] = new_snapshot_path;
688  }
689  else
690  {
691  mTabToSnapshotPath.insert(currentEditor, new_snapshot_path);
692  }
693  }
694  }
695 
696  bool isFileWatched = mFileWatcher->files().contains(currentFilename);
697  if (isFileWatched)
698  {
699  mFileWatcher->removePath(currentFilename);
700  mPathEditorMap.remove(currentFilename);
701  }
702 
703  out << currentEditor->toPlainText().toStdString();
704  out.close();
705 
706  if (!isAutosave)
707  {
708  currentEditor->document()->setModified(false);
709  gFileStatusManager->fileSaved(currentEditor->getUuid());
710  }
711 
712  if (isFileWatched)
713  {
714  mPathEditorMap.insert(selected_file_name, currentEditor);
715  mFileWatcher->addPath(selected_file_name);
716  }
717 
718  QFileInfo info(selected_file_name);
719  if (!isAutosave) {
720  if (isUnnamed)
721  {
722  QString unnamedTabName = mTabWidget->tabText(index);
723  while (unnamedTabName.endsWith("*")) unnamedTabName.chop(1);
724  mTabWidget->setTabText(index, unnamedTabName);
725  }
726  else
727  mTabWidget->setTabText(index, info.completeBaseName() + "." + info.completeSuffix());
728  }
729 
730  return true;
731  }
732 
734  {
735  return QString(".unnamed_tab%1.py").arg(index);
736  }
737 
739  {
740  PythonCodeEditor* pce = getPythonEditor(index);
741  if (!pce || pce->getRelFilename().isEmpty()) return unnamedFilename(index);
742  return QString(".autosave_tab%1_%2").arg(index).arg(QFileInfo(pce->getRelFilename()).fileName());
743  }
744 
745  void PythonEditor::saveAllTabs(const QString& genericPath, bool isAutosave)
746  {
747  mGenericPath = genericPath;
748  QDir().mkpath(mGenericPath);
749  QStringList retval;
750  for (int inx=0; inx<mTabWidget->count(); ++inx)
751  saveFile(isAutosave,GenericName,inx);
752  }
753 
755  {
756  mSerializer.serialize_control();
757  }
758 
760  {
761  return mTabWidget;
762  }
763 
764  void PythonEditor::discardTab(int index)
765  {
766  PythonCodeEditor* editor = dynamic_cast<PythonCodeEditor*>(mTabWidget->widget(index));
767  QString absFilename = editor->getAbsFilename();
768  if (!absFilename.isEmpty())
769  {
770  mFileWatcher->removePath(absFilename);
771  mPathEditorMap.remove(absFilename);
772  }
773  if (editor->document()->isModified())
774  {
776  }
777  mTabWidget->removeTab(index);
778  }
779 
780  bool PythonEditor::confirmDiscardForRange(int start, int end, int exclude)
781  {
782  QString changedFiles = "The following files have not been saved yet:\n";
783  int unsaved = 0;
784  int total = end - start - (exclude == -1 ? 0 : 1);
785  for (int t = start; t < end; t++)
786  {
787  // to disable, set exclude=-1
788  if (t == exclude)
789  continue;
790 
791  PythonCodeEditor* editor = dynamic_cast<PythonCodeEditor*>(mTabWidget->widget(t));
792  if (editor->document()->isModified())
793  {
794  QString fileName = mTabWidget->tabText(t);
795  fileName.chop(1); // removes asterisk
796  changedFiles.append(" -> " + fileName + "\n");
797  unsaved++;
798  }
799  }
800  if (unsaved)
801  {
802  QMessageBox msgBox;
803  msgBox.setStyleSheet("QLabel{min-width: 600px;}");
804  auto cancelButton = msgBox.addButton("Cancel", QMessageBox::RejectRole);
805  msgBox.addButton("Close Anyway", QMessageBox::ApplyRole);
806  msgBox.setDefaultButton(cancelButton);
807  msgBox.setInformativeText(QStringLiteral("Are you sure you want to close %1 tabs, %2 unsaved, anyway?").arg(total).arg(unsaved));
808  msgBox.setText("There are unsaved modifications that will be lost.");
809  msgBox.setDetailedText(changedFiles);
810 
811  msgBox.exec();
812  if (msgBox.clickedButton() == cancelButton)
813  {
814  return false;
815  }
816  }
817  return true;
818  }
819 
821  {
822  mActionSave->setEnabled(enable);
823  mActionSaveAs->setEnabled(enable);
824  mActionRun->setEnabled(enable);
825  mActionToggleMinimap->setEnabled(enable);
827  }
828 
830  {
831  this->saveFile(false, QueryIfEmpty);
832  saveControl();
833  }
834 
836  {
837  this->saveFile(false, QueryAlways);
838  saveControl();
839  }
840 
842  {
843  if (!mFileModifiedBar->isHidden())
844  {
845  QMessageBox::warning(this, "Script execution error", "Please respond to code editor message before executing the script");
846  return;
847  }
848 
849  mFileModifiedBar->handleScriptExecute(mTabWidget->tabText(mTabWidget->currentIndex()));
850  mFileModifiedBar->setHidden(false);
851 
852  // Update snapshots when clicking on run
853  this->updateSnapshots();
854 
856  {
857  mBlockedContextIds.append(ctx->id());
858  ctx->beginChange();
859  }
860 
861  gPythonContext->interpretScript(this,dynamic_cast<PythonCodeEditor*>(mTabWidget->currentWidget())->toPlainText());
862  }
863 
865  {
866  for (u32 ctxId : mBlockedContextIds)
867  {
869  if (ctx) ctx->endChange();
870  }
871  mBlockedContextIds.clear();
872 
873  mFileModifiedBar->setHidden(true);
874  }
875 
877  {
878  PythonCodeEditor* editor = new PythonCodeEditor();
879  editor->setFontSize(mSettingFontSize->value().toInt());
880  editor->setMinimapEnabled(mSettingMinimap->value().toBool());
881  editor->setLineWrapEnabled(mSettingLineWrap->value().toBool());
882  editor->setHighlightCurrentLineEnabled(mSettingHighlight->value().toBool());
883  editor->setLineNumberEnabled(mSettingLineNumbers->value().toBool());
884 
890 
891  new PythonSyntaxHighlighter(editor->document());
892  new PythonSyntaxHighlighter(editor->minimap()->document());
893  mTabWidget->addTab(editor, QString("Unnamed Script ").append(QString::number(++mNewFileCounter)));
894  mTabWidget->setCurrentIndex(mTabWidget->count() - 1);
895  editor->document()->setModified(false);
898  }
899 
901  {
902  QMenu context_menu(this);
903  QAction* action = context_menu.addAction("Close");
904 
905  context_menu.addSeparator();
907  action = context_menu.addAction("Close all");
909  action = context_menu.addAction("Close all others");
911  action = context_menu.addAction("Close all right");
913  action = context_menu.addAction("Close all left");
915 
916  context_menu.addSeparator();
917  action = context_menu.addAction("Save");
919  action = context_menu.addAction("Save as");
921 
922  context_menu.addSeparator();
923  action = context_menu.addAction("Show in system explorer");
924  PythonCodeEditor* editor = dynamic_cast<PythonCodeEditor*>(mTabWidget->widget(mTabRightclicked));
925  QString absFilename = editor->getAbsFilename();
926  action->setData(absFilename);
927  action->setDisabled(absFilename.isEmpty());
929 
930  context_menu.exec(QCursor::pos());
931  }
932 
934  {
935  assert(mTabRightclicked != -1);
936  this->handleTabCloseRequested(mTabRightclicked);
937  }
938 
940  {
941  assert(mTabRightclicked != -1);
942  int tabs = mTabWidget->count();
943  if (!this->confirmDiscardForRange(0, tabs))
944  return;
945  for (int t = 0; t < tabs; t++)
946  {
947  this->discardTab(0);
948  }
949  }
950 
952  {
953  assert(mTabRightclicked != -1);
954  int tabs = mTabWidget->count();
955  if (!this->confirmDiscardForRange(0, tabs, mTabRightclicked))
956  return;
957  int discard_id = 0; // keeps track of IDs shifting during deletion
958  for (int t = 0; t < tabs; t++)
959  {
960  // don't close the right-clicked tab
961  if (t == mTabRightclicked)
962  {
963  discard_id++;
964  continue;
965  }
966  this->discardTab(discard_id);
967  }
968  }
969 
971  {
972  assert(mTabRightclicked != -1);
973  if (!this->confirmDiscardForRange(0, mTabRightclicked, -1))
974  return;
975  for (int t = 0; t < mTabRightclicked; t++)
976  {
977  // IDs shift downwards during deletion
978  this->discardTab(0);
979  }
980  }
981 
983  {
984  assert(mTabRightclicked != -1);
985  int tabs = mTabWidget->count();
986  if (!this->confirmDiscardForRange(mTabRightclicked + 1, tabs, -1))
987  return;
988  for (int t = mTabRightclicked + 1; t < tabs; t++)
989  {
990  // IDs shift downwards during deletion
991  this->discardTab(mTabRightclicked + 1);
992  }
993  }
994 
996  {
997  QAction* action = dynamic_cast<QAction*>(sender());
998  if (!action)
999  {
1000  log_error("gui", "could not cast sender into QAction.");
1001  return;
1002  }
1003 
1004  //the data is set in the handleActionTabMenu (the path of the underlying file)
1005  QFileInfo info(action->data().toString());
1006  if (!info.exists())
1007  {
1008  log_error("gui", "File does not exist.");
1009  return;
1010  }
1011 
1013  }
1014 
1016  {
1017  PythonCodeEditor* editor_with_modified_base_file = mPathEditorMap.value(path);
1018  editor_with_modified_base_file->setBaseFileModified(true);
1019  int tabIndex = mTabWidget->indexOf(editor_with_modified_base_file);
1020  QString tab_name = mTabWidget->tabText(tabIndex);
1021 
1022  if (!tab_name.endsWith("*"))
1023  mTabWidget->setTabText(tabIndex, tab_name + "*");
1024 
1025  gFileStatusManager->fileChanged(editor_with_modified_base_file->getUuid(), "Python tab: " + tab_name);
1026 
1027  PythonCodeEditor* currentEditor = getPythonEditor(tabIndex);
1028 
1029  if (editor_with_modified_base_file == currentEditor)
1030  mFileModifiedBar->setHidden(false);
1031 
1032  mFileWatcher->addPath(path);
1033  }
1034 
1036  {
1037  PythonCodeEditor* currentEditor = dynamic_cast<PythonCodeEditor*>(mTabWidget->currentWidget());
1038  mNewFileCounter++;
1039  //tabLoadFile(currentEditor, currentEditor->getFileName());
1040  tabLoadFile(mTabWidget->indexOf(currentEditor), currentEditor->getAbsFilename());
1041  currentEditor->setBaseFileModified(false);
1042  mFileModifiedBar->setHidden(true);
1043  }
1044 
1046  {
1047  PythonCodeEditor* currentEditor = dynamic_cast<PythonCodeEditor*>(mTabWidget->currentWidget());
1048  currentEditor->setBaseFileModified(false);
1049  mFileModifiedBar->setHidden(true);
1050  }
1051 
1053  {
1054  PythonCodeEditor* currentEditor = dynamic_cast<PythonCodeEditor*>(mTabWidget->currentWidget());
1055  currentEditor->setBaseFileModified(false);
1056  mFileModifiedBar->setHidden(true);
1057  }
1058 
1060  {
1061  // Check for snapshots and load them if available
1062  QPair<QMap<QString, QString>, QVector<QString>> snapshots = this->loadAllSnapshots();
1063  QMap<QString, QString> saved_snapshots = snapshots.first;
1064  QVector<QString> unsaved_snapshots = snapshots.second;
1065 
1066  if (saved_snapshots.isEmpty() && unsaved_snapshots.isEmpty())
1067  {
1068  // No snapshots found. Nothing to do.
1069  return;
1070  }
1071 
1072  if (!fileName.endsWith(".hal"))
1073  {
1074  // The .v/.vhdl file was parsed again.
1075  bool deleteSnapshots = askDeleteSnapshots(snapshots);
1076  if (deleteSnapshots)
1077  {
1078  // Delete and ignore all snapshots
1079  clearAllSnapshots(true);
1080  return;
1081  }
1082  else
1083  {
1084  // The empty tab is closed to open all unstored snapshots
1085  if (mTabWidget->count() == 1)
1086  {
1087  this->discardTab(0);
1088  }
1089  }
1090  }
1091 
1092  int tabs = mTabWidget->count();
1093  // Handle the tabs that were already opened (by the infos from the .hal file)
1094  for (int idx = 0; idx < tabs; idx++)
1095  {
1096  PythonCodeEditor* editor = dynamic_cast<PythonCodeEditor*>(mTabWidget->widget(idx));
1097  QFileInfo original_path(editor->getAbsFilename());
1098 
1099  // Decide whether the snapshot file or the original should be loaded
1100  bool load_snapshot = decideLoadSnapshot(saved_snapshots, original_path);
1101 
1102  if (load_snapshot)
1103  {
1104  this->setSnapshotContent(idx, saved_snapshots[original_path.absoluteFilePath()]);
1105  }
1106  saved_snapshots.remove(original_path.absoluteFilePath());
1107  }
1108 
1109  // Handle paths that did not appear int the .hal file
1110  for (auto snapshot_original_path : saved_snapshots.keys())
1111  {
1112  QFileInfo original_path(snapshot_original_path);
1113  bool load_snapshot = decideLoadSnapshot(saved_snapshots, original_path);
1115  int tab_idx = mTabWidget->count() - 1;
1116  tabLoadFile(tab_idx, original_path.filePath());
1117  if (load_snapshot)
1118  {
1119  this->setSnapshotContent(tab_idx, saved_snapshots[snapshot_original_path]);
1120  }
1121  //(load_snapshot) ? tabLoadFile(tab_idx, original_path.filePath()) : setSnapshotContent(tab_idx, saved_snapshots[snapshot_original_path]);
1122  }
1123 
1124  // Load snapshots of unsaved tabs
1125  for (QString snapshot_content : unsaved_snapshots)
1126  {
1127  this->handleActionNewTab();
1128  this->setSnapshotContent(mTabWidget->count() - 1, snapshot_content);
1129  }
1130  updateSnapshots();
1131  }
1132 
1134  {
1135  Q_UNUSED(fileName)
1136  clearAllSnapshots(true);
1137 
1138  //clear all open tabs and reset the edior
1139  while (mTabWidget->count() > 0)
1140  discardTab(0);
1141  mNewFileCounter = 0;
1142  mLastClickTime = 0;
1144  }
1145 
1147  {
1148  if (mSearchbar->filterApplied() && mSearchbar->isVisible())
1149  mSearchAction->setIcon(gui_utility::getStyledSvgIcon(mSearchActiveIconStyle, mSearchIconPath));
1150  else if (!mSearchAction->isEnabled())
1151  mSearchAction->setIcon(gui_utility::getStyledSvgIcon(mDisabledIconStyle, mSearchIconPath));
1152  else
1153  mSearchAction->setIcon(gui_utility::getStyledSvgIcon(mSearchIconStyle, mSearchIconPath));
1154  }
1155 
1157  {
1158  if (obj == mTabWidget->tabBar() && event->type() == QEvent::MouseButtonPress)
1159  {
1160  QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
1161  // filter for right-mouse-button-pressed events
1162  if (mouseEvent->button() == Qt::MouseButton::RightButton)
1163  {
1164  mTabRightclicked = mTabWidget->tabBar()->tabAt(mouseEvent->pos());
1165  this->handleActionTabMenu();
1166  return true;
1167  }
1168  }
1169  // otherwise honor default filter
1170  return QObject::eventFilter(obj, event);
1171  }
1172 
1173  QPair<QString, QString> PythonEditor::readSnapshotFile(QFileInfo snapshot_file_path) const
1174  {
1175  QFile snapshot_file(snapshot_file_path.absoluteFilePath());
1176  if (!snapshot_file.open(QIODevice::ReadOnly))
1177  {
1178  log_error("gui", "Cannot open snapshot file {}!", snapshot_file_path.absoluteFilePath().toStdString());
1179  return QPair<QString, QString>("", "");
1180  }
1181 
1182  QTextStream stream(&snapshot_file);
1183  QString file_content = stream.readAll();
1184  QString original_file_path = file_content.section('\n', 0, 0);
1185  QString snapshot_content = file_content.section('\n', 1);
1186 
1187  return QPair<QString, QString>(original_file_path, snapshot_content);
1188  }
1189 
1190  QPair<QMap<QString, QString>, QVector<QString>> PythonEditor::loadAllSnapshots()
1191  {
1192  QString snapshot_path = this->getSnapshotDirectory(false);
1193 
1194  if (snapshot_path.isEmpty())
1195  {
1197  }
1198  QMap<QString, QString> stored_snapshot_map;
1199  QVector<QString> unstored_snapshots;
1200  QDir snapshot_dir(snapshot_path);
1201 
1202  QStringList snapshot_files = snapshot_dir.entryList(QStringList() << "*.py", QDir::Files);
1203 
1204  for (QString snapshot_file_name : snapshot_files)
1205  {
1206  QString snapshot_file_path = snapshot_dir.absoluteFilePath(snapshot_file_name);
1207  QPair<QString, QString> original_path_and_content = this->readSnapshotFile(snapshot_file_path);
1208  QString original_path = original_path_and_content.first;
1209  if (original_path.isEmpty() || (!QFileInfo(original_path).exists()))
1210  {
1211  // Original File does not exist
1212  unstored_snapshots.append(original_path_and_content.second);
1213  }
1214  else
1215  {
1216  // An original file exists
1217  stored_snapshot_map.insert(original_path_and_content.first, original_path_and_content.second);
1218  }
1219  }
1220 
1221  return QPair<QMap<QString, QString>, QVector<QString>>(stored_snapshot_map, unstored_snapshots);
1222  }
1223 
1224  bool PythonEditor::writeSnapshotFile(QFileInfo snapshot_file_path, QString original_file_path, QString content) const
1225  {
1226  QFile snapshot_file(snapshot_file_path.filePath());
1227  if (!snapshot_file.open(QIODevice::WriteOnly))
1228  {
1229  log_error("gui", "Cannot open snapshot file to write!");
1230  return false;
1231  }
1232 
1233  snapshot_file.write(original_file_path.toUtf8());
1234  snapshot_file.write("\n");
1235  snapshot_file.write(content.toUtf8());
1236  snapshot_file.close();
1237 
1238  return true;
1239  }
1240 
1241  QString PythonEditor::getSnapshotDirectory(const bool create_if_non_existent)
1242  {
1243  if (!FileManager::get_instance()->fileName().isEmpty())
1244  {
1245  QFileInfo info(FileManager::get_instance()->fileName());
1246  QDir snapshot_dir = info.absoluteDir();
1247  QString completePath = snapshot_dir.absolutePath() + "/~" + info.baseName();
1248 
1249  if (!snapshot_dir.exists(completePath))
1250  {
1251  if (create_if_non_existent)
1252  {
1253  if (!snapshot_dir.mkpath(completePath))
1254  {
1255  log_error("gui", "Failed to create snapshot directory.");
1256  return "";
1257  }
1258  }
1259  else
1260  return "";
1261  }
1262  return completePath;
1263  }
1264 
1265  return "";
1266  }
1267 
1268  void PythonEditor::updateSnapshots()
1269  {
1270  this->clearAllSnapshots(false);
1271  mTabToSnapshotPath.clear();
1272 
1273  QDir snapshot_dir = this->getSnapshotDirectory(true);
1274 
1275  int tabs = mTabWidget->count();
1276  for (int index = 0; index < tabs; index++)
1277  {
1278  PythonCodeEditor* editor = dynamic_cast<PythonCodeEditor*>(mTabWidget->widget(index));
1279  QString snapshot_file_name = "~";
1280  if (editor->getRelFilename().isEmpty())
1281  {
1282  // The Tab is unstored
1283  snapshot_file_name += "unsaved_tab";
1284  }
1285  else
1286  {
1287  // An original file exists
1288  QFileInfo original_file_name(editor->getRelFilename());
1289  snapshot_file_name += original_file_name.fileName();
1290  }
1291  //if the filename ends with .py because it is loaded from (or saved to) a file, insert the tabindex before the file extension, otherwise append the index and extension
1292 
1293  snapshot_file_name = QFileInfo(snapshot_file_name).fileName() + QString("__(%1)__.py").arg(index);
1294 
1295  QString snapshot_file_path = snapshot_dir.absoluteFilePath(snapshot_file_name);
1296 
1297  mTabToSnapshotPath.insert(editor, snapshot_file_path);
1298 
1299  if (editor->document()->isModified())
1300  {
1301  // The snapshot is only created if there are modifications
1302  this->writeSnapshotFile(snapshot_file_path, editor->getAbsFilename(), editor->toPlainText());
1303  }
1304  }
1305  }
1306 
1307  void PythonEditor::clearAllSnapshots(bool remove_dir)
1308  {
1309  QString snapshot_dir_path = this->getSnapshotDirectory(false);
1310 
1311  if (snapshot_dir_path == "")
1312  {
1313  // The directory does not exist. Nothing to do then
1314  return;
1315  }
1316  QDir snapshot_dir(snapshot_dir_path);
1317 
1318  // Make sure the directory is a snapshot directory (additional protection)
1319  if (!snapshot_dir.dirName().startsWith('~'))
1320  {
1321  log_error("gui", "Can not delete directory: '{}' is not a snapshot directory!", snapshot_dir.absolutePath().toStdString());
1322  return;
1323  }
1324 
1325  if (remove_dir)
1326  {
1327  snapshot_dir.removeRecursively();
1328  }
1329  else
1330  {
1331  // Remove all .py files in the directory
1332  snapshot_dir.setNameFilters(QStringList() << "*.py");
1333  snapshot_dir.setFilter(QDir::Files);
1334  for (QString dirFile : snapshot_dir.entryList())
1335  {
1336  snapshot_dir.remove(dirFile);
1337  }
1338  }
1339  }
1340 
1341  bool PythonEditor::decideLoadSnapshot(const QMap<QString, QString>& saved_snapshots, const QFileInfo original_path) const
1342  {
1343  bool load_snapshot = false;
1344  if (saved_snapshots.contains(original_path.absoluteFilePath()))
1345  {
1346  if (original_path.exists())
1347  {
1348  // Ask user whether the original file or the snapshot should be loaded
1349  QString original_content;
1350  QFile original_file(original_path.absoluteFilePath());
1351  if (!original_file.open(QIODevice::ReadOnly))
1352  {
1353  original_content = "Cannot open original file...";
1354  }
1355  else
1356  {
1357  original_content = QString::fromStdString(original_file.readAll().toStdString());
1358  }
1359  load_snapshot = askLoadSnapshot(original_path.absoluteFilePath(), original_content, saved_snapshots[original_path.absoluteFilePath()]);
1360  }
1361  else
1362  {
1363  load_snapshot = true;
1364  }
1365  }
1366  return load_snapshot;
1367  }
1368 
1369  void PythonEditor::setSnapshotContent(const int idx, const QString snapshot_content)
1370  {
1371  if (idx < 0 || idx >= mTabWidget->count())
1372  {
1373  log_error("gui", "Cannot insert snapshot content is tab. Index {} is out of range.", idx);
1374  }
1375  PythonCodeEditor* tab = dynamic_cast<PythonCodeEditor*>(mTabWidget->widget(idx));
1376  // Set the snapshot content
1377  tab->setPlainText(snapshot_content);
1378  // Mark the tab as modified
1379  tab->document()->setModified(true);
1380  QString tab_name = mTabWidget->tabText(idx);
1381  if (!tab_name.endsWith("*"))
1382  {
1383  tab_name += "*";
1384  mTabWidget->setTabText(idx, tab_name);
1385  }
1386  gFileStatusManager->fileChanged(tab->getUuid(), "Python tab: " + tab_name);
1387  }
1388 
1389  bool PythonEditor::askLoadSnapshot(const QString original_path, const QString original_content, const QString snapshot_content) const
1390  {
1391  QMessageBox msgBox((QWidget*)nullptr);
1392  msgBox.setIcon(QMessageBox::Question);
1393  msgBox.setWindowTitle("Python snapshot file detected");
1394  msgBox.setText("A snapshot file (for " + original_path
1395  + ") was found! This may happen due to a recent crash.\n"
1396  "Do you want to load the snapshot file or the (unsaved) original file?");
1397  auto load_snapshot_btn = msgBox.addButton("Load Snapshot", QMessageBox::ActionRole);
1398  msgBox.addButton("Load Original", QMessageBox::ActionRole);
1399 
1400  // Details
1401  QString detailed_text = "";
1402 
1403  detailed_text = "=== Original File (" + original_path + ") ===\n" + original_content + "\n=== Snapshot File ===\n" + snapshot_content;
1404 
1405  msgBox.setDetailedText(detailed_text);
1406 
1407  QSpacerItem* horizontalSpacer = new QSpacerItem(500, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
1408  QGridLayout* layout = (QGridLayout*)msgBox.layout();
1409  layout->addItem(horizontalSpacer, layout->rowCount(), 0, 1, layout->columnCount());
1410 
1411  msgBox.exec();
1412 
1413  if (msgBox.clickedButton() == (QAbstractButton*)load_snapshot_btn)
1414  {
1415  return true;
1416  }
1417  return false;
1418  }
1419 
1420  bool PythonEditor::askDeleteSnapshots(const QPair<QMap<QString, QString>, QVector<QString>>& snapshots) const
1421  {
1422  QMessageBox msgBox((QWidget*)nullptr);
1423  msgBox.setIcon(QMessageBox::Question);
1424  msgBox.setWindowTitle("Python snapshot file detected");
1425  msgBox.setText("You have just parsed an hdl file but there are still old snapshot files left. This may happen due to a recent crash. "
1426  "Do you want to ignore and delete all old snapshot files and stay with their last saved state? "
1427  "Or do you want to open the old python scripts to save them properly?");
1428  auto delete_snapshots_btn = msgBox.addButton("Delete and Ignore Snapshots", QMessageBox::ActionRole);
1429  msgBox.addButton("Open Old Scripts", QMessageBox::ActionRole);
1430 
1431  // Details
1432  QString detailed_text = "";
1433 
1434  detailed_text = "Snapshot files of the following paths were found:";
1435  for (auto origPath : snapshots.first.keys())
1436  {
1437  detailed_text += "\n'" + origPath + "'";
1438  }
1439  if (!snapshots.second.isEmpty())
1440  {
1441  detailed_text += "\n+ " + QString::number(snapshots.second.size()) + " unsaved tabs";
1442  }
1443 
1444  msgBox.setDetailedText(detailed_text);
1445 
1446  QSpacerItem* horizontalSpacer = new QSpacerItem(800, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
1447  QGridLayout* layout = (QGridLayout*)msgBox.layout();
1448  layout->addItem(horizontalSpacer, layout->rowCount(), 0, 1, layout->columnCount());
1449 
1450  msgBox.exec();
1451 
1452  if (msgBox.clickedButton() == (QAbstractButton*)delete_snapshots_btn)
1453  {
1454  return true;
1455  }
1456  return false;
1457  }
1458 
1459  QMessageBox::StandardButton PythonEditor::askSaveTab(const int tab_index) const
1460  {
1461  QMessageBox msgBox;
1462  msgBox.setStyleSheet("QLabel{min-width: 600px;}");
1463  msgBox.setText(mTabWidget->tabText(tab_index).append(" has been modified."));
1464  msgBox.setInformativeText("Do you want to save your changes?");
1467  return static_cast<QMessageBox::StandardButton>(msgBox.exec());
1468  }
1469 
1470  void PythonEditor::removeSnapshotFile(PythonCodeEditor* editor) const
1471  {
1472  if (!mTabToSnapshotPath.contains(editor))
1473  {
1474  return;
1475  }
1476 
1477  QFileInfo snapshot_path(mTabToSnapshotPath[editor]);
1478  if (snapshot_path.exists())
1479  {
1480  QFile snapshot_file(snapshot_path.filePath());
1481  snapshot_file.remove();
1482  }
1483  }
1484 
1486  {
1487  return mDisabledIconStyle;
1488  }
1489 
1491  {
1492  return mOpenIconPath;
1493  }
1494 
1496  {
1497  return mOpenIconStyle;
1498  }
1499 
1501  {
1502  return mSaveIconPath;
1503  }
1504 
1506  {
1507  return mSaveIconStyle;
1508  }
1509 
1511  {
1512  return mSaveAsIconPath;
1513  }
1514 
1516  {
1517  return mSaveAsIconStyle;
1518  }
1519 
1521  {
1522  return mRunIconPath;
1523  }
1524 
1526  {
1527  return mRunIconStyle;
1528  }
1529 
1531  {
1532  return mNewFileIconPath;
1533  }
1534 
1536  {
1537  return mNewFileIconStyle;
1538  }
1539 
1541  {
1542  return mToggleMinimapIconPath;
1543  }
1544 
1546  {
1547  return mToggleMinimapIconStyle;
1548  }
1549 
1551  {
1552  mDisabledIconStyle = style;
1553  }
1554 
1556  {
1557  mOpenIconPath = path;
1558  }
1559 
1561  {
1562  mOpenIconStyle = style;
1563  }
1564 
1566  {
1567  mSaveIconPath = path;
1568  }
1569 
1571  {
1572  mSaveIconStyle = style;
1573  }
1574 
1576  {
1577  mSaveAsIconPath = path;
1578  }
1579 
1581  {
1582  mSaveAsIconStyle = style;
1583  }
1584 
1586  {
1587  mRunIconPath = path;
1588  }
1589 
1591  {
1592  mRunIconStyle = style;
1593  }
1594 
1596  {
1597  mNewFileIconPath = path;
1598  }
1599 
1601  {
1602  mNewFileIconStyle = style;
1603  }
1604 
1606  {
1607  mToggleMinimapIconPath = path;
1608  }
1609 
1611  {
1612  mToggleMinimapIconStyle = style;
1613  }
1614 
1616  {
1617  if (!mSearchAction->isEnabled())
1618  return;
1619 
1620  if (mSearchbar->isHidden())
1621  {
1622  mSearchbar->show();
1623  mSearchbar->setFocus();
1624  }
1625  else
1626  {
1627  mSearchbar->hide();
1628  if (mTabWidget->currentWidget())
1629  mTabWidget->currentWidget()->setFocus();
1630  else
1631  this->setFocus();
1632  }
1633  }
1634 
1636  {
1638  if (mSearchbar->getSearchOptions().isCaseSensitive())
1639  options = options | QTextDocument::FindCaseSensitively;
1640  if (mSearchbar->getSearchOptions().isExactMatch())
1641  options = options | QTextDocument::FindWholeWords;
1642  return options;
1643  }
1644 
1646  {
1647  return mSearchIconPath;
1648  }
1649 
1651  {
1652  return mSearchIconStyle;
1653  }
1654 
1656  {
1657  return mSearchActiveIconStyle;
1658  }
1659 
1661  {
1662  mSearchIconPath = path;
1663  }
1664 
1666  {
1667  mSearchIconStyle = style;
1668  }
1669 
1671  {
1672  mSearchActiveIconStyle = style;
1673  }
1674 } // namespace hal
Provides an interface for triggerable functionality that can be inserted into widgets and also connec...
Definition: action.h:42
void setText(const QString &text)
Definition: action.cpp:21
void setHighlightCurrentLineEnabled(bool enabled)
void setFontSize(int pt)
void setLineWrapEnabled(bool enabled)
void setMinimapEnabled(bool enabled)
CodeEditorMinimap * minimap()
void search(const QString &string, SearchOptions searchOpts=8)
void setLineNumberEnabled(bool enabled)
PythonEditor * getPythonEditorWidget()
Abstract class for Widgets within HAL's ContentArea.
QKeySequence mSearchKeysequence
QShortcut * mSearchShortcut
QVBoxLayout * mContentLayout
static FileManager * get_instance()
void fileAboutToClose(const QString &fileName)
void fileOpened(const QString &fileName)
A dialog in form of a bar to let the user decide how to handle file changes outside of HAL.
void handleFileChanged(QString path)
void handleScriptExecute(QString path)
void fileChanged(const QUuid uuid, const QString &descriptor)
void fileSaved(const QUuid uuid)
Logical container for modules, gates, and nets.
Definition: graph_context.h:55
GraphContext * getContextById(u32 id) const
QVector< GraphContext * > getContexts() const
JsonWriteObject & add_object()
bool serialize(const std::string &filename)
JsonWriteArray & add_array(const std::string &tag)
std::filesystem::path get_filename(const std::string &relative_filename) const
std::filesystem::path get_canonical_path() const
static ProjectManager * instance()
std::string get_filename(const std::string &serializer_name)
ProjectStatus get_project_status() const
const ProjectDirectory & get_project_directory() const
Code editor to write python code.
void setFilename(const QString &name)
QString getAbsFilename() const
void keyPressed(QKeyEvent *e)
void setBaseFileModified(bool base_file_modified)
QString getRelFilename() const
Interface for handling python outputs.
Main widget that combines all neccessary functionality to develop in python (for hal).
Definition: python_editor.h:99
virtual void clear() override
void handleBaseFileModifiedIgnore()
void setToggleMinimapIconStyle(const QString &style)
void handleCurrentTabChanged(int index)
void tabLoadFile(u32 index, QString fileName)
bool handleDeserializationFromHalFile(const std::filesystem::path &path, Netlist *netlist, rapidjson::Document &document)
void handleSearchbarTextEdited(const QString &text, SearchOptions opts)
QTextDocument::FindFlags getFindFlags()
void setSearchActiveIconStyle(const QString &style)
void handleActionCloseOtherTabs()
void handleTabCloseRequested(int index)
void handleTabFileChanged(QString path)
void setRunIconStyle(const QString &style)
void setRunIconPath(const QString &path)
bool confirmDiscardForRange(int start, int end, int exclude=-1)
void discardTab(int index)
virtual QList< QShortcut * > createShortcuts() override
void setSaveAsIconPath(const QString &path)
void forwardError(const QString &output)
void forwardStdout(const QString &output)
PythonCodeEditor * getPythonEditor(int tabIndex)
void setDisabledIconStyle(const QString &style)
QString searchActiveIconStyle
void setSaveIconStyle(const QString &style)
void setSaveAsIconStyle(const QString &style)
QTabWidget * getTabWidget()
QString newFileIconStyle
void handleFileOpened(QString fileName)
QString toggleMinimapIconPath
void handleActionCloseRightTabs()
PythonEditor(QWidget *parent=nullptr)
void handleBaseFileModifiedReload()
virtual void setupToolbar(Toolbar *Toolbar) override
void setSearchIconPath(const QString &path)
void handleFileAboutToClose(const QString &fileName)
void setSaveIconPath(const QString &path)
void setToggleMinimapIconPath(const QString &path)
virtual void handleError(const QString &output) override
bool eventFilter(QObject *obj, QEvent *event) override
void handleActionCloseAllTabs()
void setSearchIconStyle(const QString &style)
void setNewFileIconStyle(const QString &style)
virtual void handleStdout(const QString &output) override
bool saveFile(bool isAutosave, QueryFilenamePolicy queryPolicy, int index=-1)
void setOpenIconStyle(const QString &style)
void setToolbarButtonsEnabled(bool enable)
void saveAllTabs(const QString &genericPath, bool isAutosave)
QString toggleMinimapIconStyle
void handleActionCloseLeftTabs()
QString autosaveFilename(int index)
QString unnamedFilename(int index) const
void setOpenIconPath(const QString &path)
void handleActionToggleMinimap()
void setNewFileIconPath(const QString &path)
QString disabledIconStyle
std::string serialize(Netlist *netlist, const std::filesystem::path &savedir, bool isAutosave)
static std::string sControlFileName
Definition: python_editor.h:87
void deserialize(Netlist *netlist, const std::filesystem::path &loaddir)
std::string serialize_control(const std::filesystem::path &savedir=std::filesystem::path(), bool isAutosave=false)
static QString sPythonRelDir
Definition: python_editor.h:86
A syntax highlighter that fits for python code.
bool isExactMatch() const
bool isCaseSensitive() const
A QFrame with a QLineEdit that can be used to input a substring to search for.
Definition: searchbar.h:48
bool filterApplied()
Definition: searchbar.cpp:259
SearchOptions getSearchOptions() const
Definition: searchbar.cpp:74
void setEmitTextWithFlags(bool)
Definition: searchbar.cpp:236
QString getCurrentText()
Definition: searchbar.cpp:166
void triggerNewSearch(const QString &text, int searchOptions)
A SettingsItem representing a Checkbox.
virtual QVariant value() const override
A SettingsItem to modify keybinds.
void keySequenceChanged(QKeySequence value)
virtual QVariant value() const override
A SettingsItem that represents a spinbox.
void intChanged(int value)
virtual QVariant value() const override
void setRange(int min, int max)
Toolbar for all ContentFrames and ContentWidgets.
Definition: toolbar.h:39
#define UNUSED(expr)
Definition: defines.h:49
#define log_error(channel,...)
Definition: log.h:78
#define log_warning(channel,...)
Definition: log.h:76
action
Definition: control.py:16
QIcon getStyledSvgIcon(const QString &from_to_colors_enabled, const QString &svg_path, QString from_to_colors_disabled=QString())
Definition: graphics.cpp:60
PythonContext * gPythonContext
Definition: plugin_gui.cpp:88
ContentManager * gContentManager
Definition: plugin_gui.cpp:78
FileStatusManager * gFileStatusManager
Definition: plugin_gui.cpp:84
GraphContextManager * gGraphContextManager
Definition: plugin_gui.cpp:85
quint32 u32
void setEnabled(bool)
void setIcon(const QIcon &icon)
void setText(const QString &text)
void trigger()
void triggered(bool checked)
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
QPoint pos()
bool openUrl(const QUrl &url)
QString absoluteFilePath(const QString &fileName) const const
QString absolutePath() const const
QString currentPath()
QString dirName() const const
QStringList entryList(QDir::Filters filters, QDir::SortFlags sort) const const
bool exists() const const
bool mkpath(const QString &dirPath) const const
bool remove(const QString &fileName)
bool removeRecursively()
void setFilter(QDir::Filters filters)
void setNameFilters(const QStringList &nameFilters)
MouseButtonPress
bool copy(const QString &newName)
virtual bool open(QIODevice::OpenMode mode) override
bool remove()
QStringList getOpenFileNames(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options)
QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options)
QString absoluteFilePath() const const
QString absolutePath() const const
QString completeBaseName() const const
QString completeSuffix() const const
bool exists() const const
QString fileName() const const
QString filePath() const const
bool isRelative() const const
QString path() const const
bool addPath(const QString &path)
void fileChanged(const QString &path)
QStringList files() const const
bool removePath(const QString &path)
QByteArray readAll()
virtual void addItem(QLayoutItem *item)=0
virtual QLayout * layout() override
void append(const T &value)
bool isEmpty() const const
T & last()
bool contains(const Key &key) const const
QMap::iterator insert(const Key &key, const T &value)
bool isEmpty() const const
QList< Key > keys() const const
int remove(const Key &key)
QAction * addAction(const QString &text)
QAction * addSeparator()
QAction * exec()
void addButton(QAbstractButton *button, QMessageBox::ButtonRole role)
QAbstractButton * clickedButton() const const
void setDetailedText(const QString &text)
virtual int exec() override
void setInformativeText(const QString &text)
QMessageBox::StandardButton question(QWidget *parent, const QString &title, const QString &text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton)
void setDefaultButton(QPushButton *button)
void setStandardButtons(QMessageBox::StandardButtons buttons)
void setText(const QString &text)
QMessageBox::StandardButton warning(QWidget *parent, const QString &title, const QString &text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton)
Qt::MouseButton button() const const
QPoint pos() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
virtual bool eventFilter(QObject *watched, QEvent *event)
void installEventFilter(QObject *filterObj)
QObject * sender() const const
QTextDocument * document() const const
QList< QTextEdit::ExtraSelection > extraSelections() const const
void setPlainText(const QString &text)
void textChanged()
QString toPlainText() const const
void activated()
void setKey(const QKeySequence &key)
QString & append(QChar ch)
QString arg(qlonglong a, int fieldWidth, int base, QChar fillChar) const const
const QChar at(int position) const const
void chop(int n)
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString fromStdString(const std::string &str)
QString fromUtf8(const char *str, int size)
bool isEmpty() const const
QString mid(int position, int n) const const
QString number(int n, int base)
QString & replace(int position, int n, QChar after)
QString section(QChar sep, int start, int end, QString::SectionFlags flags) const const
int size() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
std::string toStdString() const const
QByteArray toUtf8() const const
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
int tabAt(const QPoint &position) const const
int addTab(QWidget *page, const QString &label)
void currentChanged(int index)
void setCurrentIndex(int index)
QWidget * currentWidget() const const
int indexOf(QWidget *w) const const
void setMovable(bool movable)
void removeTab(int index)
void setTabText(int index, const QString &label)
QTabBar * tabBar() const const
void tabCloseRequested(int index)
QString tabText(int index) const const
void setTabsClosable(bool closeable)
QWidget * widget(int index) const const
typedef FindFlags
bool isModified() const const
QAction * addAction(const QString &text)
TolerantMode
bool toBool() const const
int toInt(bool *ok) const const
QString toString() const const
void append(const T &value)
bool isEmpty() const const
void ensurePolished() const const
virtual bool event(QEvent *event) override
void hide()
bool isHidden() const const
QLayout * layout() const const
void setFocus()
void setHidden(bool hidden)
void show()
QStyle * style() const const
void setStyleSheet(const QString &styleSheet)
bool isVisible() const const