| /**************************************************************************** |
| ** |
| ** Copyright (C) 2018 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtSCriptTools module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qscriptedit_p.h" |
| #include "qscriptsyntaxhighlighter_p.h" |
| |
| #include <QtGui/qpainter.h> |
| #include <QtGui/qicon.h> |
| #include <QtWidgets/qboxlayout.h> |
| #include <QtWidgets/qlabel.h> |
| #include <QtWidgets/qlineedit.h> |
| #include <QtWidgets/qmenu.h> |
| #include <QtWidgets/qaction.h> |
| #include <QtWidgets/qwidgetaction.h> |
| #include <QtCore/qdebug.h> |
| |
| QT_BEGIN_NAMESPACE |
| |
| class QScriptEditExtraArea : public QWidget |
| { |
| public: |
| QScriptEditExtraArea(QScriptEdit *edit) |
| : QWidget(edit) |
| { |
| setMouseTracking(true); |
| } |
| |
| QSize sizeHint() const { |
| return QSize(editor()->extraAreaWidth(), 0); |
| } |
| |
| protected: |
| void paintEvent(QPaintEvent *event) |
| { |
| editor()->extraAreaPaintEvent(event); |
| } |
| void mousePressEvent(QMouseEvent *event) |
| { |
| editor()->extraAreaMouseEvent(event); |
| } |
| void mouseMoveEvent(QMouseEvent *event) |
| { |
| editor()->extraAreaMouseEvent(event); |
| } |
| void mouseReleaseEvent(QMouseEvent *event) |
| { |
| editor()->extraAreaMouseEvent(event); |
| } |
| bool event(QEvent *event) |
| { |
| if (editor()->extraAreaEvent(event)) |
| return true; |
| return QWidget::event(event); |
| } |
| |
| private: |
| QScriptEdit *editor() const |
| { |
| return qobject_cast<QScriptEdit*>(parent()); |
| } |
| }; |
| |
| |
| |
| QScriptEdit::QScriptEdit(QWidget *parent) |
| : QPlainTextEdit(parent) |
| { |
| m_baseLineNumber = 1; |
| m_executionLineNumber = -1; |
| |
| m_extraArea = new QScriptEditExtraArea(this); |
| |
| QObject::connect(this, SIGNAL(blockCountChanged(int)), |
| this, SLOT(updateExtraAreaWidth())); |
| QObject::connect(this, SIGNAL(updateRequest(QRect,int)), |
| this, SLOT(updateExtraArea(QRect,int))); |
| QObject::connect(this, SIGNAL(cursorPositionChanged()), |
| this, SLOT(highlightCurrentLine())); |
| |
| updateExtraAreaWidth(); |
| |
| #ifndef QT_NO_SYNTAXHIGHLIGHTER |
| (void) new QScriptSyntaxHighlighter(document()); |
| #endif |
| } |
| |
| QScriptEdit::~QScriptEdit() |
| { |
| } |
| |
| int QScriptEdit::baseLineNumber() const |
| { |
| return m_baseLineNumber; |
| } |
| |
| void QScriptEdit::setBaseLineNumber(int base) |
| { |
| m_baseLineNumber = base; |
| m_extraArea->update(); |
| } |
| |
| int QScriptEdit::executionLineNumber() const |
| { |
| return m_executionLineNumber; |
| } |
| |
| void QScriptEdit::setExecutionLineNumber(int lineNumber, bool error) |
| { |
| m_executionLineNumber = lineNumber; |
| m_executionLineNumberHasError = error; |
| m_extraArea->update(); |
| updateExtraSelections(); |
| gotoLine(lineNumber); |
| } |
| |
| void QScriptEdit::setExecutableLineNumbers(const QSet<int> &lineNumbers) |
| { |
| m_executableLineNumbers = lineNumbers; |
| } |
| |
| bool QScriptEdit::isExecutableLine(int lineNumber) const |
| { |
| #if 0 // ### enable me once we have information about the script again |
| return m_executableLineNumbers.contains(lineNumber); |
| #else |
| Q_UNUSED(lineNumber); |
| return true; |
| #endif |
| } |
| |
| int QScriptEdit::currentLineNumber() const |
| { |
| return textCursor().blockNumber() + m_baseLineNumber; |
| } |
| |
| void QScriptEdit::gotoLine(int lineNumber) |
| { |
| #ifndef QT_NO_SYNTAXHIGHLIGHTER |
| int blockNumber = lineNumber - m_baseLineNumber; |
| const QTextBlock &block = document()->findBlockByNumber(blockNumber); |
| if (block.isValid()) { |
| setTextCursor(QTextCursor(block)); |
| centerCursor(); |
| } |
| #else |
| Q_UNUSED(lineNumber); |
| #endif |
| } |
| |
| void QScriptEdit::setBreakpoint(int lineNumber) |
| { |
| m_breakpoints[lineNumber] = BreakpointData(); |
| m_extraArea->update(); |
| } |
| |
| void QScriptEdit::setBreakpointEnabled(int lineNumber, bool enable) |
| { |
| m_breakpoints[lineNumber].enabled = enable; |
| m_extraArea->update(); |
| } |
| |
| void QScriptEdit::deleteBreakpoint(int lineNumber) |
| { |
| m_breakpoints.remove(lineNumber); |
| m_extraArea->update(); |
| } |
| |
| void QScriptEdit::paintEvent(QPaintEvent *e) |
| { |
| QPlainTextEdit::paintEvent(e); |
| } |
| |
| void QScriptEdit::resizeEvent(QResizeEvent *e) |
| { |
| QPlainTextEdit::resizeEvent(e); |
| |
| QRect cr = contentsRect(); |
| int x = isLeftToRight() ? cr.left() : cr.left() + cr.width() - extraAreaWidth(); |
| m_extraArea->setGeometry(QRect(x, cr.top(), extraAreaWidth(), cr.height())); |
| } |
| |
| void QScriptEdit::updateExtraAreaWidth() |
| { |
| if (isLeftToRight()) |
| setViewportMargins(extraAreaWidth(), 0, 0, 0); |
| else |
| setViewportMargins(0, 0, extraAreaWidth(), 0); |
| } |
| |
| void QScriptEdit::updateExtraArea(const QRect &rect, int dy) |
| { |
| if (dy) |
| m_extraArea->scroll(0, dy); |
| else |
| m_extraArea->update(0, rect.y(), m_extraArea->width(), rect.height()); |
| |
| if (rect.contains(viewport()->rect())) |
| updateExtraAreaWidth(); |
| } |
| |
| void QScriptEdit::highlightCurrentLine() |
| { |
| updateExtraSelections(); |
| } |
| |
| void QScriptEdit::updateExtraSelections() |
| { |
| QList<QTextEdit::ExtraSelection> extraSelections; |
| |
| { |
| QTextEdit::ExtraSelection selection; |
| QColor lineColor = QColor(Qt::yellow).lighter(160); |
| selection.format.setBackground(lineColor); |
| selection.format.setProperty(QTextFormat::FullWidthSelection, true); |
| selection.cursor = textCursor(); |
| selection.cursor.clearSelection(); |
| extraSelections.append(selection); |
| } |
| if (m_executionLineNumber != -1) { |
| QTextEdit::ExtraSelection selection; |
| QColor lineColor; |
| if (m_executionLineNumberHasError) |
| lineColor = QColor(Qt::red); |
| else |
| lineColor = QColor(Qt::green).lighter(160); |
| selection.format.setBackground(lineColor); |
| selection.format.setProperty(QTextFormat::FullWidthSelection, true); |
| #ifndef QT_NO_SYNTAXHIGHLIGHTER |
| int blockNumber = m_executionLineNumber - m_baseLineNumber; |
| selection.cursor = QTextCursor(document()->findBlockByNumber(blockNumber)); |
| #endif |
| selection.cursor.clearSelection(); |
| extraSelections.append(selection); |
| } |
| |
| setExtraSelections(extraSelections); |
| } |
| |
| int QScriptEdit::extraAreaWidth() const |
| { |
| int space = 0; |
| const QFontMetrics fm(fontMetrics()); |
| |
| int digits = 1; |
| int max = qMax(1, blockCount() + m_baseLineNumber); |
| while (max >= 10) { |
| max /= 10; |
| ++digits; |
| } |
| space += fm.horizontalAdvance(QLatin1Char('9')) * digits; |
| |
| int markWidth = fm.lineSpacing(); |
| space += markWidth; |
| |
| space += 4; |
| |
| return space; |
| } |
| |
| void QScriptEdit::extraAreaPaintEvent(QPaintEvent *e) |
| { |
| QRect rect = e->rect(); |
| QPalette pal = palette(); |
| pal.setCurrentColorGroup(QPalette::Active); |
| QPainter painter(m_extraArea); |
| painter.fillRect(rect, Qt::lightGray); |
| const QFontMetrics fm(fontMetrics()); |
| |
| int markWidth = fm.lineSpacing(); |
| int extraAreaWidth = m_extraArea->width(); |
| |
| QLinearGradient gradient(QPointF(extraAreaWidth - 10, 0), QPointF(extraAreaWidth, 0)); |
| gradient.setColorAt(0, pal.color(QPalette::Window)); |
| gradient.setColorAt(1, pal.color(QPalette::Base)); |
| painter.fillRect(rect, gradient); |
| |
| QLinearGradient gradient2(QPointF(0, 0), QPointF(markWidth, 0)); |
| gradient2.setColorAt(0, pal.color(QPalette::Dark)); |
| gradient2.setColorAt(1, pal.color(QPalette::Window)); |
| painter.fillRect(rect.intersected(QRect(rect.x(), rect.y(), markWidth, rect.height())), gradient2); |
| |
| painter.setPen(QPen(pal.color(QPalette::Window), 2)); |
| if (isLeftToRight()) |
| painter.drawLine(rect.x() + extraAreaWidth-1, rect.top(), rect.x() + extraAreaWidth-1, rect.bottom()); |
| else |
| painter.drawLine(rect.x(), rect.top(), rect.x(), rect.bottom()); |
| painter.setRenderHint(QPainter::Antialiasing); |
| |
| #ifndef QT_NO_SYNTAXHIGHLIGHTER |
| QTextBlock block = firstVisibleBlock(); |
| int blockNumber = block.blockNumber(); |
| qreal top = blockBoundingGeometry(block).translated(contentOffset()).top(); |
| qreal bottom = top + blockBoundingRect(block).height(); |
| |
| QString imagesPath = QString::fromLatin1(":/qt/scripttools/debugging/images"); |
| QString imageExt; |
| // SVGs don't work on all platforms, even when QT_NO_SVG is not defined, so disable SVG usage for now. |
| // #ifndef QT_NO_SVG |
| #if 0 |
| imageExt = QString::fromLatin1("svg"); |
| #else |
| imageExt = QString::fromLatin1("png"); |
| #endif |
| |
| while (block.isValid() && top <= rect.bottom()) { |
| if (block.isVisible() && bottom >= rect.top()) { |
| |
| int lineNumber = blockNumber + m_baseLineNumber; |
| if (m_breakpoints.contains(lineNumber)) { |
| int radius = fm.lineSpacing() - 1; |
| QRect r(rect.x(), (int)top, radius, radius); |
| QIcon icon(m_breakpoints[lineNumber].enabled |
| ? QString::fromLatin1("%0/breakpoint.%1").arg(imagesPath).arg(imageExt) |
| : QString::fromLatin1("%0/d_breakpoint.%1").arg(imagesPath).arg(imageExt)); |
| icon.paint(&painter, r, Qt::AlignCenter); |
| } |
| if (m_executionLineNumber == lineNumber) { |
| int radius = fm.lineSpacing() - 1; |
| QRect r(rect.x(), (int)top, radius, radius); |
| QIcon icon(QString::fromLatin1("%0/location.%1").arg(imagesPath).arg(imageExt)); |
| icon.paint(&painter, r, Qt::AlignCenter); |
| } |
| |
| if (!isExecutableLine(lineNumber)) |
| painter.setPen(pal.color(QPalette::Mid)); |
| else |
| painter.setPen(QColor(Qt::darkCyan)); |
| QString number = QString::number(lineNumber); |
| painter.drawText(rect.x() + markWidth, (int)top, rect.x() + extraAreaWidth - markWidth - 4, |
| fm.height(), Qt::AlignRight, number); |
| } |
| |
| block = block.next(); |
| top = bottom; |
| bottom = top + blockBoundingRect(block).height(); |
| ++blockNumber; |
| } |
| #endif |
| } |
| |
| void QScriptEdit::extraAreaMouseEvent(QMouseEvent *e) |
| { |
| QTextCursor cursor = cursorForPosition(QPoint(0, e->pos().y())); |
| #ifndef QT_NO_SYNTAXHIGHLIGHTER |
| cursor.setPosition(cursor.block().position()); |
| #endif |
| |
| QFontMetrics fm(font()); |
| int markWidth = fm.lineSpacing(); |
| |
| if (e->type() == QEvent::MouseMove && e->buttons() == 0) { // mouse tracking |
| bool hand = (e->pos().x() <= markWidth); |
| int lineNumber = cursor.blockNumber() + m_baseLineNumber; |
| hand = hand && isExecutableLine(lineNumber); |
| #ifndef QT_NO_CURSOR |
| if (hand != (m_extraArea->cursor().shape() == Qt::PointingHandCursor)) |
| m_extraArea->setCursor(hand ? Qt::PointingHandCursor : Qt::ArrowCursor); |
| #endif |
| } |
| |
| if (e->type() == QEvent::MouseButtonPress) { |
| if (e->button() == Qt::LeftButton) { |
| int lineNumber = cursor.blockNumber() + m_baseLineNumber; |
| bool executable = isExecutableLine(lineNumber); |
| if ((e->pos().x() <= markWidth) && executable) |
| m_extraAreaToggleBlockNumber = cursor.blockNumber(); |
| else |
| m_extraAreaToggleBlockNumber = -1; |
| } |
| } else if (e->type() == QEvent::MouseButtonRelease) { |
| if (e->button() == Qt::LeftButton) { |
| if ((m_extraAreaToggleBlockNumber != -1) && (e->pos().x() <= markWidth)) { |
| int lineNumber = m_extraAreaToggleBlockNumber + m_baseLineNumber; |
| bool on = !m_breakpoints.contains(lineNumber); |
| emit breakpointToggleRequest(lineNumber, on); |
| } |
| } else if (e->button() == Qt::RightButton) { |
| int lineNumber = cursor.blockNumber() + m_baseLineNumber; |
| if (!isExecutableLine(lineNumber)) |
| return; |
| bool has = m_breakpoints.contains(lineNumber); |
| QMenu *popup = new QMenu(); |
| QAction *toggleAct = new QAction(tr("Toggle Breakpoint"), popup); |
| popup->addAction(toggleAct); |
| QAction *disableAct = new QAction(tr("Disable Breakpoint"), popup); |
| QAction *enableAct = new QAction(tr("Enable Breakpoint"), popup); |
| QWidget *conditionWidget = new QWidget(); |
| { |
| QHBoxLayout *hbox = new QHBoxLayout(conditionWidget); |
| hbox->addWidget(new QLabel(tr("Breakpoint Condition:"))); |
| hbox->addWidget(new QLineEdit()); |
| } |
| // QWidgetAction *conditionAct = new QWidgetAction(popup); |
| // conditionAct->setDefaultWidget(conditionWidget); |
| if (has) { |
| popup->addSeparator(); |
| popup->addAction(m_breakpoints[lineNumber].enabled ? disableAct : enableAct); |
| // popup->addAction(conditionAct); |
| } |
| QAction *ret = popup->exec(e->globalPos()); |
| if (ret) { |
| if (ret == toggleAct) { |
| emit breakpointToggleRequest(lineNumber, !has); |
| } else if (ret == disableAct) { |
| emit breakpointEnableRequest(lineNumber, false); |
| } else if (ret == enableAct) { |
| emit breakpointEnableRequest(lineNumber, true); |
| }// else if (ret == conditionAct) { |
| //} |
| } |
| popup->deleteLater(); |
| } |
| } |
| } |
| |
| bool QScriptEdit::extraAreaEvent(QEvent *e) |
| { |
| if (e->type() == QEvent::ToolTip) { |
| // ### show the breakpoint's condition, if any |
| return true; |
| } |
| return false; |
| } |
| |
| QT_END_NAMESPACE |