ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
CodeEditor.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 <QtWidgets>
9 
10 #include "CodeEditor.h"
11 #include "PythonHighlighter.h"
12 
13 static const char *const INDENT_STRING = " ";
14 
15 static const char *const PYTHON_COMMENT_STR = "# ";
16 
17 CodeEditor::CodeEditor(EditorSettings *settings, QWidget *parent)
18  : QPlainTextEdit(parent), m_settings(settings), m_highlighter(new PythonHighlighter(document()))
19 {
20  connect(settings, &EditorSettings::settingsChanged, this, &CodeEditor::updateUsingSettings);
21 
22  m_lineNumberArea = new LineNumberArea(this);
23  connect(this, &QPlainTextEdit::blockCountChanged, this, &CodeEditor::updateLineNumberAreaWidth);
24  connect(this, &QPlainTextEdit::updateRequest, this, &CodeEditor::updateLineNumberArea);
25  connect(this, &QPlainTextEdit::cursorPositionChanged, this, &CodeEditor::startAllHighlighting);
26 
27  updateLineNumberAreaWidth(0);
28  setAttribute(Qt::WA_DeleteOnClose);
29  updateUsingSettings();
30  installEventFilter(this);
31 }
32 
33 bool CodeEditor::eventFilter(QObject *target, QEvent *event)
34 {
35  if (target == this && event->type() == QEvent::Wheel)
36  {
37  auto *wheel = static_cast<QWheelEvent *>(event);
38  if (wheel->modifiers() == Qt::ControlModifier)
39  {
40  if (wheel->angleDelta().y() > 0)
41  {
42  zoomIn(1);
43  }
44  else
45  {
46  zoomOut(1);
47  }
48  return true;
49  }
50  }
51  return QPlainTextEdit::eventFilter(target, event);
52 }
53 
54 void CodeEditor::updateUsingSettings()
55 {
56  QFont font;
57  font.setFamily("Courier");
58  font.setFixedPitch(true);
59  font.setPointSize(m_settings->fontSize());
60 
61  setFont(font);
62  QPalette p = palette();
63  p.setColor(QPalette::Base, m_settings->colorScheme().backgroundColor());
64  p.setColor(QPalette::Text, m_settings->colorScheme().foregroundColor());
65  setPalette(p);
66 
67  setLineWrapMode(QPlainTextEdit::NoWrap);
68 
69  m_highlighter->useColorScheme(m_settings->colorScheme());
70 
71  m_highlighter->rehighlight();
72  highlightCurrentLine();
73  this->repaint();
74 }
75 
76 void CodeEditor::startAllHighlighting()
77 {
78  const QList<QTextEdit::ExtraSelection> selections;
79  setExtraSelections(selections);
80  highlightCurrentLine();
81 }
82 
84 {
85  int digits = 1;
86  int max = qMax(1, blockCount());
87  while (max >= 10)
88  {
89  max /= 10;
90  ++digits;
91  }
92 
93  int space = 3 + fontMetrics().maxWidth() * digits;
94 
95  return space;
96 }
97 
98 void CodeEditor::updateLineNumberAreaWidth(int /* newBlockCount */)
99 {
100  setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
101 }
102 
103 void CodeEditor::updateLineNumberArea(const QRect &rect, int dy)
104 {
105  if (dy)
106  m_lineNumberArea->scroll(0, dy);
107  else
108  m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height());
109 
110  if (rect.contains(viewport()->rect()))
111  updateLineNumberAreaWidth(0);
112 }
113 
114 void CodeEditor::resizeEvent(QResizeEvent *e)
115 {
116  QPlainTextEdit::resizeEvent(e);
117 
118  QRect cr = contentsRect();
119  m_lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
120 }
121 
122 void CodeEditor::highlightCurrentLine()
123 {
124  if (!m_settings->shouldHighlightCurrentLine())
125  {
126  return;
127  }
128 
129  QList<QTextEdit::ExtraSelection> selections = extraSelections();
130 
131  if (!isReadOnly())
132  {
133  QTextEdit::ExtraSelection selection;
134 
135  selection.format.setBackground(m_settings->colorScheme().currentLineHighlightColor());
136  selection.format.setProperty(QTextFormat::FullWidthSelection, true);
137  selection.cursor = textCursor();
138  selection.cursor.clearSelection();
139  selections.append(selection);
140  }
141 
142  setExtraSelections(selections);
143 }
144 
146 {
147  QPainter painter(m_lineNumberArea);
148  painter.fillRect(event->rect(), Qt::lightGray);
149 
150  QTextBlock block = firstVisibleBlock();
151  int blockNumber = block.blockNumber();
152  int top = (int)blockBoundingGeometry(block).translated(contentOffset()).top();
153  int bottom = top + (int)blockBoundingRect(block).height();
154 
155  while (block.isValid() && top <= event->rect().bottom())
156  {
157  if (block.isVisible() && bottom >= event->rect().top())
158  {
159  QString number = QString::number(blockNumber + 1);
160  painter.setPen(Qt::black);
161  painter.drawText(
162  0, top, m_lineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, number);
163  }
164 
165  block = block.next();
166  top = bottom;
167  bottom = top + (int)blockBoundingRect(block).height();
168  ++blockNumber;
169  }
170 }
171 
173 {
174  static int sequenceNumber = 1;
175 
176  m_isUntitled = true;
177  m_curFile = tr("script%1.py").arg(sequenceNumber++);
178  setWindowTitle(m_curFile + "[*]");
179 
180  connect(document(), &QTextDocument::contentsChanged, this, &CodeEditor::documentWasModified);
181 }
182 
183 bool CodeEditor::loadFile(const QString &fileName)
184 {
185  QFile file(fileName);
186  if (!file.open(QFile::ReadOnly | QFile::Text))
187  {
188  QMessageBox::warning(
189  this, tr("MDI"), tr("Cannot read file %1:\n%2.").arg(fileName).arg(file.errorString()));
190  return false;
191  }
192 
193  QTextStream in(&file);
194  QApplication::setOverrideCursor(Qt::WaitCursor);
195  setPlainText(in.readAll());
196  QApplication::restoreOverrideCursor();
197 
198  setCurrentFile(fileName);
199 
200  connect(document(), &QTextDocument::contentsChanged, this, &CodeEditor::documentWasModified);
201 
202  return true;
203 }
204 
206 {
207  if (m_isUntitled)
208  {
209  return saveAs();
210  }
211  else
212  {
213  return saveFile(m_curFile);
214  }
215 }
216 
218 {
219  QString fileName = QFileDialog::getSaveFileName(this, tr("Save As"), m_curFile);
220  if (fileName.isEmpty())
221  return false;
222 
223  return saveFile(fileName);
224 }
225 
226 bool CodeEditor::saveFile(const QString &fileName)
227 {
228  QFile file(fileName);
229  if (!file.open(QFile::WriteOnly | QFile::Text))
230  {
231  QMessageBox::warning(this,
232  tr("MDI"),
233  tr("Cannot write file %1:\n%2.")
234  .arg(QDir::toNativeSeparators(fileName), file.errorString()));
235  return false;
236  }
237 
238  QTextStream out(&file);
239  QApplication::setOverrideCursor(Qt::WaitCursor);
240  out << toPlainText();
241  QApplication::restoreOverrideCursor();
242 
243  setCurrentFile(fileName);
244  return true;
245 }
246 
248 {
249  return strippedName(m_curFile);
250 }
251 
252 void CodeEditor::closeEvent(QCloseEvent *event)
253 {
254  if (maybeSave())
255  {
256  event->accept();
257  }
258  else
259  {
260  event->ignore();
261  }
262 }
263 
264 void CodeEditor::documentWasModified()
265 {
266  setWindowModified(document()->isModified());
267 }
268 
269 bool CodeEditor::maybeSave()
270 {
271  if (!document()->isModified())
272  return true;
273  const QMessageBox::StandardButton ret =
274  QMessageBox::warning(this,
275  tr("MDI"),
276  tr("'%1' has been modified.\n"
277  "Do you want to save your changes?")
278  .arg(userFriendlyCurrentFile()),
279  QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
280  switch (ret)
281  {
282  case QMessageBox::Save:
283  return save();
284  case QMessageBox::Cancel:
285  return false;
286  default:
287  break;
288  }
289  return true;
290 }
291 
292 void CodeEditor::setCurrentFile(const QString &fileName)
293 {
294  m_curFile = QFileInfo(fileName).canonicalFilePath();
295  m_isUntitled = false;
296  document()->setModified(false);
297  setWindowModified(false);
298  setWindowTitle(userFriendlyCurrentFile() + "[*]");
299 }
300 
301 QString CodeEditor::strippedName(const QString &fullFileName)
302 {
303  return QFileInfo(fullFileName).fileName();
304 }
305 
306 void CodeEditor::createPairedCharsSelection(int pos)
307 {
308  QList<QTextEdit::ExtraSelection> selections = extraSelections();
309 
310  QTextEdit::ExtraSelection selection;
311  QTextCharFormat format = selection.format;
312  format.setBackground(Qt::green);
313  selection.format = format;
314 
315  QTextCursor cursor = textCursor();
316  cursor.setPosition(pos);
317  cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
318  selection.cursor = cursor;
319 
320  selections.append(selection);
321 
322  setExtraSelections(selections);
323 }
324 
326 {
327  int lineCount = getSelectedLineCount();
328  QTextCursor cursor = textCursor();
329  cursor.setPosition(cursor.selectionEnd());
330 
331  for (int i = 0; i < lineCount; i++)
332  {
333  cursor.movePosition(QTextCursor::MoveOperation::StartOfLine);
334  cursor.insertText(PYTHON_COMMENT_STR);
335  cursor.movePosition(QTextCursor::MoveOperation::Up);
336  }
337 }
338 
340 {
341  int lineCount = getSelectedLineCount();
342  QTextCursor cursor = textCursor();
343  cursor.setPosition(cursor.selectionEnd());
344 
345  for (int i = 0; i < lineCount; i++)
346  {
347  cursor.movePosition(QTextCursor::MoveOperation::StartOfLine);
348  QString line = cursor.block().text();
349  if (line.startsWith(PYTHON_COMMENT_STR))
350  {
351  cursor.deleteChar();
352  cursor.deleteChar();
353  }
354  cursor.movePosition(QTextCursor::MoveOperation::Up);
355  }
356 }
357 
359 {
360  int lineCount = getSelectedLineCount();
361  QTextCursor cursor = textCursor();
362  cursor.setPosition(cursor.selectionEnd());
363 
364  for (int i = 0; i < lineCount; i++)
365  {
366  cursor.movePosition(QTextCursor::MoveOperation::StartOfLine);
367  cursor.insertText(INDENT_STRING);
368  cursor.movePosition(QTextCursor::MoveOperation::Up);
369  }
370 }
371 
373 {
374  int lineCount = getSelectedLineCount();
375  QTextCursor cursor = textCursor();
376  cursor.setPosition(cursor.selectionEnd());
377 
378  for (int i = 0; i < lineCount; i++)
379  {
380  cursor.movePosition(QTextCursor::MoveOperation::StartOfLine);
381  QString line = cursor.block().text();
382  if (line.startsWith("\t"))
383  {
384  cursor.deleteChar();
385  }
386  else if (line.startsWith(INDENT_STRING))
387  {
388  for (int i = 0; i < 4; i++)
389  {
390  cursor.deleteChar();
391  }
392  }
393  // line = cursor.block().text();
394 
395  cursor.movePosition(QTextCursor::MoveOperation::Up);
396  }
397 }
398 
399 int CodeEditor::getSelectedLineCount()
400 {
401  QTextCursor cursor = textCursor();
402  if (cursor.hasSelection())
403  {
404  cursor.setPosition(cursor.selectionStart());
405  int temp = cursor.blockNumber();
406  cursor = textCursor();
407  cursor.setPosition(cursor.selectionEnd());
408  int diff = cursor.blockNumber() - temp;
409  return diff + 1;
410  }
411  else
412  {
413  return 1;
414  }
415 }
416 
417 void CodeEditor::keyPressEvent(QKeyEvent *e)
418 {
419  switch (e->key())
420  {
421  case Qt::Key_Tab:
422  indentMore();
423  break;
424  case Qt::Key_Backtab:
425  indentLess();
426  break;
427  default:
428  QPlainTextEdit::keyPressEvent(e);
429  break;
430  }
431 }
MouseEvent event
static const char *const INDENT_STRING
Definition: CodeEditor.cpp:13
static const char *const PYTHON_COMMENT_STR
Definition: CodeEditor.cpp:15
filament::Texture::InternalFormat format
int height
void indentMore()
Definition: CodeEditor.cpp:358
CodeEditor(EditorSettings *settings, QWidget *parent=nullptr)
Definition: CodeEditor.cpp:17
void newFile()
Definition: CodeEditor.cpp:172
bool loadFile(const QString &fileName)
Definition: CodeEditor.cpp:183
bool saveFile(const QString &fileName)
Definition: CodeEditor.cpp:226
void comment()
Definition: CodeEditor.cpp:325
QString userFriendlyCurrentFile()
Definition: CodeEditor.cpp:247
void lineNumberAreaPaintEvent(QPaintEvent *event)
Definition: CodeEditor.cpp:145
bool save()
Definition: CodeEditor.cpp:205
void closeEvent(QCloseEvent *event) override
Definition: CodeEditor.cpp:252
void resizeEvent(QResizeEvent *event) override
Definition: CodeEditor.cpp:114
bool saveAs()
Definition: CodeEditor.cpp:217
void uncomment()
Definition: CodeEditor.cpp:339
void keyPressEvent(QKeyEvent *e) override
Definition: CodeEditor.cpp:417
void indentLess()
Definition: CodeEditor.cpp:372
bool eventFilter(QObject *target, QEvent *event) override
Definition: CodeEditor.cpp:33
int lineNumberAreaWidth()
Definition: CodeEditor.cpp:83
QColor foregroundColor() const
Color to be used for regular text.
Definition: ColorScheme.cpp:47
QColor currentLineHighlightColor() const
Color to be used if a highlight of the current line is shown.
Definition: ColorScheme.cpp:57
QColor backgroundColor() const
Color to be used as the background for the displayed text.
Definition: ColorScheme.cpp:52
const ColorScheme & colorScheme() const
int fontSize() const
bool shouldHighlightCurrentLine() const
void settingsChanged()
void useColorScheme(const ColorScheme &colorScheme)
int max(int a, int b)
Definition: cutil_math.h:48
std::string space(size_t n)
Definition: Common.h:73
constexpr Rgb black(0, 0, 0)
constexpr Rgb green(0, MAX, 0)
Definition: lsd.c:1170
double y
Definition: lsd.c:1173