ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
PythonEditor.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 #include "PythonEditor.h"
9 #include "CodeEditor.h"
10 #include "PythonInterpreter.h"
11 
12 // qCC
13 #include "ecvMainAppInterface.h"
14 
15 // Qt
16 #include <QtWidgets>
17 
18 static QString RecentFilesKey()
19 {
20  return QStringLiteral("recentFileList");
21 }
22 
23 static QString FileKey()
24 {
25  return QStringLiteral("file");
26 }
27 
29  : Ui::PythonEditor(), m_settings(new EditorSettings), m_mdiArea(new QMdiArea(this))
30 {
31  setupUi();
32  createActions();
33  updateMenus();
34 
35  readSettings();
36  QCoreApplication::instance()->installEventFilter(this);
37 
39  connect(
41  connect(
43 }
44 
45 void PythonEditor::closeEvent(QCloseEvent *event)
46 {
47  projectBrowser->hide();
48  m_mdiArea->closeAllSubWindows();
49  if (m_mdiArea->currentSubWindow())
50  {
51  event->ignore();
52  }
53  else
54  {
55  writeSettings();
56  event->accept();
57  QCoreApplication::instance()->removeEventFilter(this);
58  }
59 }
60 
62 {
64  m_mdiArea->addSubWindow(child);
65  child->newFile();
66  child->show();
67 }
68 
70 {
71  const QString fileName =
72  QFileDialog::getOpenFileName(this, "Open Python Script", QString(), "Python Script (*.py)");
73  if (!fileName.isEmpty())
74  {
75  openFile(fileName);
76  }
77 }
78 
80 {
81  const QString folderName = QFileDialog::getExistingDirectory(this, "Open folder");
82  if (!folderName.isEmpty())
83  {
84  projectBrowser->show();
85  projectTreeView->setRootPath(folderName);
86  }
87 }
88 
90 {
91  QWidget::changeEvent(e);
92  switch (e->type())
93  {
94  case QEvent::LanguageChange:
95  retranslateUi(this);
96  break;
97  default:
98  break;
99  }
100 }
101 
102 bool PythonEditor::openFile(const QString &fileName)
103 {
104  if (QMdiSubWindow *existing = findChildCodeEditor(fileName))
105  {
106  m_mdiArea->setActiveSubWindow(existing);
107  return true;
108  }
109  const bool succeeded = loadFile(fileName);
110  if (succeeded)
111  {
112  statusBar()->showMessage(tr("File loaded"), 2000);
113  }
114  return succeeded;
115 }
116 
117 void PythonEditor::projectTreeDoubleClicked(const QModelIndex &index)
118 {
119  const QString path = projectTreeView->relativePathAt(index);
120  if (QFileInfo(path).isFile())
121  {
122  openFile(path);
123  }
124 }
125 
126 bool PythonEditor::loadFile(const QString &fileName)
127 {
129  const bool succeeded = child->loadFile(fileName);
130  if (succeeded)
131  {
132  m_mdiArea->addSubWindow(child);
133  child->show();
134  }
135  else
136  {
137  m_mdiArea->removeSubWindow(child);
138  child->close();
139  delete child;
140  }
141  PythonEditor::prependToRecentFiles(fileName);
142  return succeeded;
143 }
144 
145 static QStringList readRecentFiles(QSettings &settings)
146 {
147  QStringList result;
148  const int count = settings.beginReadArray(RecentFilesKey());
149  for (int i = 0; i < count; ++i)
150  {
151  settings.setArrayIndex(i);
152  result.append(settings.value(FileKey()).toString());
153  }
154  settings.endArray();
155  return result;
156 }
157 
158 static void writeRecentFiles(const QStringList &files, QSettings &settings)
159 {
160  const int count = files.size();
161  settings.beginWriteArray(RecentFilesKey());
162  for (int i = 0; i < count; ++i)
163  {
164  settings.setArrayIndex(i);
165  settings.setValue(FileKey(), files.at(i));
166  }
167  settings.endArray();
168 }
169 
170 bool PythonEditor::HasRecentFiles()
171 {
172  QSettings settings(QCoreApplication::organizationName(), SettingsApplicationName());
173  const int count = settings.beginReadArray(RecentFilesKey());
174  settings.endArray();
175  return count > 0;
176 }
177 
178 void PythonEditor::prependToRecentFiles(const QString &fileName)
179 {
180  QSettings settings(QCoreApplication::organizationName(), SettingsApplicationName());
181 
182  const QStringList oldRecentFiles = readRecentFiles(settings);
183  QStringList recentFiles = oldRecentFiles;
184  recentFiles.removeAll(fileName);
185  recentFiles.prepend(fileName);
186  if (oldRecentFiles != recentFiles)
187  writeRecentFiles(recentFiles, settings);
188 
189  setRecentFilesVisible(!recentFiles.isEmpty());
190 }
191 
192 void PythonEditor::setRecentFilesVisible(bool visible)
193 {
194  m_recentFileSubMenuAct->setVisible(visible);
195  m_recentFileSeparator->setVisible(visible);
196 }
197 
199 {
200  QSettings settings(QCoreApplication::organizationName(), SettingsApplicationName());
201 
202  const QStringList recentFiles = readRecentFiles(settings);
203  const int count = qMin(MAX_RECENT_FILES, recentFiles.size());
204  int i = 0;
205  for (; i < count; ++i)
206  {
207  const QString fileName = QFileInfo(recentFiles.at(i)).fileName();
208  m_recentFileActs[i]->setText(tr("&%1 %2").arg(i + 1).arg(fileName));
209  m_recentFileActs[i]->setData(recentFiles.at(i));
210  m_recentFileActs[i]->setVisible(true);
211  }
212  for (; i < MAX_RECENT_FILES; ++i)
213  {
214  m_recentFileActs[i]->setVisible(false);
215  }
216 }
217 
219 {
220  if (const auto *action = qobject_cast<const QAction *>(sender()))
221  {
222  openFile(action->data().toString());
223  }
224 }
225 
226 bool PythonEditor::eventFilter(QObject *obj, QEvent *e)
227 {
228  switch (e->type())
229  {
230  case QEvent::Shortcut:
231  {
232  auto *sev = static_cast<QShortcutEvent *>(e);
233  if (sev->isAmbiguous())
234  {
235  for (const auto &action : actions())
236  {
237  if (action->shortcut() == sev->key())
238  {
239  action->trigger(); // Trigger the action that matches the ambiguous shortcut
240  // event.
241  return true;
242  }
243  }
244  }
245  }
246  default:
247  break;
248  }
249  return false;
250 }
251 
253 {
254  if (activeChildCodeEditor() && activeChildCodeEditor()->save())
255  {
256  statusBar()->showMessage(tr("File saved"), 2000);
257  }
258 }
259 
261 {
262  CodeEditor *child = activeChildCodeEditor();
263  if (child && child->saveAs())
264  {
265  statusBar()->showMessage(tr("File saved"), 2000);
266  PythonEditor::prependToRecentFiles(child->currentFile());
267  }
268 }
269 
270 #ifndef QT_NO_CLIPBOARD
272 {
273  if (activeChildCodeEditor())
274  {
275  activeChildCodeEditor()->cut();
276  }
277 }
278 
280 {
281  if (activeChildCodeEditor())
282  {
283  activeChildCodeEditor()->copy();
284  }
285 }
286 
288 {
289  if (activeChildCodeEditor())
290  {
291  activeChildCodeEditor()->paste();
292  }
293 }
294 #endif
295 
297 {
298  if (activeChildCodeEditor())
299  {
300  activeChildCodeEditor()->comment();
301  }
302 }
304 {
305  if (activeChildCodeEditor())
306  {
307  activeChildCodeEditor()->uncomment();
308  }
309 }
311 {
312  if (activeChildCodeEditor())
313  {
314  activeChildCodeEditor()->indentMore();
315  }
316 }
318 {
319  if (activeChildCodeEditor())
320  {
321  activeChildCodeEditor()->indentLess();
322  }
323 }
324 
326 {
327  actionRun->setEnabled(false);
328  if (activeChildCodeEditor())
329  {
330  statusbar->showMessage(QString("Executing %1").arg(activeChildCodeEditor()->windowTitle()));
331  }
332 }
333 
335 {
336  actionRun->setEnabled(true);
337  statusbar->clearMessage();
338 }
339 
340 void PythonEditor::setupUi()
341 {
342  Ui::PythonEditor::setupUi(this);
343 
344  // Setup MDI Area
345  setCentralWidget(m_mdiArea);
346  m_mdiArea->showMaximized();
347  connect(m_mdiArea, &QMdiArea::subWindowActivated, this, &PythonEditor::updateMenus);
348 
349  // setup ViewToolBar
350  viewsToolBar->setMovable(false);
351  projectBrowser->toggleViewAction()->setEnabled(true);
352  viewsToolBar->addAction(projectBrowser->toggleViewAction());
353  scriptOutputConsoleDock->toggleViewAction()->setEnabled(true);
354  viewsToolBar->addAction(scriptOutputConsoleDock->toggleViewAction());
355 
356  // Setup Project View
357  projectBrowser->hide();
358  connect(projectTreeView,
359  &ProjectView::doubleClicked,
360  this,
362 
363  scriptOutputConsoleDock->hide();
364 }
365 
366 void PythonEditor::createActions()
367 {
368  connect(actionNew, &QAction::triggered, this, &PythonEditor::newFile);
369  connect(actionSave, &QAction::triggered, this, &PythonEditor::save);
370  connect(actionOpen, &QAction::triggered, this, &PythonEditor::promptForFileToOpen);
371  connect(actionOpenFolder, &QAction::triggered, this, &PythonEditor::promptForFolderToOpen);
372  connect(actionSaveAs, &QAction::triggered, this, &PythonEditor::saveAs);
373  connect(actionRun, &QAction::triggered, this, &PythonEditor::runExecute);
374  connect(actionClose, &QAction::triggered, this, [=]() { close(); });
375  connect(actionSettings, &QAction::triggered, m_settings, &EditorSettings::show);
376 
377  menuFile->addSeparator();
378 
379  QMenu *recentMenu = menuFile->addMenu(tr("Recent..."));
380  connect(recentMenu, &QMenu::aboutToShow, this, &PythonEditor::updateRecentFileActions);
381  m_recentFileSubMenuAct = recentMenu->menuAction();
382 
383  for (auto &recentFileAct : m_recentFileActs)
384  {
385  recentFileAct = recentMenu->addAction(QString());
386  connect(recentFileAct, &QAction::triggered, this, &PythonEditor::openRecentFile);
387  recentFileAct->setVisible(false);
388  }
389 
390  m_recentFileSeparator = menuFile->addSeparator();
391 
392  setRecentFilesVisible(PythonEditor::HasRecentFiles());
393  menuFile->addSeparator();
394 
395  actionCut->setShortcuts(QKeySequence::Cut);
396  actionCopy->setShortcuts(QKeySequence::Copy);
397  actionPaste->setShortcuts(QKeySequence::Paste);
398  connect(actionCut, &QAction::triggered, this, &PythonEditor::cut);
399  connect(actionCopy, &QAction::triggered, this, &PythonEditor::copy);
400  connect(actionPaste, &QAction::triggered, this, &PythonEditor::paste);
401 
402  connect(actionComment, &QAction::triggered, this, &PythonEditor::comment);
403  connect(actionUncomment, &QAction::triggered, this, &PythonEditor::uncomment);
404  connect(actionIndentMore, &QAction::triggered, this, &PythonEditor::indentMore);
405  connect(actionIndentLess, &QAction::triggered, this, &PythonEditor::indentLess);
406 
407  m_windowMenu = menuBar()->addMenu(tr("&Window"));
408  connect(m_windowMenu, &QMenu::aboutToShow, this, &PythonEditor::updateWindowMenu);
409 
410  m_closeAct = new QAction(tr("Cl&ose"), this);
411  m_closeAct->setStatusTip(tr("Close the active window"));
412  connect(m_closeAct, &QAction::triggered, m_mdiArea, &QMdiArea::closeActiveSubWindow);
413 
414  m_closeAllAct = new QAction(tr("Close &All"), this);
415  m_closeAllAct->setStatusTip(tr("Close all the windows"));
416  connect(m_closeAllAct, &QAction::triggered, m_mdiArea, &QMdiArea::closeAllSubWindows);
417 
418  m_tileAct = new QAction(tr("&Tile"), this);
419  m_tileAct->setStatusTip(tr("Tile the windows"));
420  connect(m_tileAct, &QAction::triggered, m_mdiArea, &QMdiArea::tileSubWindows);
421 
422  m_cascadeAct = new QAction(tr("&Cascade"), this);
423  m_cascadeAct->setStatusTip(tr("Cascade the windows"));
424  connect(m_cascadeAct, &QAction::triggered, m_mdiArea, &QMdiArea::cascadeSubWindows);
425 
426  m_nextAct = new QAction(tr("Ne&xt"), this);
427  m_nextAct->setShortcuts(QKeySequence::NextChild);
428  m_nextAct->setStatusTip(tr("Move the focus to the next window"));
429  connect(m_nextAct, &QAction::triggered, m_mdiArea, &QMdiArea::activateNextSubWindow);
430 
431  m_previousAct = new QAction(tr("Pre&vious"), this);
432  m_previousAct->setShortcuts(QKeySequence::PreviousChild);
433  m_previousAct->setStatusTip(tr("Move the focus to the previous "
434  "window"));
435  connect(m_previousAct, &QAction::triggered, m_mdiArea, &QMdiArea::activatePreviousSubWindow);
436 
437  m_windowMenuSeparatorAct = new QAction(this);
438  m_windowMenuSeparatorAct->setSeparator(true);
439 
441 
442  menuBar()->addSeparator();
443 
444  addAction(actionNew); // Actions must be added to be able to find shortcuts in event filter
445  addAction(actionSave);
446  addAction(actionOpen);
447  addAction(actionSaveAs);
448  addAction(actionRun);
449  addAction(actionClose);
450  addAction(actionComment);
451  addAction(actionUncomment);
452  addAction(actionIndentMore);
453  addAction(actionIndentLess);
454 }
455 
456 void PythonEditor::updateMenus()
457 {
458  const bool hasChildCodeEditor = (activeChildCodeEditor() != nullptr);
459  actionSave->setEnabled(hasChildCodeEditor);
460  actionSaveAs->setEnabled(hasChildCodeEditor);
461  actionRun->setEnabled(hasChildCodeEditor);
462  m_closeAct->setEnabled(hasChildCodeEditor);
463  m_closeAllAct->setEnabled(hasChildCodeEditor);
464  m_tileAct->setEnabled(hasChildCodeEditor);
465  m_cascadeAct->setEnabled(hasChildCodeEditor);
466  m_nextAct->setEnabled(hasChildCodeEditor);
467  m_previousAct->setEnabled(hasChildCodeEditor);
468  m_windowMenuSeparatorAct->setVisible(hasChildCodeEditor);
469  actionComment->setEnabled(hasChildCodeEditor);
470  actionUncomment->setEnabled(hasChildCodeEditor);
471  actionIndentMore->setEnabled(hasChildCodeEditor);
472  actionIndentLess->setEnabled(hasChildCodeEditor);
473 
474 #ifndef QT_NO_CLIPBOARD
475  actionPaste->setEnabled(hasChildCodeEditor);
476  const bool hasSelection =
477  (activeChildCodeEditor() && activeChildCodeEditor()->textCursor().hasSelection());
478  actionCut->setEnabled(hasSelection);
479  actionCopy->setEnabled(hasSelection);
480 #endif
481 }
482 
484 {
485  m_windowMenu->clear();
486  m_windowMenu->addAction(m_closeAct);
487  m_windowMenu->addAction(m_closeAllAct);
488  m_windowMenu->addSeparator();
489  m_windowMenu->addAction(m_tileAct);
490  m_windowMenu->addAction(m_cascadeAct);
491  m_windowMenu->addSeparator();
492  m_windowMenu->addAction(m_nextAct);
493  m_windowMenu->addAction(m_previousAct);
494  m_windowMenu->addAction(m_windowMenuSeparatorAct);
495 
496  QList<QMdiSubWindow *> windows = m_mdiArea->subWindowList();
497  m_windowMenuSeparatorAct->setVisible(!windows.isEmpty());
498 
499  for (int i = 0; i < windows.size(); ++i)
500  {
501  QMdiSubWindow *mdiSubWindow = windows.at(i);
502  auto *child = qobject_cast<CodeEditor *>(mdiSubWindow->widget());
503 
504  QString text;
505  if (i < 9)
506  {
507  text = tr("&%1 %2").arg(i + 1).arg(child->userFriendlyCurrentFile());
508  }
509  else
510  {
511  text = tr("%1 %2").arg(i + 1).arg(child->userFriendlyCurrentFile());
512  }
513  QAction *action = m_windowMenu->addAction(text);
514  connect(action,
515  &QAction::triggered,
516  mdiSubWindow,
517  [this, mdiSubWindow]() { m_mdiArea->setActiveSubWindow(mdiSubWindow); });
518  action->setCheckable(true);
519  action->setChecked(child == activeChildCodeEditor());
520  }
521 }
522 
524 {
525  auto *child = new CodeEditor(this->m_settings);
526 
527 #ifndef QT_NO_CLIPBOARD
528  connect(child, &QPlainTextEdit::copyAvailable, actionCut, &QAction::setEnabled);
529  connect(child, &QPlainTextEdit::copyAvailable, actionCopy, &QAction::setEnabled);
530 #endif
531 
532  return child;
533 }
534 
535 void PythonEditor::readSettings()
536 {
537  QSettings settings(QCoreApplication::organizationName(), SettingsApplicationName());
538  const QByteArray geometry = settings.value("geometry", QByteArray()).toByteArray();
539  if (geometry.isEmpty())
540  {
541  // Qt5/Qt6 Compatibility: QDesktopWidget removed in Qt6, use QScreen instead
542 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
543  const QRect availableGeometry = screen()->availableGeometry();
544 #else
545  const QRect availableGeometry = QApplication::desktop()->availableGeometry(this);
546 #endif
547  resize(availableGeometry.width() / 3, availableGeometry.height() / 2);
548  move((availableGeometry.width() - width()) / 2,
549  (availableGeometry.height() - height()) / 2);
550  }
551  else
552  {
553  restoreGeometry(geometry);
554  }
555 }
556 
557 void PythonEditor::writeSettings()
558 {
559  QSettings settings(QCoreApplication::organizationName(), SettingsApplicationName());
560  settings.setValue("geometry", saveGeometry());
561 }
562 
563 CodeEditor *PythonEditor::activeChildCodeEditor() const
564 {
565  if (QMdiSubWindow *activeSubWindow = m_mdiArea->activeSubWindow())
566  {
567  return qobject_cast<CodeEditor *>(activeSubWindow->widget());
568  }
569  return nullptr;
570 }
571 
572 QMdiSubWindow *PythonEditor::findChildCodeEditor(const QString &fileName) const
573 {
574  QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath();
575 
576  for (QMdiSubWindow *window : m_mdiArea->subWindowList())
577  {
578  auto *mdiChild = qobject_cast<CodeEditor *>(window->widget());
579  if (mdiChild->currentFile() == canonicalFilePath)
580  {
581  return window;
582  }
583  }
584  return nullptr;
585 }
586 
588 {
589  return QString(QCoreApplication::applicationName()).append(":PythonRuntime");
590 }
591 
592 void PythonEditor::runExecute()
593 {
594  if (activeChildCodeEditor())
595  {
596  scriptOutputConsoleDock->show();
597  this->scriptOutputConsole->clear();
598  Q_EMIT executionCalled(qPrintable(activeChildCodeEditor()->toPlainText()),
599  this->scriptOutputConsole);
600  }
601 }
MouseEvent event
int width
int height
int count
static void writeRecentFiles(const QStringList &files, QSettings &settings)
static QString RecentFilesKey()
static QString FileKey()
static QStringList readRecentFiles(QSettings &settings)
#define MAX_RECENT_FILES
Definition: PythonEditor.h:48
core::Tensor result
Definition: VtkUtils.cpp:76
void indentMore()
Definition: CodeEditor.cpp:358
void newFile()
Definition: CodeEditor.cpp:172
bool loadFile(const QString &fileName)
Definition: CodeEditor.cpp:183
void comment()
Definition: CodeEditor.cpp:325
QString userFriendlyCurrentFile()
Definition: CodeEditor.cpp:247
bool saveAs()
Definition: CodeEditor.cpp:217
void uncomment()
Definition: CodeEditor.cpp:339
void indentLess()
Definition: CodeEditor.cpp:372
QString currentFile() const
Definition: CodeEditor.h:40
void promptForFileToOpen()
void executionFinished()
bool eventFilter(QObject *obj, QEvent *e) override
void executionStarted()
void closeEvent(QCloseEvent *event) override
void updateRecentFileActions()
void updateWindowMenu()
CodeEditor * createChildCodeEditor()
PythonEditor(PythonInterpreter *interpreter)
void changeEvent(QEvent *e) override
void executionCalled(const std::string &evalStatement, QListWidget *output)
static QString SettingsApplicationName()
bool openFile(const QString &fileName)
void promptForFolderToOpen()
void projectTreeDoubleClicked(const QModelIndex &index)
void openRecentFile()
void executeCode(const std::string &code, QListWidget *output)
void executionFinished()
static const std::string path
Definition: PointCloud.cpp:59
bool Copy(const std::string &from, const std::string &to, bool include_parent_dir=false, const std::string &extname="")
Copy a file or directory.
Definition: FileSystem.cpp:249
std::string toString(T x)
Definition: Common.h:80