HAL
python_code_editor.cpp
Go to the documentation of this file.
3 
7 #include "gui/gui_globals.h"
8 
9 #include <QApplication>
10 #include <QDesktopWidget>
11 #include <QTextDocumentFragment>
12 #include <QVBoxLayout>
13 #include <QShortcut>
14 #include <QDir>
15 
16 namespace hal
17 {
18  PythonCodeEditor::PythonCodeEditor(QWidget *parent) : CodeEditor(parent), mUuid(QUuid::createUuid())
19  {
20  QShortcut* redo_shortcut = new QShortcut(QKeySequence(tr("Ctrl+y")), this);
21  connect(redo_shortcut, &QShortcut::activated, this, &PythonCodeEditor::handleRedoRequested);
22 
23  mBaseFileModified = false;
24  }
25 
26  void PythonCodeEditor::keyPressEvent(QKeyEvent* e)
27  {
28  Q_EMIT keyPressed(e);
29  if (textCursor().hasSelection() && !(e->key() == Qt::Key_Tab || e->key() == Qt::Key_Backtab))
30  {
32  }
33  else
34  {
35  switch(e->key())
36  {
37  case Qt::Key_Tab: handleTabKeyPressed(); break;
38  case Qt::Key_Backtab: handleShiftTabKeyPressed(); break;
39  case Qt::Key_Return: handleReturnKeyPressed(); break;
40  case Qt::Key_Backspace: handleBackspaceKeyPressed(e); break;
41  case Qt::Key_Delete: handleDeleteKeyPressed(e); break;
42  case Qt::Key_Insert: handleInsertKeyPressed(); break;
44  }
45  }
46  }
47 
48  void PythonCodeEditor::handleShiftTabKeyPressed()
49  {
50  PythonCodeEditor::indentSelection(false);
51  }
52 
53  void PythonCodeEditor::handleTabKeyPressed()
54  {
55  PythonCodeEditor::indentSelection(true);
56  }
57 
58  void PythonCodeEditor::handleReturnKeyPressed()
59  {
60  QString current_line;
61  {
62  auto cursor = textCursor();
64  current_line = cursor.selectedText();
65  }
66  u32 num_spaces = 0;
67  for (const auto& c : current_line)
68  {
69  if (c != ' ')
70  {
71  break;
72  }
73  num_spaces++;
74  }
75 
76  auto trimmed = current_line.trimmed();
77  if (!trimmed.isEmpty() && trimmed.at(trimmed.size() - 1) == ':')
78  {
79  num_spaces += 4;
80  }
81 
82  num_spaces -= num_spaces % 4;
83  insertPlainText("\n");
84  for (u32 i = 0; i < num_spaces / 4; ++i)
85  {
86  insertPlainText(" ");
87  }
89  }
90 
91  void PythonCodeEditor::handleBackspaceKeyPressed(QKeyEvent* e)
92  {
93  QString current_line;
94  {
95  auto cursor = textCursor();
97  current_line = cursor.selectedText();
98  }
99 
100  if (!current_line.isEmpty() && current_line.trimmed().isEmpty() && current_line.size() % 4 == 0)
101  {
102  auto cursor = textCursor();
105  cursor.removeSelectedText();
106  return;
107  }
108 
110  }
111 
112  void PythonCodeEditor::handleDeleteKeyPressed(QKeyEvent* e)
113  {
114  auto cursor = textCursor();
115  if (cursor.positionInBlock() % 4 == 0)
116  {
118  if (cursor.selectedText() == " ")
119  {
120  cursor.removeSelectedText();
121  return;
122  }
123  }
124 
126  }
127 
128  void PythonCodeEditor::handleInsertKeyPressed()
129  {
131  }
132 
133  void PythonCodeEditor::handleRedoRequested()
134  {
135  redo();
136  }
137 
138  void PythonCodeEditor::handleAutocomplete()
139  {
140  auto cursor = textCursor();
141  // auto row = cursor.blockNumber();
142  // auto col = cursor.positionInBlock();
144  auto text = cursor.selectedText().replace(QChar(0x2029), '\n');
145  auto candidates = gPythonContext->complete(text, false);
146 
147  if (candidates.size() == 1)
148  {
149  textCursor().insertText(QString::fromStdString(std::get<1>(candidates.at(0))));
150  }
151  else if (candidates.size() > 1)
152  {
153  auto dialog = new PythonEditorCodeCompletionDialog(this, candidates);
154  connect(dialog, &PythonEditorCodeCompletionDialog::completionSelected, this, &PythonCodeEditor::performCodeCompletion);
155  auto menu_width = dialog->width();
156  auto menu_height = dialog->height();
157 
158  auto desk_rect = QApplication::desktop()->screenGeometry();
159  auto desk_width = desk_rect.width();
160  auto desk_height = desk_rect.height();
161  auto anchor = this->cursorRect().bottomRight();
162  anchor.setX(anchor.x() + viewportMargins().left());
163  auto anchor_global = this->mapToGlobal(anchor);
164 
165  if (anchor_global.x() + menu_width > desk_width)
166  {
167  anchor.setX(anchor.x() - menu_width);
168  anchor_global = this->mapToGlobal(anchor);
169  }
170 
171  if (anchor_global.y() + menu_height > desk_height)
172  {
173  anchor.setY(cursorRect().topRight().y() - menu_height);
174  anchor_global = this->mapToGlobal(anchor);
175  }
176  dialog->move(anchor_global);
177  dialog->exec();
178  }
179  }
180 
181  int PythonCodeEditor::nextIndent(bool indentUnindent, int current_indent)
182  {
183  int nextIndent;
184  if (indentUnindent)
185  {
186  nextIndent = 4 - (current_indent % 4);
187  }
188  else
189  {
190  nextIndent = current_indent % 4;
191  if (nextIndent == 0)
192  {
193  nextIndent = 4;
194  }
195  }
196  return nextIndent;
197  }
198 
199  void PythonCodeEditor::indentSelection(bool indent)
200  {
201  auto cursor = textCursor();
202  bool preSelected = cursor.hasSelection();
203  int start = cursor.selectionStart();
204  int end = cursor.selectionEnd();
205  if (preSelected)
206  {
207  // expand selection to the start of the first line
208  cursor.setPosition(end, QTextCursor::MoveAnchor);
209  cursor.setPosition(start, QTextCursor::KeepAnchor);
211  }
212  else
213  {
214  // select first line (to decide auto-completion)
216  QString line_start_to_cursor = cursor.selection().toPlainText();
217  bool onlySpaces = true;
218  for (const auto& c : line_start_to_cursor)
219  {
220  if (c != ' ')
221  {
222  onlySpaces = false;
223  break;
224  }
225  }
226  if (indent && !onlySpaces)
227  {
228  // if the cursor is in a word without a selection, show autocompletion menu
229  // (skip this if we are un-indenting, meaning Shift+Tab has been pressed)
230  handleAutocomplete();
231  return;
232  }
233  // select whole line (to count total spaces)
234  // don't mess with this order, it'll affect the text insertion later on
237  }
238 
239  QStringList selected_lines = cursor.selection().toPlainText().split('\n');
240  cursor.clearSelection();
241 
242  // calculate number of spaces at the beginning of the first line and
243  // check if the line consists entirely of spaces or is empty
244  int n_spaces = 0;
245  if (!selected_lines.isEmpty())
246  {
247  for (const auto& c : selected_lines[0])
248  {
249  if (c != ' ')
250  {
251  break;
252  }
253  n_spaces++;
254  }
255  }
256  // calculate indent to use for all selected lines based on the amount of
257  // spaces needed to align the first line to the nextIndent 4-block
258  const int constant_indent = nextIndent(indent, n_spaces);
259 
260  // (un)indent all selected lines
261  const int size = selected_lines.size();
262  QString padding;
263  if (indent)
264  {
265  padding = "";
266  padding.fill(' ', constant_indent);
267  }
268  cursor.beginEditBlock();
269  for (int i = 0; i < size; i++)
270  {
271  if (indent)
272  {
273  cursor.insertText(padding);
274  end+=constant_indent;
275  }
276  else
277  {
278  QString current_line = selected_lines[i];
279  // count how many spaces there really are so we don't cut off text
280  int spaces = 0;
281  while(spaces < constant_indent)
282  {
283  if (current_line[spaces] != ' ')
284  {
285  break;
286  }
287  spaces++;
288  }
289  // un-indent line by removing spaces
290  cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, spaces);
291  cursor.removeSelectedText();
292  end-=spaces;
293  }
296  }
297  cursor.endEditBlock();
298  // restore selection
299  cursor.setPosition(start, QTextCursor::MoveAnchor);
300  cursor.setPosition(end, QTextCursor::KeepAnchor);
301  }
302 
303  void PythonCodeEditor::performCodeCompletion(std::tuple<std::string, std::string> completion)
304  {
305  textCursor().insertText(QString::fromStdString(std::get<1>(completion)));
306  }
307 
309  {
310  return mRelFilename;
311  }
312 
314  {
315  if (mRelFilename.isEmpty() || mRelFilename.startsWith('/'))
316  return mRelFilename;
317  QDir projectPydir(QString::fromStdString(ProjectManager::instance()->get_project_directory().get_filename("py")));
318  return projectPydir.absoluteFilePath(mRelFilename);
319  }
320 
322  {
323  mRelFilename = name;
324  QString projectLoc = QString::fromStdString(ProjectManager::instance()->get_project_directory().get_filename("py")) + '/';
325  if (name.startsWith(projectLoc))
326  mRelFilename.remove(0,projectLoc.size());
327  }
328 
329  void PythonCodeEditor::setBaseFileModified(bool base_file_modified)
330  {
331  mBaseFileModified = base_file_modified;
332  }
333 
335  {
336  return mBaseFileModified;
337 
338  }
339 
341  {
342  return mUuid;
343  }
344 }
A plain text edit widget that is intended for editing code.
Definition: code_editor.h:52
static ProjectManager * instance()
void setFilename(const QString &name)
QString getAbsFilename() const
void keyPressed(QKeyEvent *e)
void setBaseFileModified(bool base_file_modified)
PythonCodeEditor(QWidget *parent=nullptr)
QString getRelFilename() const
void completionSelected(std::tuple< std::string, std::string > selected)
PythonContext * gPythonContext
Definition: plugin_gui.cpp:88
quint32 u32
std::string name
QMargins viewportMargins() const const
QDesktopWidget * desktop()
const QRect screenGeometry(const QWidget *widget) const const
QString absoluteFilePath(const QString &fileName) const const
int key() const const
bool isEmpty() const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QString tr(const char *sourceText, const char *disambiguation, int n)
QRect cursorRect() const const
void ensureCursorVisible()
void insertPlainText(const QString &text)
virtual void keyPressEvent(QKeyEvent *e) override
void setOverwriteMode(bool overwrite)
QTextCursor textCursor() const const
void setX(int x)
QPoint bottomRight() const const
int width() const const
void activated()
QString & fill(QChar ch, int size)
QString fromStdString(const std::string &str)
bool isEmpty() const const
QString & remove(int position, int n)
int size() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString trimmed() const const
QTextStream & left(QTextStream &stream)
void insertText(const QString &text)
QPoint mapToGlobal(const QPoint &pos) const const