Mantid
Loading...
Searching...
No Matches
ScriptEditor.cpp
Go to the documentation of this file.
1// Mantid Repository : https://github.com/mantidproject/mantid
2//
3// Copyright © 2018 ISIS Rutherford Appleton Laboratory UKRI,
4// NScD Oak Ridge National Laboratory, European Spallation Source,
5// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS
6// SPDX - License - Identifier: GPL - 3.0 +
7//---------------------------------------------
8// Includes
9//-----------------------------------------------
13
14// Qt
15#include <QApplication>
16#include <QFile>
17#include <QFileDialog>
18
19#include <QAction>
20#include <QClipboard>
21#include <QKeyEvent>
22#include <QMenu>
23#include <QMessageBox>
24#include <QMimeData>
25#include <QMouseEvent>
26#include <QPrintDialog>
27#include <QPrinter>
28#include <QScrollBar>
29#include <QSettings>
30#include <QShortcut>
31#include <QTextStream>
32#include <QThread>
33
34// Qscintilla
35#include <Qsci/qsciapis.h>
36#include <Qsci/qscicommandset.h>
37
38// std
39#include <cmath>
40#include <stdexcept>
41#include <utility>
42
43namespace {
44
52QsciLexer *createLexerFromName(const QString &lexerName, const QFont &font) {
53 if (lexerName == "Python") {
54 return new QsciLexerPython;
55 } else if (lexerName == "AlternateCSPython") {
56 return new AlternateCSPythonLexer(font);
57 } else {
58 throw std::invalid_argument("createLexerFromLanguage: Unsupported "
59 "name. Supported names=Python, AlternateCSPython");
60 }
61}
62} // namespace
63
64// The colour for a success marker
65QColor ScriptEditor::g_success_colour = QColor("lightgreen");
66// The colour for an error marker
67QColor ScriptEditor::g_error_colour = QColor("red");
68
69//------------------------------------------------
70// Public member functions
71//------------------------------------------------
79ScriptEditor::ScriptEditor(const QString &lexerName, const QFont &font, QWidget *parent)
80 : ScriptEditor(parent, createLexerFromName(lexerName, font)) {}
81
88ScriptEditor::ScriptEditor(QWidget *parent, QsciLexer *codelexer, QString settingsGroup)
89 : QsciScintilla(parent), m_filename(""), m_progressArrowKey(markerDefine(QsciScintilla::RightArrow)),
90 m_currentExecLine(0), m_completer(nullptr), m_previousKey(0), m_findDialog(new FindReplaceDialog(this)),
91 m_settingsGroup(std::move(settingsGroup)) {
92// Older versions of QScintilla still use just CR as the line ending, which is
93// pre-OSX.
94// New versions just use unix-style for everything but Windows.
95#if defined(Q_OS_WIN)
96 setEolMode(EolWindows);
97#else
98 setEolMode(EolUnix);
99#endif
100
101 // Remove the shortcut for zooming in because this is dealt with in
102 // keyPressEvent
103 clearKeyBinding("Ctrl++");
104
105 // Syntax highlighting and code completion
106 setLexer(codelexer);
107 readSettings();
108
109 setMarginLineNumbers(1, true);
110
111 // Editor properties
112 setAutoIndent(true);
113 setFocusPolicy(Qt::StrongFocus);
114 emit undoAvailable(isUndoAvailable());
115 emit redoAvailable(isRedoAvailable());
116}
117
122 if (m_completer) {
123 delete m_completer;
124 }
125 if (QsciLexer *current = lexer()) {
126 delete current;
127 }
128}
129
133void ScriptEditor::setSettingsGroup(const QString &name) { m_settingsGroup = name; }
134
136
141
146
151
156void ScriptEditor::setLexer(QsciLexer *codelexer) {
157 if (!codelexer) {
158 if (m_completer) {
159 delete m_completer;
160 m_completer = nullptr;
161 }
162 return;
163 }
164
165 // Delete the current lexer if one is installed
166 if (QsciLexer *current = lexer()) {
167 delete current;
168 }
169 this->QsciScintilla::setLexer(codelexer);
170
171 if (m_completer) {
172 delete m_completer;
173 m_completer = nullptr;
174 }
175
176 m_completer = new QsciAPIs(codelexer);
177}
178
182void ScriptEditor::setAutoMarginResize() { connect(this, SIGNAL(linesChanged()), this, SLOT(padMargin())); }
183
187void ScriptEditor::enableAutoCompletion(AutoCompletionSource source) {
188 setAutoCompletionSource(source);
189 setAutoCompletionThreshold(2);
190 setCallTipsStyle(QsciScintilla::CallTipsNoAutoCompletionContext);
191 setCallTipsVisible(0); // This actually makes all of them visible
192}
193
198 setAutoCompletionSource(QsciScintilla::AcsNone);
199 setAutoCompletionThreshold(-1);
200 setCallTipsVisible(-1);
201}
202
206QSize ScriptEditor::sizeHint() const { return QSize(600, 500); }
207
212 QString selectedFilter;
213 QString filter = "Scripts (*.py *.PY);;All Files (*)";
214 QString filename = QFileDialog::getSaveFileName(nullptr, "Save file...", "", filter, &selectedFilter);
215
216 if (filename.isEmpty()) {
218 }
219 if (QFileInfo(filename).suffix().isEmpty()) {
220 QString ext = selectedFilter.section('(', 1).section(' ', 0, 0);
221 ext.remove(0, 1);
222 if (ext != ")")
223 filename += ext;
224 }
225 saveScript(filename);
226}
227
230 QString filename = fileName();
231 if (filename.isEmpty()) {
232 saveAs();
233 return;
234 } else {
235 saveScript(filename);
236 }
237}
238
244void ScriptEditor::saveScript(const QString &filename) {
245 QFile file(filename);
246 if (!file.open(QIODevice::WriteOnly)) {
247 QString msg = QString("Could not open file \"%1\" for writing.").arg(filename);
248 throw std::runtime_error(qPrintable(msg));
249 }
250
251 m_filename = filename;
252 writeToDevice(file);
253 file.close();
254 setModified(false);
255}
256
265void ScriptEditor::setText(int lineno, const QString &txt, int index) {
266 int line_length = txt.length();
267 // Index is max of the length of current/new text
268 setSelection(lineno, index, lineno, qMax(line_length, this->text(lineno).length()));
269 removeSelectedText();
270 insertAt(txt, lineno, index);
271 setCursorPosition(lineno, line_length);
272}
273
280void ScriptEditor::keyPressEvent(QKeyEvent *event) {
281 // The built-in shortcut Ctrl++ from QScintilla doesn't work for some reason
282 // Creating a new QShortcut makes Ctrl++ to zoom in on the IPython console
283 // stop working
284 // So here is where Ctrl++ is detected to zoom in
285 if (QApplication::keyboardModifiers() & Qt::ControlModifier &&
286 (event->key() == Qt::Key_Plus || event->key() == Qt::Key_Equal)) {
287 zoomIn();
288 emit textZoomedIn();
289 } else {
290 // Avoids a bug in QScintilla
292 }
293
294 // There is a built in Ctrl+- shortcut for zooming out, but a signal is
295 // emitted here to tell the other editor tabs to also zoom out
296 if (QApplication::keyboardModifiers() & Qt::ControlModifier && (event->key() == Qt::Key_Minus)) {
297 emit textZoomedOut();
298 }
299}
300
301/*
302 * @param filename The new filename
303 */
304void ScriptEditor::setFileName(const QString &filename) {
305 m_filename = filename;
306 emit fileNameChanged(filename);
307}
308
312void ScriptEditor::wheelEvent(QWheelEvent *e) {
313 if (e->modifiers() == Qt::ControlModifier) {
314 if (e->angleDelta().y() > 0) {
315 zoomIn();
316 emit textZoomedIn(); // allows tracking
317 } else {
318 zoomOut();
319 emit textZoomedOut(); // allows tracking
320 }
321 } else {
322 QsciScintilla::wheelEvent(e);
323 }
324}
325/*
326 * Remove shortcut key binding from its command.
327 * @param keyCombination :: QString of the key combination e.g. "Ctrl+/".
328 */
329void ScriptEditor::clearKeyBinding(const QString &keyCombination) {
330 int keyIdentifier = QKeySequence(keyCombination)[0];
331 if (QsciCommand::validKey(keyIdentifier)) {
332 QsciCommand *cmd = standardCommands()->boundTo(keyIdentifier);
333 if (cmd) {
334 cmd->setKey(0);
335 } else {
336 throw std::invalid_argument("Key combination is not set by Scintilla.");
337 }
338 } else {
339 throw std::invalid_argument("Key combination is not valid!");
340 }
341}
342
343//-----------------------------------------------
344// Public slots
345//-----------------------------------------------
348 const int minWidth = 38;
349 int width = minWidth;
350 int ntens = static_cast<int>(std::log10(static_cast<double>(lines())));
351 if (ntens > 1) {
352 width += 5 * ntens;
353 }
354 setMarginWidth(1, width);
355}
356
362 if (enabled) {
363 setMarkerBackgroundColor(QColor("gray"), m_progressArrowKey);
364 markerAdd(0, m_progressArrowKey);
365 } else {
366 markerDeleteAll();
367 }
368}
369
379 if (QThread::currentThread() != QApplication::instance()->thread()) {
380 QMetaObject::invokeMethod(this, "updateProgressMarker", Qt::AutoConnection, Q_ARG(int, lineno), Q_ARG(bool, error));
381 } else {
383 }
384}
385
394
395 m_currentExecLine = lineno;
396 if (error) {
397 setMarkerBackgroundColor(g_error_colour, m_progressArrowKey);
398 } else {
399 setMarkerBackgroundColor(g_success_colour, m_progressArrowKey);
400 }
401 markerDeleteAll();
402 // Check the lineno actually exists, -1 means delete
403 if (lineno <= 0 || lineno > this->lines())
404 return;
405
406 ensureLineVisible(lineno);
408 progressMade(lineno);
409}
410
413
418void ScriptEditor::updateCompletionAPI(const QStringList &keywords) {
419 if (!m_completer)
420 return;
421 QStringListIterator iter(keywords);
422 m_completer->clear();
423 while (iter.hasNext()) {
424 QString item = iter.next();
425 m_completer->add(item);
426 }
447 m_completer->add("{");
448
449 m_completer->prepare();
450}
451
456void ScriptEditor::dragMoveEvent(QDragMoveEvent *de) {
457 if (!de->mimeData()->hasUrls())
458 // pass to base class - This handles text appropriately
459 QsciScintilla::dragMoveEvent(de);
460}
461
466void ScriptEditor::dragEnterEvent(QDragEnterEvent *de) {
467 if (!de->mimeData()->hasUrls()) {
468 QsciScintilla::dragEnterEvent(de);
469 }
470}
471
480QByteArray ScriptEditor::fromMimeData(const QMimeData *source, bool &rectangular) const {
481 return QsciScintilla::fromMimeData(source, rectangular);
482}
483
488void ScriptEditor::dropEvent(QDropEvent *de) {
489 if (!de->mimeData()->hasUrls()) {
490 QDropEvent localDrop(*de);
491 // pass to base class - This handles text appropriately
492 QsciScintilla::dropEvent(&localDrop);
493 }
494}
495
500 QPrinter printer(QPrinter::HighResolution);
501 auto *print_dlg = new QPrintDialog(&printer, this);
502 print_dlg->setWindowTitle(tr("Print Script"));
503 if (print_dlg->exec() != QDialog::Accepted) {
504 return;
505 }
506 QTextDocument document(text());
507 document.print(&printer);
508}
509
514
520void ScriptEditor::zoomTo(int level) {
521#ifdef __APPLE__
522 // Make all fonts 4 points bigger on the Mac because otherwise they're tiny!
523 level += 4;
524#endif
525 QsciScintilla::zoomTo(level);
526}
527
531void ScriptEditor::writeToDevice(QIODevice &device) const { this->write(&device); }
532
533//------------------------------------------------
534// Private member functions
535//------------------------------------------------
536
545 // Hack to get around a bug in QScitilla
546 // If you pressed ( after typing in a autocomplete command the calltip does
547 // not appear, you have to delete the ( and type it again
548 // This does that for you!
549 if (event->text() == "(") {
550 auto *backspEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Backspace, Qt::NoModifier);
551 auto *bracketEvent = new QKeyEvent(*event);
552 QsciScintilla::keyPressEvent(bracketEvent);
553 QsciScintilla::keyPressEvent(backspEvent);
554
555 delete backspEvent;
556 delete bracketEvent;
557 }
558
559 QsciScintilla::keyPressEvent(event);
560
561// Only need to do this for Unix and for QScintilla version < 2.4.2. Moreover,
562// only Gnome but I don't think we can detect that
563#ifdef Q_OS_LINUX
564#if QSCINTILLA_VERSION < 0x020402
565 // If an autocomplete box has surfaced, correct the window flags.
566 // Unfortunately the only way to
567 // do this is to search through the child objects.
568 if (isListActive()) {
569 QObjectList children = this->children();
570 QListIterator<QObject *> itr(children);
571 // Search is performed in reverse order as we want the last one created
572 itr.toBack();
573 while (itr.hasPrevious()) {
574 QObject *child = itr.previous();
575 if (child->inherits("QListWidget")) {
576 QWidget *w = qobject_cast<QWidget *>(child);
577 w->setWindowFlags(Qt::ToolTip | Qt::WindowStaysOnTopHint);
578 w->show();
579 break;
580 }
581 }
582 }
583#endif
584#endif
585}
586
587void ScriptEditor::replaceAll(const QString &searchString, const QString &replaceString, bool regex, bool caseSensitive,
588 bool matchWords, bool wrap, bool forward) {
589 int line(-1), index(-1), prevLine(-1), prevIndex(-1);
590
591 // Mark this as a set of actions that can be undone as one
592 this->beginUndoAction();
593 bool found = this->findFirst(searchString, regex, caseSensitive, matchWords, wrap, forward, 0, 0);
594 // If find first fails then there is nothing to replace
595 if (!found) {
596 QMessageBox::information(this, "Mantid - Find and Replace", "No matches found in current document.");
597 }
598
599 while (found) {
600 this->getCursorPosition(&prevLine, &prevIndex);
601 this->replace(replaceString);
602 found = this->findNext();
603 this->getCursorPosition(&line, &index);
604 // if the next match is on the previous line
605 // or if it is on the same line, but closer to the start
606 // it means we have wrapped around the text in the editor
607 if (line < prevLine || (line == prevLine && index <= prevIndex)) {
608 break;
609 }
610 }
611 this->endUndoAction();
612}
613
614int ScriptEditor::getZoom() const { return static_cast<int>(SendScintilla(SCI_GETZOOM)); }
double error
Definition: IndexPeaks.cpp:133
std::map< DeltaEMode::Type, std::string > index
Definition: DeltaEMode.cpp:19
Defines a Python lexer with a alternative colour scheme to the standard one provided by QsciLexerPyth...
Raises a dialog allowing the user to find/replace text in the editor.
Exception type to indicate that saving was cancelled.
Definition: ScriptEditor.h:45
This class provides an area to write scripts.
Definition: ScriptEditor.h:37
void saveAs()
Save the script, opening a dialog.
void wheelEvent(QWheelEvent *e) override
Override so that ctrl + mouse wheel will zoom in and out.
void updateProgressMarker(int lineno, bool error=false)
Update the progress marker.
int m_currentExecLine
Hold the line number of the currently executing line.
Definition: ScriptEditor.h:172
void setSettingsGroup(const QString &name)
Set the name of the group to save the settings for.
ScriptEditor(const QString &lexerName, const QFont &font=QFont(), QWidget *parent=nullptr)
Construction based on a string defining the langauge used for syntax highlighting.
void keyPressEvent(QKeyEvent *event) override
Capture key presses.
void dropEvent(QDropEvent *de) override
Accept a drag drop event and process the data appropriately.
void updateProgressMarkerFromThread(int lineno, bool error=false)
Update the progress marker potentially from a separate thread.
virtual void showFindReplaceDialog()
Raise find replace dialog.
void setLexer(QsciLexer *) override
Set a new code lexer for this object.
void undoAvailable(bool)
Inform observers that undo information is available.
void dragMoveEvent(QDragMoveEvent *de) override
Accept a drag move event and selects whether to accept the action.
void replaceAll(const QString &search, const QString &replace, bool regex, bool caseSensitive, bool matchWords, bool wrap, bool forward=true)
Replace all occurences of a string.
int getZoom() const
Get the current zoom factor.
int m_progressArrowKey
The margin marker.
Definition: ScriptEditor.h:170
void redoAvailable(bool)
Inform observers that redo information is available.
void setMarkerState(bool enabled)
Set the marker state.
void writeSettings()
Write settings from persistent store.
void padMargin()
Ensure the margin width is big enough to hold everything + padding.
QString settingsGroup() const
Settings group.
void fileNameChanged(const QString &fileName)
Notify that the filename has been modified.
QByteArray fromMimeData(const QMimeData *source, bool &rectangular) const override
If the QMimeData object holds workspaces names then extract text from a QMimeData object and add the ...
void enableAutoCompletion(AutoCompletionSource source=QsciScintilla::AcsAPIs)
Enable the auto complete.
void setFileName(const QString &filename)
Set a new file name.
void disableAutoCompletion()
Disable the auto complete.
void clearKeyBinding(const QString &keyCombination)
Clear keyboard shortcut binding.
void zoomTo(int level) override
Override zoomTo slot.
void forwardKeyPressToBase(QKeyEvent *event)
Forward a KeyPress event to QsciScintilla base class.
void readSettings()
Read settings from persistent store.
QsciAPIs * m_completer
A pointer to a QsciAPI object that handles the code completion.
Definition: ScriptEditor.h:174
void progressMade(const int progress)
Progress has been made in script execution.
void print()
Print the text within the widget.
~ScriptEditor() override
Destructor.
virtual void writeToDevice(QIODevice &device) const
Write to the given device.
QString fileName() const
The current filename.
Definition: ScriptEditor.h:88
void saveToCurrentFile()
Save to the current filename, opening a dialog if blank.
static QColor g_success_colour
The colour of the marker for a success state.
Definition: ScriptEditor.h:176
static QColor g_error_colour
The colour of the marker for an error state.
Definition: ScriptEditor.h:178
FindReplaceDialog * m_findDialog
A pointer to the find replace dialog.
Definition: ScriptEditor.h:182
QString m_filename
The file name associated with this editor.
Definition: ScriptEditor.h:167
QString m_settingsGroup
Name of group that the settings are stored under.
Definition: ScriptEditor.h:184
void dragEnterEvent(QDragEnterEvent *de) override
Accept a drag enter event and selects whether to accept the action.
void textZoomedIn()
Emitted when a zoom in is requested.
void textZoomedOut()
Emitted when a zoom out is requested.
void saveScript(const QString &filename)
Save a the text to the given filename.
void setText(int lineno, const QString &text, int index=0)
Set the text on a given line number.
void updateCompletionAPI(const QStringList &keywords)
Refresh the autocomplete information base on a new set of keywords.
void markExecutingLineAsError()
Mark the progress arrow as an error.
QSize sizeHint() const override
Default size hint.
void setAutoMarginResize()
Make the object resize to margin to fit the contents with padding.
STL namespace.