diff --git a/kdevplatform/shell/textdocument.cpp b/kdevplatform/shell/textdocument.cpp index 672803d3bc..1952b6bec5 100644 --- a/kdevplatform/shell/textdocument.cpp +++ b/kdevplatform/shell/textdocument.cpp @@ -1,772 +1,753 @@ /*************************************************************************** * Copyright 2007 Alexander Dymo * * * * This program 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 program 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 General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "textdocument.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "core.h" #include "mainwindow.h" #include "uicontroller.h" #include "partcontroller.h" #include "plugincontroller.h" #include "documentcontroller.h" #include "ktexteditorpluginintegration.h" #include "debug.h" #include #include namespace KDevelop { const int MAX_DOC_SETTINGS = 20; // This sets cursor position and selection on the view to the given // range. Selection is set only for non-empty ranges // Factored into a function since its needed in 3 places already static void selectAndReveal( KTextEditor::View* view, const KTextEditor::Range& range ) { Q_ASSERT(view); if (range.isValid()) { view->setCursorPosition(range.start()); if (!range.isEmpty()) { view->setSelection(range); } } } class TextDocumentPrivate { public: explicit TextDocumentPrivate(TextDocument *textDocument) : document(nullptr), state(IDocument::Clean), encoding(), q(textDocument) , m_loaded(false), m_addedContextMenu(nullptr) { } ~TextDocumentPrivate() { delete m_addedContextMenu; m_addedContextMenu = nullptr; saveSessionConfig(); delete document; } QPointer document; IDocument::DocumentState state; QString encoding; void setStatus(KTextEditor::Document* document, bool dirty) { QIcon statusIcon; if (document->isModified()) if (dirty) { state = IDocument::DirtyAndModified; statusIcon = QIcon::fromTheme(QStringLiteral("edit-delete")); } else { state = IDocument::Modified; statusIcon = QIcon::fromTheme(QStringLiteral("document-save")); } else if (dirty) { state = IDocument::Dirty; statusIcon = QIcon::fromTheme(QStringLiteral("document-revert")); } else { state = IDocument::Clean; } q->notifyStateChanged(); Core::self()->uiControllerInternal()->setStatusIcon(q, statusIcon); } inline KConfigGroup katePartSettingsGroup() const { return KSharedConfig::openConfig()->group("KatePart Settings"); } inline QString docConfigGroupName() const { return document->url().toDisplayString(QUrl::PreferLocalFile); } inline KConfigGroup docConfigGroup() const { return katePartSettingsGroup().group(docConfigGroupName()); } void saveSessionConfig() { if(document && document->url().isValid()) { // make sure only MAX_DOC_SETTINGS entries are stored KConfigGroup katePartSettings = katePartSettingsGroup(); // ordered list of documents QStringList documents = katePartSettings.readEntry("documents", QStringList()); // ensure this document is "new", i.e. at the end of the list documents.removeOne(docConfigGroupName()); documents.append(docConfigGroupName()); // remove "old" documents + their group while(documents.size() >= MAX_DOC_SETTINGS) { katePartSettings.group(documents.takeFirst()).deleteGroup(); } // update order katePartSettings.writeEntry("documents", documents); // actually save session config KConfigGroup group = docConfigGroup(); document->writeSessionConfig(group); } } void loadSessionConfig() { if (!document || !katePartSettingsGroup().hasGroup(docConfigGroupName())) { return; } document->readSessionConfig(docConfigGroup(), {QStringLiteral("SkipUrl")}); } // Determines whether the current contents of this document in the editor // could be retrieved from the VCS if they were dismissed. void queryCanRecreateFromVcs(KTextEditor::Document* document) const { IProject* project = nullptr; // Find projects by checking which one contains the file's parent directory, // to avoid issues with the cmake manager temporarily removing files from a project // during reloading. KDevelop::Path path(document->url()); foreach ( KDevelop::IProject* current, Core::self()->projectController()->projects() ) { if ( current->path().isParentOf(path) ) { project = current; break; } } if (!project) { return; } IContentAwareVersionControl* iface; iface = qobject_cast< KDevelop::IContentAwareVersionControl* >(project->versionControlPlugin()); if (!iface) { return; } if ( !qobject_cast( document ) ) { return; } CheckInRepositoryJob* req = iface->isInRepository( document ); if ( !req ) { return; } QObject::connect(req, &CheckInRepositoryJob::finished, q, &TextDocument::repositoryCheckFinished); // Abort the request when the user edits the document QObject::connect(q->textDocument(), &KTextEditor::Document::textChanged, req, &CheckInRepositoryJob::abort); } void modifiedOnDisk(KTextEditor::Document *document, bool /*isModified*/, KTextEditor::ModificationInterface::ModifiedOnDiskReason reason) { bool dirty = false; switch (reason) { case KTextEditor::ModificationInterface::OnDiskUnmodified: break; case KTextEditor::ModificationInterface::OnDiskModified: case KTextEditor::ModificationInterface::OnDiskCreated: case KTextEditor::ModificationInterface::OnDiskDeleted: dirty = true; break; } // In some cases, the VCS (e.g. git) can know whether the old contents are "valuable", i.e. // not retrieveable from the VCS. If that is not the case, then the document can safely be // reloaded without displaying a dialog asking the user. if ( dirty ) { queryCanRecreateFromVcs(document); } setStatus(document, dirty); } TextDocument * const q; bool m_loaded; // we want to remove the added stuff when the menu hides QMenu* m_addedContextMenu; }; class TextViewPrivate { public: explicit TextViewPrivate(TextView* q) : q(q) {} TextView* const q; QPointer view; KTextEditor::Range initialRange; }; TextDocument::TextDocument(const QUrl &url, ICore* core, const QString& encoding) :PartDocument(url, core), d(new TextDocumentPrivate(this)) { d->encoding = encoding; } TextDocument::~TextDocument() = default; bool TextDocument::isTextDocument() const { if( !d->document ) { /// @todo Somehow it can happen that d->document is zero, which makes /// code relying on "isTextDocument() == (bool)textDocument()" crash qCWarning(SHELL) << "Broken text-document: " << url(); return false; } return true; } KTextEditor::Document *TextDocument::textDocument() const { return d->document; } QWidget *TextDocument::createViewWidget(QWidget *parent) { KTextEditor::View* view = nullptr; if (!d->document) { d->document = Core::self()->partControllerInternal()->createTextPart(); // Connect to the first text changed signal, it occurs before the completed() signal connect(d->document.data(), &KTextEditor::Document::textChanged, this, &TextDocument::slotDocumentLoaded); // Also connect to the completed signal, sometimes the first text changed signal is missed because the part loads too quickly (? TODO - confirm this is necessary) connect(d->document.data(), static_cast(&KTextEditor::Document::completed), this, &TextDocument::slotDocumentLoaded); // force a reparse when a document gets reloaded connect(d->document.data(), &KTextEditor::Document::reloaded, this, [] (KTextEditor::Document* document) { ICore::self()->languageController()->backgroundParser()->addDocument(IndexedString(document->url()), (TopDUContext::Features) ( TopDUContext::AllDeclarationsContextsAndUses | TopDUContext::ForceUpdate ), BackgroundParser::BestPriority, nullptr); }); // Set encoding passed via constructor // Needs to be done before openUrl, else katepart won't use the encoding // @see KTextEditor::Document::setEncoding if (!d->encoding.isEmpty()) d->document->setEncoding(d->encoding); if (!url().isEmpty() && !DocumentController::isEmptyDocumentUrl(url())) d->document->openUrl( url() ); d->setStatus(d->document, false); /* It appears, that by default a part will be deleted the first view containing it is deleted. Since we do want to have several views, disable that behaviour. */ d->document->setAutoDeletePart(false); Core::self()->partController()->addPart(d->document, false); d->loadSessionConfig(); connect(d->document.data(), &KTextEditor::Document::modifiedChanged, this, &TextDocument::newDocumentStatus); connect(d->document.data(), &KTextEditor::Document::textChanged, this, &TextDocument::textChanged); connect(d->document.data(), &KTextEditor::Document::documentUrlChanged, this, &TextDocument::documentUrlChanged); connect(d->document.data(), &KTextEditor::Document::documentSavedOrUploaded, this, &TextDocument::documentSaved ); if (qobject_cast(d->document)) { // can't use new signal/slot syntax here, MarkInterface is not a QObject connect(d->document.data(), SIGNAL(marksChanged(KTextEditor::Document*)), this, SLOT(saveSessionConfig())); } if (auto iface = qobject_cast(d->document)) { iface->setModifiedOnDiskWarning(true); // can't use new signal/slot syntax here, ModificationInterface is not a QObject connect(d->document.data(), SIGNAL(modifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason)), this, SLOT(modifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason))); } notifyTextDocumentCreated(); } view = d->document->createView(parent, Core::self()->uiControllerInternal()->defaultMainWindow()->kateWrapper()->interface()); // get rid of some actions regarding the config dialog, we merge that one into the kdevelop menu already delete view->actionCollection()->action(QStringLiteral("set_confdlg")); delete view->actionCollection()->action(QStringLiteral("editor_options")); view->setStatusBarEnabled(Core::self()->partControllerInternal()->showTextEditorStatusBar()); connect(view, &KTextEditor::View::contextMenuAboutToShow, this, &TextDocument::populateContextMenu); if (KTextEditor::CodeCompletionInterface* cc = dynamic_cast(view)) cc->setAutomaticInvocationEnabled(core()->languageController()->completionSettings()->automaticCompletionEnabled()); return view; } KParts::Part *TextDocument::partForView(QWidget *view) const { if (d->document && d->document->views().contains((KTextEditor::View*)view)) return d->document; return nullptr; } // KDevelop::IDocument implementation void TextDocument::reload() { if (!d->document) return; KTextEditor::ModificationInterface* modif=nullptr; if(d->state==Dirty) { modif = qobject_cast(d->document); modif->setModifiedOnDiskWarning(false); } d->document->documentReload(); if(modif) modif->setModifiedOnDiskWarning(true); } bool TextDocument::save(DocumentSaveMode mode) { if (!d->document) return true; if (mode & Discard) return true; switch (d->state) { case IDocument::Clean: return true; case IDocument::Modified: break; case IDocument::Dirty: case IDocument::DirtyAndModified: if (!(mode & Silent)) { int code = KMessageBox::warningYesNoCancel( Core::self()->uiController()->activeMainWindow(), i18n("The file \"%1\" is modified on disk.\n\nAre " "you sure you want to overwrite it? (External " "changes will be lost.)", d->document->url().toLocalFile()), i18nc("@title:window", "Document Externally Modified")); if (code != KMessageBox::Yes) return false; } break; } if (!KDevelop::ensureWritable(QList() << url())) { return false; } QUrl urlBeforeSave = d->document->url(); if (d->document->documentSave()) { if (d->document->url() != urlBeforeSave) notifyUrlChanged(); return true; } return false; } IDocument::DocumentState TextDocument::state() const { return d->state; } KTextEditor::Cursor KDevelop::TextDocument::cursorPosition() const { if (!d->document) { return KTextEditor::Cursor::invalid(); } KTextEditor::View *view = activeTextView(); if (view) return view->cursorPosition(); return KTextEditor::Cursor::invalid(); } void TextDocument::setCursorPosition(const KTextEditor::Cursor &cursor) { if (!cursor.isValid() || !d->document) return; KTextEditor::View *view = activeTextView(); // Rodda: Cursor must be accurate here, to the definition of accurate for KTextEditor::Cursor. // ie, starting from 0,0 if (view) view->setCursorPosition(cursor); } KTextEditor::Range TextDocument::textSelection() const { if (!d->document) { return KTextEditor::Range::invalid(); } KTextEditor::View *view = activeTextView(); if (view && view->selection()) { return view->selectionRange(); } return PartDocument::textSelection(); } QString TextDocument::text(const KTextEditor::Range &range) const { if (!d->document) { return QString(); } return d->document->text( range ); } QString TextDocument::textLine() const { if (!d->document) { return QString(); } KTextEditor::View *view = activeTextView(); if (view) { return d->document->line( view->cursorPosition().line() ); } return PartDocument::textLine(); } QString TextDocument::textWord() const { if (!d->document) { return QString(); } KTextEditor::View *view = activeTextView(); if (view) { KTextEditor::Cursor start = view->cursorPosition(); qCDebug(SHELL) << "got start position from view:" << start.line() << start.column(); QString linestr = textLine(); int startPos = qMax( qMin( start.column(), linestr.length() - 1 ), 0 ); int endPos = startPos; startPos --; while (startPos >= 0 && (linestr[startPos].isLetterOrNumber() || linestr[startPos] == QLatin1Char('_') || linestr[startPos] == QLatin1Char('~'))) { --startPos; } while (endPos < linestr.length() && (linestr[endPos].isLetterOrNumber() || linestr[endPos] == QLatin1Char('_') || linestr[endPos] == QLatin1Char('~'))) { ++endPos; } if( startPos != endPos ) { qCDebug(SHELL) << "found word" << startPos << endPos << linestr.mid( startPos+1, endPos - startPos - 1 ); return linestr.mid( startPos + 1, endPos - startPos - 1 ); } } return PartDocument::textWord(); } void TextDocument::setTextSelection(const KTextEditor::Range &range) { if (!range.isValid() || !d->document) return; KTextEditor::View *view = activeTextView(); if (view) { selectAndReveal(view, range); } } bool TextDocument::close(DocumentSaveMode mode) { if (!PartDocument::close(mode)) return false; if ( d->document ) { d->saveSessionConfig(); delete d->document; //We have to delete the document right now, to prevent random crashes in the event handler } return true; } Sublime::View* TextDocument::newView(Sublime::Document* doc) { Q_UNUSED(doc); return new TextView(this); } } KDevelop::TextView::TextView(TextDocument * doc) : View(doc, View::TakeOwnership), d(new TextViewPrivate(this)) { } KDevelop::TextView::~TextView() = default; QWidget * KDevelop::TextView::createWidget(QWidget * parent) { auto textDocument = qobject_cast(document()); Q_ASSERT(textDocument); QWidget* widget = textDocument->createViewWidget(parent); d->view = qobject_cast(widget); Q_ASSERT(d->view); connect(d->view.data(), &KTextEditor::View::cursorPositionChanged, this, &KDevelop::TextView::sendStatusChanged); return widget; } -QString KDevelop::TextView::viewState() const -{ - if (d->view) { - if (d->view->selection()) { - KTextEditor::Range selection = d->view->selectionRange(); - return QStringLiteral("Selection=%1,%2,%3,%4").arg(selection.start().line()) - .arg(selection.start().column()) - .arg(selection.end().line()) - .arg(selection.end().column()); - } else { - KTextEditor::Cursor cursor = d->view->cursorPosition(); - return QStringLiteral("Cursor=%1,%2").arg(cursor.line()).arg(cursor.column()); - } - } - else { - KTextEditor::Range selection = d->initialRange; - return QStringLiteral("Selection=%1,%2,%3,%4").arg(selection.start().line()) - .arg(selection.start().column()) - .arg(selection.end().line()) - .arg(selection.end().column()); - } -} - void KDevelop::TextView::setInitialRange(const KTextEditor::Range& range) { if (d->view) { selectAndReveal(d->view, range); } else { d->initialRange = range; } } KTextEditor::Range KDevelop::TextView::initialRange() const { return d->initialRange; } -void KDevelop::TextView::setState(const QString & state) +void KDevelop::TextView::readSessionConfig(KConfigGroup& config) { - static QRegExp reCursor(QStringLiteral("Cursor=([\\d]+),([\\d]+)")); - static QRegExp reSelection(QStringLiteral("Selection=([\\d]+),([\\d]+),([\\d]+),([\\d]+)")); - if (reCursor.exactMatch(state)) { - setInitialRange(KTextEditor::Range(KTextEditor::Cursor(reCursor.cap(1).toInt(), reCursor.cap(2).toInt()), 0)); - } else if (reSelection.exactMatch(state)) { - KTextEditor::Range range(reSelection.cap(1).toInt(), reSelection.cap(2).toInt(), reSelection.cap(3).toInt(), reSelection.cap(4).toInt()); - setInitialRange(range); + if (!d->view) { + return; + } + d->view->readSessionConfig(config); +} + +void KDevelop::TextView::writeSessionConfig(KConfigGroup& config) +{ + if (!d->view) { + return; } + d->view->writeSessionConfig(config); } QString KDevelop::TextDocument::documentType() const { return QStringLiteral("Text"); } QIcon KDevelop::TextDocument::defaultIcon() const { if (d->document) { QMimeType mime = QMimeDatabase().mimeTypeForName(d->document->mimeType()); QIcon icon = QIcon::fromTheme(mime.iconName()); if (!icon.isNull()) { return icon; } } return PartDocument::defaultIcon(); } KTextEditor::View *KDevelop::TextView::textView() const { return d->view; } QString KDevelop::TextView::viewStatus() const { // only show status when KTextEditor's own status bar isn't already enabled const bool showStatus = !Core::self()->partControllerInternal()->showTextEditorStatusBar(); if (!showStatus) { return QString(); } const KTextEditor::Cursor pos = d->view ? d->view->cursorPosition() : KTextEditor::Cursor::invalid(); return i18n(" Line: %1 Col: %2 ", pos.line() + 1, pos.column() + 1); } void KDevelop::TextView::sendStatusChanged() { emit statusChanged(this); } KTextEditor::View* KDevelop::TextDocument::activeTextView() const { KTextEditor::View* fallback = nullptr; for (auto view : views()) { auto textView = qobject_cast(view)->textView(); if (!textView) { continue; } if (textView->hasFocus()) { return textView; } else if (textView->isVisible()) { fallback = textView; } else if (!fallback) { fallback = textView; } } return fallback; } void KDevelop::TextDocument::newDocumentStatus(KTextEditor::Document *document) { bool dirty = (d->state == IDocument::Dirty || d->state == IDocument::DirtyAndModified); d->setStatus(document, dirty); } void KDevelop::TextDocument::textChanged(KTextEditor::Document *document) { Q_UNUSED(document); notifyContentChanged(); } void KDevelop::TextDocument::populateContextMenu( KTextEditor::View* v, QMenu* menu ) { if (d->m_addedContextMenu) { foreach ( QAction* action, d->m_addedContextMenu->actions() ) { menu->removeAction(action); } delete d->m_addedContextMenu; } d->m_addedContextMenu = new QMenu(); EditorContext c(v, v->cursorPosition()); auto extensions = Core::self()->pluginController()->queryPluginsForContextMenuExtensions(&c, d->m_addedContextMenu); ContextMenuExtension::populateMenu(d->m_addedContextMenu, extensions); foreach ( QAction* action, d->m_addedContextMenu->actions() ) { menu->addAction(action); } } void KDevelop::TextDocument::repositoryCheckFinished(bool canRecreate) { if ( d->state != IDocument::Dirty && d->state != IDocument::DirtyAndModified ) { // document is not dirty for whatever reason, nothing to do. return; } if ( ! canRecreate ) { return; } KTextEditor::ModificationInterface* modIface = qobject_cast( d->document ); Q_ASSERT(modIface); // Ok, all safe, we can clean up the document. Close it if the file is gone, // and reload if it's still there. d->setStatus(d->document, false); modIface->setModifiedOnDisk(KTextEditor::ModificationInterface::OnDiskUnmodified); if ( QFile::exists(d->document->url().path()) ) { reload(); } else { close(KDevelop::IDocument::Discard); } } void KDevelop::TextDocument::slotDocumentLoaded() { if (d->m_loaded) return; // Tell the editor integrator first d->m_loaded = true; notifyLoaded(); } void KDevelop::TextDocument::documentSaved(KTextEditor::Document* document, bool saveAs) { Q_UNUSED(document); Q_UNUSED(saveAs); notifySaved(); notifyStateChanged(); } void KDevelop::TextDocument::documentUrlChanged(KTextEditor::Document* document) { Q_UNUSED(document); if (url() != d->document->url()) setUrl(d->document->url()); } #include "moc_textdocument.cpp" diff --git a/kdevplatform/shell/textdocument.h b/kdevplatform/shell/textdocument.h index 79af51da16..719e3c49ca 100644 --- a/kdevplatform/shell/textdocument.h +++ b/kdevplatform/shell/textdocument.h @@ -1,130 +1,131 @@ /*************************************************************************** * Copyright 2007 Alexander Dymo * * * * This program 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 program 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 General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #ifndef KDEVPLATFORM_TEXTDOCUMENT_H #define KDEVPLATFORM_TEXTDOCUMENT_H #include #include #include "partdocument.h" #include "shellexport.h" class QMenu; namespace KTextEditor { class View; } namespace KDevelop { /** Text document which represents KTextEditor documents. Usually Kate documents are represented by this class but TextDocument is not limited to Kate. Each conforming text editor will work. */ class KDEVPLATFORMSHELL_EXPORT TextDocument: public PartDocument { Q_OBJECT public: TextDocument(const QUrl &url, ICore*, const QString& encoding ); ~TextDocument() override; QWidget *createViewWidget(QWidget *parent = nullptr) override; KParts::Part *partForView(QWidget *view) const override; bool close(DocumentSaveMode mode = Default) override; bool save(DocumentSaveMode mode = Default) override; DocumentState state() const override; KTextEditor::Cursor cursorPosition() const override; void setCursorPosition(const KTextEditor::Cursor &cursor) override; KTextEditor::Range textSelection() const override; void setTextSelection(const KTextEditor::Range &range) override; QString text(const KTextEditor::Range &range) const override; QString textLine() const override; QString textWord() const override; bool isTextDocument() const override; KTextEditor::Document* textDocument() const override; QString documentType() const override; QIcon defaultIcon() const override; KTextEditor::View* activeTextView() const override; public Q_SLOTS: void reload() override; protected: Sublime::View *newView(Sublime::Document *doc) override; private: Q_PRIVATE_SLOT(d, void saveSessionConfig()) Q_PRIVATE_SLOT(d, void modifiedOnDisk(KTextEditor::Document *, bool, KTextEditor::ModificationInterface::ModifiedOnDiskReason)) void newDocumentStatus(KTextEditor::Document*); void populateContextMenu(KTextEditor::View*, QMenu*); void textChanged(KTextEditor::Document*); void documentUrlChanged(KTextEditor::Document*); void slotDocumentLoaded(); void documentSaved(KTextEditor::Document*,bool); void repositoryCheckFinished(bool); private: const QScopedPointer d; friend class TextDocumentPrivate; }; class KDEVPLATFORMSHELL_EXPORT TextView : public Sublime::View { Q_OBJECT public: explicit TextView(TextDocument* doc); ~TextView() override; QWidget *createWidget(QWidget *parent = nullptr) override; KTextEditor::View *textView() const; QString viewStatus() const override; - QString viewState() const override; - void setState(const QString& state) override; + + void readSessionConfig(KConfigGroup & config) override; + void writeSessionConfig(KConfigGroup & config) override; void setInitialRange(const KTextEditor::Range& range); KTextEditor::Range initialRange() const; private: void sendStatusChanged(); private: const QScopedPointer d; }; } #endif diff --git a/kdevplatform/shell/workingsets/workingset.cpp b/kdevplatform/shell/workingsets/workingset.cpp index 312dab9dac..5da25c5ed8 100644 --- a/kdevplatform/shell/workingsets/workingset.cpp +++ b/kdevplatform/shell/workingsets/workingset.cpp @@ -1,564 +1,565 @@ /* Copyright David Nolden This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "workingset.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #define SYNC_OFTEN using namespace KDevelop; bool WorkingSet::m_loading = false; namespace { QIcon generateIcon(const WorkingSetIconParameters& params) { QImage pixmap(16, 16, QImage::Format_ARGB32); // fill the background with a transparent color pixmap.fill(QColor::fromRgba(qRgba(0, 0, 0, 0))); const uint coloredCount = params.coloredCount; // coordinates of the rectangles to draw, for 16x16 icons specifically QList rects; rects << QRect(1, 1, 5, 5) << QRect(1, 9, 5, 5) << QRect(9, 1, 5, 5) << QRect(9, 9, 5, 5); if ( params.swapDiagonal ) { rects.swap(1, 2); } QPainter painter(&pixmap); // color for non-colored squares, paint them brighter if the working set is the active one const int inact = 40; QColor darkColor = QColor::fromRgb(inact, inact, inact); // color for colored squares // this code is not fragile, you can just tune the magic formulas at random and see what looks good. // just make sure to keep it within the 0-360 / 0-255 / 0-255 space of the HSV model QColor brightColor = QColor::fromHsv(params.hue, qMin(255, 215 + (params.setId*5) % 150), 205 + (params.setId*11) % 50); // Y'UV "Y" value, the approximate "lightness" of the color // If it is above 0.6, then making the color darker a bit is okay, // if it is below 0.35, then the color should be a bit brighter. float brightY = 0.299 * brightColor.redF() + 0.587 * brightColor.greenF() + 0.114 * brightColor.blueF(); if ( brightY > 0.6 ) { if ( params.setId % 7 < 2 ) { // 2/7 chance to make the color significantly darker brightColor = brightColor.darker(120 + (params.setId*7) % 35); } else if ( params.setId % 5 == 0 ) { // 1/5 chance to make it a bit darker brightColor = brightColor.darker(110 + (params.setId*3) % 10); } } if ( brightY < 0.35 ) { // always make the color brighter to avoid very dark colors (like rgb(0, 0, 255)) brightColor = brightColor.lighter(120 + (params.setId*13) % 55); } int at = 0; foreach ( const QRect rect, rects ) { QColor currentColor; // pick the colored squares; you can get different patterns by re-ordering the "rects" list if ( (at + params.setId*7) % 4 < coloredCount ) { currentColor = brightColor; } else { currentColor = darkColor; } // draw the filling of the square painter.setPen(QColor(currentColor)); painter.setBrush(QBrush(currentColor)); painter.drawRect(rect); // draw a slight set-in shadow for the square -- it's barely recognizeable, // but it looks way better than without painter.setBrush(Qt::NoBrush); painter.setPen(QColor(0, 0, 0, 50)); painter.drawRect(rect); painter.setPen(QColor(0, 0, 0, 25)); painter.drawRect(rect.x() + 1, rect.y() + 1, rect.width() - 2, rect.height() - 2); at += 1; } return QIcon(QPixmap::fromImage(pixmap)); } } WorkingSet::WorkingSet(const QString& id) : QObject() , m_id(id) , m_icon(generateIcon(WorkingSetIconParameters(id))) { } void WorkingSet::saveFromArea( Sublime::Area* a, Sublime::AreaIndex * area, KConfigGroup setGroup, KConfigGroup areaGroup ) { if (area->isSplit()) { setGroup.writeEntry("Orientation", area->orientation() == Qt::Horizontal ? "Horizontal" : "Vertical"); if (area->first()) { saveFromArea(a, area->first(), KConfigGroup(&setGroup, "0"), KConfigGroup(&areaGroup, "0")); } if (area->second()) { saveFromArea(a, area->second(), KConfigGroup(&setGroup, "1"), KConfigGroup(&areaGroup, "1")); } } else { setGroup.writeEntry("View Count", area->viewCount()); areaGroup.writeEntry("View Count", area->viewCount()); int index = 0; foreach (Sublime::View* view, area->views()) { //The working set config gets an updated list of files QString docSpec = view->document()->documentSpecifier(); //only save the documents from protocols KIO understands //otherwise we try to load kdev:// too early if (!KProtocolInfo::isKnownProtocol(QUrl(docSpec))) { continue; } setGroup.writeEntry(QStringLiteral("View %1").arg(index), docSpec); //The area specific config stores the working set documents in order along with their state areaGroup.writeEntry(QStringLiteral("View %1").arg(index), docSpec); - areaGroup.writeEntry(QStringLiteral("View %1 State").arg(index), view->viewState()); + KConfigGroup viewGroup(&areaGroup, QStringLiteral("View %1 Config").arg(index)); + view->writeSessionConfig(viewGroup); ++index; } } } bool WorkingSet::isEmpty() const { KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup group = setConfig.group(m_id); return !group.hasKey("Orientation") && group.readEntry("View Count", 0) == 0; } struct DisableMainWindowUpdatesFromArea { explicit DisableMainWindowUpdatesFromArea(Sublime::Area* area) : m_area(area) { if(area) { foreach(Sublime::MainWindow* window, Core::self()->uiControllerInternal()->mainWindows()) { if(window->area() == area) { if(window->updatesEnabled()) { wasUpdatesEnabled.insert(window); window->setUpdatesEnabled(false); } } } } } ~DisableMainWindowUpdatesFromArea() { if(m_area) { foreach(Sublime::MainWindow* window, wasUpdatesEnabled) { window->setUpdatesEnabled(wasUpdatesEnabled.contains(window)); } } } Sublime::Area* m_area; QSet wasUpdatesEnabled; }; void loadFileList(QStringList& ret, KConfigGroup group) { if (group.hasKey("Orientation")) { QStringList subgroups = group.groupList(); if (subgroups.contains(QStringLiteral("0"))) { { KConfigGroup subgroup(&group, "0"); loadFileList(ret, subgroup); } if (subgroups.contains(QStringLiteral("1"))) { KConfigGroup subgroup(&group, "1"); loadFileList(ret, subgroup); } } } else { int viewCount = group.readEntry("View Count", 0); for (int i = 0; i < viewCount; ++i) { QString specifier = group.readEntry(QStringLiteral("View %1").arg(i), QString()); ret << specifier; } } } QStringList WorkingSet::fileList() const { QStringList ret; KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup group = setConfig.group(m_id); loadFileList(ret, group); return ret; } void WorkingSet::loadToArea(Sublime::Area* area, Sublime::AreaIndex* areaIndex) { PushValue enableLoading(m_loading, true); /// We cannot disable the updates here, because (probably) due to a bug in Qt, /// which causes the updates to stay disabled forever after some complex operations /// on the sub-views. This could be reproduced by creating two working-sets with complex /// split-view configurations and switching between them. Re-enabling the updates doesn't help. // DisableMainWindowUpdatesFromArea updatesDisabler(area); qCDebug(SHELL) << "loading working-set" << m_id << "into area" << area; QMultiMap recycle; foreach( Sublime::View* view, area->views() ) recycle.insert( view->document()->documentSpecifier(), area->removeView(view) ); qCDebug(SHELL) << "recycling" << recycle.size() << "old views"; Q_ASSERT( area->views().empty() ); KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup setGroup = setConfig.group(m_id); KConfigGroup areaGroup = setConfig.group(m_id + QLatin1Char('|') + area->title()); loadToArea(area, areaIndex, setGroup, areaGroup, recycle); // Delete views which were not recycled qCDebug(SHELL) << "deleting " << recycle.size() << " old views"; qDeleteAll( recycle ); area->setActiveView(nullptr); //activate view in the working set /// @todo correctly select one out of multiple equal views QString activeView = areaGroup.readEntry("Active View", QString()); foreach (Sublime::View *v, area->views()) { if (v->document()->documentSpecifier() == activeView) { area->setActiveView(v); break; } } if( !area->activeView() && !area->views().isEmpty() ) area->setActiveView( area->views().at(0) ); if( area->activeView() ) { foreach(Sublime::MainWindow* window, Core::self()->uiControllerInternal()->mainWindows()) { if(window->area() == area) { window->activateView( area->activeView() ); } } } } void WorkingSet::loadToArea(Sublime::Area* area, Sublime::AreaIndex* areaIndex, KConfigGroup setGroup, KConfigGroup areaGroup, QMultiMap& recycle) { Q_ASSERT( !areaIndex->isSplit() ); if (setGroup.hasKey("Orientation")) { QStringList subgroups = setGroup.groupList(); /// @todo also save and restore the ratio if (subgroups.contains(QStringLiteral("0")) && subgroups.contains(QStringLiteral("1"))) { // qCDebug(SHELL) << "has zero, split:" << split; Qt::Orientation orientation = setGroup.readEntry("Orientation", "Horizontal") == QLatin1String("Vertical") ? Qt::Vertical : Qt::Horizontal; if(!areaIndex->isSplit()){ areaIndex->split(orientation); }else{ areaIndex->setOrientation(orientation); } loadToArea(area, areaIndex->first(), KConfigGroup(&setGroup, "0"), KConfigGroup(&areaGroup, "0"), recycle); loadToArea(area, areaIndex->second(), KConfigGroup(&setGroup, "1"), KConfigGroup(&areaGroup, "1"), recycle); if( areaIndex->first()->viewCount() == 0 ) areaIndex->unsplit(areaIndex->first()); else if( areaIndex->second()->viewCount() == 0 ) areaIndex->unsplit(areaIndex->second()); } } else { //Load all documents from the workingset into this areaIndex int viewCount = setGroup.readEntry("View Count", 0); QMap createdViews; for (int i = 0; i < viewCount; ++i) { QString specifier = setGroup.readEntry(QStringLiteral("View %1").arg(i), QString()); if (specifier.isEmpty()) { continue; } Sublime::View* previousView = area->views().empty() ? nullptr : area->views().at(area->views().size() - 1); QMultiMap::iterator it = recycle.find( specifier ); if( it != recycle.end() ) { area->addView( *it, areaIndex, previousView ); recycle.erase( it ); continue; } IDocument* doc = Core::self()->documentControllerInternal()->openDocument(QUrl::fromUserInput(specifier), KTextEditor::Cursor::invalid(), IDocumentController::DoNotActivate | IDocumentController::DoNotCreateView); Sublime::Document *document = dynamic_cast(doc); if (document) { Sublime::View* view = document->createView(); area->addView(view, areaIndex, previousView); createdViews[i] = view; } else { qCWarning(SHELL) << "Unable to create view" << specifier; } } //Load state for (int i = 0; i < viewCount; ++i) { - QString state = areaGroup.readEntry(QStringLiteral("View %1 State").arg(i)); - if (!state.isEmpty() && createdViews.contains(i)) - createdViews[i]->setState(state); + KConfigGroup viewGroup(&areaGroup, QStringLiteral("View %1 Config").arg(i)); + if (viewGroup.exists() && createdViews.contains(i)) + createdViews[i]->readSessionConfig(viewGroup); } } } void deleteGroupRecursive(KConfigGroup group) { // qCDebug(SHELL) << "deleting" << group.name(); foreach(const QString& entry, group.entryMap().keys()) { group.deleteEntry(entry); } Q_ASSERT(group.entryMap().isEmpty()); foreach(const QString& subGroup, group.groupList()) { deleteGroupRecursive(group.group(subGroup)); group.deleteGroup(subGroup); } //Why doesn't this work? // Q_ASSERT(group.groupList().isEmpty()); group.deleteGroup(); } void WorkingSet::deleteSet(bool force, bool silent) { if(m_areas.isEmpty() || force) { emit aboutToRemove(this); KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup group = setConfig.group(m_id); deleteGroupRecursive(group); #ifdef SYNC_OFTEN setConfig.sync(); #endif if(!silent) emit setChangedSignificantly(); } } void WorkingSet::saveFromArea(Sublime::Area* area, Sublime::AreaIndex* areaIndex) { qCDebug(SHELL) << "saving" << m_id << "from area"; bool wasPersistent = isPersistent(); KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup setGroup = setConfig.group(m_id); deleteGroupRecursive(setGroup); KConfigGroup areaGroup = setConfig.group(m_id + QLatin1Char('|') + area->title()); QString lastActiveView = areaGroup.readEntry("Active View", ""); deleteGroupRecursive(areaGroup); if (area->activeView() && area->activeView()->document()) areaGroup.writeEntry("Active View", area->activeView()->document()->documentSpecifier()); else areaGroup.writeEntry("Active View", lastActiveView); saveFromArea(area, areaIndex, setGroup, areaGroup); if(isEmpty()) { deleteGroupRecursive(setGroup); deleteGroupRecursive(areaGroup); } setPersistent(wasPersistent); #ifdef SYNC_OFTEN setConfig.sync(); #endif emit setChangedSignificantly(); } void WorkingSet::areaViewAdded(Sublime::AreaIndex*, Sublime::View*) { Sublime::Area* area = qobject_cast(sender()); Q_ASSERT(area); Q_ASSERT(area->workingSet() == m_id); qCDebug(SHELL) << "added view in" << area << ", id" << m_id; if (m_loading) { qCDebug(SHELL) << "doing nothing because loading"; return; } changed(area); } void WorkingSet::areaViewRemoved(Sublime::AreaIndex*, Sublime::View* view) { Sublime::Area* area = qobject_cast(sender()); Q_ASSERT(area); Q_ASSERT(area->workingSet() == m_id); qCDebug(SHELL) << "removed view in" << area << ", id" << m_id; if (m_loading) { qCDebug(SHELL) << "doing nothing because loading"; return; } foreach(Sublime::Area* otherArea, m_areas) { if(otherArea == area) continue; bool hadDocument = false; foreach(Sublime::View* areaView, otherArea->views()) if(view->document() == areaView->document()) hadDocument = true; if(!hadDocument) { // We do this to prevent UI flicker. The view has already been removed from // one of the connected areas, so the working-set has already recorded the change. return; } } changed(area); } void WorkingSet::setPersistent(bool persistent) { KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup group = setConfig.group(m_id); group.writeEntry("persistent", persistent); #ifdef SYNC_OFTEN group.sync(); #endif qCDebug(SHELL) << "setting" << m_id << "persistent:" << persistent; } bool WorkingSet::isPersistent() const { KConfigGroup setConfig(Core::self()->activeSession()->config(), "Working File Sets"); KConfigGroup group = setConfig.group(m_id); return group.readEntry("persistent", false); } QIcon WorkingSet::icon() const { return m_icon; } bool WorkingSet::isConnected( Sublime::Area* area ) { return m_areas.contains( area ); } QString WorkingSet::id() const { return m_id; } bool WorkingSet::hasConnectedAreas() const { return !m_areas.isEmpty(); } bool WorkingSet::hasConnectedAreas( QList< Sublime::Area* > areas ) const { foreach( Sublime::Area* area, areas ) if ( m_areas.contains( area ) ) return true; return false; } void WorkingSet::connectArea( Sublime::Area* area ) { if ( m_areas.contains( area ) ) { qCDebug(SHELL) << "tried to double-connect area"; return; } qCDebug(SHELL) << "connecting" << m_id << "to area" << area; // Q_ASSERT(area->workingSet() == m_id); m_areas.push_back( area ); connect( area, &Sublime::Area::viewAdded, this, &WorkingSet::areaViewAdded ); connect( area, &Sublime::Area::viewRemoved, this, &WorkingSet::areaViewRemoved ); } void WorkingSet::disconnectArea( Sublime::Area* area ) { if ( !m_areas.contains( area ) ) { qCDebug(SHELL) << "tried to disconnect not connected area"; return; } qCDebug(SHELL) << "disconnecting" << m_id << "from area" << area; // Q_ASSERT(area->workingSet() == m_id); disconnect( area, &Sublime::Area::viewAdded, this, &WorkingSet::areaViewAdded ); disconnect( area, &Sublime::Area::viewRemoved, this, &WorkingSet::areaViewRemoved ); m_areas.removeAll( area ); } void WorkingSet::changed( Sublime::Area* area ) { if ( m_loading ) { return; } { //Do not capture changes done while loading PushValue enableLoading( m_loading, true ); qCDebug(SHELL) << "recording change done to" << m_id; saveFromArea( area, area->rootIndex() ); for (auto it = m_areas.begin(); it != m_areas.end(); ++it ) { if (( *it ) != area ) { loadToArea(( *it ), ( *it )->rootIndex() ); } } } emit setChangedSignificantly(); } diff --git a/kdevplatform/sublime/view.cpp b/kdevplatform/sublime/view.cpp index b054aef206..05590abfb9 100644 --- a/kdevplatform/sublime/view.cpp +++ b/kdevplatform/sublime/view.cpp @@ -1,139 +1,139 @@ /*************************************************************************** * Copyright 2006-2007 Alexander Dymo * * * * This program 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 program 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 General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include "view.h" #include #include "document.h" #include "tooldocument.h" namespace Sublime { class View; class Document; class ViewPrivate { public: ViewPrivate(); Document *doc; QWidget *widget; void unsetWidget(); View::WidgetOwnership ws; }; ViewPrivate::ViewPrivate() :doc(nullptr), widget(nullptr) { } void ViewPrivate::unsetWidget() { widget = nullptr; } View::View(Document *doc, WidgetOwnership ws ) :QObject(doc), d(new ViewPrivate) { d->doc = doc; d->ws = ws; } View::~View() { if (d->widget && d->ws == View::TakeOwnership ) { d->widget->hide(); d->widget->setParent(nullptr); d->widget->deleteLater(); } } Document *View::document() const { return d->doc; } QWidget *View::widget(QWidget *parent) { if (!d->widget) { d->widget = createWidget(parent); connect(d->widget, &QWidget::destroyed, this, [&] { d->unsetWidget(); }); } return d->widget; } QWidget *View::createWidget(QWidget *parent) { return d->doc->createViewWidget(parent); } bool View::hasWidget() const { return d->widget != nullptr; } void View::requestRaise() { emit raise(this); } -QString View::viewState() const +void View::readSessionConfig(KConfigGroup& config) { - return QString(); + Q_UNUSED(config); } -void View::setState(const QString & state) +void View::writeSessionConfig(KConfigGroup& config) { - Q_UNUSED(state); + Q_UNUSED(config); } QList View::toolBarActions() const { ToolDocument* tooldoc = dynamic_cast( document() ); if( tooldoc ) { return tooldoc->factory()->toolBarActions( d->widget ); } return QList(); } QList< QAction* > View::contextMenuActions() const { ToolDocument* tooldoc = dynamic_cast( document() ); if( tooldoc ) { return tooldoc->factory()->contextMenuActions( d->widget ); } return QList(); } QString View::viewStatus() const { return QString(); } void View::notifyPositionChanged(int newPositionInArea) { emit positionChanged(this, newPositionInArea); } } #include "moc_view.cpp" diff --git a/kdevplatform/sublime/view.h b/kdevplatform/sublime/view.h index 87677f8864..f2fb084fbd 100644 --- a/kdevplatform/sublime/view.h +++ b/kdevplatform/sublime/view.h @@ -1,109 +1,123 @@ /*************************************************************************** * Copyright 2006-2007 Alexander Dymo * * * * This program 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 program 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 General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #ifndef KDEVPLATFORM_SUBLIMEVIEW_H #define KDEVPLATFORM_SUBLIMEVIEW_H #include #include #include "sublimeexport.h" +class KConfigGroup; + class QAction; namespace Sublime { class Document; /** @short View - the wrapper to the widget that knows about its document Views are the convenient way to manage a widget. It is specifically designed to be light and fast. Use @ref Document::createView() to get the new view for the document and call @ref View::widget() to create and get the actual widget. It is not possible to create a view by hand. You need either subclass it or use a Document. If you create a subclass of View you need to override Sublime::View::createWidget to provide a custom widget for your view. */ class KDEVPLATFORMSUBLIME_EXPORT View: public QObject { Q_OBJECT public: enum WidgetOwnership { TakeOwnership, DoNotTakeOwnerShip }; ~View() override; /**@return the toolbar actions for this view, this needs to be called _after_ the first call to widget() */ QList toolBarActions() const; /**@return the toolbar actions for this view, this needs to be called _after_ the first call to widget() */ QList contextMenuActions() const; /**@return the document for this view.*/ Document *document() const; /**@return widget for this view (creates it if it's not yet created).*/ QWidget *widget(QWidget *parent = nullptr); /**@return true if this view has an initialized widget.*/ bool hasWidget() const; /// Retrieve information to be placed in the status bar. virtual QString viewStatus() const; - /// Retrieve view state for saving into configuration. - virtual QString viewState() const; - /// Restore view state from configuration - virtual void setState(const QString& state); + /** + * Read session settings from the given \p config. + * + * The default implementation is a no-op + * + * @see KTextEditor::View::readSessionConfig() + */ + virtual void readSessionConfig(KConfigGroup &config); + /** + * Write session settings to the \p config. + * + * The default implementation is a no-op + * + * @see KTextEditor::View::writeSessionConfig() + */ + virtual void writeSessionConfig(KConfigGroup &config); void notifyPositionChanged(int newPositionInArea); Q_SIGNALS: void raise(Sublime::View*); /// Notify that the status for this document has changed void statusChanged(Sublime::View*); void positionChanged(Sublime::View*, int); public Q_SLOTS: void requestRaise(); protected: explicit View(Document *doc, WidgetOwnership ws = DoNotTakeOwnerShip ); /** * override this function to create a custom widget in your View subclass * @param parent the parent widget * @returns a new widget which is used for this view */ virtual QWidget *createWidget(QWidget *parent); private: //copy is not allowed, create a new view from the document instead View(const View &v); const QScopedPointer d; friend class Document; }; } Q_DECLARE_METATYPE(Sublime::View*) #endif