ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
PythonRepl.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 "PythonRepl.h"
9 #include "PythonHighlighter.h"
10 #include "PythonInterpreter.h"
11 
12 #include <ui_PythonREPL.h>
13 
15 
16 // Qt5/Qt6 Compatibility
17 #include <QtCompat.h>
18 
19 namespace py = pybind11;
20 
24 const static QString replArrows(QStringLiteral(">>> "));
25 const static QString continuationDots(QStringLiteral("... "));
26 
33 bool KeyPressEater::eventFilter(QObject *obj, QEvent *event)
34 {
35  if (event->type() == QEvent::KeyPress)
36  {
37  auto *keyEvent = static_cast<QKeyEvent *>(event);
38  if (keyEvent->modifiers() & Qt::ShiftModifier &&
39  (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return))
40  {
41  m_repl->codeEdit()->appendPlainText(continuationDots);
42  return true;
43  }
44  QTextCursor cursor = m_repl->codeEdit()->textCursor();
45 
46  switch (keyEvent->key())
47  {
48  case Qt::Key_Enter:
49  Q_FALLTHROUGH();
50  case Qt::Key_Return:
51  {
52  if (m_repl->codeEdit()->document()->characterCount() < replArrows.size())
53  {
54  return true;
55  }
56 
57  // Try to be smart, create a new line if the python code will need one
58 
59  const int lastCharPos = m_repl->codeEdit()->document()->characterCount() - 2;
60  if (m_repl->codeEdit()->document()->characterAt(lastCharPos) == QLatin1Char(':'))
61  {
62  m_repl->codeEdit()->appendPlainText(continuationDots);
63  return true;
64  }
65 
66  QString pythonCode = m_repl->codeEdit()->toPlainText();
67  if (!m_repl->m_interpreter->isExecuting())
68  {
69  m_repl->executeCode(pythonCode);
70  m_repl->m_history.add(std::move(pythonCode));
71  }
72  return true;
73  }
74  case Qt::Key_Backspace:
75  Q_FALLTHROUGH();
76  case Qt::Key_Delete:
77  if (cursor.columnNumber() > replArrows.size())
78  {
79  return QObject::eventFilter(obj, event);
80  }
81  else
82  {
83  return true;
84  }
85  case Qt::Key_Left:
86  if (cursor.columnNumber() > replArrows.size())
87  {
88  return QObject::eventFilter(obj, event);
89  }
90  return true;
91  case Qt::Key_Down:
92  if (cursor.blockNumber() < m_repl->codeEdit()->blockCount() - 1)
93  {
94  return QObject::eventFilter(obj, event);
95  }
96  else if (!m_repl->m_history.empty())
97  {
98  m_repl->codeEdit()->clear();
99  m_repl->codeEdit()->insertPlainText(m_repl->m_history.newer());
100  }
101  return true;
102  case Qt::Key_Up:
103  if (cursor.blockNumber() > 0)
104  {
105  return QObject::eventFilter(obj, event);
106  }
107  else if (!m_repl->m_history.empty())
108  {
109  m_repl->codeEdit()->clear();
110  m_repl->codeEdit()->insertPlainText(m_repl->m_history.older());
111  return true;
112  }
113  case Qt::Key_Home:
114  m_repl->codeEdit()->moveCursor(QTextCursor::StartOfLine);
115  m_repl->codeEdit()->moveCursor(QTextCursor::NextWord);
116  return true;
117  default:
118  {
119  int posDiff = replArrows.size() - cursor.columnNumber();
120  if (posDiff > 0)
121  {
122  cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, posDiff);
123  m_repl->codeEdit()->setTextCursor(cursor);
124  }
125  return QObject::eventFilter(obj, event);
126  }
127  }
128  }
129  else
130  {
131  return QObject::eventFilter(obj, event);
132  }
133 }
134 
135 KeyPressEater::KeyPressEater(PythonRepl *repl, QObject *parent) : QObject(parent), m_repl(repl) {}
136 
137 PythonRepl::PythonRepl(PythonInterpreter *interpreter, QMainWindow *parent)
138  : m_interpreter(interpreter), QMainWindow(parent), m_ui(new Ui_PythonREPL), m_state()
139 {
140  m_buf.reserve(255);
141 
142  setupUI();
144 }
145 
147 {
148  delete m_ui;
149 }
150 
151 QPlainTextEdit *PythonRepl::codeEdit()
152 {
153  return m_ui->codeEdit;
154 }
155 
157 {
158  return m_ui->outputDisplay;
159 }
160 
162 {
163  m_ui->setupUi(this);
164  const auto keyPressEater = new KeyPressEater(this);
165 
166  const auto codeEditorHeight = static_cast<int>(height() * 0.25);
167  m_ui->splitter->setSizes({height() - codeEditorHeight, codeEditorHeight});
168  new PythonHighlighter(codeEdit()->document());
169 
170  codeEdit()->installEventFilter(keyPressEater);
171  codeEdit()->resize(codeEdit()->width(), 20);
173  codeEdit(), QTCOMPAT_FONTMETRICS_WIDTH(codeEdit()->fontMetrics(), QString(' ')) * 8);
174 
175  QFont font("Monospace");
176  font.setStyleHint(QFont::TypeWriter);
177 
178  codeEdit()->setFont(font);
179 
180  connect(m_ui->toolBar->actions().at(0), &QAction::triggered, this, &PythonRepl::reset);
181 }
182 
184 {
185  m_state = PythonInterpreter::State();
186  m_ui->outputDisplay->clear();
188 }
189 
191 {
192  executeCode(replArrows + "import pycc");
193  executeCode(replArrows + "import cccorelib");
194  executeCode(replArrows + "cc = pycc.GetInstance()");
195 }
196 
197 void PythonRepl::executeCode(const QString &pythonCode)
198 {
201  m_buf.clear();
202  auto iter = pythonCode.begin() + replArrows.size();
203  while (iter < pythonCode.end())
204  {
205  // FIXME this will return 0 on non latin chars
206  // we should do better
207  const char c = iter->toLatin1();
208  if (c == 0)
209  {
210  outputDisplay()->addItem("Input contains non Latin1 chars");
211  return;
212  }
213  m_buf += c;
214  if (*iter == '\n')
215  {
216  iter += continuationDots.size();
217  }
218  iter++;
219  }
220  outputDisplay()->addItem(pythonCode);
221  codeEdit()->clear();
222  codeEdit()->insertPlainText(replArrows);
223  m_interpreter->executeStatementWithState(m_buf, outputDisplay(), m_state);
224  outputDisplay()->scrollToBottom();
225 }
226 
227 void History::add(QString &&cmd)
228 {
229  m_commands.push_back(cmd);
230  m_current = m_commands.rbegin();
231 }
232 
233 const QString &History::older()
234 {
235  if (m_current == m_commands.rend())
236  {
237  m_current = m_commands.rbegin();
238  }
239  const QString &current = *m_current;
240  ++m_current;
241  return current;
242 }
243 
244 const QString &History::newer()
245 {
246  if (m_current == m_commands.rend())
247  {
248  return replArrows;
249  }
250 
251  const QString &current = *m_current;
252  if (m_current == m_commands.rbegin())
253  {
254  m_current = m_commands.rend();
255  }
256  else
257  {
258  --m_current;
259  }
260  return current;
261 }
262 
263 bool History::empty() const
264 {
265  return m_commands.empty();
266 }
267 
268 size_t History::size() const
269 {
270  return m_commands.size();
271 }
MouseEvent event
int width
int height
static const QString continuationDots(QStringLiteral("... "))
static const QString replArrows(QStringLiteral(">>> "))
void qtCompatSetTabStopWidth(QPlainTextEdit *edit, int width)
Definition: QtCompat.h:1010
#define QTCOMPAT_FONTMETRICS_WIDTH(fm, text)
Definition: QtCompat.h:339
bool empty() const
Definition: PythonRepl.cpp:263
const QString & newer()
Definition: PythonRepl.cpp:244
const QString & older()
Definition: PythonRepl.cpp:233
size_t size() const
Definition: PythonRepl.cpp:268
void add(QString &&cmd)
Definition: PythonRepl.cpp:227
Class used by the REPL to handle key presses.
Definition: PythonRepl.h:45
KeyPressEater(PythonRepl *repl, QObject *parent=nullptr)
Definition: PythonRepl.cpp:135
bool eventFilter(QObject *obj, QEvent *event) override
Definition: PythonRepl.cpp:33
void executeStatementWithState(const std::string &code, QListWidget *output, PythonInterpreter::State &state)
Homemade REPL (Read Print Eval Loop)
Definition: PythonRepl.h:59
PythonRepl(PythonInterpreter *interpreter, QMainWindow *parent=nullptr)
Definition: PythonRepl.cpp:137
void importNeededPackages()
Definition: PythonRepl.cpp:190
QListWidget * outputDisplay()
Definition: PythonRepl.cpp:156
void setupUI()
Definition: PythonRepl.cpp:161
QPlainTextEdit * codeEdit()
Definition: PythonRepl.cpp:151
void reset()
Definition: PythonRepl.cpp:183
~PythonRepl() noexcept override
Definition: PythonRepl.cpp:146
void executeCode(const QString &pythonCode)
Definition: PythonRepl.cpp:197