FreeCAD

Форк
0
/
PythonConsole.cpp 
1721 строка · 57.1 Кб
1
/***************************************************************************
2
 *   Copyright (c) 2004 Werner Mayer <wmayer[at]users.sourceforge.net>     *
3
 *                                                                         *
4
 *   This file is part of the FreeCAD CAx development system.              *
5
 *                                                                         *
6
 *   This library is free software; you can redistribute it and/or         *
7
 *   modify it under the terms of the GNU Library General Public           *
8
 *   License as published by the Free Software Foundation; either          *
9
 *   version 2 of the License, or (at your option) any later version.      *
10
 *                                                                         *
11
 *   This library  is distributed in the hope that it will be useful,      *
12
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
13
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
14
 *   GNU Library General Public License for more details.                  *
15
 *                                                                         *
16
 *   You should have received a copy of the GNU Library General Public     *
17
 *   License along with this library; see the file COPYING.LIB. If not,    *
18
 *   write to the Free Software Foundation, Inc., 59 Temple Place,         *
19
 *   Suite 330, Boston, MA  02111-1307, USA                                *
20
 *                                                                         *
21
 ***************************************************************************/
22

23
#include "PreCompiled.h"
24
#ifndef _PreComp_
25
# include <QApplication>
26
# include <QClipboard>
27
# include <QDockWidget>
28
# include <QKeyEvent>
29
# include <QMenu>
30
# include <QMessageBox>
31
# include <QMimeData>
32
# include <QTextCursor>
33
# include <QTextDocumentFragment>
34
# include <QTextStream>
35
# include <QTime>
36
# include <QUrl>
37
#endif
38

39
#include <Base/Interpreter.h>
40
#include <App/Color.h>
41

42
#include "PythonConsole.h"
43
#include "PythonConsolePy.h"
44
#include "PythonTracing.h"
45
#include "Application.h"
46
#include "CallTips.h"
47
#include "FileDialog.h"
48
#include "MainWindow.h"
49
#include "Tools.h"
50

51

52
using namespace Gui;
53

54
namespace Gui
55
{
56

57
static const QChar promptEnd( QLatin1Char(' ') );  //< char for detecting prompt end
58

59
inline int promptLength( const QString &lineStr )
60
{
61
    return lineStr.indexOf( promptEnd ) + 1;
62
}
63

64
inline QString stripPromptFrom( const QString &lineStr )
65
{
66
    return lineStr.mid( promptLength(lineStr) );
67
}
68

69
/**
70
 * cursorBeyond checks if cursor is at a valid position to accept keyEvents.
71
 * @param cursor - cursor to check
72
 * @param limit  - cursor that marks the begin of the input region
73
 * @param shift  - offset for shifting the limit for non-selection cursors [default: 0]
74
 * @return true if a keyEvent is ok at cursor's position, false otherwise
75
 */
76
inline bool cursorBeyond( const QTextCursor &cursor, const QTextCursor &limit, int shift = 0 )
77
{
78
    int pos = limit.position();
79
    if (cursor.hasSelection()) {
80
        return (cursor.selectionStart() >= pos && cursor.selectionEnd() >= pos);
81
    }
82
    
83
    return cursor.position() >= (pos + shift);
84
}
85

86
struct PythonConsoleP
87
{
88
    enum Output {Error = 20, Message = 21};
89
    enum CopyType {Normal, History, Command};
90
    CopyType type;
91
    PyObject *_stdoutPy=nullptr, *_stderrPy=nullptr, *_stdinPy=nullptr, *_stdin=nullptr;
92
    InteractiveInterpreter* interpreter=nullptr;
93
    CallTipsList* callTipsList=nullptr;
94
    ConsoleHistory history;
95
    QString output, error, info, historyFile;
96
    QStringList statements;
97
    bool interactive;
98
    QMap<QString, QColor> colormap; // Color map
99
    ParameterGrp::handle hGrpSettings;
100
    PythonConsoleP()
101
    {
102
        type = Normal;
103
        interactive = false;
104
        historyFile = QString::fromUtf8((App::Application::getUserAppDataDir() + "PythonHistory.log").c_str());
105
        colormap[QLatin1String("Text")] = qApp->palette().windowText().color();
106
        colormap[QLatin1String("Bookmark")] = Qt::cyan;
107
        colormap[QLatin1String("Breakpoint")] = Qt::red;
108
        colormap[QLatin1String("Keyword")] = Qt::blue;
109
        colormap[QLatin1String("Comment")] = QColor(0, 170, 0);
110
        colormap[QLatin1String("Block comment")] = QColor(160, 160, 164);
111
        colormap[QLatin1String("Number")] = Qt::blue;
112
        colormap[QLatin1String("String")] = Qt::red;
113
        colormap[QLatin1String("Character")] = Qt::red;
114
        colormap[QLatin1String("Class name")] = QColor(255, 170, 0);
115
        colormap[QLatin1String("Define name")] = QColor(255, 170, 0);
116
        colormap[QLatin1String("Operator")] = QColor(160, 160, 164);
117
        colormap[QLatin1String("Python output")] = QColor(170, 170, 127);
118
        colormap[QLatin1String("Python error")] = Qt::red;
119
    }
120
};
121

122
struct InteractiveInterpreterP
123
{
124
    PyObject* interpreter{nullptr};
125
    PyObject* sysmodule{nullptr};
126
    QStringList buffer;
127
    PythonTracing trace;
128
};
129

130
} // namespace Gui
131

132
InteractiveInterpreter::InteractiveInterpreter()
133
{
134
    // import code.py and create an instance of InteractiveInterpreter
135
    Base::PyGILStateLocker lock;
136
    PyObject* module = PyImport_ImportModule("code");
137
    if (!module) {
138
        throw Base::PyException();
139
    }
140
    PyObject* func = PyObject_GetAttrString(module, "InteractiveInterpreter");
141
    PyObject* args = Py_BuildValue("()");
142
    d = new InteractiveInterpreterP;
143
#if PY_VERSION_HEX < 0x03090000
144
    d->interpreter = PyEval_CallObject(func,args);
145
#else
146
    d->interpreter = PyObject_CallObject(func,args);
147
#endif
148
    Py_DECREF(args);
149
    Py_DECREF(func);
150
    Py_DECREF(module);
151

152
    setPrompt();
153
}
154

155
InteractiveInterpreter::~InteractiveInterpreter()
156
{
157
    Base::PyGILStateLocker lock;
158
    Py_XDECREF(d->interpreter);
159
    Py_XDECREF(d->sysmodule);
160
    delete d;
161
}
162

163
/**
164
 * Set the ps1 and ps2 members of the sys module if not yet defined.
165
 */
166
void InteractiveInterpreter::setPrompt()
167
{
168
    // import code.py and create an instance of InteractiveInterpreter
169
    Base::PyGILStateLocker lock;
170
    d->sysmodule = PyImport_ImportModule("sys");
171
    if (!PyObject_HasAttrString(d->sysmodule, "ps1")) {
172
        PyObject_SetAttrString(d->sysmodule, "ps1", PyUnicode_FromString(">>> "));
173
    }
174
    if (!PyObject_HasAttrString(d->sysmodule, "ps2")) {
175
        PyObject_SetAttrString(d->sysmodule, "ps2", PyUnicode_FromString("... "));
176
    }
177
}
178

179
/**
180
 * Compile a command and determine whether it is incomplete.
181
 *
182
 * The source string may contain line feeds and/or carriage returns. \n
183
 * Return value / exceptions raised:
184
 * - Return a code object if the command is complete and valid
185
 * - Return None if the command is incomplete
186
 * - Raise SyntaxError, ValueError or OverflowError if the command is a
187
 * syntax error (OverflowError and ValueError can be produced by
188
 * malformed literals).
189
 */
190
PyObject* InteractiveInterpreter::compile(const char* source) const
191
{
192
    Base::PyGILStateLocker lock;
193
    PyObject* func = PyObject_GetAttrString(d->interpreter, "compile");
194
    PyObject* args = Py_BuildValue("(s)", source);
195
#if PY_VERSION_HEX < 0x03090000
196
    PyObject* eval = PyEval_CallObject(func,args);  // must decref later
197
#else
198
    PyObject* eval = PyObject_CallObject(func,args);  // must decref later
199
#endif
200

201
    Py_XDECREF(args);
202
    Py_XDECREF(func);
203

204
    if (eval){
205
        return eval;
206
    } else {
207
        // do not throw Base::PyException as this clears the error indicator
208
        throw Base::RuntimeError("Code evaluation failed");
209
    }
210

211
    // can never happen
212
    return nullptr;
213
}
214

215
/**
216
 * Compile a command and determine whether it is incomplete.
217
 *
218
 * The source string may contain line feeds and/or carriage returns. \n
219
 * Return value:
220
 * - Return  1 if the command is incomplete
221
 * - Return  0 if the command is complete and valid
222
 * - Return -1 if the command is a syntax error
223
 * .
224
 * (OverflowError and ValueError can be produced by malformed literals).
225
 */
226
int InteractiveInterpreter::compileCommand(const char* source) const
227
{
228
    Base::PyGILStateLocker lock;
229
    PyObject* func = PyObject_GetAttrString(d->interpreter, "compile");
230
    PyObject* args = Py_BuildValue("(s)", source);
231
#if PY_VERSION_HEX < 0x03090000
232
    PyObject* eval = PyEval_CallObject(func,args);  // must decref later
233
#else
234
    PyObject* eval = PyObject_CallObject(func,args);  // must decref later
235
#endif
236

237
    Py_DECREF(args);
238
    Py_DECREF(func);
239

240
    int ret = 0;
241
    if (eval){
242
        if (PyObject_TypeCheck(Py_None, eval->ob_type)) {
243
            ret = 1; // incomplete
244
        }
245
        else {
246
            ret = 0; // complete
247
        }
248
        Py_DECREF(eval);
249
    } else {
250
        ret = -1;    // invalid
251
    }
252

253
    return ret;
254
}
255

256
/**
257
 * Compile and run some source in the interpreter.
258
 *
259
 * One several things can happen:
260
 *
261
 * - The input is incorrect; compile() raised an exception (SyntaxError or OverflowError).
262
 *   A syntax traceback will be printed by calling Python's PyErr_Print() method to the redirected stderr.
263
 *
264
 * - The input is incomplete, and more input is required; compile() returned 'None'.
265
 *   Nothing happens.
266
 *
267
 * - The input is complete; compile() returned a code object.  The code is executed by calling
268
 *   runCode() (which also handles run-time exceptions, except for SystemExit).
269
 * .
270
 * The return value is True if the input is incomplete, False in the other cases (unless
271
 * an exception is raised). The return value can be used to decide whether to use sys.ps1
272
 * or sys.ps2 to prompt the next line.
273
 */
274
bool InteractiveInterpreter::runSource(const char* source) const
275
{
276
    Base::PyGILStateLocker lock;
277
    PyObject* code;
278
    try {
279
        code = compile(source);
280
    } catch (const Base::Exception&) {
281
        // A system, overflow or value error was raised.
282
        // We clear the traceback info as this might be a longly
283
        // message we don't need.
284
        PyObject *errobj, *errdata, *errtraceback;
285
        PyErr_Fetch(&errobj, &errdata, &errtraceback);
286
        PyErr_Restore(errobj, errdata, nullptr);
287
        // print error message
288
        if (PyErr_Occurred()) PyErr_Print();
289
            return false;
290
    }
291

292
    // the command is incomplete
293
    if (PyObject_TypeCheck(Py_None, code->ob_type)) {
294
        Py_DECREF(code);
295
        return true;
296
    }
297

298
    // run the code and return false
299
    runCode((PyCodeObject*)code);
300
    return false;
301
}
302

303
bool InteractiveInterpreter::isOccupied() const
304
{
305
    return d->trace.isActive();
306
}
307

308
bool InteractiveInterpreter::interrupt() const
309
{
310
    return d->trace.interrupt();
311
}
312

313
/* Execute a code object.
314
 *
315
 * When an exception occurs,  a traceback is displayed.
316
 * All exceptions are caught except SystemExit, which is reraised.
317
 */
318
void InteractiveInterpreter::runCode(PyCodeObject* code) const
319
{
320
    if (isOccupied()) {
321
        return;
322
    }
323

324
    d->trace.fetchFromSettings();
325
    PythonTracingLocker tracelock(d->trace);
326

327
    Base::PyGILStateLocker lock;
328
    PyObject *module, *dict, *presult;           /* "exec code in d, d" */
329
    module = PyImport_AddModule("__main__");     /* get module, init python */
330
    if (!module) {
331
        throw Base::PyException();                 /* not incref'd */
332
    }
333
    dict = PyModule_GetDict(module);             /* get dict namespace */
334
    if (!dict) {
335
        throw Base::PyException();                 /* not incref'd */
336
    }
337

338
    // It seems that the return value is always 'None' or Null
339
    presult = PyEval_EvalCode((PyObject*)code, dict, dict); /* run compiled bytecode */
340
    Py_XDECREF(code);                            /* decref the code object */
341
    if (!presult) {
342
        if (PyErr_ExceptionMatches(PyExc_SystemExit)) {
343
            // throw SystemExit exception
344
            throw Base::SystemExitException();
345
        }
346
        if (PyErr_Occurred()) {                   /* get latest python exception information */
347
            PyObject *errobj, *errdata, *errtraceback;
348
            PyErr_Fetch(&errobj, &errdata, &errtraceback);
349
            // the error message can be empty so errdata will be null
350
            if (errdata && PyDict_Check(errdata)) {
351
                PyObject* value = PyDict_GetItemString(errdata, "swhat");
352
                if (value) {
353
                    Base::RuntimeError e;
354
                    e.setPyObject(errdata);
355
                    Py_DECREF(errdata);
356

357
                    std::stringstream str;
358
                    str << e.what();
359
                    if (!e.getFunction().empty()) {
360
                        str << " In " << e.getFunction();
361
                    }
362
                    if (!e.getFile().empty() && e.getLine() > 0) {
363
                        std::string file = e.getFile();
364
                        std::size_t pos = file.find("src");
365
                        if (pos!=std::string::npos) {
366
                            file = file.substr(pos);
367
                        }
368
                        str << " in " << file << ":" << e.getLine();
369
                    }
370

371
                    std::string err = str.str();
372
                    errdata = PyUnicode_FromString(err.c_str());
373
                }
374
            }
375
            PyErr_Restore(errobj, errdata, errtraceback);
376
            PyErr_Print();                           /* and print the error to the error output */
377
        }
378
    } else {
379
        Py_DECREF(presult);
380
    }
381
}
382

383
/**
384
 * Store the line into the internal buffer and compile the total buffer.
385
 * In case it is a complete Python command the buffer is emptied.
386
 */
387
bool InteractiveInterpreter::push(const char* line)
388
{
389
    d->buffer.append(QString::fromUtf8(line));
390
    QString source = d->buffer.join(QLatin1String("\n"));
391
    try {
392
        bool more = runSource(source.toUtf8());
393
        if (!more) {
394
            d->buffer.clear();
395
        }
396
        return more;
397
    } catch (const Base::SystemExitException&) {
398
        d->buffer.clear();
399
        throw;
400
    } catch (...) {
401
        // indication of unhandled exception
402
        d->buffer.clear();
403
        if (PyErr_Occurred()) {
404
            PyErr_Print();
405
        }
406
        throw;
407
    }
408

409
    return false;
410
}
411

412
bool InteractiveInterpreter::hasPendingInput( ) const
413
{
414
    return (!d->buffer.isEmpty());
415
}
416

417
QStringList InteractiveInterpreter::getBuffer() const
418
{
419
    return d->buffer;
420
}
421

422
void InteractiveInterpreter::setBuffer(const QStringList& buf)
423
{
424
    d->buffer = buf;
425
}
426

427
void InteractiveInterpreter::clearBuffer()
428
{
429
    d->buffer.clear();
430
}
431

432
/* TRANSLATOR Gui::PythonConsole */
433

434
/**
435
 *  Constructs a PythonConsole which is a child of 'parent'.
436
 */
437
PythonConsole::PythonConsole(QWidget *parent)
438
  : TextEdit(parent), WindowParameter( "Editor" ), _sourceDrain(nullptr)
439
{
440
    d = new PythonConsoleP();
441
    d->interactive = false;
442

443
    // create an instance of InteractiveInterpreter
444
    try {
445
        d->interpreter = new InteractiveInterpreter();
446
    } catch (const Base::Exception& e) {
447
        setPlainText(QString::fromLatin1(e.what()));
448
        setEnabled(false);
449
    }
450

451
    // use the console highlighter
452
    pythonSyntax = new PythonConsoleHighlighter(this);
453
    pythonSyntax->setDocument(this->document());
454

455
    // create the window for call tips
456
    d->callTipsList = new CallTipsList(this);
457
    d->callTipsList->setFrameStyle(QFrame::Box);
458
    d->callTipsList->setFrameShadow(QFrame::Raised);
459
    d->callTipsList->setLineWidth(2);
460
    installEventFilter(d->callTipsList);
461
    viewport()->installEventFilter(d->callTipsList);
462
    d->callTipsList->setSelectionMode( QAbstractItemView::SingleSelection );
463
    d->callTipsList->hide();
464

465
    QFont serifFont(QLatin1String("Courier"), 10, QFont::Normal);
466
    setFont(serifFont);
467

468
    // set colors and font from settings
469
    ParameterGrp::handle hPrefGrp = getWindowParameter();
470
    hPrefGrp->Attach(this);
471
    hPrefGrp->NotifyAll();
472

473
    d->hGrpSettings = WindowParameter::getDefaultParameter()->GetGroup("PythonConsole");
474
    d->hGrpSettings->Attach(this);
475
    d->hGrpSettings->NotifyAll();
476

477
    // disable undo/redo stuff
478
    setUndoRedoEnabled( false );
479
    setAcceptDrops( true );
480

481
    // try to override Python's stdout/err
482
    Base::PyGILStateLocker lock;
483
    d->_stdoutPy = new PythonStdout(this);
484
    d->_stderrPy = new PythonStderr(this);
485
    d->_stdinPy  = new PythonStdin (this);
486
    d->_stdin  = PySys_GetObject("stdin");
487

488
    // Don't override stdin when running FreeCAD as Python module
489
    auto& cfg = App::Application::Config();
490
    auto overrideStdIn = cfg.find("DontOverrideStdIn");
491
    if (overrideStdIn == cfg.end()) {
492
        PySys_SetObject("stdin", d->_stdinPy);
493
    }
494

495
    const char* version  = PyUnicode_AsUTF8(PySys_GetObject("version"));
496
    const char* platform = PyUnicode_AsUTF8(PySys_GetObject("platform"));
497
    d->info = QString::fromLatin1("Python %1 on %2\n"
498
    "Type 'help', 'copyright', 'credits' or 'license' for more information.")
499
    .arg(QString::fromLatin1(version), QString::fromLatin1(platform));
500
    d->output = d->info;
501
    printPrompt(PythonConsole::Complete);
502
    loadHistory();
503

504
    flusher = new QTimer(this);
505
    connect(flusher, &QTimer::timeout, this, &PythonConsole::flushOutput);
506
    flusher->start(100);
507
}
508

509
/** Destroys the object and frees any allocated resources */
510
PythonConsole::~PythonConsole()
511
{
512
    saveHistory();
513
    Base::PyGILStateLocker lock;
514
    d->hGrpSettings->Detach(this);
515
    getWindowParameter()->Detach(this);
516
    delete pythonSyntax;
517
    Py_XDECREF(d->_stdoutPy);
518
    Py_XDECREF(d->_stderrPy);
519
    Py_XDECREF(d->_stdinPy);
520
    delete d->interpreter;
521
    delete d;
522
}
523

524
/** Set new font and colors according to the parameters. */
525
void PythonConsole::OnChange(Base::Subject<const char*> &rCaller, const char* sReason )
526
{
527
    const auto & rGrp = static_cast<ParameterGrp &>(rCaller);
528

529
    if (strcmp(sReason, "PythonWordWrap") == 0) {
530
        bool pythonWordWrap = rGrp.GetBool("PythonWordWrap", true);
531
        if (pythonWordWrap) {
532
            setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
533
        }
534
        else {
535
            setWordWrapMode(QTextOption::NoWrap);
536
        }
537
    }
538

539
    if (strcmp(sReason, "FontSize") == 0 || strcmp(sReason, "Font") == 0) {
540
        int fontSize = rGrp.GetInt("FontSize", 10);
541
        QString fontFamily = QString::fromLatin1(rGrp.GetASCII("Font", "Courier").c_str());
542

543
        QFont font(fontFamily, fontSize);
544
        setFont(font);
545
        QFontMetrics metric(font);
546
        int width = QtTools::horizontalAdvance(metric, QLatin1String("0000"));
547
#if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
548
        setTabStopWidth(width);
549
#else
550
        setTabStopDistance(width);
551
#endif
552
    }
553
    else {
554
        QMap<QString, QColor>::Iterator it = d->colormap.find(QString::fromLatin1(sReason));
555
        if (it != d->colormap.end()) {
556
            QColor color = it.value();
557
            unsigned int col = App::Color::asPackedRGB<QColor>(color);
558
            auto value = static_cast<unsigned long>(col);
559
            value = rGrp.GetUnsigned(sReason, value);
560
            col = static_cast<unsigned int>(value);
561
            color.setRgb((col>>24)&0xff, (col>>16)&0xff, (col>>8)&0xff);
562
            pythonSyntax->setColor(QString::fromLatin1(sReason), color);
563
        }
564
    }
565

566
    if (strcmp(sReason, "PythonBlockCursor") == 0) {
567
        bool block = rGrp.GetBool("PythonBlockCursor", false);
568
        if (block) {
569
            setCursorWidth(QFontMetrics(font()).averageCharWidth());
570
        }
571
        else {
572
            setCursorWidth(1);
573
        }
574
    }
575
}
576

577
/**
578
 * Checks the input of the console to make the correct indentations.
579
 * After a command is prompted completely the Python interpreter is started.
580
 */
581
void PythonConsole::keyPressEvent(QKeyEvent * e)
582
{
583
    bool restartHistory = true;
584
    QTextCursor cursor = this->textCursor();
585
    QTextCursor inputLineBegin = this->inputBegin();
586

587
    if (e->key() == Qt::Key_C && e->modifiers() == Qt::ControlModifier) {
588
        if (d->interpreter->interrupt()) {
589
            return;
590
        }
591
    }
592

593
    if (!cursorBeyond( cursor, inputLineBegin ))
594
    {
595
        /**
596
         * The cursor is placed not on the input line (or within the prompt string)
597
         * So we handle key input as follows:
598
         *   - don't allow changing previous lines.
599
         *   - allow full movement (no prompt restriction)
600
         *   - allow copying content (Ctrl+C)
601
         *   - "escape" to end of input line
602
         */
603
        switch (e->key())
604
        {
605
          case Qt::Key_Return:
606
          case Qt::Key_Enter:
607
          case Qt::Key_Escape:
608
          case Qt::Key_Backspace:
609
              this->moveCursor( QTextCursor::End );
610
              break;
611

612
          default:
613
              if (e->text().isEmpty() ||
614
                  e->matches(QKeySequence::Copy) ||
615
                  e->matches(QKeySequence::SelectAll)) {
616
                  TextEdit::keyPressEvent(e);
617
              }
618
              else if (!e->text().isEmpty() &&
619
                  (e->modifiers() == Qt::NoModifier ||
620
                   e->modifiers() == Qt::ShiftModifier)) {
621
                  this->moveCursor(QTextCursor::End);
622
                  TextEdit::keyPressEvent(e);
623
              }
624
              break;
625
        }
626
    }
627
    else
628
    {
629
        /**
630
         * The cursor sits somewhere on the input line (after the prompt)
631
         * Here we handle key input a bit different:
632
         *   - restrict cursor movement to input line range (excluding the prompt characters)
633
         *   - roam the history by Up/Down keys
634
         *   - show call tips on period
635
         */
636
        QTextBlock inputBlock = inputLineBegin.block();              //< get the last paragraph's text
637
        QString    inputLine  = inputBlock.text();
638
        QString    inputStrg  = stripPromptFrom( inputLine );
639
        if (this->_sourceDrain && !this->_sourceDrain->isEmpty()) {
640
            inputStrg = inputLine.mid(this->_sourceDrain->length());
641
        }
642

643
        switch (e->key())
644
        {
645
          case Qt::Key_Escape:
646
          {
647
              // disable current input string - i.e. put it to history but don't execute it.
648
              if (!inputStrg.isEmpty())
649
              {
650
                  d->history.append( QLatin1String("# ") + inputStrg );  //< put commented string to history ...
651
                  inputLineBegin.insertText( QString::fromLatin1("# ") ); //< and comment it on console
652
                  setTextCursor( inputLineBegin );
653
                  printPrompt(d->interpreter->hasPendingInput()          //< print adequate prompt
654
                      ? PythonConsole::Incomplete
655
                      : PythonConsole::Complete);
656
              }
657
          }   break;
658

659
          case Qt::Key_Return:
660
          case Qt::Key_Enter:
661
          {
662
              d->history.append( inputStrg ); //< put statement to history
663
              runSource( inputStrg );         //< commit input string
664
          }   break;
665

666
          case Qt::Key_Period:
667
          {
668
              // In Qt 4.8 there is a strange behaviour because when pressing ":"
669
              // then key is also set to 'Period' instead of 'Colon'. So we have
670
              // to make sure we only handle the period.
671
              if (e->text() == QLatin1String(".")) {
672
                  // analyse context and show available call tips
673
                  int contextLength = cursor.position() - inputLineBegin.position();
674
                  TextEdit::keyPressEvent(e);
675
                  d->callTipsList->showTips( inputStrg.left( contextLength ) );
676
              }
677
              else {
678
                  TextEdit::keyPressEvent(e);
679
              }
680
          }   break;
681

682
          case Qt::Key_Home:
683
          {
684
              QTextCursor::MoveMode mode = (e->modifiers() & Qt::ShiftModifier)? QTextCursor::KeepAnchor
685
                                                                    /* else */ : QTextCursor::MoveAnchor;
686
              cursor.setPosition( inputLineBegin.position(), mode );
687
              setTextCursor( cursor );
688
              ensureCursorVisible();
689
          }   break;
690

691
          case Qt::Key_Up:
692
          {
693
              // if possible, move back in history
694
              if (d->history.prev( inputStrg ))
695
                  { overrideCursor( d->history.value() ); }
696
              restartHistory = false;
697
          }   break;
698

699
          case Qt::Key_Down:
700
          {
701
              // if possible, move forward in history
702
              if (d->history.next())
703
                  { overrideCursor( d->history.value() ); }
704
              restartHistory = false;
705
          }   break;
706

707
          case Qt::Key_Left:
708
          {
709
              if (cursor > inputLineBegin)
710
                  { TextEdit::keyPressEvent(e); }
711
              restartHistory = false;
712
          }   break;
713

714
          case Qt::Key_Right:
715
          {
716
              TextEdit::keyPressEvent(e);
717
              restartHistory = false;
718
          }   break;
719

720
          case Qt::Key_Backspace:
721
          {
722
              if (cursorBeyond( cursor, inputLineBegin, +1 ))
723
                  { TextEdit::keyPressEvent(e); }
724
          }   break;
725

726
          default:
727
          {
728
              TextEdit::keyPressEvent(e);
729
          }   break;
730
        }
731
        // This can't be done in CallTipsList::eventFilter() because we must first perform
732
        // the event and afterwards update the list widget
733
        if (d->callTipsList->isVisible())
734
            { d->callTipsList->validateCursor(); }
735

736
        // disable history restart if input line changed
737
        restartHistory &= (inputLine != inputBlock.text());
738
    }
739
    // any cursor move resets the history to its latest item.
740
    if (restartHistory) {
741
        d->history.restart();
742
    }
743
}
744

745
/**
746
 * Insert an output message to the console. This message comes from
747
 * the Python interpreter and is redirected from sys.stdout.
748
 */
749
void PythonConsole::insertPythonOutput( const QString& msg )
750
{
751
    d->output += msg;
752
}
753

754
/**
755
 * Insert an error message to the console. This message comes from
756
 * the Python interpreter and is redirected from sys.stderr.
757
 */
758
void PythonConsole::insertPythonError ( const QString& err )
759
{
760
    d->error += err;
761
}
762

763
void PythonConsole::onFlush()
764
{
765
    printPrompt(PythonConsole::Flush);
766
}
767

768
void PythonConsole::flushOutput()
769
{
770
    if (d->interpreter->isOccupied()) {
771
        if (d->output.length() > 0 || d->error.length() > 0) {
772
            printPrompt(PythonConsole::Complete);
773
        }
774
    }
775
}
776

777
/** Prints the ps1 prompt (>>> ) for complete and ps2 prompt (... ) for
778
 * incomplete commands to the console window.
779
 */
780
void PythonConsole::printPrompt(PythonConsole::Prompt mode)
781
{
782
    // write normal messages
783
    if (!d->output.isEmpty()) {
784
        appendOutput(d->output, (int)PythonConsoleP::Message);
785
        d->output.clear();
786
    }
787

788
    // write error messages
789
    if (!d->error.isEmpty()) {
790
        appendOutput(d->error, (int)PythonConsoleP::Error);
791
        d->error.clear();
792
    }
793

794
    // Append the prompt string
795
    QTextCursor cursor = textCursor();
796

797
    if (mode != PythonConsole::Special)
798
    {
799
      cursor.beginEditBlock();
800
      cursor.movePosition(QTextCursor::End);
801
      QTextBlock block = cursor.block();
802

803
      // Python's print command appends a trailing '\n' to the system output.
804
      // In this case, however, we should not add a new text block. We force
805
      // the current block to be normal text (user state = 0) to be highlighted
806
      // correctly and append the '>>> ' or '... ' to this block.
807
      if (block.length() > 1)
808
          cursor.insertBlock(cursor.blockFormat(), cursor.charFormat());
809
      else
810
          block.setUserState(0);
811

812
      switch (mode)
813
      {
814
      case PythonConsole::Incomplete:
815
          cursor.insertText(QString::fromLatin1("... "));
816
          break;
817
      case PythonConsole::Complete:
818
          cursor.insertText(QString::fromLatin1(">>> "));
819
          break;
820
      default:
821
          break;
822
      }
823
      cursor.endEditBlock();
824
    }
825
    // move cursor to the end
826
    cursor.movePosition(QTextCursor::End);
827
    setTextCursor(cursor);
828
}
829

830
/**
831
 * Appends \a output to the console and set \a state as user state to
832
 * the text block which is needed for the highlighting.
833
 */
834
void PythonConsole::appendOutput(const QString& output, int state)
835
{
836
    QTextCursor cursor = textCursor();
837
    cursor.movePosition(QTextCursor::End);
838
    int pos = cursor.position() + 1;
839

840
    // delay rehighlighting
841
    cursor.beginEditBlock();
842
    appendPlainText(output);
843

844
    QTextBlock block = this->document()->findBlock(pos);
845
    while (block.isValid()) {
846
        block.setUserState(state);
847
        block = block.next();
848
    }
849
    cursor.endEditBlock(); // start highlightiong
850
}
851

852
/**
853
 * Builds up the Python command and pass it to the interpreter.
854
 */
855
void PythonConsole::runSource(const QString& line)
856
{
857
    /**
858
     * Check if there's a "source drain", which wants to consume the source in another way then just executing it.
859
     * If so, put the source to the drain and emit a signal to notify the consumer, whomever this may be.
860
     */
861
    if (this->_sourceDrain) {
862
        *this->_sourceDrain = line;
863
        Q_EMIT pendingSource();
864
        return;
865
    }
866

867
    if (d->interpreter->isOccupied()) {
868
        insertPythonError(QString::fromLatin1("Previous command still running!"));
869
        return;
870
    }
871

872
    bool incomplete = false;
873
    Base::PyGILStateLocker lock;
874
    PyObject* default_stdout = PySys_GetObject("stdout");
875
    PyObject* default_stderr = PySys_GetObject("stderr");
876
    PySys_SetObject("stdout", d->_stdoutPy);
877
    PySys_SetObject("stderr", d->_stderrPy);
878
    d->interactive = true;
879

880
    try {
881
        d->history.markScratch();        //< mark current history position ...
882
        // launch the command now
883
        incomplete = d->interpreter->push(line.toUtf8());
884
        if (!incomplete) {
885
            d->history.doScratch();
886
        }    //< ... and scratch history entries that might have been added by executing the line.
887
        setFocus(); // if focus was lost
888
    }
889
    catch (const Base::SystemExitException&) {
890
        // In Python the exception must be cleared because when the message box below appears
891
        // callable Python objects can be invoked and due to a failing assert the application
892
        // will be aborted.
893
        PyErr_Clear();
894

895
        ParameterGrp::handle hPrefGrp = getWindowParameter();
896
        bool check = hPrefGrp->GetBool("CheckSystemExit",true);
897
        int ret = QMessageBox::Yes;
898
        if (check) {
899
            ret = QMessageBox::question(this, tr("System exit"),
900
                tr("The application is still running.\nDo you want to exit without saving your data?"),
901
                QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
902
        }
903
        if (ret == QMessageBox::Yes) {
904
            PyErr_Clear();
905
            throw;
906
        }
907
        else {
908
            PyErr_Clear();
909
        }
910
    }
911
    catch (const Py::Exception&) {
912
        QMessageBox::critical(this, tr("Python console"), tr("Unhandled PyCXX exception."));
913
    }
914
    catch (const Base::Exception&) {
915
        QMessageBox::critical(this, tr("Python console"), tr("Unhandled FreeCAD exception."));
916
    }
917
    catch (const std::exception&) {
918
        QMessageBox::critical(this, tr("Python console"), tr("Unhandled std C++ exception."));
919
    }
920
    catch (...) {
921
        QMessageBox::critical(this, tr("Python console"), tr("Unhandled unknown C++ exception."));
922
    }
923

924
    printPrompt(incomplete ? PythonConsole::Incomplete
925
                           : PythonConsole::Complete);
926
    PySys_SetObject("stdout", default_stdout);
927
    PySys_SetObject("stderr", default_stderr);
928
    d->interactive = false;
929
    for (const auto & it : d->statements) {
930
        printStatement(it);
931
    }
932
    d->statements.clear();
933
}
934

935
bool PythonConsole::isComment(const QString& source) const
936
{
937
    if (source.isEmpty()) {
938
        return false;
939
    }
940
    int i=0;
941
    while (i < source.length()) {
942
        QChar ch = source.at(i++);
943
        if (ch.isSpace()) {
944
            continue;
945
        }
946
        else if (ch == QLatin1Char('#')) {
947
            return true;
948
        }
949
        else {
950
            return false;
951
        }
952
    }
953

954
    return false;
955
}
956

957
/**
958
 * Prints the Python statement cmd to the console.
959
 * @note The statement gets only printed and added to the history but not invoked.
960
 */
961
void PythonConsole::printStatement( const QString& cmd )
962
{
963
    // If we are in interactive mode we have to wait until the command is finished,
964
    // afterwards we can print the statements.
965
    if (d->interactive) {
966
        d->statements << cmd;
967
        return;
968
    }
969

970
    QTextCursor cursor = textCursor();
971
    QStringList statements = cmd.split(QLatin1String("\n"));
972
    for (const auto & statement : statements) {
973
        // go to the end before inserting new text
974
        cursor.movePosition(QTextCursor::End);
975
        cursor.insertText( statement );
976
        d->history.append( statement );
977
        printPrompt(PythonConsole::Complete);
978
    }
979
}
980

981
/**
982
 * Shows the Python window and sets the focus to set text cursor.
983
 */
984
void PythonConsole::showEvent (QShowEvent * e)
985
{
986
    TextEdit::showEvent(e);
987
    // set also the text cursor to the edit field
988
    setFocus();
989
}
990

991
void PythonConsole::visibilityChanged (bool visible)
992
{
993
    if (visible) {
994
        setFocus();
995
    }
996
}
997

998
void PythonConsole::changeEvent(QEvent *e)
999
{
1000
    if (e->type() == QEvent::ParentChange) {
1001
        auto dw = qobject_cast<QDockWidget*>(this->parentWidget());
1002
        if (dw) {
1003
            connect(dw, &QDockWidget::visibilityChanged, this, &PythonConsole::visibilityChanged);
1004
        }
1005
    }
1006
    else if (e->type() == QEvent::StyleChange) {
1007
        QPalette pal = qApp->palette();
1008
        QColor color = pal.windowText().color();
1009
        unsigned int text = App::Color::asPackedRGB<QColor>(color);
1010
        auto value = static_cast<unsigned long>(text);
1011
        // if this parameter is not already set use the style's window text color
1012
        value = getWindowParameter()->GetUnsigned("Text", value);
1013
        getWindowParameter()->SetUnsigned("Text", value);
1014
    }
1015
    TextEdit::changeEvent(e);
1016
}
1017

1018
void PythonConsole::mouseReleaseEvent( QMouseEvent *e )
1019
{
1020
    if (e->button() == Qt::MiddleButton && e->spontaneous()) {
1021
        // on Linux-like systems the middle mouse button is typically connected to a paste operation
1022
        // which will insert some text at the mouse position
1023
        QTextCursor cursor = this->textCursor();
1024
        if (cursor < this->inputBegin()) {
1025
            cursor.movePosition( QTextCursor::End );
1026
            this->setTextCursor( cursor );
1027
        }
1028
        // the text will be pasted at the cursor position (as for Ctrl-V operation)
1029
        QRect newPos = this->cursorRect();
1030

1031
        // Now we must amend the received event and pass forward. As e->setLocalPos() is only
1032
        // available in Qt>=5.8, let's stop the original event propagation and generate a fake event
1033
        // with corrected pointer position (inside the prompt line of the widget)
1034
#if QT_VERSION < QT_VERSION_CHECK(6,4,0)
1035
        QMouseEvent newEv(e->type(), QPoint(newPos.x(),newPos.y()),
1036
                          e->button(), e->buttons(), e->modifiers());
1037
#else
1038
        QMouseEvent newEv(e->type(), QPoint(newPos.x(),newPos.y()), e->globalPosition(),
1039
                          e->button(), e->buttons(), e->modifiers());
1040
#endif
1041
        e->accept();
1042
        QCoreApplication::sendEvent(this->viewport(), &newEv);
1043
        return;
1044
    }
1045
    TextEdit::mouseReleaseEvent( e );
1046
}
1047

1048
/**
1049
 * Drops the event \a e and writes the right Python command.
1050
 */
1051
void PythonConsole::dropEvent (QDropEvent * e)
1052
{
1053
    const QMimeData* mimeData = e->mimeData();
1054
    if (mimeData->hasFormat(QLatin1String("text/x-action-items"))) {
1055
        QByteArray itemData = mimeData->data(QLatin1String("text/x-action-items"));
1056
        QDataStream dataStream(&itemData, QIODevice::ReadOnly);
1057

1058
        int ctActions; dataStream >> ctActions;
1059
        for (int i=0; i<ctActions; i++) {
1060
            QString action;
1061
            dataStream >> action;
1062
            printStatement(QString::fromLatin1("Gui.runCommand(\"%1\")").arg(action));
1063
        }
1064

1065
        e->setDropAction(Qt::CopyAction);
1066
        e->accept();
1067
    }
1068
    else {
1069
        // always copy text when doing drag and drop
1070
        if (mimeData->hasText()) {
1071
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
1072
            QTextCursor cursor = this->cursorForPosition(e->pos());
1073
#else
1074
            QTextCursor cursor = this->cursorForPosition(e->position().toPoint());
1075
#endif
1076
            QTextCursor inputLineBegin = this->inputBegin();
1077

1078
            if (!cursorBeyond( cursor, inputLineBegin )) {
1079
                this->moveCursor(QTextCursor::End);
1080

1081
                QRect newPos = this->cursorRect();
1082

1083
#if QT_VERSION < QT_VERSION_CHECK(6,0,0)
1084
                QDropEvent newEv(QPoint(newPos.x(), newPos.y()), Qt::CopyAction, mimeData, e->mouseButtons(), e->keyboardModifiers());
1085
#else
1086
                QDropEvent newEv(QPoint(newPos.x(), newPos.y()), Qt::CopyAction, mimeData, e->buttons(), e->modifiers());
1087
#endif
1088
                e->accept();
1089
                QPlainTextEdit::dropEvent(&newEv);
1090
            }
1091
            else {
1092
                e->setDropAction(Qt::CopyAction);
1093
                QPlainTextEdit::dropEvent(e);
1094
            }
1095
        }
1096
        else {
1097
            // this will call insertFromMimeData
1098
            QPlainTextEdit::dropEvent(e);
1099
        }
1100
    }
1101
}
1102

1103
/** Dragging of action objects is allowed. */
1104
void PythonConsole::dragMoveEvent( QDragMoveEvent *e )
1105
{
1106
    const QMimeData* mimeData = e->mimeData();
1107
    if (mimeData->hasFormat(QLatin1String("text/x-action-items"))) {
1108
        e->accept();
1109
    }
1110
    else {
1111
        // this will call canInsertFromMimeData
1112
        QPlainTextEdit::dragMoveEvent(e);
1113
    }
1114
}
1115

1116
/** Dragging of action objects is allowed. */
1117
void PythonConsole::dragEnterEvent (QDragEnterEvent * e)
1118
{
1119
    const QMimeData* mimeData = e->mimeData();
1120
    if (mimeData->hasFormat(QLatin1String("text/x-action-items"))) {
1121
        e->accept();
1122
    }
1123
    else {
1124
        // this will call canInsertFromMimeData
1125
        QPlainTextEdit::dragEnterEvent(e);
1126
    }
1127
}
1128

1129
bool PythonConsole::canInsertFromMimeData (const QMimeData * source) const
1130
{
1131
    if (source->hasText()) {
1132
        return true;
1133
    }
1134
    if (source->hasUrls()) {
1135
        QList<QUrl> uri = source->urls();
1136
        for (const auto & it : uri) {
1137
            QFileInfo info(it.toLocalFile());
1138
            if (info.exists() && info.isFile()) {
1139
                QString ext = info.suffix().toLower();
1140
                if (ext == QLatin1String("py") || ext == QLatin1String("fcmacro")) {
1141
                    return true;
1142
                }
1143
            }
1144
        }
1145
    }
1146

1147
    return false;
1148
}
1149

1150
/**
1151
 * Allow to paste plain text or urls of text files.
1152
 */
1153
void PythonConsole::insertFromMimeData (const QMimeData * source)
1154
{
1155
    if (!source) {
1156
        return;
1157
    }
1158
    // First check on urls instead of text otherwise it may happen that a url
1159
    // is handled as text
1160
    bool existingFile = false;
1161
    if (source->hasUrls()) {
1162
        QList<QUrl> uri = source->urls();
1163
        for (const auto & it : uri) {
1164
            // get the file name and check the extension
1165
            QFileInfo info(it.toLocalFile());
1166
            QString ext = info.suffix().toLower();
1167
            if (info.exists()) {
1168
                existingFile = true;
1169
                if (info.isFile() && (ext == QLatin1String("py") || ext == QLatin1String("fcmacro"))) {
1170
                    // load the file and read-in the source code
1171
                    QFile file(info.absoluteFilePath());
1172
                    if (file.open(QIODevice::ReadOnly)) {
1173
                        QTextStream str(&file);
1174
                        runSourceFromMimeData(str.readAll());
1175
                    }
1176
                    file.close();
1177
                }
1178
            }
1179
        }
1180
    }
1181

1182
    // Some applications copy text into the clipboard with the formats
1183
    // 'text/plain' and 'text/uri-list'. In case the url is not an existing
1184
    // file we can handle it as normal text, then. See forum thread:
1185
    // https://forum.freecad.org/viewtopic.php?f=3&t=34618
1186
    if (source->hasText() && !existingFile) {
1187
        runSourceFromMimeData(source->text());
1188
    }
1189
}
1190

1191
QTextCursor PythonConsole::inputBegin() const
1192
{
1193
    // construct cursor at begin of input line ...
1194
    QTextCursor inputLineBegin(this->textCursor());
1195
    inputLineBegin.movePosition(QTextCursor::End);
1196
    inputLineBegin.movePosition(QTextCursor::StartOfBlock);
1197
    // ... and move cursor right beyond the prompt.
1198
    int prompt = promptLength(inputLineBegin.block().text());
1199
    if (this->_sourceDrain && !this->_sourceDrain->isEmpty()) {
1200
        prompt = this->_sourceDrain->length();
1201
    }
1202
    inputLineBegin.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, prompt);
1203
    return inputLineBegin;
1204
}
1205

1206
QMimeData * PythonConsole::createMimeDataFromSelection () const
1207
{
1208
    auto mime = new QMimeData();
1209

1210
    switch (d->type) {
1211
        case PythonConsoleP::Normal:
1212
            {
1213
                const QTextDocumentFragment fragment(textCursor());
1214
                mime->setText(fragment.toPlainText());
1215
            }   break;
1216
        case PythonConsoleP::Command:
1217
            {
1218
                QTextCursor cursor = textCursor();
1219
                int s = cursor.selectionStart();
1220
                int e = cursor.selectionEnd();
1221
                QTextBlock b;
1222
                QStringList lines;
1223
                for (b = document()->begin(); b.isValid(); b = b.next()) {
1224
                    int pos = b.position();
1225
                    if ( pos >= s && pos <= e ) {
1226
                        if (b.userState() > -1 && b.userState() < pythonSyntax->maximumUserState()) {
1227
                            lines << stripPromptFrom( b.text() );
1228
                        }
1229
                    }
1230
                }
1231

1232
                QString text = lines.join(QLatin1String("\n"));
1233
                mime->setText(text);
1234
            }   break;
1235
        case PythonConsoleP::History:
1236
            {
1237
                const QStringList& hist = d->history.values();
1238
                QString text = hist.join(QLatin1String("\n"));
1239
                mime->setText(text);
1240
            }   break;
1241
    }
1242

1243
    return mime;
1244
}
1245

1246
void PythonConsole::runSourceFromMimeData(const QString& source)
1247
{
1248
    // When inserting a big text block we must break it down into several command
1249
    // blocks instead of processing the text block as a whole or each single line.
1250
    // If we processed the complete block as a whole only the first valid Python
1251
    // command would be executed and the rest would be ignored. However, if we
1252
    // processed each line separately the interpreter might be confused that a block
1253
    // is complete but it might be not. This is for instance, if a class or method
1254
    // definition contains several empty lines which leads to error messages (almost
1255
    // indentation errors) later on.
1256
    QString text = source;
1257
    if (text.isNull()) {
1258
        return;
1259
    }
1260

1261
#if defined (Q_OS_LINUX)
1262
    // Need to convert CRLF to LF
1263
    text.replace(QLatin1String("\r\n"), QLatin1String("\n"));
1264
#elif defined(Q_OS_WIN32)
1265
    // Need to convert CRLF to LF
1266
    text.replace(QLatin1String("\r\n"), QLatin1String("\n"));
1267
#elif defined(Q_OS_MAC)
1268
    //need to convert CR to LF
1269
    text.replace(QLatin1Char('\r'), QLatin1Char('\n'));
1270
#endif
1271

1272
    // separate the lines and get the last one
1273
    QStringList lines = text.split(QLatin1Char('\n'));
1274
    QString last = lines.back();
1275
    lines.pop_back();
1276

1277
    QTextCursor cursor = textCursor();
1278
    QStringList buffer = d->interpreter->getBuffer();
1279
    d->interpreter->clearBuffer();
1280

1281
    int countNewlines = lines.count(), i = 0;
1282
    for (QStringList::Iterator it = lines.begin(); it != lines.end(); ++it, ++i) {
1283
        QString line = *it;
1284

1285
        // insert the text to the current cursor position
1286
        cursor.insertText(*it);
1287

1288
        // for the very first line get the complete block
1289
        // because it may differ from the inserted text
1290
        if (i == 0) {
1291
            // get the text from the current cursor position to the end, remove it
1292
            // and add it to the last line
1293
            cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
1294
            QString select = cursor.selectedText();
1295
            cursor.removeSelectedText();
1296
            last = last + select;
1297
            line = stripPromptFrom( cursor.block().text() );
1298
        }
1299

1300
        // put statement to the history
1301
        d->history.append(line);
1302

1303
        buffer.append(line);
1304
        int ret = d->interpreter->compileCommand(buffer.join(QLatin1String("\n")).toUtf8());
1305
        if (ret == 1) { // incomplete
1306
            printPrompt(PythonConsole::Incomplete);
1307
        }
1308
        else if (ret == 0) { // complete
1309
            // check if the following lines belong to the previous block
1310
            int k=i+1;
1311
            QString nextline;
1312
            while ((nextline.isEmpty() || isComment(nextline)) && k < countNewlines) {
1313
                nextline = lines[k];
1314
                k++;
1315
            }
1316

1317
            int ret = d->interpreter->compileCommand(nextline.toUtf8());
1318

1319
            // If the line is valid, i.e. complete or incomplete the previous block
1320
            // is finished
1321
            if (ret == -1) {
1322
                // the command is not finished yet
1323
                printPrompt(PythonConsole::Incomplete);
1324
            }
1325
            else {
1326
                runSource(buffer.join(QLatin1String("\n")));
1327
                buffer.clear();
1328
            }
1329
        }
1330
        else { // invalid
1331
            runSource(buffer.join(QLatin1String("\n")));
1332
            ensureCursorVisible();
1333
            return; // exit the method on error
1334
        }
1335
    }
1336

1337
    // set the incomplete block to the interpreter and insert the last line
1338
    d->interpreter->setBuffer(buffer);
1339
    cursor.insertText(last);
1340
    ensureCursorVisible();
1341
}
1342

1343
/**
1344
 * Overwrites the text of the cursor.
1345
 */
1346
void PythonConsole::overrideCursor(const QString& txt)
1347
{
1348
    // Go to the last line and the fourth position, right after the prompt
1349
    QTextCursor cursor = this->inputBegin();
1350
    int blockLength = this->textCursor().block().text().length();
1351

1352
    cursor.movePosition( QTextCursor::Right, QTextCursor::KeepAnchor, blockLength ); //<< select text to override
1353
    cursor.removeSelectedText();
1354
    cursor.insertText(txt);
1355
    // move cursor to the end
1356
    cursor.movePosition(QTextCursor::End);
1357
    setTextCursor(cursor);
1358
}
1359

1360
void PythonConsole::contextMenuEvent ( QContextMenuEvent * e )
1361
{
1362
    QMenu menu(this);
1363
    QAction *a;
1364
    bool mayPasteHere = cursorBeyond( this->textCursor(), this->inputBegin() );
1365

1366
    a = menu.addAction(tr("&Copy"), this, &PythonConsole::copy);
1367
    a->setShortcut(QKeySequence(QString::fromLatin1("CTRL+C")));
1368
    a->setEnabled(textCursor().hasSelection());
1369

1370
    a = menu.addAction(tr("&Copy command"), this, &PythonConsole::onCopyCommand);
1371
    a->setEnabled(textCursor().hasSelection());
1372

1373
    a = menu.addAction(tr("&Copy history"), this, &PythonConsole::onCopyHistory);
1374
    a->setEnabled(!d->history.isEmpty());
1375

1376
    a = menu.addAction( tr("Save history as..."), this, &PythonConsole::onSaveHistoryAs);
1377
    a->setEnabled(!d->history.isEmpty());
1378

1379
    QAction* saveh = menu.addAction(tr("Save history"));
1380
    saveh->setToolTip(tr("Saves Python history across %1 sessions").arg(qApp->applicationName()));
1381
    saveh->setCheckable(true);
1382
    saveh->setChecked(d->hGrpSettings->GetBool("SavePythonHistory", false));
1383

1384
    menu.addSeparator();
1385

1386
    a = menu.addAction(tr("&Paste"), this, &PythonConsole::paste);
1387
    a->setShortcut(QKeySequence(QString::fromLatin1("CTRL+V")));
1388
    const QMimeData *md = QApplication::clipboard()->mimeData();
1389
    a->setEnabled( mayPasteHere && md && canInsertFromMimeData(md));
1390

1391
    a = menu.addAction(tr("Select All"), this, &PythonConsole::selectAll);
1392
    a->setShortcut(QKeySequence(QString::fromLatin1("CTRL+A")));
1393
    a->setEnabled(!document()->isEmpty());
1394

1395
    a = menu.addAction(tr("Clear console"), this, &PythonConsole::onClearConsole);
1396
    a->setEnabled(!document()->isEmpty());
1397

1398
    menu.addSeparator();
1399
    menu.addAction( tr("Insert file name..."), this, &PythonConsole::onInsertFileName);
1400
    menu.addSeparator();
1401

1402
    QAction* wrap = menu.addAction(tr("Word wrap"));
1403
    wrap->setCheckable(true);
1404

1405
    wrap->setChecked(d->hGrpSettings->GetBool("PythonWordWrap", true));
1406
    QAction* exec = menu.exec(e->globalPos());
1407
    if (exec == wrap) {
1408
        d->hGrpSettings->SetBool("PythonWordWrap", wrap->isChecked());
1409
    }
1410
    else if (exec == saveh) {
1411
        d->hGrpSettings->SetBool("SavePythonHistory", saveh->isChecked());
1412
    }
1413
}
1414

1415
void PythonConsole::onClearConsole()
1416
{
1417
    clear();
1418
    d->output = d->info;
1419
    printPrompt(PythonConsole::Complete);
1420
}
1421

1422
void PythonConsole::onSaveHistoryAs()
1423
{
1424
    QString cMacroPath = QString::fromUtf8(getDefaultParameter()->GetGroup( "Macro" )->
1425
        GetASCII("MacroPath",App::Application::getUserMacroDir().c_str()).c_str());
1426
    QString fn = FileDialog::getSaveFileName(this, tr("Save History"), cMacroPath,
1427
        QString::fromLatin1("%1 (*.FCMacro *.py)").arg(tr("Macro Files")));
1428
    if (!fn.isEmpty()) {
1429
        int dot = fn.indexOf(QLatin1Char('.'));
1430
        if (dot != -1) {
1431
            QFile f(fn);
1432
            if (f.open(QIODevice::WriteOnly)) {
1433
                QTextStream t (&f);
1434
                const QStringList& hist = d->history.values();
1435
                for (const auto & it : hist) {
1436
                    t << it << "\n";
1437
                }
1438
                f.close();
1439
            }
1440
        }
1441
    }
1442
}
1443

1444
void PythonConsole::onInsertFileName()
1445
{
1446
    QString fn = Gui::FileDialog::getOpenFileName(Gui::getMainWindow(), tr("Insert file name"), QString(),
1447
        QString::fromLatin1("%1 (*.*)").arg(tr("All Files")));
1448
    if ( !fn.isEmpty() ) {
1449
        insertPlainText(fn);
1450
    }
1451
}
1452

1453
/**
1454
 * Copy the history of the console into the clipboard.
1455
 */
1456
void PythonConsole::onCopyHistory()
1457
{
1458
    if (d->history.isEmpty()) {
1459
        return;
1460
    }
1461
    d->type = PythonConsoleP::History;
1462
    QMimeData *data = createMimeDataFromSelection();
1463
    QApplication::clipboard()->setMimeData(data);
1464
    d->type = PythonConsoleP::Normal;
1465
}
1466

1467
/**
1468
 * Copy the selected commands into the clipboard. This is a subset of the history.
1469
 */
1470
void PythonConsole::onCopyCommand()
1471
{
1472
    d->type = PythonConsoleP::Command;
1473
    copy();
1474
    d->type = PythonConsoleP::Normal;
1475
}
1476

1477
QString PythonConsole::readline( )
1478
{
1479
    QEventLoop loop;
1480
    // output is set to the current prompt which we need to extract
1481
    // the actual user input
1482
    QString    inputBuffer = d->output;
1483

1484
    printPrompt(PythonConsole::Special);
1485
    this->_sourceDrain = &inputBuffer;     //< enable source drain ...
1486
    // ... and wait until we get notified about pendingSource
1487
    QObject::connect( this, &PythonConsole::pendingSource, &loop, &QEventLoop::quit);
1488
    // application is about to quit
1489
    if (loop.exec() != 0) {
1490
        PyErr_SetInterrupt();
1491
    }            //< send SIGINT to python
1492
    this->_sourceDrain = nullptr;             //< disable source drain
1493
    return inputBuffer.append(QChar::fromLatin1('\n')); //< pass a newline here, since the readline-caller may need it!
1494
}
1495

1496
/**
1497
 * loads history contents from the default history file
1498
 */
1499
void PythonConsole::loadHistory() const
1500
{
1501
    // only load contents if history is empty, to not overwrite anything
1502
    if (!d->history.isEmpty()) {
1503
        return;
1504
    }
1505

1506
    if (!d->hGrpSettings->GetBool("SavePythonHistory", false)) {
1507
        return;
1508
    }
1509
    QFile f(d->historyFile);
1510
    if (f.open(QIODevice::ReadOnly | QIODevice::Text)) {
1511
        QString l;
1512
        while (!f.atEnd()) {
1513
            l = QString::fromUtf8(f.readLine());
1514
            if (!l.isEmpty()) {
1515
                l.chop(1); // removes the last \n
1516
                d->history.append(l);
1517
            }
1518
        }
1519
        f.close();
1520
    }
1521
}
1522

1523
/**
1524
 * saves the current history to the default history file
1525
 */
1526
void PythonConsole::saveHistory() const
1527
{
1528
    if (d->history.isEmpty()) {
1529
        return;
1530
    }
1531
    if (!d->hGrpSettings->GetBool("SavePythonHistory", false)) {
1532
        return;
1533
    }
1534
    QFile f(d->historyFile);
1535
    if (f.open(QIODevice::WriteOnly)) {
1536
        QTextStream t (&f);
1537
        QStringList hist = d->history.values();
1538
        // only save last 100 entries so we don't inflate forever...
1539
        if (hist.length() > 100) {
1540
            hist = hist.mid(hist.length()-100);
1541
        }
1542
        for (const auto & it : hist) {
1543
            t << it << "\n";
1544
        }
1545
        f.close();
1546
    }
1547
}
1548

1549
// ---------------------------------------------------------------------
1550

1551
PythonConsoleHighlighter::PythonConsoleHighlighter(QObject* parent)
1552
  : PythonSyntaxHighlighter(parent)
1553
{
1554
}
1555

1556
PythonConsoleHighlighter::~PythonConsoleHighlighter() = default;
1557

1558
void PythonConsoleHighlighter::highlightBlock(const QString& text)
1559
{
1560
    const int ErrorOutput   = (int)PythonConsoleP::Error;
1561
    const int MessageOutput = (int)PythonConsoleP::Message;
1562

1563
    // Get user state to re-highlight the blocks in the appropriate format
1564
    int stateOfPara = currentBlockState();
1565

1566
    switch (stateOfPara)
1567
    {
1568
    case ErrorOutput:
1569
        {
1570
            // Error output
1571
            QTextCharFormat errorFormat;
1572
            errorFormat.setForeground(color(QLatin1String("Python error")));
1573
            errorFormat.setFontItalic(true);
1574
            setFormat( 0, text.length(), errorFormat);
1575
        }   break;
1576
    case MessageOutput:
1577
        {
1578
            // Normal output
1579
            QTextCharFormat outputFormat;
1580
            outputFormat.setForeground(color(QLatin1String("Python output")));
1581
            setFormat( 0, text.length(), outputFormat);
1582
        }   break;
1583
    default:
1584
        {
1585
            PythonSyntaxHighlighter::highlightBlock(text);
1586
        }   break;
1587
    }
1588
}
1589

1590
void PythonConsoleHighlighter::colorChanged(const QString& type, const QColor& col)
1591
{
1592
    Q_UNUSED(type);
1593
    Q_UNUSED(col);
1594
}
1595

1596
// ---------------------------------------------------------------------
1597

1598
ConsoleHistory::ConsoleHistory()
1599
: _scratchBegin(0)
1600
{
1601
    _it = _history.cend();
1602
}
1603

1604
ConsoleHistory::~ConsoleHistory() = default;
1605

1606
void ConsoleHistory::first()
1607
{
1608
    _it = _history.cbegin();
1609
}
1610

1611
bool ConsoleHistory::more()
1612
{
1613
    return (_it != _history.cend());
1614
}
1615

1616
/**
1617
 * next switches the history pointer to the next item.
1618
 * While searching the next item, the routine respects the search prefix set by prev().
1619
 * @return true if the pointer was switched to a later item, false otherwise.
1620
 */
1621
bool ConsoleHistory::next()
1622
{
1623
    bool wentNext = false;
1624

1625
    // if we didn't reach history's end ...
1626
    if (_it != _history.cend())
1627
    {
1628
        // we go forward until we find an item matching the prefix.
1629
        for (++_it; _it != _history.cend(); ++_it) {
1630
            if (!_it->isEmpty() && _it->startsWith( _prefix )) {
1631
                break;
1632
            }
1633
        }
1634
        // we did a step - no matter of a matching prefix.
1635
        wentNext = true;
1636
    }
1637
    return wentNext;
1638
}
1639

1640
/**
1641
 * prev switches the history pointer to the previous item.
1642
 * The optional parameter prefix allows to search the history selectively for commands that start
1643
 *   with a certain character sequence.
1644
 * @param prefix - prefix string for searching backwards in history, empty string by default
1645
 * @return true if the pointer was switched to an earlier item, false otherwise.
1646
 */
1647
bool ConsoleHistory::prev( const QString &prefix )
1648
{
1649
    bool wentPrev = false;
1650

1651
    // store prefix if it's the first history access
1652
    if (_it == _history.cend()) {
1653
        _prefix = prefix;
1654
    }
1655

1656
    // while we didn't go back or reach history's begin ...
1657
    while (!wentPrev && _it != _history.cbegin()) {
1658
        // go back in history and check if item matches prefix
1659
        // Skip empty items
1660
        --_it;
1661
        wentPrev = (!_it->isEmpty() && _it->startsWith( _prefix ));
1662
    }
1663
    return wentPrev;
1664
}
1665

1666
bool ConsoleHistory::isEmpty() const
1667
{
1668
    return _history.isEmpty();
1669
}
1670

1671
const QString& ConsoleHistory::value() const
1672
{
1673
    return ((_it != _history.end())? *_it
1674
                        /* else */ :  _prefix);
1675
}
1676

1677
void ConsoleHistory::append( const QString& item )
1678
{
1679
    _history.append( item );
1680
    // reset iterator to make the next history
1681
    //   access begin with the latest item.
1682
    _it = _history.cend();
1683
}
1684

1685
const QStringList& ConsoleHistory::values() const
1686
{
1687
    return this->_history;
1688
}
1689

1690
/**
1691
 * restart resets the history access to the latest item.
1692
 */
1693
void ConsoleHistory::restart( )
1694
{
1695
    _it = _history.cend();
1696
}
1697

1698
/**
1699
 * markScratch stores the current end index of the history list.
1700
 * Note: with simply remembering a start index, it does not work to nest scratch regions.
1701
 * However, just replace the index keeping by a stack - in case this is be a concern.
1702
 */
1703
void ConsoleHistory::markScratch( )
1704
{
1705
    _scratchBegin = _history.length();
1706
}
1707

1708
/**
1709
 * doScratch removes the tail of the history list, starting from the index marked lately.
1710
 */
1711
void ConsoleHistory::doScratch( )
1712
{
1713
    if (_scratchBegin < _history.length()) {
1714
        _history.erase( _history.begin() + _scratchBegin, _history.end() );
1715
        this->restart();
1716
    }
1717
}
1718

1719
// -----------------------------------------------------
1720

1721
#include "moc_PythonConsole.cpp"
1722

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.