Files
create/src/Gui/EditorView.cpp
2025-11-11 13:49:01 +01:00

932 lines
25 KiB
C++

/***************************************************************************
* Copyright (c) 2007 Werner Mayer <wmayer[at]users.sourceforge.net> *
* *
* This file is part of the FreeCAD CAx development system. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Library General Public *
* License as published by the Free Software Foundation; either *
* version 2 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Library General Public License for more details. *
* *
* You should have received a copy of the GNU Library General Public *
* License along with this library; see the file COPYING.LIB. If not, *
* write to the Free Software Foundation, Inc., 59 Temple Place, *
* Suite 330, Boston, MA 02111-1307, USA *
* *
***************************************************************************/
#include <QApplication>
#include <QCheckBox>
#include <QClipboard>
#include <QDateTime>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QLineEdit>
#include <QMessageBox>
#include <QPrinter>
#include <QPrintDialog>
#include <QPlainTextEdit>
#include <QPrintPreviewDialog>
#include <QSpacerItem>
#include <QStyle>
#include <QTextCursor>
#include <QTextDocument>
#include <QTextStream>
#include <QTimer>
#include <QToolButton>
#include "EditorView.h"
#include "Application.h"
#include "FileDialog.h"
#include "Macro.h"
#include "MainWindow.h"
#include "PythonEditor.h"
#include "PythonTracing.h"
#include "WaitCursor.h"
#include <Base/Exception.h>
#include <Base/Interpreter.h>
#include <Base/Parameter.h>
#include <Gui/PreferencePages/DlgSettingsPDF.h>
using namespace Gui;
namespace Gui
{
class EditorViewP
{
public:
TextEdit* textEdit;
SearchBar* searchBar;
QString fileName;
EditorView::DisplayName displayName;
QTimer* activityTimer;
qint64 timeStamp;
bool lock;
bool aboutToClose;
QStringList undos;
QStringList redos;
};
} // namespace Gui
// -------------------------------------------------------
/* TRANSLATOR Gui::EditorView */
TYPESYSTEM_SOURCE_ABSTRACT(Gui::EditorView, Gui::MDIView)
/**
* Constructs a EditorView which is a child of 'parent', with the
* name 'name'.
*/
EditorView::EditorView(TextEdit* editor, QWidget* parent)
: MDIView(nullptr, parent, Qt::WindowFlags())
, WindowParameter("Editor")
{
d = new EditorViewP;
d->lock = false;
d->aboutToClose = false;
d->displayName = EditorView::FullName;
// create the editor first
d->textEdit = editor;
d->textEdit->setLineWrapMode(QPlainTextEdit::NoWrap);
d->searchBar = new SearchBar();
d->searchBar->setEditor(editor);
// clang-format off
// update editor actions on request
Gui::MainWindow* mw = Gui::getMainWindow();
connect(editor, &QPlainTextEdit::undoAvailable, mw, &MainWindow::updateEditorActions);
connect(editor, &QPlainTextEdit::redoAvailable, mw, &MainWindow::updateEditorActions);
connect(editor, &QPlainTextEdit::copyAvailable, mw, &MainWindow::updateEditorActions);
connect(editor, &TextEdit::showSearchBar, d->searchBar, &SearchBar::activate);
connect(editor, &TextEdit::findNext, d->searchBar, &SearchBar::findNext);
connect(editor, &TextEdit::findPrevious, d->searchBar, &SearchBar::findPrevious);
// clang-format on
// Create the layout containing the workspace and a tab bar
auto hbox = new QFrame(this);
hbox->setFrameShape(QFrame::StyledPanel);
hbox->setFrameShadow(QFrame::Sunken);
auto layout = new QVBoxLayout();
layout->setContentsMargins(1, 1, 1, 1);
layout->addWidget(d->textEdit);
layout->addWidget(d->searchBar);
d->textEdit->setParent(hbox);
d->searchBar->setParent(hbox);
hbox->setLayout(layout);
setCentralWidget(hbox);
setCurrentFileName(QString());
d->textEdit->setFocus();
setWindowIcon(d->textEdit->windowIcon());
ParameterGrp::handle hPrefGrp = getWindowParameter();
hPrefGrp->Attach(this);
hPrefGrp->NotifyAll();
d->activityTimer = new QTimer(this);
// clang-format off
connectionList <<
connect(d->activityTimer, &QTimer::timeout,
this, &EditorView::checkTimestamp) <<
connect(d->textEdit->document(), &QTextDocument::modificationChanged,
this, &EditorView::setWindowModified) <<
connect(d->textEdit->document(), &QTextDocument::undoAvailable,
this, &EditorView::undoAvailable) <<
connect(d->textEdit->document(), &QTextDocument::redoAvailable,
this, &EditorView::redoAvailable) <<
connect(d->textEdit->document(), &QTextDocument::contentsChange,
this, &EditorView::contentsChange);
// clang-format on
}
/** Destroys the object and frees any allocated resources */
EditorView::~EditorView()
{
d->activityTimer->stop();
// to avoid the assert introduced a debug version of Qt >6.3. See QTBUG-105473
for (auto conn : connectionList) { // NOLINT(performance-for-range-copy)
disconnect(conn);
}
delete d->activityTimer;
delete d;
getWindowParameter()->Detach(this);
}
QPlainTextEdit* EditorView::getEditor() const
{
return d->textEdit;
}
void EditorView::showEvent(QShowEvent* event)
{
Gui::MainWindow* mw = Gui::getMainWindow();
mw->updateEditorActions();
MDIView::showEvent(event);
}
void EditorView::hideEvent(QHideEvent* event)
{
MDIView::hideEvent(event);
}
void EditorView::closeEvent(QCloseEvent* event)
{
MDIView::closeEvent(event);
if (event->isAccepted()) {
d->aboutToClose = true;
Gui::MainWindow* mw = Gui::getMainWindow();
mw->updateEditorActions();
}
}
void EditorView::OnChange(Base::Subject<const char*>& rCaller, const char* rcReason)
{
Q_UNUSED(rCaller);
ParameterGrp::handle hPrefGrp = getWindowParameter();
if (strcmp(rcReason, "EnableLineNumber") == 0) {
// bool show = hPrefGrp->GetBool( "EnableLineNumber", true );
}
}
void EditorView::checkTimestamp()
{
QFileInfo fi(d->fileName);
qint64 timeStamp = fi.lastModified().toSecsSinceEpoch();
if (timeStamp != d->timeStamp) {
switch (QMessageBox::question(
this,
tr("Modified file"),
tr("%1.\n\nThis has been modified outside of the source "
"editor. Reload it?")
.arg(d->fileName),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes
)) {
case QMessageBox::Yes:
// updates time stamp and timer
open(d->fileName);
return;
case QMessageBox::No:
d->timeStamp = timeStamp;
break;
default:
break;
}
}
d->activityTimer->setSingleShot(true);
d->activityTimer->start(3000);
}
/**
* Runs the action specified by \a pMsg.
*/
bool EditorView::onMsg(const char* pMsg, const char** /*ppReturn*/)
{
// don't allow any actions if the editor is being closed
if (d->aboutToClose) {
return false;
}
if (strcmp(pMsg, "Save") == 0) {
saveFile();
return true;
}
else if (strcmp(pMsg, "SaveAs") == 0) {
saveAs();
return true;
}
else if (strcmp(pMsg, "Cut") == 0) {
cut();
return true;
}
else if (strcmp(pMsg, "Copy") == 0) {
copy();
return true;
}
else if (strcmp(pMsg, "Paste") == 0) {
paste();
return true;
}
else if (strcmp(pMsg, "Undo") == 0) {
undo();
return true;
}
else if (strcmp(pMsg, "Redo") == 0) {
redo();
return true;
}
else if (strcmp(pMsg, "ViewFit") == 0) {
// just ignore this
return true;
}
return false;
}
/**
* Checks if the action \a pMsg is available. This is for enabling/disabling
* the corresponding buttons or menu items for this action.
*/
bool EditorView::onHasMsg(const char* pMsg) const
{
// don't allow any actions if the editor is being closed
if (d->aboutToClose) {
return false;
}
if (strcmp(pMsg, "Run") == 0) {
return true;
}
if (strcmp(pMsg, "DebugStart") == 0) {
return true;
}
if (strcmp(pMsg, "DebugStop") == 0) {
return true;
}
if (strcmp(pMsg, "SaveAs") == 0) {
return true;
}
if (strcmp(pMsg, "Print") == 0) {
return true;
}
if (strcmp(pMsg, "PrintPreview") == 0) {
return true;
}
if (strcmp(pMsg, "PrintPdf") == 0) {
return true;
}
if (strcmp(pMsg, "Save") == 0) {
return d->textEdit->document()->isModified();
}
else if (strcmp(pMsg, "Cut") == 0) {
bool canWrite = !d->textEdit->isReadOnly();
return (canWrite && (d->textEdit->textCursor().hasSelection()));
}
else if (strcmp(pMsg, "Copy") == 0) {
return (d->textEdit->textCursor().hasSelection());
}
else if (strcmp(pMsg, "Paste") == 0) {
QClipboard* cb = QApplication::clipboard();
QString text;
// Copy text from the clipboard (paste)
text = cb->text();
bool canWrite = !d->textEdit->isReadOnly();
return (!text.isEmpty() && canWrite);
}
else if (strcmp(pMsg, "Undo") == 0) {
return d->textEdit->document()->isUndoAvailable();
}
else if (strcmp(pMsg, "Redo") == 0) {
return d->textEdit->document()->isRedoAvailable();
}
return false;
}
/** Checking on close state. */
bool EditorView::canClose()
{
if (!d->textEdit->document()->isModified()) {
return true;
}
this->setFocus(); // raises the view to front
switch (QMessageBox::question(
this,
tr("Unsaved document"),
tr("The document has been modified.\n"
"Save all changes?"),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel,
QMessageBox::Cancel
)) {
case QMessageBox::Yes:
return saveFile();
case QMessageBox::No:
return true;
case QMessageBox::Cancel:
return false;
default:
return false;
}
}
void EditorView::setDisplayName(EditorView::DisplayName type)
{
d->displayName = type;
}
/**
* Saves the content of the editor to a file specified by the appearing file dialog.
*/
bool EditorView::saveAs()
{
QString fn = FileDialog::getSaveFileName(
this,
QObject::tr("Save Macro"),
QString(),
QStringLiteral("%1 (*.FCMacro);;Python (*.py)").arg(tr("FreeCAD macro"))
);
if (fn.isEmpty()) {
return false;
}
setCurrentFileName(fn);
return saveFile();
}
/**
* Opens the file \a fileName.
*/
bool EditorView::open(const QString& fileName)
{
if (!QFile::exists(fileName)) {
return false;
}
QFile file(fileName);
if (!file.open(QFile::ReadOnly)) {
return false;
}
d->lock = true;
d->textEdit->setPlainText(QString::fromUtf8(file.readAll()));
d->lock = false;
d->undos.clear();
d->redos.clear();
file.close();
QFileInfo fi(fileName);
d->timeStamp = fi.lastModified().toSecsSinceEpoch();
d->activityTimer->setSingleShot(true);
d->activityTimer->start(3000);
setCurrentFileName(fileName);
return true;
}
/**
* Copies the selected text to the clipboard and deletes it from the text edit.
* If there is no selected text nothing happens.
*/
void EditorView::cut()
{
d->textEdit->cut();
}
/**
* Copies any selected text to the clipboard.
*/
void EditorView::copy()
{
d->textEdit->copy();
}
/**
* Pastes the text from the clipboard into the text edit at the current cursor position.
* If there is no text in the clipboard nothing happens.
*/
void EditorView::paste()
{
d->textEdit->paste();
}
/**
* Undoes the last operation.
* If there is no operation to undo, i.e. there is no undo step in the undo/redo history, nothing
* happens.
*/
void EditorView::undo()
{
d->lock = true;
if (!d->undos.isEmpty()) {
d->redos << d->undos.back();
d->undos.pop_back();
}
d->textEdit->document()->undo();
d->lock = false;
}
/**
* Redoes the last operation.
* If there is no operation to undo, i.e. there is no undo step in the undo/redo history, nothing
* happens.
*/
void EditorView::redo()
{
d->lock = true;
if (!d->redos.isEmpty()) {
d->undos << d->redos.back();
d->redos.pop_back();
}
d->textEdit->document()->redo();
d->lock = false;
}
/**
* Shows the printer dialog.
*/
void EditorView::print()
{
QPrinter printer(QPrinter::ScreenResolution);
printer.setFullPage(true);
QPrintDialog dlg(&printer, this);
if (dlg.exec() == QDialog::Accepted) {
d->textEdit->document()->print(&printer);
}
}
void EditorView::printPreview()
{
QPrinter printer(QPrinter::ScreenResolution);
QPrintPreviewDialog dlg(&printer, this);
connect(&dlg, &QPrintPreviewDialog::paintRequested, this, qOverload<QPrinter*>(&EditorView::print));
dlg.exec();
}
void EditorView::print(QPrinter* printer)
{
d->textEdit->document()->print(printer);
}
/**
* Prints the document into a Pdf file.
*/
void EditorView::printPdf()
{
QString filename = FileDialog::getSaveFileName(
this,
tr("Export PDF"),
QString(),
QStringLiteral("%1 (*.pdf)").arg(tr("PDF file"))
);
if (!filename.isEmpty()) {
QPrinter printer(QPrinter::ScreenResolution);
// setPdfVersion sets the printed PDF Version to what is chosen in
// Preferences/Import-Export/PDF more details under:
// https://www.kdab.com/creating-pdfa-documents-qt/
printer.setPdfVersion(Gui::Dialog::DlgSettingsPDF::evaluatePDFVersion());
printer.setOutputFormat(QPrinter::PdfFormat);
printer.setOutputFileName(filename);
printer.setCreator(QString::fromStdString(App::Application::getNameWithVersion()));
d->textEdit->document()->print(&printer);
}
}
void EditorView::setCurrentFileName(const QString& fileName)
{
d->fileName = fileName;
Q_EMIT changeFileName(d->fileName);
d->textEdit->document()->setModified(false);
QString name;
QFileInfo fi(fileName);
switch (d->displayName) {
case FullName:
name = fileName;
break;
case FileName:
name = fi.fileName();
break;
case BaseName:
name = fi.baseName();
break;
}
QString shownName;
if (fileName.isEmpty()) {
shownName = tr("untitled[*]");
}
else {
shownName = QStringLiteral("%1[*]").arg(name);
}
shownName += tr(" - Editor");
setWindowTitle(shownName);
setWindowModified(false);
}
QString EditorView::fileName() const
{
return d->fileName;
}
/**
* Saves the contents to a file.
*/
bool EditorView::saveFile()
{
if (d->fileName.isEmpty()) {
return saveAs();
}
QFile file(d->fileName);
if (!file.open(QFile::WriteOnly)) {
return false;
}
QTextStream ts(&file);
#if QT_VERSION < 0x060000
ts.setCodec("UTF-8");
#endif
ts << d->textEdit->document()->toPlainText();
file.close();
d->textEdit->document()->setModified(false);
QFileInfo fi(d->fileName);
d->timeStamp = fi.lastModified().toSecsSinceEpoch();
return true;
}
void EditorView::undoAvailable(bool undo)
{
if (!undo) {
d->undos.clear();
}
}
void EditorView::redoAvailable(bool redo)
{
if (!redo) {
d->redos.clear();
}
}
void EditorView::contentsChange(int position, int charsRemoved, int charsAdded)
{
Q_UNUSED(position);
if (d->lock) {
return;
}
if (charsRemoved > 0 && charsAdded > 0) {
return; // syntax highlighting
}
else if (charsRemoved > 0) {
d->undos << tr("%1 chars removed").arg(charsRemoved);
}
else if (charsAdded > 0) {
d->undos << tr("%1 chars added").arg(charsAdded);
}
else {
d->undos << tr("Formatted");
}
d->redos.clear();
}
/**
* Get the undo history.
*/
QStringList EditorView::undoActions() const
{
return d->undos;
}
/**
* Get the redo history.
*/
QStringList EditorView::redoActions() const
{
return d->redos;
;
}
void EditorView::focusInEvent(QFocusEvent*)
{
d->textEdit->setFocus();
}
// ---------------------------------------------------------
TYPESYSTEM_SOURCE_ABSTRACT(Gui::PythonEditorView, Gui::EditorView)
PythonEditorView::PythonEditorView(PythonEditor* editor, QWidget* parent)
: EditorView(editor, parent)
, _pye(editor)
{
connect(this, &PythonEditorView::changeFileName, editor, &PythonEditor::setFileName);
watcher = new PythonTracingWatcher(this);
}
PythonEditorView::~PythonEditorView()
{
delete watcher;
}
/**
* Runs the action specified by \a pMsg.
*/
bool PythonEditorView::onMsg(const char* pMsg, const char** ppReturn)
{
if (strcmp(pMsg, "Run") == 0) {
executeScript();
return true;
}
else if (strcmp(pMsg, "StartDebug") == 0) {
QTimer::singleShot(300, this, &PythonEditorView::startDebug);
return true;
}
else if (strcmp(pMsg, "ToggleBreakpoint") == 0) {
toggleBreakpoint();
return true;
}
return EditorView::onMsg(pMsg, ppReturn);
}
/**
* Checks if the action \a pMsg is available. This is for enabling/disabling
* the corresponding buttons or menu items for this action.
*/
bool PythonEditorView::onHasMsg(const char* pMsg) const
{
if (strcmp(pMsg, "Run") == 0) {
return true;
}
if (strcmp(pMsg, "StartDebug") == 0) {
return true;
}
if (strcmp(pMsg, "ToggleBreakpoint") == 0) {
return true;
}
return EditorView::onHasMsg(pMsg);
}
/**
* Runs the opened script in the macro manager.
*/
void PythonEditorView::executeScript()
{
// always save the macro when it is modified
if (EditorView::onHasMsg("Save")) {
EditorView::onMsg("Save", nullptr);
}
try {
getMainWindow()->setCursor(Qt::WaitCursor);
PythonTracingLocker tracelock(watcher->getTrace());
Application::Instance->macroManager()->run(Gui::MacroManager::File, fileName().toUtf8());
getMainWindow()->unsetCursor();
}
catch (const Base::SystemExitException&) {
// handle SystemExit exceptions
Base::PyGILStateLocker locker;
Base::PyException e;
e.reportException();
getMainWindow()->unsetCursor();
}
}
void PythonEditorView::startDebug()
{
_pye->startDebug();
}
void PythonEditorView::toggleBreakpoint()
{
_pye->toggleBreakpoint();
}
void PythonEditorView::showDebugMarker(int line)
{
_pye->showDebugMarker(line);
}
void PythonEditorView::hideDebugMarker()
{
_pye->hideDebugMarker();
}
// ----------------------------------------------------------------------------
SearchBar::SearchBar(QWidget* parent)
: QWidget(parent)
, textEditor(nullptr)
{
horizontalLayout = new QHBoxLayout(this);
horizontalLayout->setSpacing(3);
closeButton = new QToolButton(this);
closeButton->setIcon(style()->standardIcon(QStyle::SP_DialogCloseButton));
closeButton->setAutoRaise(true);
connect(closeButton, &QToolButton::clicked, this, &SearchBar::deactivate);
horizontalLayout->addWidget(closeButton);
searchText = new QLineEdit(this);
searchText->setClearButtonEnabled(true);
horizontalLayout->addWidget(searchText);
connect(searchText, &QLineEdit::returnPressed, this, &SearchBar::findNext);
connect(searchText, &QLineEdit::textChanged, this, &SearchBar::findCurrent);
connect(searchText, &QLineEdit::textChanged, this, &SearchBar::updateButtons);
prevButton = new QToolButton(this);
prevButton->setIcon(style()->standardIcon(QStyle::SP_ArrowBack));
prevButton->setAutoRaise(true);
prevButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
horizontalLayout->addWidget(prevButton);
connect(prevButton, &QToolButton::clicked, this, &SearchBar::findPrevious);
nextButton = new QToolButton(this);
nextButton->setIcon(style()->standardIcon(QStyle::SP_ArrowForward));
nextButton->setAutoRaise(true);
nextButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
horizontalLayout->addWidget(nextButton);
connect(nextButton, &QToolButton::clicked, this, &SearchBar::findNext);
matchCase = new QCheckBox(this);
horizontalLayout->addWidget(matchCase);
connect(matchCase, &QCheckBox::toggled, this, &SearchBar::findCurrent);
matchWord = new QCheckBox(this);
horizontalLayout->addWidget(matchWord);
connect(matchWord, &QCheckBox::toggled, this, &SearchBar::findCurrent);
horizontalSpacer = new QSpacerItem(192, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);
horizontalLayout->addItem(horizontalSpacer);
retranslateUi();
setMinimumWidth(minimumSizeHint().width());
updateButtons();
hide();
}
void SearchBar::setEditor(QPlainTextEdit* textEdit)
{
textEditor = textEdit;
}
void SearchBar::keyPressEvent(QKeyEvent* event)
{
if (event->key() == Qt::Key_Escape) {
hide();
return;
}
QWidget::keyPressEvent(event);
}
void SearchBar::retranslateUi()
{
prevButton->setText(tr("Previous"));
nextButton->setText(tr("Next"));
matchCase->setText(tr("Case sensitive"));
matchWord->setText(tr("Whole words"));
}
void SearchBar::activate()
{
show();
searchText->selectAll();
searchText->setFocus(Qt::ShortcutFocusReason);
}
void SearchBar::deactivate()
{
if (textEditor) {
textEditor->setFocus();
}
hide();
}
void SearchBar::findPrevious()
{
findText(true, false, searchText->text());
}
void SearchBar::findNext()
{
findText(true, true, searchText->text());
}
void SearchBar::findCurrent()
{
findText(false, true, searchText->text());
}
void SearchBar::findText(bool skip, bool next, const QString& str)
{
if (!textEditor) {
return;
}
QTextCursor cursor = textEditor->textCursor();
QTextDocument* doc = textEditor->document();
if (!doc || cursor.isNull()) {
return;
}
if (cursor.hasSelection()) {
cursor.setPosition((skip && next) ? cursor.position() : cursor.anchor());
}
bool found = true;
QTextCursor newCursor = cursor;
if (!str.isEmpty()) {
QTextDocument::FindFlags options;
if (!next) {
options |= QTextDocument::FindBackward;
}
if (matchCase->isChecked()) {
options |= QTextDocument::FindCaseSensitively;
}
if (matchWord->isChecked()) {
options |= QTextDocument::FindWholeWords;
}
newCursor = doc->find(str, cursor, options);
if (newCursor.isNull()) {
QTextCursor ac(doc);
ac.movePosition(
options & QTextDocument::FindBackward ? QTextCursor::End : QTextCursor::Start
);
newCursor = doc->find(str, ac, options);
if (newCursor.isNull()) {
found = false;
newCursor = cursor;
}
}
}
if (!isVisible()) {
show();
}
textEditor->setTextCursor(newCursor);
QString styleSheet;
if (!found) {
styleSheet = QStringLiteral(
" QLineEdit {\n"
" background-color: rgb(221,144,161);\n"
" }\n"
);
}
searchText->setStyleSheet(styleSheet);
}
void SearchBar::updateButtons()
{
bool empty = searchText->text().isEmpty();
prevButton->setDisabled(empty);
nextButton->setDisabled(empty);
}
void SearchBar::changeEvent(QEvent* event)
{
if (event->type() == QEvent::LanguageChange) {
retranslateUi();
}
QWidget::changeEvent(event);
}
#include "moc_EditorView.cpp"