mirror of
https://bitbucket.org/smil3y/kde-extraapps.git
synced 2025-02-25 03:12:53 +00:00
916 lines
29 KiB
C++
916 lines
29 KiB
C++
/***************************************************************************
|
|
* Copyright 2007 Alexander Dymo <adymo@kdevelop.org> *
|
|
* *
|
|
* 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 <QFile>
|
|
#include <QPointer>
|
|
#include <QTextCodec>
|
|
#include <QMenu>
|
|
#include <QAction>
|
|
#include <QVBoxLayout>
|
|
#include <QWidget>
|
|
#include <QLabel>
|
|
#include <QLayout>
|
|
|
|
#include <klocale.h>
|
|
#include <kmessagebox.h>
|
|
#include <kconfiggroup.h>
|
|
#include <kstandarddirs.h>
|
|
#include <kxmlguifactory.h>
|
|
#include <kdeversion.h>
|
|
|
|
#include <ktexteditor/view.h>
|
|
#include <ktexteditor/document.h>
|
|
#include <ktexteditor/modificationinterface.h>
|
|
#include <ktexteditor/codecompletioninterface.h>
|
|
#include <ktexteditor/markinterface.h>
|
|
#include <ktexteditor/configinterface.h>
|
|
#include <ktexteditor/sessionconfiginterface.h>
|
|
|
|
#include <interfaces/context.h>
|
|
#include <interfaces/contextmenuextension.h>
|
|
#include <interfaces/ilanguagecontroller.h>
|
|
#include <interfaces/icompletionsettings.h>
|
|
#include <interfaces/iprojectcontroller.h>
|
|
#include <interfaces/iproject.h>
|
|
|
|
#include <vcs/interfaces/icontentawareversioncontrol.h>
|
|
|
|
#include <language/interfaces/editorcontext.h>
|
|
|
|
#include <project/projectutils.h>
|
|
|
|
#include "core.h"
|
|
#include "mainwindow.h"
|
|
#include "uicontroller.h"
|
|
#include "partcontroller.h"
|
|
#include "plugincontroller.h"
|
|
#include "documentcontroller.h"
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
struct TextDocumentPrivate {
|
|
TextDocumentPrivate(TextDocument *textDocument)
|
|
: encoding(""), m_textDocument(textDocument)
|
|
, m_loaded(false), m_addedContextMenu(0)
|
|
{
|
|
document = 0;
|
|
state = IDocument::Clean;
|
|
}
|
|
|
|
~TextDocumentPrivate()
|
|
{
|
|
if (m_addedContextMenu) {
|
|
delete m_addedContextMenu;
|
|
m_addedContextMenu = 0;
|
|
}
|
|
if (document) {
|
|
saveSessionConfig();
|
|
delete document;
|
|
}
|
|
}
|
|
|
|
QPointer<KTextEditor::Document> document;
|
|
IDocument::DocumentState state;
|
|
QString encoding;
|
|
|
|
void newDocumentStatus(KTextEditor::Document *document)
|
|
{
|
|
bool dirty = (state == IDocument::Dirty || state == IDocument::DirtyAndModified);
|
|
|
|
setStatus(document, dirty);
|
|
}
|
|
|
|
void textChanged(KTextEditor::Document *document)
|
|
{
|
|
Q_UNUSED(document);
|
|
m_textDocument->notifyContentChanged();
|
|
}
|
|
|
|
void populateContextMenu( KTextEditor::View* v, QMenu* menu )
|
|
{
|
|
if (m_addedContextMenu) {
|
|
foreach ( QAction* action, m_addedContextMenu->actions() ) {
|
|
menu->removeAction(action);
|
|
}
|
|
delete m_addedContextMenu;
|
|
}
|
|
|
|
m_addedContextMenu = new QMenu();
|
|
|
|
Context* c = new EditorContext( v, v->cursorPosition() );
|
|
QList<ContextMenuExtension> extensions = Core::self()->pluginController()->queryPluginsForContextMenuExtensions( c );
|
|
|
|
ContextMenuExtension::populateMenu(m_addedContextMenu, extensions);
|
|
|
|
{
|
|
KUrl url = v->document()->url();
|
|
IProject* project = Core::self()->projectController()->findProjectForUrl( url );
|
|
if(project)
|
|
{
|
|
QList< ProjectBaseItem* > items = project->itemsForUrl( url );
|
|
if(!items.isEmpty())
|
|
populateParentItemsMenu( items.front(), m_addedContextMenu );
|
|
}
|
|
}
|
|
|
|
foreach ( QAction* action, m_addedContextMenu->actions() ) {
|
|
menu->addAction(action);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// 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 = 0;
|
|
// 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.
|
|
foreach ( KDevelop::IProject* current, Core::self()->projectController()->projects() ) {
|
|
if ( current->folder().isParentOf(document->url()) ) {
|
|
project = current;
|
|
break;
|
|
}
|
|
}
|
|
if (!project) {
|
|
return;
|
|
}
|
|
IContentAwareVersionControl* iface;
|
|
iface = qobject_cast< KDevelop::IContentAwareVersionControl* >(project->versionControlPlugin());
|
|
if (!iface) {
|
|
return;
|
|
}
|
|
if ( !qobject_cast<KTextEditor::ModificationInterface*>( document ) ) {
|
|
return;
|
|
}
|
|
|
|
CheckInRepositoryJob* req = iface->isInRepository( document );
|
|
if ( !req ) {
|
|
return;
|
|
}
|
|
QObject::connect(req, SIGNAL(finished(bool)),
|
|
m_textDocument, SLOT(repositoryCheckFinished(bool)));
|
|
// Abort the request when the user edits the document
|
|
QObject::connect(m_textDocument->textDocument(), SIGNAL(textChanged(KTextEditor::Document*)),
|
|
req, SLOT(abort()));
|
|
}
|
|
|
|
void repositoryCheckFinished(bool canRecreate) {
|
|
if ( state != IDocument::Dirty && state != IDocument::DirtyAndModified ) {
|
|
// document is not dirty for whatever reason, nothing to do.
|
|
return;
|
|
}
|
|
if ( ! canRecreate ) {
|
|
return;
|
|
}
|
|
KTextEditor::ModificationInterface* modIface = qobject_cast<KTextEditor::ModificationInterface*>( 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.
|
|
setStatus(document, false);
|
|
modIface->setModifiedOnDisk(KTextEditor::ModificationInterface::OnDiskUnmodified);
|
|
if ( QFile::exists(document->url().path()) ) {
|
|
m_textDocument->reload();
|
|
} else {
|
|
m_textDocument->close(KDevelop::IDocument::Discard);
|
|
}
|
|
}
|
|
|
|
void setStatus(KTextEditor::Document* document, bool dirty)
|
|
{
|
|
QIcon statusIcon;
|
|
|
|
if (document->isModified())
|
|
if (dirty) {
|
|
state = IDocument::DirtyAndModified;
|
|
statusIcon = KIcon("edit-delete");
|
|
} else {
|
|
state = IDocument::Modified;
|
|
statusIcon = KIcon("document-save");
|
|
}
|
|
else
|
|
if (dirty) {
|
|
state = IDocument::Dirty;
|
|
statusIcon = KIcon("document-revert");
|
|
} else {
|
|
state = IDocument::Clean;
|
|
}
|
|
|
|
m_textDocument->notifyStateChanged();
|
|
Core::self()->uiControllerInternal()->setStatusIcon(m_textDocument, statusIcon);
|
|
}
|
|
|
|
void documentUrlChanged(KTextEditor::Document* document)
|
|
{
|
|
if (m_textDocument->url() != document->url())
|
|
m_textDocument->setUrl(document->url());
|
|
}
|
|
|
|
void slotDocumentLoaded()
|
|
{
|
|
if (m_loaded)
|
|
return;
|
|
// Tell the editor integrator first
|
|
m_loaded = true;
|
|
m_textDocument->notifyLoaded();
|
|
}
|
|
|
|
void documentSaved(KTextEditor::Document* document, bool saveAs)
|
|
{
|
|
Q_UNUSED(document);
|
|
Q_UNUSED(saveAs);
|
|
m_textDocument->notifySaved();
|
|
m_textDocument->notifyStateChanged();
|
|
}
|
|
|
|
inline KConfigGroup katePartSettingsGroup() const
|
|
{
|
|
return KGlobal::config()->group("KatePart Settings");
|
|
}
|
|
|
|
inline QString docConfigGroupName() const
|
|
{
|
|
return document->url().pathOrUrl();
|
|
}
|
|
|
|
inline KConfigGroup docConfigGroup() const
|
|
{
|
|
return katePartSettingsGroup().group(docConfigGroupName());
|
|
}
|
|
|
|
void saveSessionConfig()
|
|
{
|
|
if(!document->url().isValid()) {
|
|
return;
|
|
}
|
|
if (KTextEditor::ParameterizedSessionConfigInterface *sessionConfigIface =
|
|
qobject_cast<KTextEditor::ParameterizedSessionConfigInterface*>(document))
|
|
{
|
|
// 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();
|
|
sessionConfigIface->writeParameterizedSessionConfig(group,
|
|
KTextEditor::ParameterizedSessionConfigInterface::SkipUrl);
|
|
}
|
|
}
|
|
|
|
void loadSessionConfig()
|
|
{
|
|
if (!document || !katePartSettingsGroup().hasGroup(docConfigGroupName())) {
|
|
return;
|
|
}
|
|
if (KTextEditor::ParameterizedSessionConfigInterface *sessionConfigIface =
|
|
qobject_cast<KTextEditor::ParameterizedSessionConfigInterface*>(document))
|
|
{
|
|
sessionConfigIface->readParameterizedSessionConfig(docConfigGroup(),
|
|
KTextEditor::ParameterizedSessionConfigInterface::SkipUrl);
|
|
}
|
|
}
|
|
|
|
|
|
private:
|
|
TextDocument *m_textDocument;
|
|
bool m_loaded;
|
|
// we want to remove the added stuff when the menu hides
|
|
QMenu* m_addedContextMenu;
|
|
};
|
|
|
|
class TextViewPrivate
|
|
{
|
|
public:
|
|
TextViewPrivate() : editor(0) {}
|
|
TextEditorWidget* editor;
|
|
KTextEditor::Range initialRange;
|
|
};
|
|
|
|
class TextEditorWidgetPrivate
|
|
{
|
|
public:
|
|
TextEditorWidgetPrivate()
|
|
{
|
|
widget = 0;
|
|
widgetLayout = 0;
|
|
view = 0;
|
|
statusLabel = 0;
|
|
}
|
|
|
|
void viewEditModeChanged(KTextEditor::View* view, KTextEditor::View::EditMode mode)
|
|
{
|
|
#ifdef KTEXTEDITOR_HAS_VIMODE
|
|
if ( ! statusLabel ) {
|
|
statusLabel = new QLabel();
|
|
view->layout()->addWidget(statusLabel);
|
|
}
|
|
if ( mode == KTextEditor::View::EditViMode ) {
|
|
statusLabel->setText(view->viewMode() + " <i>" + view->document()->url().fileName() + "</i>");
|
|
statusLabel->setHidden(false);
|
|
}
|
|
else {
|
|
statusLabel->setHidden(true);
|
|
}
|
|
#else
|
|
Q_UNUSED(view);
|
|
Q_UNUSED(mode);
|
|
#endif
|
|
}
|
|
QWidget* widget;
|
|
QVBoxLayout* widgetLayout;
|
|
QPointer<KTextEditor::View> view;
|
|
QLabel* statusLabel;
|
|
QString status;
|
|
const TextView* textView;
|
|
|
|
};
|
|
|
|
TextDocument::TextDocument(const KUrl &url, ICore* core, const QString& encoding)
|
|
:PartDocument(url, core), d(new TextDocumentPrivate(this))
|
|
{
|
|
d->encoding = encoding;
|
|
}
|
|
|
|
TextDocument::~TextDocument()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
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
|
|
qWarning() << "Broken text-document: " << url();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
KTextEditor::Document *TextDocument::textDocument() const
|
|
{
|
|
return d->document;
|
|
}
|
|
|
|
QWidget *TextDocument::createViewWidget(QWidget *parent)
|
|
{
|
|
KTextEditor::View* view = 0L;
|
|
|
|
if (!d->document)
|
|
{
|
|
d->document = Core::self()->partControllerInternal()->createTextPart(Core::self()->documentController()->encoding());
|
|
|
|
// Connect to the first text changed signal, it occurs before the completed() signal
|
|
connect(d->document, SIGNAL(textChanged(KTextEditor::Document*)), this, SLOT(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, SIGNAL(completed()), this, SLOT(slotDocumentLoaded()));
|
|
|
|
// 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, SIGNAL(modifiedChanged(KTextEditor::Document*)),
|
|
this, SLOT(newDocumentStatus(KTextEditor::Document*)));
|
|
connect(d->document, SIGNAL(textChanged(KTextEditor::Document*)),
|
|
this, SLOT(textChanged(KTextEditor::Document*)));
|
|
connect(d->document, SIGNAL(documentUrlChanged(KTextEditor::Document*)),
|
|
this, SLOT(documentUrlChanged(KTextEditor::Document*)));
|
|
connect(d->document, SIGNAL(documentSavedOrUploaded(KTextEditor::Document*,bool)),
|
|
this, SLOT(documentSaved(KTextEditor::Document*,bool)));
|
|
connect(d->document, SIGNAL(marksChanged(KTextEditor::Document*)),
|
|
this, SLOT(saveSessionConfig()));
|
|
|
|
KTextEditor::ModificationInterface *iface = qobject_cast<KTextEditor::ModificationInterface*>(d->document);
|
|
if (iface)
|
|
{
|
|
iface->setModifiedOnDiskWarning(true);
|
|
connect(d->document, SIGNAL(modifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason)),
|
|
this, SLOT(modifiedOnDisk(KTextEditor::Document*,bool,KTextEditor::ModificationInterface::ModifiedOnDiskReason)));
|
|
}
|
|
|
|
notifyTextDocumentCreated();
|
|
}
|
|
|
|
view = d->document->createView(parent);
|
|
|
|
if (view) {
|
|
connect(view, SIGNAL(contextMenuAboutToShow(KTextEditor::View*,QMenu*)), this, SLOT(populateContextMenu(KTextEditor::View*,QMenu*)));
|
|
|
|
//in KDE >= 4.4 we can use KXMLGuiClient::replaceXMLFile to provide
|
|
//katepart with our own restructured UI configuration
|
|
const QString uiFile = KGlobal::mainComponent().componentName() + "/katepartui.rc";
|
|
QStringList katePartUIs = KGlobal::mainComponent().dirs()->findAllResources("data", uiFile);
|
|
if (!katePartUIs.isEmpty()) {
|
|
const QString katePartUI = katePartUIs.last();
|
|
const QString katePartLocalUI = KStandardDirs::locateLocal("data", uiFile);
|
|
if (!QFile::exists(katePartLocalUI)) {
|
|
// prevent warning:
|
|
// kdevelop/kdeui (kdelibs): No such XML file ".../.kde/share/apps/kdevelop/katepartui.rc"
|
|
QFile::copy(katePartUI, katePartLocalUI);
|
|
}
|
|
view->replaceXMLFile(katePartUI, katePartLocalUI);
|
|
}
|
|
}
|
|
|
|
if (KTextEditor::CodeCompletionInterface* cc = dynamic_cast<KTextEditor::CodeCompletionInterface*>(view))
|
|
cc->setAutomaticInvocationEnabled(core()->languageController()->completionSettings()->automaticCompletionEnabled());
|
|
|
|
if (KTextEditor::ConfigInterface *config = qobject_cast<KTextEditor::ConfigInterface*>(view)) {
|
|
config->setConfigValue("allow-mark-menu", false);
|
|
config->setConfigValue("default-mark-type", KTextEditor::MarkInterface::BreakpointActive);
|
|
}
|
|
|
|
return view;
|
|
}
|
|
|
|
KParts::Part *TextDocument::partForView(QWidget *view) const
|
|
{
|
|
if (d->document && d->document->views().contains((KTextEditor::View*)view))
|
|
return d->document;
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
// KDevelop::IDocument implementation
|
|
|
|
void TextDocument::reload()
|
|
{
|
|
if (!d->document)
|
|
return;
|
|
|
|
KTextEditor::ModificationInterface* modif=0;
|
|
if(d->state==Dirty) {
|
|
modif = qobject_cast<KTextEditor::ModificationInterface*>(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;
|
|
if (!(mode & Silent))
|
|
{
|
|
int code = KMessageBox::warningYesNoCancel(
|
|
Core::self()->uiController()->activeMainWindow(),
|
|
i18n("The document \"%1\" has unsaved changes. Would you like to save them?", d->document->url().toLocalFile()),
|
|
i18nc("@title:window", "Close Document"));
|
|
if (code != KMessageBox::Yes)
|
|
return false;
|
|
}
|
|
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;
|
|
}
|
|
|
|
KUrl 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 = d->document->activeView();
|
|
|
|
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 = d->document->activeView();
|
|
|
|
// 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 = d->document->activeView();
|
|
|
|
if (view && view->selection()) {
|
|
return view->selectionRange();
|
|
}
|
|
|
|
return PartDocument::textSelection();
|
|
}
|
|
|
|
QString TextDocument::textLine() const
|
|
{
|
|
if (!d->document) {
|
|
return QString();
|
|
}
|
|
|
|
KTextEditor::View *view = d->document->activeView();
|
|
|
|
if (view) {
|
|
return d->document->line( view->cursorPosition().line() );
|
|
}
|
|
|
|
return PartDocument::textLine();
|
|
}
|
|
|
|
QString TextDocument::textWord() const
|
|
{
|
|
if (!d->document) {
|
|
return QString();
|
|
}
|
|
|
|
KTextEditor::View *view = d->document->activeView();
|
|
|
|
if (view) {
|
|
KTextEditor::Cursor start = view->cursorPosition();
|
|
kDebug() << "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] == '_' || linestr[startPos] == '~' ) )
|
|
{
|
|
--startPos;
|
|
}
|
|
|
|
while( endPos < linestr.length() && ( linestr[endPos].isLetterOrNumber() || linestr[endPos] == '_' || linestr[endPos] == '~' ) )
|
|
{
|
|
++endPos;
|
|
}
|
|
if( startPos != endPos )
|
|
{
|
|
kDebug() << "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 = d->document->activeView();
|
|
|
|
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);
|
|
|
|
emit viewNumberChanged(this);
|
|
return new TextView(this);
|
|
}
|
|
|
|
}
|
|
|
|
KDevelop::TextView::TextView(TextDocument * doc)
|
|
: View(doc, View::TakeOwnership), d( new TextViewPrivate )
|
|
{
|
|
}
|
|
|
|
KDevelop::TextView::~TextView()
|
|
{
|
|
delete d->editor; //We have to delete the view right now, to prevent random crashes in the event handler
|
|
d->editor = 0;
|
|
delete d;
|
|
}
|
|
|
|
QWidget * KDevelop::TextView::createWidget(QWidget * parent)
|
|
{
|
|
TextEditorWidget* teWidget = new TextEditorWidget(this, parent);
|
|
connect(teWidget, SIGNAL(statusChanged()), this, SLOT(sendStatusChanged()));
|
|
|
|
d->editor = teWidget;
|
|
connect(d->editor, SIGNAL(destroyed(QObject*)), this, SLOT(editorDestroyed(QObject*)));
|
|
|
|
return teWidget;
|
|
}
|
|
|
|
void KDevelop::TextView::editorDestroyed(QObject* obj) {
|
|
if(obj == d->editor)
|
|
d->editor = 0;
|
|
}
|
|
|
|
QString KDevelop::TextView::viewState() const
|
|
{
|
|
if( d->editor && d->editor->editorView() )
|
|
{
|
|
if (d->editor->editorView()->selection()) {
|
|
KTextEditor::Range selection = d->editor->editorView()->selectionRange();
|
|
return QString("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->editor->editorView()->cursorPosition();
|
|
return QString("Cursor=%1,%2").arg(cursor.line()).arg(cursor.column());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
kDebug() << "TextView's internal KTE view disappeared!";
|
|
return QString();
|
|
}
|
|
}
|
|
|
|
void KDevelop::TextView::setInitialRange(const KTextEditor::Range& range)
|
|
{
|
|
if(d->editor && d->editor->isInitialized()) {
|
|
selectAndReveal(d->editor->editorView(), range);
|
|
} else {
|
|
d->initialRange = range;
|
|
}
|
|
}
|
|
|
|
KTextEditor::Range KDevelop::TextView::initialRange() const
|
|
{
|
|
return d->initialRange;
|
|
}
|
|
|
|
void KDevelop::TextView::setState(const QString & state)
|
|
{
|
|
static QRegExp reCursor("Cursor=([\\d]+),([\\d]+)");
|
|
static QRegExp reSelection("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);
|
|
}
|
|
}
|
|
|
|
QString KDevelop::TextDocument::documentType() const
|
|
{
|
|
return "Text";
|
|
}
|
|
|
|
QIcon KDevelop::TextDocument::defaultIcon() const
|
|
{
|
|
if (d->document) {
|
|
KMimeType::Ptr mime = KMimeType::mimeType(d->document->mimeType());
|
|
KIcon icon(mime->iconName());
|
|
if (!icon.isNull()) {
|
|
return icon;
|
|
}
|
|
}
|
|
return PartDocument::defaultIcon();
|
|
}
|
|
|
|
KTextEditor::View *KDevelop::TextView::textView() const
|
|
{
|
|
if (d->editor)
|
|
return d->editor->editorView();
|
|
|
|
return 0;
|
|
}
|
|
|
|
QString KDevelop::TextView::viewStatus() const
|
|
{
|
|
if (d->editor)
|
|
return d->editor->status();
|
|
|
|
return QString();
|
|
}
|
|
|
|
void KDevelop::TextView::sendStatusChanged()
|
|
{
|
|
emit statusChanged(this);
|
|
}
|
|
|
|
KDevelop::TextEditorWidget::TextEditorWidget(const TextView* view, QWidget* parent)
|
|
: QWidget(parent), KXMLGUIClient(), d(new TextEditorWidgetPrivate)
|
|
{
|
|
d->widgetLayout = new QVBoxLayout(this);
|
|
d->widgetLayout->setMargin(0);
|
|
d->widgetLayout->setSpacing(0);
|
|
d->textView = view;
|
|
d->view = 0;
|
|
|
|
setLayout(d->widgetLayout);
|
|
initialize();
|
|
}
|
|
|
|
KDevelop::TextEditorWidget::~TextEditorWidget()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
void KDevelop::TextEditorWidget::initialize()
|
|
{
|
|
if(d->view)
|
|
return;
|
|
TextDocument* doc = static_cast<TextDocument*>(d->textView->document());
|
|
KTextEditor::View* view = qobject_cast<KTextEditor::View*>(doc->createViewWidget(this));
|
|
KTextEditor::Range ir = d->textView->initialRange();
|
|
selectAndReveal(view, ir);
|
|
setEditorView(view);
|
|
}
|
|
|
|
void KDevelop::TextEditorWidget::viewStatusChanged(KTextEditor::View* view, const KTextEditor::Cursor& )
|
|
{
|
|
// This fetches the virtual cursor, which expands tab characters properly, i.e. instead of col == 1
|
|
// you'll get col == 9 with this when a tab is at the start of the line.
|
|
KTextEditor::Cursor pos = view->cursorPositionVirtual();
|
|
d->status = i18n(" Line: %1 Col: %2 ", KGlobal::locale()->formatNumber(pos.line() + 1, 0), KGlobal::locale()->formatNumber(pos.column() + 1, 0));
|
|
emit statusChanged();
|
|
}
|
|
|
|
void KDevelop::TextEditorWidget::setEditorView(KTextEditor::View* view)
|
|
{
|
|
if (d->view)
|
|
{
|
|
disconnect(view, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)));
|
|
removeChildClient(view);
|
|
}
|
|
|
|
d->view = view;
|
|
insertChildClient(d->view);
|
|
connect(view, SIGNAL(cursorPositionChanged(KTextEditor::View*,KTextEditor::Cursor)),
|
|
this, SLOT(viewStatusChanged(KTextEditor::View*,KTextEditor::Cursor)));
|
|
|
|
viewStatusChanged(view, view->cursorPosition());
|
|
|
|
connect(view, SIGNAL(viewEditModeChanged(KTextEditor::View*,KTextEditor::View::EditMode)),
|
|
this, SLOT(viewEditModeChanged(KTextEditor::View*,KTextEditor::View::EditMode)));
|
|
|
|
d->widgetLayout->insertWidget(0, d->view);
|
|
setFocusProxy(view);
|
|
}
|
|
|
|
QString KDevelop::TextEditorWidget::status() const
|
|
{
|
|
return d->status;
|
|
}
|
|
|
|
KTextEditor::View* KDevelop::TextEditorWidget::editorView()
|
|
{
|
|
if(!d->view)
|
|
initialize();
|
|
return d->view;
|
|
}
|
|
|
|
bool KDevelop::TextEditorWidget::isInitialized() const
|
|
{
|
|
return d->view!=0;
|
|
}
|
|
|
|
void KDevelop::TextEditorWidget::showEvent(QShowEvent* event)
|
|
{
|
|
if(!d->view)
|
|
initialize();
|
|
QWidget::showEvent(event);
|
|
}
|
|
|
|
#include "moc_textdocument.cpp"
|