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
134
136
140const QString &ScriptEditor::settingsGroup() const { return m_settingsGroup; }
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
458void ScriptEditor::markFileAsModified() { this->setText(0, text(0), 0); }
459
464void ScriptEditor::dragMoveEvent(QDragMoveEvent *de) {
465 if (!de->mimeData()->hasUrls())
466 // pass to base class - This handles text appropriately
467 QsciScintilla::dragMoveEvent(de);
468}
469
474void ScriptEditor::dragEnterEvent(QDragEnterEvent *de) {
475 if (!de->mimeData()->hasUrls()) {
476 QsciScintilla::dragEnterEvent(de);
477 }
478}
479
488QByteArray ScriptEditor::fromMimeData(const QMimeData *source, bool &rectangular) const {
489 return QsciScintilla::fromMimeData(source, rectangular);
490}
491
496void ScriptEditor::dropEvent(QDropEvent *de) {
497 if (!de->mimeData()->hasUrls()) {
498 QDropEvent localDrop(*de);
499 // pass to base class - This handles text appropriately
500 QsciScintilla::dropEvent(&localDrop);
501 }
502}
503
504void ScriptEditor::focusInEvent(QFocusEvent *fe) {
505 if (fe->gotFocus()) { // Probably always true but no harm in checking
507 QsciScintilla::focusInEvent(fe);
508 }
509}
510
515 QPrinter printer(QPrinter::HighResolution);
516 auto *print_dlg = new QPrintDialog(&printer, this);
517 print_dlg->setWindowTitle(tr("Print Script"));
518 if (print_dlg->exec() != QDialog::Accepted) {
519 return;
520 }
521 QTextDocument document(text());
522 document.print(&printer);
523}
524
529
533void ScriptEditor::writeToDevice(QIODevice &device) const { this->write(&device); }
534
535//------------------------------------------------
536// Private member functions
537//------------------------------------------------
538
547 // Hack to get around a bug in QScitilla
548 // If you pressed ( after typing in a autocomplete command the calltip does
549 // not appear, you have to delete the ( and type it again
550 // This does that for you!
551 if (event->text() == "(") {
552 auto *backspEvent = new QKeyEvent(QEvent::KeyPress, Qt::Key_Backspace, Qt::NoModifier);
553 auto *bracketEvent = new QKeyEvent(*event);
554 QsciScintilla::keyPressEvent(bracketEvent);
555 QsciScintilla::keyPressEvent(backspEvent);
556
557 delete backspEvent;
558 delete bracketEvent;
559 }
560
561 QsciScintilla::keyPressEvent(event);
562
563// Only need to do this for Unix and for QScintilla version < 2.4.2. Moreover,
564// only Gnome but I don't think we can detect that
565#ifdef Q_OS_LINUX
566#if QSCINTILLA_VERSION < 0x020402
567 // If an autocomplete box has surfaced, correct the window flags.
568 // Unfortunately the only way to
569 // do this is to search through the child objects.
570 if (isListActive()) {
571 QObjectList children = this->children();
572 QListIterator<QObject *> itr(children);
573 // Search is performed in reverse order as we want the last one created
574 itr.toBack();
575 while (itr.hasPrevious()) {
576 QObject *child = itr.previous();
577 if (child->inherits("QListWidget")) {
578 QWidget *w = qobject_cast<QWidget *>(child);
579 w->setWindowFlags(Qt::ToolTip | Qt::WindowStaysOnTopHint);
580 w->show();
581 break;
582 }
583 }
584 }
585#endif
586#endif
587}
588
589void ScriptEditor::replaceAll(const QString &searchString, const QString &replaceString, bool regex, bool caseSensitive,
590 bool matchWords, bool wrap, bool forward) {
591 int line(-1), index(-1), prevLine(-1), prevIndex(-1);
592
593 // Mark this as a set of actions that can be undone as one
594 this->beginUndoAction();
595 bool found = this->findFirst(searchString, regex, caseSensitive, matchWords, wrap, forward, 0, 0);
596 // If find first fails then there is nothing to replace
597 if (!found) {
598 QMessageBox::information(this, "Mantid - Find and Replace", "No matches found in current document.");
599 }
600
601 while (found) {
602 this->getCursorPosition(&prevLine, &prevIndex);
603 this->replace(replaceString);
604 found = this->findNext();
605 this->getCursorPosition(&line, &index);
606 // if the next match is on the previous line
607 // or if it is on the same line, but closer to the start
608 // it means we have wrapped around the text in the editor
609 if (line < prevLine || (line == prevLine && index <= prevIndex)) {
610 break;
611 }
612 }
613 this->endUndoAction();
614}
615
616int ScriptEditor::getZoom() const { return static_cast<int>(SendScintilla(SCI_GETZOOM)); }
std::string name
Definition Run.cpp:60
double error
std::map< DeltaEMode::Type, std::string > index
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.
This class provides an area to write scripts.
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 focusInEvent(QFocusEvent *fe) override
void updateProgressMarker(int lineno, bool error=false)
Update the progress marker.
int m_currentExecLine
Hold the line number of the currently executing line.
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.
const QString & settingsGroup() const
Settings group.
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 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.
void editorFocusIn(const QString &filename)
The editor now has focus.
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.
void markFileAsModified()
Mark the file as modified.
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 forwardKeyPressToBase(QKeyEvent *event)
Forward a KeyPress event to QsciScintilla base class.
void readSettings()
Read settings from persistent store.
const QString & fileName() const
The current filename.
QsciAPIs * m_completer
A pointer to a QsciAPI object that handles the code completion.
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.
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.
static QColor g_error_colour
The colour of the marker for an error state.
void setLexer(QsciLexer *) override final
Set a new code lexer for this object.
FindReplaceDialog * m_findDialog
A pointer to the find replace dialog.
QString m_filename
The file name associated with this editor.
QString m_settingsGroup
Name of group that the settings are stored under.
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.