diff --git a/rkward/windows/katepluginintegration.cpp b/rkward/windows/katepluginintegration.cpp index 0a251132..804630ee 100644 --- a/rkward/windows/katepluginintegration.cpp +++ b/rkward/windows/katepluginintegration.cpp @@ -1,552 +1,547 @@ /*************************************************************************** katepluginintegration - description ------------------- begin : Mon Jun 12 2017 copyright : (C) 2017-2020 by Thomas Friedrichsmeier email : thomas.friedrichsmeier@kdemail.net ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "katepluginintegration.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../rkward.h" #include "rkworkplace.h" #include "rkworkplaceview.h" #include "rkcommandeditorwindow.h" #include "../misc/rkdummypart.h" #include "../misc/rkcommonfunctions.h" #include "../settings/rksettingsmodulecommandeditor.h" #include "../debug.h" /// BEGIN KTextEditor::Application interface KatePluginIntegrationApp::KatePluginIntegrationApp(QObject *parent) : QObject (parent) { RK_TRACE (APP); dummy_view = 0; window = new KatePluginIntegrationWindow(this); app = new KTextEditor::Application(this); KTextEditor::Editor::instance()->setApplication(app); // enumerate all available kate plugins QVector plugins = KPluginLoader::findPlugins(QStringLiteral ("ktexteditor"), [](const KPluginMetaData &md) { return md.serviceTypes().contains(QLatin1String("KTextEditor/Plugin")); }); for (int i = plugins.size() -1; i >= 0; --i) { PluginInfo info; info.plugin = 0; info.data = plugins[i]; // Note: creates a lookup-table *and* eliminates potential dupes later in the search path known_plugins.insert(idForPlugin(info.data), info); // TODO: remove me qDebug ("%s", qPrintable(info.data.fileName())); } // NOTE: Destructor is too late for this, esp. As plugin destructors will try to unregister from the guiFactory(), and such. connect(RKWardMainWindow::getMain(), &RKWardMainWindow::aboutToQuitRKWard, this, &KatePluginIntegrationApp::saveConfigAndUnload); } KatePluginIntegrationApp::~KatePluginIntegrationApp() { RK_TRACE (APP); } KTextEditor::View *KatePluginIntegrationApp::dummyView() { if (!dummy_view) { RK_TRACE (APP); KTextEditor::Document *doc = KTextEditor::Editor::instance()->createDocument (this); dummy_view = doc->createView(0); dummy_view->hide(); // Make sure it does not accumulate cruft. connect(doc, &KTextEditor::Document::textChanged, doc, &KTextEditor::Document::clear); } return dummy_view; } QString KatePluginIntegrationApp::idForPlugin(const KTextEditor::Plugin *plugin) const { for (auto it = known_plugins.constBegin(); it != known_plugins.constEnd(); ++it) { if (it.value().plugin == plugin) return it.key(); } return QString(); } QString KatePluginIntegrationApp::idForPlugin(const KPluginMetaData &plugin) const { return QFileInfo(plugin.fileName()).baseName(); } QObject* KatePluginIntegrationApp::loadPlugin (const QString& identifier) { RK_TRACE (APP); if (!known_plugins.contains (identifier)) { RK_DEBUG (APP, DL_WARNING, "Plugin %s is not known", qPrintable (identifier)); return 0; } KPluginFactory *factory = KPluginLoader(known_plugins[identifier].data.fileName ()).factory (); if (factory) { KTextEditor::Plugin *plugin = factory->create(this, QVariantList () << identifier); if (plugin) { known_plugins[identifier].plugin = plugin; emit KTextEditor::Editor::instance()->application()->pluginCreated(identifier, plugin); QObject* created = mainWindow()->createPluginView(plugin); if (created) { emit mainWindow()->main->pluginViewCreated(identifier, created); KTextEditor::SessionConfigInterface *interface = qobject_cast(created); if (interface) { // NOTE: Some plugins (noteably the Search in files plugin) will misbehave, unless readSessionConfig has been called! KConfigGroup group = KSharedConfig::openConfig()->group(QStringLiteral("KatePlugin:%1:").arg(identifier)); interface->readSessionConfig(group); } } return plugin; } } return 0; } void KatePluginIntegrationApp::saveConfigAndUnload() { RK_TRACE (APP); for (auto it = known_plugins.constBegin(); it != known_plugins.constEnd(); ++it) { KTextEditor::Plugin* plugin = it.value().plugin; if (!plugin) continue; QObject* view = mainWindow()->pluginView(it.key()); if (view) { KTextEditor::SessionConfigInterface* interface = qobject_cast(view); if (interface) { KConfigGroup group = KSharedConfig::openConfig()->group(QStringLiteral("KatePlugin:%1:").arg(it.key())); interface->writeSessionConfig(group); } emit mainWindow()->main->pluginViewDeleted(it.key(), view); delete view; } emit app->pluginDeleted(it.key(), plugin); delete plugin; } known_plugins.clear(); } QList KatePluginIntegrationApp::mainWindows() { RK_TRACE (APP); QList ret; ret.append (window->main); return ret; } KTextEditor::MainWindow *KatePluginIntegrationApp::activeMainWindow() { RK_TRACE (APP); return window->main; } RKCommandEditorWindow* findWindowForView(KTextEditor::View *view) { RK_TRACE (APP); QList w = RKWorkplace::mainWorkplace()->getObjectList(RKMDIWindow::CommandEditorWindow); for (int i = 0; i < w.size(); ++i) { KTextEditor::View *v = static_cast(w[i])->getView(); if (v && (v == view)) { return static_cast(w[i]); } } return 0; } RKCommandEditorWindow* findWindowForDocument(KTextEditor::Document *document) { RK_TRACE (APP); QList w = RKWorkplace::mainWorkplace()->getObjectList(RKMDIWindow::CommandEditorWindow); for (int i = 0; i < w.size(); ++i) { KTextEditor::View *v = static_cast(w[i])->getView(); if (v && (v->document() == document)) { return static_cast(w[i]); } } return 0; } QList KatePluginIntegrationApp::documents() { RK_TRACE (APP); QList w = RKWorkplace::mainWorkplace()->getObjectList(RKMDIWindow::CommandEditorWindow); QList ret; for (int i = 0; i < w.size (); ++i) { KTextEditor::View *v = static_cast(w[i])->getView(); if (v) ret.append(v->document()); } if (ret.isEmpty()) { // See the NOTE in KatePluginIntegrationWindow::activeView() ret.append(dummyView()->document()); } return ret; } KTextEditor::Document *KatePluginIntegrationApp::findUrl(const QUrl &url) { RK_TRACE (APP); QUrl _url = url.adjusted(QUrl::NormalizePathSegments); // Needed? QList w = RKWorkplace::mainWorkplace()->getObjectList(RKMDIWindow::CommandEditorWindow); for (int i = 0; i < w.size (); ++i) { if (_url == static_cast(w[i])->url().adjusted(QUrl::NormalizePathSegments)) { KTextEditor::View *v = static_cast(w[i])->getView(); if (v) return v->document(); } } return 0; } KTextEditor::Document *KatePluginIntegrationApp::openUrl(const QUrl &url, const QString &encoding) { RK_TRACE (APP); KTextEditor::View *v = window->openUrl(url, encoding); if (v) return v->document(); return 0; } bool KatePluginIntegrationApp::closeDocument(KTextEditor::Document *document) { RK_TRACE (APP); RKMDIWindow *w = findWindowForDocument(document); if (w) return RKWorkplace::mainWorkplace()->closeWindow(w); // NOTE: Closes only a single view of the document return false; } bool KatePluginIntegrationApp::closeDocuments(const QList &documents) { RK_TRACE (APP); bool allfound = true; QList w = RKWorkplace::mainWorkplace()->getObjectList(RKMDIWindow::CommandEditorWindow); QList toclose; for (int i = 0; i < documents.size(); ++i) { bool found = false; for (int j = 0; j < w.size(); ++j) { KTextEditor::View *v = static_cast(w[j])->getView(); if (v && v->document() == documents[i]) { toclose.append(w[i]); found = true; break; } } if (!found) allfound = false; } return RKWorkplace::mainWorkplace()->closeWindows(toclose) && allfound; } KTextEditor::Plugin *KatePluginIntegrationApp::plugin(const QString &name) { RK_TRACE (APP); if (known_plugins.contains(name)) { return known_plugins[name].plugin; } return 0; } /// END KTextEditor::Application interface /// BEGIN KTextEditor::MainWindow interface KatePluginIntegrationWindow::KatePluginIntegrationWindow (KatePluginIntegrationApp *parent) : QObject (parent), KXMLGUIClient () { RK_TRACE (APP); // This one is passed to each created plugin main = new KTextEditor::MainWindow(this); // While this one may be accessed from plugins via KTextEditor::Editor::instance()->application() app = parent; active_plugin = 0; + + connect(RKWorkplace::getHistory(), &RKMDIWindowHistory::activeWindowChanged, this, &KatePluginIntegrationWindow::activeWindowChanged); } class KatePluginToolWindow : public RKMDIWindow { Q_OBJECT public: KatePluginToolWindow(QWidget *parent) : RKMDIWindow(parent, RKMDIWindow::KatePluginWindow, true) { RK_TRACE (APP); QVBoxLayout *layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); setPart(new RKDummyPart(this, this)); initializeActivationSignals(); setFocusPolicy(Qt::ClickFocus); } ~KatePluginToolWindow() { RK_TRACE (APP); } /** This is a bit lame, but the plugin does not add itself to the parent widget's layout by itself. So we need this override * to do that. Where did the good old KVBox go? */ void childEvent(QChildEvent *ev) override { if ((ev->type() == QEvent::ChildAdded) && qobject_cast(ev->child())) { QWidget *widget = qobject_cast(ev->child()); layout()->addWidget(widget); setFocusProxy(widget); } QWidget::childEvent(ev); } }; QWidget* KatePluginIntegrationWindow::createToolView (KTextEditor::Plugin *plugin, const QString &identifier, KTextEditor::MainWindow::ToolViewPosition pos, const QIcon &icon, const QString &text) { RK_TRACE (APP); RK_DEBUG(APP, DL_DEBUG, "createToolView for %p, %s, position %d, %s", plugin, qPrintable(identifier), pos, qPrintable(text)); // TODO: Set proper RKMDIWindow:type KatePluginToolWindow *window = new KatePluginToolWindow(RKWorkplace::mainWorkplace()->view()); window->setCaption(text); window->setWindowIcon(icon); RKWorkplace::mainWorkplace()->placeInToolWindowBar(window, pos); RKToolWindowList::registerToolWindow(window, identifier, (RKToolWindowList::Placement) pos, 0); plugin_resources[plugin].windows.append(window); return window; } bool KatePluginIntegrationWindow::showToolView (QWidget *widget) { RK_TRACE (APP); RKMDIWindow *w = qobject_cast(widget); if (w) w->activate(); else { RK_DEBUG(APP, DL_ERROR, "Failed to find mdi window for (plugin) tool view %p", widget); RK_ASSERT(w); widget->show(); } return true; } KXMLGUIFactory *KatePluginIntegrationWindow::guiFactory () { RK_TRACE (APP); // NOTE: We'd rather like to add the plugin to our own RKMDIWindows, rather than // allowing it direct access to the guiFactory() // See the HACK in createPluginView(). return factory (); } QWidget *KatePluginIntegrationWindow::window() { RK_TRACE (APP); return RKWorkplace::mainWorkplace()->view()->window(); } QList KatePluginIntegrationWindow::views() { RK_TRACE (APP); QList w = RKWorkplace::mainWorkplace()->getObjectList(RKMDIWindow::CommandEditorWindow); QList ret; for (int i = 0; i < w.size (); ++i) { ret.append (static_cast(w[i])->getView()); } return ret; } +void KatePluginIntegrationWindow::activeWindowChanged(RKMDIWindow* window) { + RK_TRACE (APP); + + if (window->isType(RKMDIWindow::CommandEditorWindow)) { + emit main->viewChanged(static_cast(window)->getView()); + } +} + KTextEditor::View *KatePluginIntegrationWindow::activeView() { RK_TRACE (APP); RKMDIWindow *w = RKWorkplace::mainWorkplace()->activeWindow(RKMDIWindow::AnyWindowState); if (w && w->isType (RKMDIWindow::CommandEditorWindow)) { return static_cast(w)->getView(); } // NOTE: As far as RKWard is concerned, the active window will most likely be the tool window at this point, while the // intention will be to get an active window that the tool should operate on. So get the last used window from // history. (Another option would be to check which window is on top in the view area, but this will be difficult // for split views. RKMDIWindow* candidate = RKWorkplace::getHistory()->previousDocumentWindow(); if (candidate && candidate->isType(RKMDIWindow::CommandEditorWindow)) return static_cast(candidate)->getView(); // NOTE: It looks like some plugins assume this cannot return 0. That's a bug in the plugin, but still one that could // be quite prevalent, as in kate, that assumption holds. So, to be safe, we create a dummy window on the fly. return app->dummyView(); } KTextEditor::View *KatePluginIntegrationWindow::activateView(KTextEditor::Document *document) { RK_TRACE (APP); RKCommandEditorWindow* w = findWindowForDocument(document); if (w) { w->activate(); return w->getView(); } if (app->dummy_view && document == app->dummy_view->document()) return app->dummy_view; return 0; } KTextEditor::View *KatePluginIntegrationWindow::openUrl(const QUrl &url, const QString &encoding) { RK_TRACE (APP); RKMDIWindow *w = RKWorkplace::mainWorkplace()->openScriptEditor(url, encoding); if (w) return static_cast(w)->getView(); RK_ASSERT(w); // should not happen return 0; } QObject *KatePluginIntegrationWindow::pluginView(const QString &name) { RK_TRACE (APP); return plugin_resources.value(app->plugin(name)).view; } bool KatePluginIntegrationWindow::closeSplitView(KTextEditor::View* view) { RK_TRACE (APP); // TODO: This should close the area that this view is in, not necessarily the view itself. However, if the same doc // is also present in the area to merge into, then close this view, keeping the other. return closeView(view); } bool KatePluginIntegrationWindow::closeView(KTextEditor::View* view) { RK_TRACE (APP); RKMDIWindow *w = findWindowForView(view); if (w) return RKWorkplace::mainWorkplace()->closeWindow(w); return false; } bool KatePluginIntegrationWindow::hideToolView(QWidget* widget) { RK_TRACE (APP); RKMDIWindow *w = qobject_cast(widget); if (w) w->close(false); else { RK_ASSERT(w); widget->hide(); } return true; } /* These appear to be truly optional, so let's disable them for now. void KatePluginIntegrationWindow::hideViewBar(KTextEditor::View* view) {} void KatePluginIntegrationWindow::showViewBar(KTextEditor::View* view) {} void KatePluginIntegrationWindow::deleteViewBar(KTextEditor::View* view) {} void KatePluginIntegrationWindow::addWidgetToViewBar(KTextEditor::View* view, QWidget* bar) {} QWidget *KatePluginIntegrationWindow::createViewBar(KTextEditor::View *view) {} */ bool KatePluginIntegrationWindow::moveToolView(QWidget* widget, KTextEditor::MainWindow::ToolViewPosition pos) { RK_TRACE (APP); RKMDIWindow *w = qobject_cast(widget); if (w) { RKWorkplace::mainWorkplace ()->placeInToolWindowBar (w, pos); return true; } return false; } void KatePluginIntegrationWindow::splitView(Qt::Orientation orientation) { RK_TRACE (APP); RKWorkplace::mainWorkplace()->view()->splitView(orientation); } bool KatePluginIntegrationWindow::viewsInSameSplitView(KTextEditor::View* view1, KTextEditor::View* view2) { RK_TRACE (APP); // TODO not sure what the semantics of this really are. The two views are in the same view area (not visible, simultaneously), or in two areas split side-by-side? // However, this is essentially unused in kate. return false; } -void KatePluginIntegrationWindow::fixupPluginUI(const QString &id, const PluginResources &resources) { +void KatePluginIntegrationWindow::fixUpPluginUI(const QString &id, const PluginResources &resources) { RK_TRACE (APP); KXMLGUIClient* hacked_parent = this; // KF6 TODO: In KF6, plugins will probably be limited to one UI client, in the first place. for (int i = 0; i < resources.clients.size(); ++i) { KXMLGUIClient* client = resources.clients[i]; RKMDIWindow* window = resources.windows.value(i); if (window) { hacked_parent = window->getPart();; } factory()->removeClient(client); if (i == 0 && id == QStringLiteral("katesearchplugin")) { window->setCaption(i18nc("Tab title", "Search in Scripts")); RKCommonFunctions::removeContainers(client, QStringList() << "search_in_files", true); // TODO: Rename "Search more" to "Search in Scripts". These should still be accessible, globally. } else if (i == 0 && id == QStringLiteral("kateprojectplugin")) { - RKCommonFunctions::moveContainer(client, "Menu", "project", "edit", true, false); + RKCommonFunctions::moveContainer(client, "Menu", "projects", "view", true, false); } RKCommonFunctions::moveContainer(client, "Menu", "tools", "edit", true, true); hacked_parent->insertChildClient(client); } /* TODO: Ok, I guess we need even more specialization. kateprojectplugin: - - "Project" menu should go to "View"? - Actions should probably be accessible, globally katesearchplugin: - should go to next / previous match be accessible, globally? katesnippetsplugin: - ok as is, I think */ + // TODO: If child clients were added to the window, itself, we need to tell the main window to rebuild. + // Right now, this is handled during startup, only. + } QObject* KatePluginIntegrationWindow::createPluginView(KTextEditor::Plugin* plugin) { RK_TRACE (APP); // HACK: Currently, plugins will add themselves to the main window's UI, without asking. We don't want that, as // our MDI windows are enabled / disabled on activation. To hack around this, the catch the added clients, // and put them, where they belong. connect(factory(), &KXMLGUIFactory::clientAdded, this, &KatePluginIntegrationWindow::catchXMLGUIClientsHack); active_plugin = plugin; PluginResources& resources = plugin_resources.insert(plugin, PluginResources()).value(); resources.view = plugin->createView(main); active_plugin = 0; disconnect(factory(), &KXMLGUIFactory::clientAdded, this, &KatePluginIntegrationWindow::catchXMLGUIClientsHack); - KXMLGUIClient* hacked_parent = this; - QString id = app->idForPlugin(plugin); - for (int i = 0; i < resources.clients.size(); ++i) { - KXMLGUIClient* client = resources.clients[i]; - RKMDIWindow* window = resources.windows.value(i); - if (window) { - hacked_parent = window->getPart();; - } - factory()->removeClient(client); - fixupPluginUI(id, i, client, window); - hacked_parent->insertChildClient(client); - } - // TODO: If child clients were added to the window, itself, we need to tell the main window to rebuild. - // Right now, this is handled during startup, only. - + fixUpPluginUI(app->idForPlugin(plugin), resources); connect(plugin, &QObject::destroyed, [&]() { plugin_resources.remove(plugin); }); return resources.view; } void KatePluginIntegrationWindow::catchXMLGUIClientsHack(KXMLGUIClient* client) { RK_TRACE (APP); if (active_plugin) { RK_ASSERT(plugin_resources.contains(active_plugin)); plugin_resources[active_plugin].clients.append(client); } else { RK_DEBUG(APP, DL_DEBUG, "XML client created by unknown kate plugin"); } } -// TODO: Don't forget to make sure to emit all the signals! -// - MainWindow signals -// TODO: Apply plugin specific hacks as needed (e.g. moving "Tool" menu, removing broken actions) // TODO: new RKToplevelWindowGUI should be called after all plugins are loaded (and have registered their tool views). However // that may be a problem, if there is no KXMLGUIFactory around, yet. So, annoyingly, we need to create the GUI, before we // have everything to populate it. /// END KTextEditor::MainWindow interface #include "katepluginintegration.moc" diff --git a/rkward/windows/katepluginintegration.h b/rkward/windows/katepluginintegration.h index 7ae8eb9b..f3abb307 100644 --- a/rkward/windows/katepluginintegration.h +++ b/rkward/windows/katepluginintegration.h @@ -1,122 +1,123 @@ /*************************************************************************** katepluginintegration - description ------------------- begin : Mon Jun 12 2017 copyright : (C) 2017-2020 by Thomas Friedrichsmeier email : thomas.friedrichsmeier@kdemail.net ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #ifndef KATEPLUGININTEGRATION_H #define KATEPLUGININTEGRATION_H #include #include #include #include #include class KatePluginIntegrationWindow; class RKMDIWindow; /** This class provides implementations for the KTextEditor::Application interface. * Note that there is a separate interface KatePluginIntegrationWindow / KTextEditor::MainWindow that serves * as an abstraction of main windows. Even though - for now - we will be creating (detachable) plugins only in * one "main" window, we follow this separation to be on the safe side for future extensions (and also there is * a name-clash in on of the slots (openUrl()), otherwise. */ class KatePluginIntegrationApp : public QObject { Q_OBJECT public: KatePluginIntegrationApp(QObject *parent); ~KatePluginIntegrationApp(); QObject* loadPlugin(const QString& identifier); KatePluginIntegrationWindow *mainWindow() const { return window; }; private slots: friend class KatePluginIntegrationWindow; void saveConfigAndUnload(); // These are the implementations of the KTextEditor::Application interface. // NOTE that they are not technically overrides, but get invoked via QMetaObject::invokeMethod() QList mainWindows(); KTextEditor::MainWindow *activeMainWindow(); QList documents(); KTextEditor::Document *findUrl(const QUrl &url); KTextEditor::Document *openUrl(const QUrl &url, const QString &encoding = QString()); bool closeDocument(KTextEditor::Document *document); bool closeDocuments(const QList &documents); KTextEditor::Plugin *plugin(const QString &name); private: KatePluginIntegrationWindow *window; // For now, only one main window KTextEditor::Application *app; /** Provides a hidden dummy view (created on the fly as needed), for plugins that assume there is always at least one view/document around. */ KTextEditor::View *dummyView(); KTextEditor::View *dummy_view; struct PluginInfo { KPluginMetaData data; KTextEditor::Plugin *plugin; }; QMap known_plugins; QString idForPlugin(const KTextEditor::Plugin *plugin) const; QString idForPlugin(const KPluginMetaData &plugin) const; }; class KatePluginIntegrationWindow : public QObject, public KXMLGUIClient { Q_OBJECT public: KatePluginIntegrationWindow(KatePluginIntegrationApp *parent); KTextEditor::MainWindow *mainWindow() const { return main; }; private slots: // These are the implementations of the KTextEditor::MainWindow interface. // NOTE that they are not technically overrides, but get invoked via QMetaObject::invokeMethod() QWidget *createToolView(KTextEditor::Plugin *plugin, const QString &identifier, KTextEditor::MainWindow::ToolViewPosition pos, const QIcon &icon, const QString &text); KXMLGUIFactory *guiFactory(); QWidget *window(); QList views(); KTextEditor::View *activeView(); KTextEditor::View *activateView(KTextEditor::Document *document); KTextEditor::View *openUrl(const QUrl &url, const QString &encoding = QString()); bool closeView(KTextEditor::View *view); void splitView(Qt::Orientation orientation); bool closeSplitView(KTextEditor::View *view); bool viewsInSameSplitView(KTextEditor::View *view1, KTextEditor::View *view2); bool moveToolView(QWidget *widget, KTextEditor::MainWindow::ToolViewPosition pos); bool showToolView(QWidget *widget); bool hideToolView(QWidget *widget); QObject *pluginView(const QString &name); /* Apparently, these are truely optional, so let's disable them for the time being QWidget *createViewBar(KTextEditor::View *view); void deleteViewBar(KTextEditor::View *view); void showViewBar(KTextEditor::View *view); void hideViewBar(KTextEditor::View *view); void addWidgetToViewBar(KTextEditor::View *view, QWidget *bar); */ private: friend class KatePluginIntegrationApp; KTextEditor::MainWindow *main; QObject* createPluginView(KTextEditor::Plugin* plugin); struct PluginResources { PluginResources() : view(0) {}; QObject *view; QList clients; QList windows; }; QHash plugin_resources; KatePluginIntegrationApp *app; private slots: void catchXMLGUIClientsHack(KXMLGUIClient* client); + void activeWindowChanged(RKMDIWindow *window); private: KTextEditor::Plugin* active_plugin; void fixUpPluginUI(const QString &id, const PluginResources &resources); }; #endif diff --git a/rkward/windows/rkcommandeditorwindow.cpp b/rkward/windows/rkcommandeditorwindow.cpp index 7beca4a9..fe4d5091 100644 --- a/rkward/windows/rkcommandeditorwindow.cpp +++ b/rkward/windows/rkcommandeditorwindow.cpp @@ -1,1400 +1,1403 @@ /*************************************************************************** rkcommandeditorwindow - description ------------------- begin : Mon Aug 30 2004 copyright : (C) 2004-2020 by Thomas Friedrichsmeier email : thomas.friedrichsmeier@kdemail.net ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "rkcommandeditorwindow.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 #include #include #include #include #include #include "../misc/rkcommonfunctions.h" #include "../misc/rkstandardicons.h" #include "../misc/rkstandardactions.h" #include "../misc/rkxmlguisyncer.h" #include "../misc/rkjobsequence.h" #include "../misc/rkxmlguipreviewarea.h" #include "../core/robjectlist.h" #include "../rbackend/rkrinterface.h" #include "../settings/rksettings.h" #include "../settings/rksettingsmodulecommandeditor.h" #include "../rkconsole.h" #include "../rkglobals.h" #include "../rkward.h" #include "rkhelpsearchwindow.h" #include "rkhtmlwindow.h" #include "rkworkplace.h" #include "katepluginintegration.h" #include "rkcodecompletion.h" #include "../debug.h" RKCommandEditorWindowPart::RKCommandEditorWindowPart (QWidget *parent) : KParts::Part (parent) { RK_TRACE (COMMANDEDITOR); setComponentName (QCoreApplication::applicationName (), QGuiApplication::applicationDisplayName ()); setWidget (parent); setXMLFile ("rkcommandeditorwindowpart.rc"); } RKCommandEditorWindowPart::~RKCommandEditorWindowPart () { RK_TRACE (COMMANDEDITOR); } #define GET_HELP_URL 1 #define NUM_BLOCK_RECORDS 6 //static QMap RKCommandEditorWindow::unnamed_documents; KTextEditor::Document* createDocument(bool with_signals) { if (with_signals) { emit KTextEditor::Editor::instance()->application()->aboutToCreateDocuments(); } KTextEditor::Document* ret = KTextEditor::Editor::instance()->createDocument (RKWardMainWindow::getMain ()); if (with_signals) { emit KTextEditor::Editor::instance()->application()->documentCreated(ret); emit KTextEditor::Editor::instance()->application()->documentsCreated(QList() << ret); } return ret; } RKCommandEditorWindow::RKCommandEditorWindow (QWidget *parent, const QUrl _url, const QString& encoding, int flags) : RKMDIWindow (parent, RKMDIWindow::CommandEditorWindow) { RK_TRACE (COMMANDEDITOR); QString id_header = QStringLiteral ("unnamedscript://"); KTextEditor::Editor* editor = KTextEditor::Editor::instance (); RK_ASSERT (editor); QUrl url = _url; m_doc = 0; preview_dir = 0; visible_to_kateplugins = flags & RKCommandEditorFlags::VisibleToKTextEditorPlugins; bool use_r_highlighting = (flags & RKCommandEditorFlags::ForceRHighlighting) || (url.isEmpty() && (flags & RKCommandEditorFlags::DefaultToRHighlighting)) || RKSettingsModuleCommandEditor::matchesScriptFileFilter (url.fileName ()); // Lookup of existing text editor documents: First, if no url is given at all, create a new document, and register an id, in case this window will get split, later if (url.isEmpty ()) { m_doc = createDocument (visible_to_kateplugins); _id = id_header + KRandom::randomString (16).toLower (); RK_ASSERT (!unnamed_documents.contains (_id)); unnamed_documents.insert (_id, m_doc); } else if (url.url ().startsWith (id_header)) { // Next, handle the case that a pseudo-url is passed in _id = url.url (); m_doc = unnamed_documents.value (_id); url.clear (); if (!m_doc) { // can happen while restoring saved workplace. m_doc = createDocument (visible_to_kateplugins); unnamed_documents.insert (_id, m_doc); } } else { // regular url given. Try to find an existing document for that url // NOTE: we cannot simply use the same map as above, for this purpose, as document urls may change. // instead we iterate over the document list. QList docs = editor->documents (); for (int i = 0; i < docs.count (); ++i) { if (docs[i]->url ().matches (url, QUrl::NormalizePathSegments | QUrl::StripTrailingSlash | QUrl::PreferLocalFile)) { m_doc = docs[i]; break; } } } // if an existing document is re-used, try to honor encoding. if (m_doc) { if (!encoding.isEmpty () && (m_doc->encoding () != encoding)) { m_doc->setEncoding (encoding); m_doc->documentReload (); } } // no existing document was found, so create one and load the url if (!m_doc) { m_doc = createDocument (visible_to_kateplugins); // The document may have to outlive this window // encoding must be set *before* loading the file if (!encoding.isEmpty ()) m_doc->setEncoding (encoding); if (!url.isEmpty ()) { if (m_doc->openUrl (url)) { // KF5 TODO: Check which parts of this are still needed in KF5, and which no longer work if (!(flags & RKCommandEditorFlags::DeleteOnClose)) { // don't litter config with temporary files QString p_url = RKWorkplace::mainWorkplace ()->portableUrl (m_doc->url ()); KConfigGroup conf (RKWorkplace::mainWorkplace ()->workspaceConfig (), QString ("SkriptDocumentSettings %1").arg (p_url)); // HACK: Hmm. KTextEditor::Document's readSessionConfig() simply restores too much. Yes, I want to load bookmarks and stuff. // I do not want to mess with encoding, or risk loading a different url, after the doc is already loaded! if (!encoding.isEmpty () && (conf.readEntry ("Encoding", encoding) != encoding)) conf.writeEntry ("Encoding", encoding); if (conf.readEntry ("URL", url) != url) conf.writeEntry ("URL", url); // HACK: What the...?! Somehow, at least on longer R scripts, stored Mode="Normal" in combination with R Highlighting // causes code folding to fail (KDE 4.8.4, http://sourceforge.net/p/rkward/bugs/122/). // Forcing Mode == Highlighting appears to help. if (use_r_highlighting) conf.writeEntry ("Mode", conf.readEntry ("Highlighting", "Normal")); m_doc->readSessionConfig (conf); } } else { KMessageBox::messageBox (this, KMessageBox::Error, i18n ("Unable to open \"%1\"", url.toDisplayString ()), i18n ("Could not open command file")); } } } setReadOnly (flags & RKCommandEditorFlags::ReadOnly); if (flags & RKCommandEditorFlags::DeleteOnClose) { if (flags & RKCommandEditorFlags::ReadOnly) { RKCommandEditorWindow::delete_on_close = url; } else { RK_ASSERT (false); } } RK_ASSERT (m_doc); // yes, we want to be notified, if the file has changed on disk. // why, oh why is this not the default? // this needs to be set *before* the view is created! KTextEditor::ModificationInterface* em_iface = qobject_cast (m_doc); if (em_iface) em_iface->setModifiedOnDiskWarning (true); else RK_ASSERT (false); m_view = m_doc->createView (this, RKWardMainWindow::getMain ()->katePluginIntegration ()->mainWindow ()->mainWindow()); + if (visible_to_kateplugins) { + emit RKWardMainWindow::getMain ()->katePluginIntegration ()->mainWindow ()->mainWindow()->viewCreated (m_view); + } preview = new RKXMLGUIPreviewArea (QString(), this); preview_manager = new RKPreviewManager (this); connect (preview_manager, &RKPreviewManager::statusChanged, [this]() { preview_timer.start (500); }); m_view = m_doc->createView (this); RKWorkplace::mainWorkplace()->registerNamedWindow (preview_manager->previewId(), this, preview); if (!url.isEmpty ()) { KConfigGroup viewconf (RKWorkplace::mainWorkplace ()->workspaceConfig (), QString ("SkriptViewSettings %1").arg (RKWorkplace::mainWorkplace ()->portableUrl (url))); m_view->readSessionConfig (viewconf); } setFocusProxy (m_view); setFocusPolicy (Qt::StrongFocus); RKCommandEditorWindowPart* part = new RKCommandEditorWindowPart (m_view); part->insertChildClient (m_view); setPart (part); fixupPartGUI (); setMetaInfo (i18n ("Script Editor"), QUrl (), RKSettings::PageCommandEditor); initializeActions (part->actionCollection ()); initializeActivationSignals (); RKXMLGUISyncer::self()->registerChangeListener (m_view, this, SLOT (fixupPartGUI())); QHBoxLayout *layout = new QHBoxLayout (this); layout->setContentsMargins (0, 0, 0, 0); QSplitter* preview_splitter = new QSplitter (this); preview_splitter->addWidget (m_view); QWidget *preview_widget = preview->wrapperWidget (); preview_splitter->addWidget (preview_widget); preview_widget->hide (); connect (m_doc, &KTextEditor::Document::documentSavedOrUploaded, this, &RKCommandEditorWindow::documentSaved); layout->addWidget(preview_splitter); setGlobalContextProperty ("current_filename", m_doc->url ().url ()); connect (m_doc, &KTextEditor::Document::documentUrlChanged, [this]() { updateCaption(); setGlobalContextProperty ("current_filename", m_doc->url ().url ()); }); connect (m_doc, &KTextEditor::Document::modifiedChanged, this, &RKCommandEditorWindow::updateCaption); // of course most of the time this causes a redundant call to updateCaption. Not if a modification is undone, however. #ifdef __GNUC__ #warning remove this in favor of KTextEditor::Document::restore() #endif connect (m_doc, &KTextEditor::Document::modifiedChanged, this, &RKCommandEditorWindow::autoSaveHandlerModifiedChanged); connect (m_doc, &KTextEditor::Document::textChanged, this, &RKCommandEditorWindow::textChanged); connect (m_view, &KTextEditor::View::selectionChanged, this, &RKCommandEditorWindow::selectionChanged); // somehow the katepart loses the context menu each time it loses focus connect (m_view, &KTextEditor::View::focusIn, this, &RKCommandEditorWindow::focusIn); hinter = 0; if (use_r_highlighting) { RKCommandHighlighter::setHighlighting (m_doc, RKCommandHighlighter::RScript); if (flags & RKCommandEditorFlags::UseCodeHinting) { new RKCompletionManager (m_view); //hinter = new RKFunctionArgHinter (this, m_view); } } else { RKCommandHighlighter::setHighlighting (m_doc, RKCommandHighlighter::Automatic); } smart_iface = qobject_cast (m_doc); initBlocks (); RK_ASSERT (smart_iface); connect (&autosave_timer, &QTimer::timeout, this, &RKCommandEditorWindow::doAutoSave); connect (&preview_timer, &QTimer::timeout, this, &RKCommandEditorWindow::doRenderPreview); updateCaption (); // initialize QTimer::singleShot (0, this, SLOT (setPopupMenu())); } RKCommandEditorWindow::~RKCommandEditorWindow () { RK_TRACE (COMMANDEDITOR); bool have_url = !url().isEmpty(); // cache early, as potentially needed after destruction of m_doc (at which point calling url() may crash if (have_url) { QString p_url = RKWorkplace::mainWorkplace ()->portableUrl (m_doc->url ()); KConfigGroup conf (RKWorkplace::mainWorkplace ()->workspaceConfig (), QString ("SkriptDocumentSettings %1").arg (p_url)); m_doc->writeSessionConfig (conf); KConfigGroup viewconf (RKWorkplace::mainWorkplace ()->workspaceConfig (), QString ("SkriptViewSettings %1").arg (p_url)); m_view->writeSessionConfig (viewconf); } delete hinter; discardPreview (); delete m_view; QList views = m_doc->views (); if (views.isEmpty ()) { if (visible_to_kateplugins) { emit KTextEditor::Editor::instance()->application()->documentWillBeDeleted(m_doc); emit KTextEditor::Editor::instance()->application()->aboutToDeleteDocuments(QList() << m_doc); } delete m_doc; if (visible_to_kateplugins) { emit KTextEditor::Editor::instance()->application()->documentDeleted(m_doc); emit KTextEditor::Editor::instance()->application()->documentsDeleted(QList() << m_doc); } if (!delete_on_close.isEmpty ()) KIO::del (delete_on_close)->start (); unnamed_documents.remove (_id); } // NOTE, under rather unlikely circumstances, the above may leave stale ids->stale pointers in the map: Create unnamed window, split it, save to a url, split again, close the first two windows, close the last. This situation should be caught by the following, however: if (have_url && !_id.isEmpty ()) { unnamed_documents.remove (_id); } } void RKCommandEditorWindow::fixupPartGUI () { RK_TRACE (COMMANDEDITOR); // strip down the katepart's GUI. remove some stuff we definitely don't need. RKCommonFunctions::removeContainers (m_view, QString ("bookmarks,tools_spelling,tools_spelling_from_cursor,tools_spelling_selection,switch_to_cmd_line").split (','), true); RKCommonFunctions::moveContainer (m_view, "Menu", "tools", "edit", true); } QAction *findAction (KTextEditor::View* view, const QString &actionName) { // katepart has more than one actionCollection QList acs = view->findChildren(); acs.append (view->actionCollection ()); foreach (KActionCollection* ac, acs) { QAction* found = ac->action (actionName); if (found) return found; } return 0; } void RKCommandEditorWindow::initializeActions (KActionCollection* ac) { RK_TRACE (COMMANDEDITOR); RKStandardActions::copyLinesToOutput (this, this, SLOT (copyLinesToOutput())); RKStandardActions::pasteSpecial (this, this, SLOT (paste(QString))); action_run_all = RKStandardActions::runAll (this, this, SLOT (runAll())); action_run_current = RKStandardActions::runCurrent (this, this, SLOT (runCurrent()), true); // NOTE: enter_and_submit is not currently added to the menu QAction *action = ac->addAction ("enter_and_submit", this, SLOT (enterAndSubmit())); action->setText (i18n ("Insert line break and run")); ac->setDefaultShortcuts (action, QList() << Qt::AltModifier + Qt::Key_Return << Qt::AltModifier + Qt::Key_Enter); ac->setDefaultShortcut (action, Qt::AltModifier + Qt::Key_Return); // KF5 TODO: This line needed only for KF5 < 5.2, according to documentation RKStandardActions::functionHelp (this, this); RKStandardActions::onlineHelp (this, this); actionmenu_run_block = new KActionMenu (i18n ("Run block"), this); actionmenu_run_block->setDelayed (false); // KDE4: TODO does not work correctly in the tool bar. ac->addAction ("run_block", actionmenu_run_block); connect (actionmenu_run_block->menu(), &QMenu::aboutToShow, this, &RKCommandEditorWindow::clearUnusedBlocks); actionmenu_mark_block = new KActionMenu (i18n ("Mark selection as block"), this); ac->addAction ("mark_block", actionmenu_mark_block); connect (actionmenu_mark_block->menu(), &QMenu::aboutToShow, this, &RKCommandEditorWindow::clearUnusedBlocks); actionmenu_unmark_block = new KActionMenu (i18n ("Unmark block"), this); ac->addAction ("unmark_block", actionmenu_unmark_block); connect (actionmenu_unmark_block->menu(), &QMenu::aboutToShow, this, &RKCommandEditorWindow::clearUnusedBlocks); action_setwd_to_script = ac->addAction ("setwd_to_script", this, SLOT (setWDToScript())); action_setwd_to_script->setText (i18n ("CD to script directory")); action_setwd_to_script->setStatusTip (i18n ("Change the working directory to the directory of this script")); action_setwd_to_script->setToolTip (action_setwd_to_script->statusTip ()); action_setwd_to_script->setIcon (RKStandardIcons::getIcon (RKStandardIcons::ActionCDToScript)); KActionMenu* actionmenu_preview = new KActionMenu (QIcon::fromTheme ("view-preview"), i18n ("Preview"), this); actionmenu_preview->setDelayed (false); preview_modes = new QActionGroup (this); actionmenu_preview->addAction (action_no_preview = new QAction (RKStandardIcons::getIcon (RKStandardIcons::ActionDelete), i18n ("No preview"), preview_modes)); actionmenu_preview->addAction (new QAction (QIcon::fromTheme ("preview_math"), i18n ("R Markdown"), preview_modes)); actionmenu_preview->addAction (new QAction (RKStandardIcons::getIcon (RKStandardIcons::WindowOutput), i18n ("RKWard Output"), preview_modes)); actionmenu_preview->addAction (new QAction (RKStandardIcons::getIcon (RKStandardIcons::WindowX11), i18n ("Plot"), preview_modes)); actionmenu_preview->addAction (new QAction (RKStandardIcons::getIcon (RKStandardIcons::WindowConsole), i18n ("R Console"), preview_modes)); QList preview_actions = preview_modes->actions (); preview_actions[NoPreview]->setToolTip (i18n ("Disable preview")); preview_actions[RMarkdownPreview]->setToolTip (i18n ("Preview the script as rendered from RMarkdown format (appropriate for .Rmd files).")); preview_actions[ConsolePreview]->setToolTip (i18n ("Preview the script as if it was run in the interactive R Console")); preview_actions[GraphPreview]->setToolTip (i18n ("Preview any onscreen graphics produced by running this script. This preview will be empty, if there is no call to plot() or other graphics commands.")); preview_actions[OutputWindow]->setToolTip (i18n ("Preview any output to the RKWard Output Window. This preview will be empty, if there is no call to rk.print() or other RKWard output commands.")); for (int i = 0; i < preview_actions.size (); ++i) { preview_actions[i]->setCheckable (true); preview_actions[i]->setStatusTip (preview_actions[i]->toolTip ()); } action_no_preview->setChecked (true); connect (preview, &RKXMLGUIPreviewArea::previewClosed, this, &RKCommandEditorWindow::discardPreview); connect (preview_modes, &QActionGroup::triggered, this, &RKCommandEditorWindow::changePreviewMode); actionmenu_preview->addSeparator (); action_preview_as_you_type = new QAction (QIcon::fromTheme ("input-keyboard"), i18nc ("Checkable action: the preview gets updated while typing", "Update as you type"), ac); action_preview_as_you_type->setToolTip (i18n ("When this option is enabled, an update of the preview will be triggered every time you modify the script. When this option is disabled, the preview will be updated whenever you save the script, only.")); action_preview_as_you_type->setCheckable (true); action_preview_as_you_type->setChecked (m_doc->url ().isEmpty ()); // By default, update as you type for unsaved "quick and dirty" scripts, preview on save for saved scripts actionmenu_preview->addAction (action_preview_as_you_type); ac->addAction ("render_preview", actionmenu_preview); file_save = findAction (m_view, "file_save"); if (file_save) file_save->setText (i18n ("Save Script...")); file_save_as = findAction (m_view, "file_save_as"); if (file_save_as) file_save_as->setText (i18n ("Save Script As...")); } void RKCommandEditorWindow::initBlocks () { RK_TRACE (COMMANDEDITOR); if (!smart_iface) return; // may happen in KDE => 4.6 if compiled with KDE <= 4.4 RK_ASSERT (block_records.isEmpty ()); KActionCollection* ac = getPart ()->actionCollection (); int i = 0; QColor colors[NUM_BLOCK_RECORDS]; colors[i++] = QColor (255, 0, 0); colors[i++] = QColor (0, 255, 0); colors[i++] = QColor (0, 0, 255); colors[i++] = QColor (255, 255, 0); colors[i++] = QColor (255, 0, 255); colors[i++] = QColor (0, 255, 255); RK_ASSERT (i == NUM_BLOCK_RECORDS); // sorry for those idiotic shortcuts, but I just could not find any decent unused ones i = 0; QKeySequence shortcuts[NUM_BLOCK_RECORDS]; shortcuts[i++] = QKeySequence (Qt::AltModifier | Qt::ShiftModifier | Qt::Key_F1); shortcuts[i++] = QKeySequence (Qt::AltModifier | Qt::ShiftModifier | Qt::Key_F2); shortcuts[i++] = QKeySequence (Qt::AltModifier | Qt::ShiftModifier | Qt::Key_F3); shortcuts[i++] = QKeySequence (Qt::AltModifier | Qt::ShiftModifier | Qt::Key_F4); shortcuts[i++] = QKeySequence (Qt::AltModifier | Qt::ShiftModifier | Qt::Key_F5); shortcuts[i++] = QKeySequence (Qt::AltModifier | Qt::ShiftModifier | Qt::Key_F6); RK_ASSERT (i == NUM_BLOCK_RECORDS); for (i = 0; i < NUM_BLOCK_RECORDS; ++i) { BlockRecord record; QColor shaded = colors[i]; shaded.setAlpha (30); record.attribute = KTextEditor::Attribute::Ptr (new KTextEditor::Attribute ()); record.attribute->setBackgroundFillWhitespace (false); record.attribute->setBackground (shaded); QPixmap colorsquare (16, 16); colorsquare.fill (colors[i]); QIcon icon (colorsquare); record.mark = ac->addAction ("markblock" + QString::number (i), this, SLOT (markBlock())); record.mark->setIcon (icon); record.mark->setData (i); actionmenu_mark_block->addAction (record.mark); record.unmark = ac->addAction ("unmarkblock" + QString::number (i), this, SLOT (unmarkBlock())); record.unmark->setIcon (icon); record.unmark->setData (i); actionmenu_unmark_block->addAction (record.unmark); record.run = ac->addAction ("runblock" + QString::number (i), this, SLOT (runBlock())); ac->setDefaultShortcut (record.run, shortcuts[i]); record.run->setIcon (icon); record.run->setData (i); actionmenu_run_block->addAction (record.run); // these two not strictly needed due to removeBlock(), below. Silences a GCC warning, however. record.range = 0; record.active = false; block_records.append (record); removeBlock (i, true); // initialize to empty } RK_ASSERT (block_records.size () == NUM_BLOCK_RECORDS); } void RKCommandEditorWindow::focusIn (KTextEditor::View* v) { RK_TRACE (COMMANDEDITOR); RK_ASSERT (v == m_view); setPopupMenu (); } /** NOTE: this function still needed? - Still needed in KDE 4.3.4. */ void RKCommandEditorWindow::setPopupMenu () { RK_TRACE (COMMANDEDITOR); if (!getPart ()->factory ()) return; m_view->setContextMenu (static_cast (getPart ()->factory ()->container ("ktexteditor_popup", getPart ()))); } QString RKCommandEditorWindow::fullCaption () { RK_TRACE (COMMANDEDITOR); if (m_doc->url ().isEmpty ()) { return (shortCaption ()); } else { QString cap = m_doc->url ().toDisplayString (QUrl::PreferLocalFile | QUrl::PrettyDecoded); if (isModified ()) cap.append (i18n (" [modified]")); return (cap); } } void RKCommandEditorWindow::closeEvent (QCloseEvent *e) { if (isModified ()) { int status = KMessageBox::warningYesNo (this, i18n ("The document \"%1\" has been modified. Close it anyway?", windowTitle ()), i18n ("File not saved")); if (status != KMessageBox::Yes) { e->ignore (); return; } } QWidget::closeEvent (e); } void RKCommandEditorWindow::setWindowStyleHint (const QString& hint) { RK_TRACE (COMMANDEDITOR); m_view->setStatusBarEnabled (hint != "preview"); RKMDIWindow::setWindowStyleHint (hint); } void RKCommandEditorWindow::copy () { RK_TRACE (COMMANDEDITOR); QApplication::clipboard()->setText (m_view->selectionText ()); } void RKCommandEditorWindow::setReadOnly (bool ro) { RK_TRACE (COMMANDEDITOR); m_doc->setReadWrite (!ro); } void RKCommandEditorWindow::autoSaveHandlerModifiedChanged () { RK_TRACE (COMMANDEDITOR); if (!isModified ()) { autosave_timer.stop (); if (RKSettingsModuleCommandEditor::autosaveKeep ()) return; if (!previous_autosave_url.isValid ()) return; if (previous_autosave_url.isLocalFile ()) { QFile::remove (previous_autosave_url.toLocalFile ()); } else { RKJobSequence* dummy = new RKJobSequence (); dummy->addJob (KIO::del (previous_autosave_url)); connect (dummy, &RKJobSequence::finished, this, &RKCommandEditorWindow::autoSaveHandlerJobFinished); dummy->start (); } previous_autosave_url.clear (); } } void RKCommandEditorWindow::changePreviewMode (QAction *mode) { RK_TRACE (COMMANDEDITOR); if (mode != action_no_preview) { if (!preview_dir) { // triggered on change from no preview to some preview, but not between previews if (KMessageBox::warningContinueCancel (this, i18n ("

The preview feature tries to avoid making any lasting changes to your workspace (technically, by making use of a local() evaluation environment). However, there are cases where using the preview feature may cause unexpected side-effects.

In particular, this is the case for scripts that contain explicit assignments to globalenv(), or to scripts that alter files on your filesystem. Further, attaching/detaching packages or package namespaces will affect the entire running R session.

Please keep this in mind when using the preview feature, and especially when using the feature on scripts originating from untrusted sources.

"), i18n ("Potential side-effects of previews"), KStandardGuiItem::cont (), KStandardGuiItem::cancel (), QStringLiteral ("preview_side_effects")) != KMessageBox::Continue) { discardPreview (); } } preview_manager->setUpdatePending (); preview_timer.start (0); } else { discardPreview (); } } void RKCommandEditorWindow::discardPreview () { RK_TRACE (COMMANDEDITOR); if (preview_dir) { preview->wrapperWidget ()->hide (); preview_manager->setPreviewDisabled (); RKGlobals::rInterface ()->issueCommand (QString (".rk.killPreviewDevice(%1)\nrk.discard.preview.data (%1)").arg (RObject::rQuote(preview_manager->previewId ())), RCommand::App | RCommand::Sync); delete preview_dir; preview_dir = 0; delete preview_input_file; preview_input_file = 0; } action_no_preview->setChecked (true); } void RKCommandEditorWindow::documentSaved () { RK_TRACE (COMMANDEDITOR); if (!action_preview_as_you_type->isChecked ()) { if (!action_no_preview->isChecked ()) { preview_manager->setUpdatePending (); preview_timer.start (0); } } } void RKCommandEditorWindow::textChanged () { RK_TRACE (COMMANDEDITOR); // render preview if (!action_no_preview->isChecked ()) { if (action_preview_as_you_type->isChecked ()) { preview_manager->setUpdatePending (); preview_timer.start (500); // brief delay to buffer keystrokes } } else { discardPreview (); } // auto save if (!isModified ()) return; // may happen after load or undo if (!RKSettingsModuleCommandEditor::autosaveEnabled ()) return; if (!autosave_timer.isActive ()) { autosave_timer.start (RKSettingsModuleCommandEditor::autosaveInterval () * 60 * 1000); } } void RKCommandEditorWindow::doAutoSave () { RK_TRACE (COMMANDEDITOR); RK_ASSERT (isModified ()); QTemporaryFile save (QDir::tempPath () + QLatin1String ("/rkward_XXXXXX") + RKSettingsModuleCommandEditor::autosaveSuffix ()); RK_ASSERT (save.open ()); QTextStream out (&save); out.setCodec ("UTF-8"); // make sure that all characters can be saved, without nagging the user out << m_doc->text (); save.close (); save.setAutoRemove (false); RKJobSequence* alljobs = new RKJobSequence (); // The KJob-Handling below seems to be a bit error-prone, at least for the file-protocol on Windows. // Thus, for the simple case of local files, we use QFile, instead. connect (alljobs, &RKJobSequence::finished, this, &RKCommandEditorWindow::autoSaveHandlerJobFinished); // backup the old autosave file in case something goes wrong during pushing the new one QUrl backup_autosave_url; if (previous_autosave_url.isValid ()) { backup_autosave_url = previous_autosave_url; backup_autosave_url = backup_autosave_url.adjusted(QUrl::RemoveFilename); backup_autosave_url.setPath(backup_autosave_url.path() + backup_autosave_url.fileName () + '~'); if (previous_autosave_url.isLocalFile ()) { QFile::remove (backup_autosave_url.toLocalFile ()); QFile::copy (previous_autosave_url.toLocalFile (), backup_autosave_url.toLocalFile ()); } else { alljobs->addJob (KIO::file_move (previous_autosave_url, backup_autosave_url, -1, KIO::HideProgressInfo | KIO::Overwrite)); } } // push the newly written file if (url ().isValid ()) { QUrl autosave_url = url (); autosave_url = autosave_url.adjusted(QUrl::RemoveFilename); autosave_url.setPath(autosave_url.path() + autosave_url.fileName () + RKSettingsModuleCommandEditor::autosaveSuffix ()); if (autosave_url.isLocalFile ()) { QFile::remove (autosave_url.toLocalFile ()); save.copy (autosave_url.toLocalFile ()); save.remove (); } else { alljobs->addJob (KIO::file_move (QUrl::fromLocalFile (save.fileName ()), autosave_url, -1, KIO::HideProgressInfo | KIO::Overwrite)); } previous_autosave_url = autosave_url; } else { // i.e., the document is still "Untitled" previous_autosave_url = QUrl::fromLocalFile (save.fileName ()); } // remove the backup if (backup_autosave_url.isValid ()) { if (backup_autosave_url.isLocalFile ()) { QFile::remove (backup_autosave_url.toLocalFile ()); } else { alljobs->addJob (KIO::del (backup_autosave_url, KIO::HideProgressInfo)); } } alljobs->start (); // do not create any more autosaves until the text is changed, again autosave_timer.stop (); } void RKCommandEditorWindow::autoSaveHandlerJobFinished (RKJobSequence* seq) { RK_TRACE (COMMANDEDITOR); if (seq->hadError ()) { KMessageBox::detailedError (this, i18n ("An error occurred while trying to create an autosave of the script file '%1':", url ().url ()), "- " + seq->errors ().join ("\n- ")); } } QUrl RKCommandEditorWindow::url () const { // RK_TRACE (COMMANDEDITOR); return (m_doc->url ()); } bool RKCommandEditorWindow::isModified () { RK_TRACE (COMMANDEDITOR); return m_doc->isModified(); } void RKCommandEditorWindow::insertText (const QString &text) { // KDE4: inline? RK_TRACE (COMMANDEDITOR); m_view->insertText (text); setFocus(); } void RKCommandEditorWindow::restoreScrollPosition () { RK_TRACE (COMMANDEDITOR); KTextEditor::Cursor c = saved_scroll_position; c.setLine (qMin (c.line (), m_doc->lines () - 1)); if (c.column () >= m_doc->lineLength (c.line ())) c.setColumn (0); m_view->setCursorPosition (c); } void RKCommandEditorWindow::saveScrollPosition () { RK_TRACE (COMMANDEDITOR); KTextEditor::Cursor c = m_view->cursorPosition (); if (!c.isValid ()) c = KTextEditor::Cursor::start (); saved_scroll_position = c; } void RKCommandEditorWindow::setText (const QString &text) { RK_TRACE (COMMANDEDITOR); bool old_rw = m_doc->isReadWrite (); if (!old_rw) m_doc->setReadWrite (true); m_doc->setText (text); KTextEditor::MarkInterface *markiface = qobject_cast (m_doc); if (markiface) markiface->clearMarks (); if (!old_rw) m_doc->setReadWrite (false); } void RKCommandEditorWindow::highlightLine (int linenum) { RK_TRACE (COMMANDEDITOR); KTextEditor::MarkInterface *markiface = qobject_cast (m_doc); if (!markiface) { RK_ASSERT (markiface); return; } bool old_rw = m_doc->isReadWrite (); if (!old_rw) m_doc->setReadWrite (true); markiface->addMark (linenum, KTextEditor::MarkInterface::Execution); m_view->setCursorPosition (KTextEditor::Cursor (linenum, 0)); if (!old_rw) m_doc->setReadWrite (false); } void RKCommandEditorWindow::updateCaption () { RK_TRACE (COMMANDEDITOR); QString name = url ().fileName (); if (name.isEmpty ()) name = url ().toDisplayString (); if (name.isEmpty ()) name = i18n ("Unnamed"); if (isModified ()) name.append (i18n (" [modified]")); setCaption (name); // Well, these do not really belong, here, but need to happen on pretty much the same occasions: action_setwd_to_script->setEnabled (!url ().isEmpty ()); RKWardMainWindow::getMain ()->addScriptUrl (url ()); } void RKCommandEditorWindow::currentHelpContext (QString *symbol, QString *package) { RK_TRACE (COMMANDEDITOR); Q_UNUSED (package); KTextEditor::Cursor c = m_view->cursorPosition(); QString line = m_doc->line(c.line ()) + ' '; *symbol = RKCommonFunctions::getCurrentSymbol (line, c.column ()); } QString RKCommandEditorWindow::provideContext (int line_rev) { RK_TRACE (COMMANDEDITOR); KTextEditor::Cursor c = m_view->cursorPosition(); int current_line_num=c.line(); int cursor_pos=c.column(); if (line_rev > current_line_num) return QString (); QString ret = m_doc->line (current_line_num - line_rev); if (line_rev == 0) ret = ret.left (cursor_pos); return ret; } void RKCommandEditorWindow::paste (const QString& text) { RK_TRACE (COMMANDEDITOR); m_view->insertText (text); } void RKCommandEditorWindow::setWDToScript () { RK_TRACE (COMMANDEDITOR); RK_ASSERT (!url ().isEmpty ()); QString dir = url ().adjusted (QUrl::RemoveFilename).path (); #ifdef Q_OS_WIN // KURL::directory () returns a leading slash on windows as of KDElibs 4.3 while (dir.startsWith ('/')) dir.remove (0, 1); #endif RKConsole::pipeUserCommand ("setwd (\"" + dir + "\")"); } void RKCommandEditorWindow::runCurrent () { RK_TRACE (COMMANDEDITOR); if (m_view->selection ()) { RKConsole::pipeUserCommand (m_view->selectionText ()); } else { KTextEditor::Cursor c = m_view->cursorPosition(); QString command = m_doc->line (c.line()); if (!command.isEmpty ()) RKConsole::pipeUserCommand (command + '\n'); // advance to next line (NOTE: m_view->down () won't work on auto-wrapped lines) c.setLine(c.line() + 1); m_view->setCursorPosition (c); } } void RKCommandEditorWindow::enterAndSubmit () { RK_TRACE (COMMANDEDITOR); KTextEditor::Cursor c = m_view->cursorPosition (); int line = c.line (); m_doc->insertText (c, "\n"); QString command = m_doc->line (line); if (!command.isEmpty ()) RKConsole::pipeUserCommand (command + '\n'); } void RKCommandEditorWindow::copyLinesToOutput () { RK_TRACE (COMMANDEDITOR); RKCommandHighlighter::copyLinesToOutput (m_view, RKCommandHighlighter::RScript); } void RKCommandEditorWindow::doRenderPreview () { RK_TRACE (COMMANDEDITOR); if (action_no_preview->isChecked ()) return; if (!preview_manager->needsCommand ()) return; int mode = preview_modes->actions ().indexOf (preview_modes->checkedAction ()); if (!preview_dir) { preview_dir = new QTemporaryDir (); preview_input_file = 0; } if (preview_input_file) { // When switching between .Rmd and .R previews, discard input file if ((mode == RMarkdownPreview) != (preview_input_file->fileName().endsWith (".Rmd"))) { delete preview_input_file; preview_input_file = 0; } else { preview_input_file->remove (); // If re-using an existing filename, remove it first. Somehow, contrary to documentation, this does not happen in open(WriteOnly), below. } } if (!preview_input_file) { // NOT an else! if (m_doc->url ().isEmpty () || !m_doc->url ().isLocalFile ()) { preview_input_file = new QFile (QDir (preview_dir->path()).absoluteFilePath (mode == RMarkdownPreview ? "script.Rmd" : "script.R")); } else { // If the file is already saved, save the preview input as a temp file in the same folder. // esp. .Rmd files might try to include other files by relative path. QString tempfiletemplate = m_doc->url ().toLocalFile (); tempfiletemplate.append ("_XXXXXX.rkward_preview.R"); if (mode == RMarkdownPreview) tempfiletemplate.append ("md"); preview_input_file = new QTemporaryFile (tempfiletemplate); } } QString output_file = QDir (preview_dir->path()).absoluteFilePath ("output.html"); // NOTE: not needed by all types of preview if (mode != GraphPreview && !preview->findChild()) { // (lazily) initialize the preview window with _something_, as an RKMDIWindow is needed to display messages (important, if there is an error during the first preview) RKGlobals::rInterface()->issueCommand (".rk.with.window.hints (rk.show.html(" + RObject::rQuote (output_file) + "), \"\", " + RObject::rQuote (preview_manager->previewId ()) + ", style=\"preview\")", RCommand::App | RCommand::Sync); } RK_ASSERT (preview_input_file->open (QIODevice::WriteOnly)); QTextStream out (preview_input_file); out.setCodec ("UTF-8"); // make sure that all characters can be saved, without nagging the user out << m_doc->text (); preview_input_file->close (); QString command; if (mode == RMarkdownPreview) { preview->setLabel (i18n ("Preview of rendered R Markdown")); command = "if (!nzchar(Sys.which(\"pandoc\"))) {\n" " output <- rk.set.output.html.file(%2, silent=TRUE)\n" " rk.header (" + RObject::rQuote (i18n ("Pandoc is not installed")) + ")\n" " rk.print (" + RObject::rQuote (i18n ("The software pandoc, required to rendering R markdown files, is not installed, or not in the system path of " "the running R session. You will need to install pandoc from https://pandoc.org/.
" "If it is installed, but cannot be found, try adding it to the system path of the running R session at " "Settings->Configure RKward->R-backend.")) + ")\n" " rk.set.output.html.file(output, silent=TRUE)\n" "} else {\n" " require(rmarkdown)\n" " rmarkdown::render (%1, output_format=\"html_document\", output_file=%2, quiet=TRUE)\n" "}\n" "rk.show.html(%2)\n"; command = command.arg (RObject::rQuote (preview_input_file->fileName ()), RObject::rQuote (output_file)); } else if (mode == RKOutputPreview) { preview->setLabel (i18n ("Preview of generated RKWard output")); command = "output <- rk.set.output.html.file(%2, silent=TRUE)\n" "try(rk.flush.output(ask=FALSE, style=\"preview\", silent=TRUE))\n" "try(source(%1, local=TRUE))\n" "rk.set.output.html.file(output, silent=TRUE)\n" "rk.show.html(%2)\n"; command = command.arg (RObject::rQuote (preview_input_file->fileName ()), RObject::rQuote (output_file)); } else if (mode == GraphPreview) { preview->setLabel (i18n ("Preview of generated plot")); command = "olddev <- dev.cur()\n" ".rk.startPreviewDevice(%2)\n" "try(source(%1, local=TRUE))\n" "if (olddev != 1) dev.set(olddev)\n"; command = command.arg (RObject::rQuote (preview_input_file->fileName ()), RObject::rQuote (preview_manager->previewId ())); } else if (mode == ConsolePreview) { preview->setLabel (i18n ("Preview of script running in interactive R Console")); command = "output <- rk.set.output.html.file(%2, silent=TRUE)\n" "on.exit(rk.set.output.html.file(output, silent=TRUE))\n" "try(rk.flush.output(ask=FALSE, style=\"preview\", silent=TRUE))\n" "exprs <- expression(NULL)\n" "rk.capture.output(suppress.messages=TRUE)\n" "try({\n" " exprs <- parse (%1, keep.source=TRUE)\n" "})\n" ".rk.cat.output(rk.end.capture.output(TRUE))\n" "for (i in seq_len(length(exprs))) {\n" " rk.print.code(as.character(attr(exprs, \"srcref\")[[i]]))\n" " rk.capture.output(suppress.messages=TRUE)\n" " try({\n" " withAutoprint(exprs[[i]], evaluated=TRUE, echo=FALSE)\n" " })\n" " .rk.cat.output(rk.end.capture.output(TRUE))\n" "}\n" "rk.set.output.html.file(output, silent=TRUE)\n" "rk.show.html(%2)\n"; command = command.arg (RObject::rQuote (preview_input_file->fileName ())).arg (RObject::rQuote (output_file)); } else { RK_ASSERT (false); } preview->wrapperWidget ()->show (); RCommand *rcommand = new RCommand (".rk.with.window.hints (local ({\n" + command + QStringLiteral ("}), \"\", ") + RObject::rQuote (preview_manager->previewId ()) + ", style=\"preview\")", RCommand::App); preview_manager->setCommand (rcommand); } void RKCommandEditorWindow::runAll () { RK_TRACE (COMMANDEDITOR); QString command = m_doc->text (); if (command.isEmpty ()) return; RKConsole::pipeUserCommand (command); } void RKCommandEditorWindow::runBlock () { RK_TRACE (COMMANDEDITOR); QAction* action = qobject_cast(sender ()); if (!action) { RK_ASSERT (false); return; } clearUnusedBlocks (); // this block might have been removed meanwhile int index = action->data ().toInt (); RK_ASSERT ((index >= 0) && (index < block_records.size ())); if (block_records[index].active) { QString command = m_doc->text (*(block_records[index].range)); if (command.isEmpty ()) return; RKConsole::pipeUserCommand (command); } } void RKCommandEditorWindow::markBlock () { RK_TRACE (COMMANDEDITOR); QAction* action = qobject_cast(sender ()); if (!action) { RK_ASSERT (false); return; } int index = action->data ().toInt (); RK_ASSERT ((index >= 0) && (index < block_records.size ())); if (m_view->selection ()) { addBlock (index, m_view->selectionRange ()); } else { RK_ASSERT (false); } } void RKCommandEditorWindow::unmarkBlock () { RK_TRACE (COMMANDEDITOR); QAction* action = qobject_cast(sender ()); if (!action) { RK_ASSERT (false); return; } int index = action->data ().toInt (); RK_ASSERT ((index >= 0) && (index < block_records.size ())); removeBlock (index); } void RKCommandEditorWindow::clearUnusedBlocks () { RK_TRACE (COMMANDEDITOR); for (int i = 0; i < block_records.size (); ++i) { if (block_records[i].active) { // TODO: do we need to check whether the range was deleted? Does the katepart do such evil things? if (block_records[i].range->isEmpty ()) { removeBlock (i, true); } } } } void RKCommandEditorWindow::addBlock (int index, const KTextEditor::Range& range) { RK_TRACE (COMMANDEDITOR); if (!smart_iface) return; // may happen in KDE => 4.6 if compiled with KDE <= 4.4 RK_ASSERT ((index >= 0) && (index < block_records.size ())); clearUnusedBlocks (); removeBlock (index); KTextEditor::MovingRange* srange = smart_iface->newMovingRange (range); srange->setInsertBehaviors (KTextEditor::MovingRange::ExpandRight); QString actiontext = i18n ("%1 (Active)", index + 1); block_records[index].range = srange; srange->setAttribute (block_records[index].attribute); block_records[index].active = true; block_records[index].mark->setText (actiontext); block_records[index].unmark->setText (actiontext); block_records[index].unmark->setEnabled (true); block_records[index].run->setText (actiontext); block_records[index].run->setEnabled (true); } void RKCommandEditorWindow::removeBlock (int index, bool was_deleted) { RK_TRACE (COMMANDEDITOR); if (!smart_iface) return; // may happen in KDE => 4.6 if compiled with KDE <= 4.4 RK_ASSERT ((index >= 0) && (index < block_records.size ())); if (!was_deleted) { delete (block_records[index].range); } QString actiontext = i18n ("%1 (Unused)", index + 1); block_records[index].range = 0; block_records[index].active = false; block_records[index].mark->setText (actiontext); block_records[index].unmark->setText (actiontext); block_records[index].unmark->setEnabled (false); block_records[index].run->setText (actiontext); block_records[index].run->setEnabled (false); } void RKCommandEditorWindow::selectionChanged (KTextEditor::View* view) { RK_TRACE (COMMANDEDITOR); RK_ASSERT (view == m_view); if (view->selection ()) { actionmenu_mark_block->setEnabled (true); } else { actionmenu_mark_block->setEnabled (false); } } //////////////////////// RKFunctionArgHinter ////////////////////////////// #include #include #include "../core/rfunctionobject.h" RKFunctionArgHinter::RKFunctionArgHinter (RKScriptContextProvider *provider, KTextEditor::View* view) { RK_TRACE (COMMANDEDITOR); RKFunctionArgHinter::provider = provider; RKFunctionArgHinter::view = view; const QObjectList children = view->children (); for (QObjectList::const_iterator it = children.constBegin(); it != children.constEnd (); ++it) { QObject *obj = *it; obj->installEventFilter (this); } arghints_popup = new QLabel (0, Qt::ToolTip); arghints_popup->setMargin (2); // HACK trying hard to trick the style into using the correct color // ... and sometimes we still get white on yellow in some styles. Sigh... // A simple heuristic tries to detect the worst cases of unreasonably low contrast, and forces black on light blue, then. QPalette p = QToolTip::palette (); QColor b = p.color (QPalette::Inactive, QPalette::ToolTipBase); QColor f = p.color (QPalette::Inactive, QPalette::ToolTipText); if ((qAbs (f.greenF () - b.greenF ()) + qAbs (f.redF () - b.redF ()) + qAbs (f.yellowF () - b.yellowF ())) < .6) { f = Qt::black; b = QColor (192, 219, 255); } p.setColor (QPalette::Inactive, QPalette::WindowText, f); p.setColor (QPalette::Inactive, QPalette::Window, b); p.setColor (QPalette::Inactive, QPalette::ToolTipText, f); p.setColor (QPalette::Inactive, QPalette::ToolTipBase, b); arghints_popup->setForegroundRole (QPalette::ToolTipText); arghints_popup->setBackgroundRole (QPalette::ToolTipBase); arghints_popup->setPalette (p); arghints_popup->setFrameStyle (QFrame::Box); arghints_popup->setLineWidth (1); arghints_popup->setWordWrap (true); arghints_popup->setWindowOpacity (arghints_popup->style ()->styleHint (QStyle::SH_ToolTipLabel_Opacity, 0, arghints_popup) / 255.0); arghints_popup->hide (); active = false; connect (&updater, &QTimer::timeout, this, &RKFunctionArgHinter::updateArgHintWindow); } RKFunctionArgHinter::~RKFunctionArgHinter () { RK_TRACE (COMMANDEDITOR); delete arghints_popup; } void RKFunctionArgHinter::tryArgHint () { RK_TRACE (COMMANDEDITOR); if (!RKSettingsModuleCommandEditor::argHintingEnabled ()) return; // do this in the next event cycle to make sure any inserted characters have truly been inserted QTimer::singleShot (0, this, SLOT (tryArgHintNow())); } void RKFunctionArgHinter::tryArgHintNow () { RK_TRACE (COMMANDEDITOR); // find the active opening brace int line_rev = -1; QList unclosed_braces; QString full_context; while (unclosed_braces.isEmpty ()) { QString context_line = provider->provideContext (++line_rev); if (context_line.isNull ()) break; full_context.prepend (context_line); for (int i = 0; i < context_line.length (); ++i) { QChar c = context_line.at (i); if (c == '"' || c == '\'' || c == '`') { // NOTE: this algo does not produce good results on string constants spanning newlines. i = RKCommonFunctions::quoteEndPosition (c, context_line, i + 1); if (i < 0) break; continue; } else if (c == '\\') { ++i; continue; } else if (c == '(') { unclosed_braces.append (i); } else if (c == ')') { if (!unclosed_braces.isEmpty()) unclosed_braces.pop_back (); } } } int potential_symbol_end = unclosed_braces.isEmpty () ? -1 : unclosed_braces.last () - 1; // now find out where the symbol to the left of the opening brace ends // there cannot be a line-break between the opening brace, and the symbol name (or can there?), so no need to fetch further context while ((potential_symbol_end >= 0) && full_context.at (potential_symbol_end).isSpace ()) { --potential_symbol_end; } if (potential_symbol_end <= 0) { hideArgHint (); return; } // now identify the symbol and object (if any) QString effective_symbol = RKCommonFunctions::getCurrentSymbol (full_context, potential_symbol_end); if (effective_symbol.isEmpty ()) { hideArgHint (); return; } RObject *object = RObjectList::getObjectList ()->findObject (effective_symbol); if ((!object) || (!object->isType (RObject::Function))) { hideArgHint (); return; } // initialize and show popup arghints_popup->setText (effective_symbol + " (" + static_cast (object)->printArgs () + ')'); arghints_popup->resize (arghints_popup->sizeHint () + QSize (2, 2)); active = true; updater.start (50); updateArgHintWindow (); } void RKFunctionArgHinter::updateArgHintWindow () { RK_TRACE (COMMANDEDITOR); if (!active) return; arghints_popup->move (view->mapToGlobal (view->cursorPositionCoordinates () + QPoint (0, arghints_popup->fontMetrics ().lineSpacing () + arghints_popup->margin ()*2))); if (view->hasFocus ()) arghints_popup->show (); else arghints_popup->hide (); } void RKFunctionArgHinter::hideArgHint () { RK_TRACE (COMMANDEDITOR); arghints_popup->hide (); active = false; updater.stop (); } bool RKFunctionArgHinter::eventFilter (QObject *, QEvent *e) { if (e->type () == QEvent::KeyPress || e->type () == QEvent::ShortcutOverride) { RK_TRACE (COMMANDEDITOR); // avoid loads of empty traces, putting this here QKeyEvent *k = static_cast (e); if (k->key() == Qt::Key_Enter || k->key() == Qt::Key_Return || k->key () == Qt::Key_Up || k->key () == Qt::Key_Down || k->key () == Qt::Key_Left || k->key () == Qt::Key_Right || k->key () == Qt::Key_Home || k->key () == Qt::Key_Tab) { hideArgHint (); } else if (k->key () == Qt::Key_Backspace || k->key () == Qt::Key_Delete){ tryArgHint (); } else { QString text = k->text (); if ((text == "(") || (text == ")") || (text == ",")) { tryArgHint (); } } } return false; } // static KTextEditor::Document* RKCommandHighlighter::_doc = 0; KTextEditor::View* RKCommandHighlighter::_view = 0; KTextEditor::Document* RKCommandHighlighter::getDoc () { if (_doc) return _doc; RK_TRACE (COMMANDEDITOR); KTextEditor::Editor* editor = KTextEditor::Editor::instance (); RK_ASSERT (editor); _doc = editor->createDocument (RKWardMainWindow::getMain ()); // NOTE: A (dummy) view is needed to access highlighting attributes. _view = _doc->createView (0); _view->hide (); RK_ASSERT (_doc); return _doc; } KTextEditor::View* RKCommandHighlighter::getView () { if (!_view) getDoc (); return _view; } #include #include "rkhtmlwindow.h" #include "rkhtmlwindow.h" #include "rkhtmlwindow.h" ////////// // NOTE: Most of the exporting code is copied from the katepart HTML exporter plugin more or less verbatim! (Source license: LGPL v2) ////////// QString exportText(const QString& text, const KTextEditor::Attribute::Ptr& attrib, const KTextEditor::Attribute::Ptr& m_defaultAttribute) { if ( !attrib || !attrib->hasAnyProperty() || attrib == m_defaultAttribute ) { return (text.toHtmlEscaped()); } QString ret; if ( attrib->fontBold() ) { ret.append (""); } if ( attrib->fontItalic() ) { ret.append (""); } bool writeForeground = attrib->hasProperty(QTextCharFormat::ForegroundBrush) && (!m_defaultAttribute || attrib->foreground().color() != m_defaultAttribute->foreground().color()); bool writeBackground = attrib->hasProperty(QTextCharFormat::BackgroundBrush) && (!m_defaultAttribute || attrib->background().color() != m_defaultAttribute->background().color()); if ( writeForeground || writeBackground ) { ret.append (QString("") .arg(writeForeground ? QString(QLatin1String("color:") + attrib->foreground().color().name() + QLatin1Char(';')) : QString()) .arg(writeBackground ? QString(QLatin1String("background:") + attrib->background().color().name() + QLatin1Char(';')) : QString())); } ret.append (text.toHtmlEscaped()); if ( writeBackground || writeForeground ) { ret.append (""); } if ( attrib->fontItalic() ) { ret.append (""); } if ( attrib->fontBold() ) { ret.append (""); } return ret; } QString RKCommandHighlighter::commandToHTML (const QString r_command, HighlightingMode mode) { KTextEditor::Document* doc = getDoc (); KTextEditor::View* view = getView (); doc->setText (r_command); if (r_command.endsWith ('\n')) doc->removeLine (doc->lines () - 1); setHighlighting (doc, mode); QString ret; QString opening; KTextEditor::Attribute::Ptr m_defaultAttribute = view->defaultStyleAttribute (KTextEditor::dsNormal); if ( !m_defaultAttribute ) { opening = "
";
 	} else {
 		opening = QString("
")
 				.arg(m_defaultAttribute->fontBold() ? "font-weight:bold;" : "")
 				.arg(m_defaultAttribute->fontItalic() ? "text-style:italic;" : "");
 				// Note: copying the default text/background colors is pointless in our case, and leads to subtle inconsistencies.
 	}
 
 	const KTextEditor::Attribute::Ptr noAttrib(0);
 
 	if (mode == RScript) ret = opening.arg ("code");
 	enum {
 		Command,
 		Output,
 		Warning,
 		None
 	} previous_chunk = None;
 	for (int i = 0; i < doc->lines (); ++i)
 	{
 		const QString &line = doc->line(i);
 		QList attribs = view->lineAttributes(i);
 		int lineStart = 0;
 
 		if (mode == RInteractiveSession) {
 			if (line.startsWith ("> ") || line.startsWith ("+ ")) {
 				lineStart = 2;	// skip the prompt. Prompt will be indicated by the CSS, instead
 				if (previous_chunk != Command) {
 					if (previous_chunk != None) ret.append ("
"); ret.append (opening.arg ("code")); previous_chunk = Command; } } else { if (previous_chunk != Output) { if (previous_chunk != None) ret.append ("
"); ret.append (opening.arg ("output_normal")); previous_chunk = Output; } ret.append (line.toHtmlEscaped () + '\n'); // don't copy output "highlighting". It is set using CSS, instead continue; } } int handledUntil = lineStart; int remainingChars = line.length(); foreach ( const KTextEditor::AttributeBlock& block, attribs ) { if ((block.start + block.length) <= handledUntil) continue; int start = qMax(block.start, lineStart); if ( start > handledUntil ) { ret += exportText( line.mid( handledUntil, start - handledUntil ), noAttrib, m_defaultAttribute ); } int end = qMin (block.start + block.length, remainingChars); int length = end - start; ret += exportText( line.mid( start, length ), block.attribute, m_defaultAttribute); handledUntil = end; } if ( handledUntil < lineStart + remainingChars ) { ret += exportText( line.mid( handledUntil, remainingChars ), noAttrib, m_defaultAttribute ); } if (i < (doc->lines () - 1)) ret.append ("\n"); } ret.append ("\n"); return ret; } /** set syntax highlighting-mode to R syntax. Outside of class, in order to allow use from the on demand code highlighter */ void RKCommandHighlighter::setHighlighting (KTextEditor::Document *doc, HighlightingMode mode) { RK_TRACE (COMMANDEDITOR); QString mode_string; if (mode == RScript) mode_string = "R Script"; else if (mode == RInteractiveSession) mode_string = "R interactive session"; else { QString fn = doc->url ().fileName ().toLower (); if (fn.endsWith (QLatin1String (".pluginmap")) || fn.endsWith (QLatin1String (".rkh")) || fn.endsWith (QLatin1String (".xml")) || fn.endsWith (QLatin1String (".inc"))) { mode_string = "XML"; } else if (fn.endsWith (QLatin1String (".js"))) { mode_string = "JavaScript"; } else { return; } } if (!(doc->setHighlightingMode (mode_string) && doc->setMode (mode_string))) RK_DEBUG (COMMANDEDITOR, DL_ERROR, "R syntax highlighting definition ('%s')not found!", qPrintable (mode_string)); } void RKCommandHighlighter::copyLinesToOutput (KTextEditor::View *view, HighlightingMode mode) { RK_TRACE (COMMANDEDITOR); // expand selection to full lines (or current line) KTextEditor::Document *doc = view->document (); KTextEditor::Range sel = view->selectionRange (); if (!sel.isValid ()) { KTextEditor::Cursor pos = view->cursorPosition (); sel.setRange (KTextEditor::Cursor (pos.line (), 0), KTextEditor::Cursor (pos.line (), doc->lineLength (pos.line ()))); } else { sel.setRange (KTextEditor::Cursor (sel.start ().line (), 0), KTextEditor::Cursor (sel.end ().line (), doc->lineLength (sel.end ().line ()))); } // highlight and submit QString highlighted = commandToHTML (doc->text (sel), mode); if (!highlighted.isEmpty ()) { RKGlobals::rInterface ()->issueCommand (".rk.cat.output (" + RObject::rQuote (highlighted) + ")\n", RCommand::App | RCommand::Silent); } } diff --git a/rkward/windows/rkworkplace.cpp b/rkward/windows/rkworkplace.cpp index 228ef0e6..28de1f7e 100644 --- a/rkward/windows/rkworkplace.cpp +++ b/rkward/windows/rkworkplace.cpp @@ -1,1174 +1,1176 @@ /*************************************************************************** rkworkplace - description ------------------- begin : Thu Sep 21 2006 copyright : (C) 2006-2020 by Thomas Friedrichsmeier email : thomas.friedrichsmeier@kdemail.net ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #include "rkworkplace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "detachedwindowcontainer.h" #include "rkcommandeditorwindow.h" #include "rkhtmlwindow.h" #include "rkworkplaceview.h" #include "rktoolwindowbar.h" #include "rktoolwindowlist.h" #include "../core/robject.h" #include "../core/rcontainerobject.h" #include "../core/robjectlist.h" #include "../dataeditor/rkeditor.h" #include "../dataeditor/rkeditordataframe.h" #include "../robjectviewer.h" #include "../settings/rksettingsmodulegeneral.h" #include "../settings/rksettingsmodulecommandeditor.h" #include "../rbackend/rkrinterface.h" #include "../windows/rkwindowcatcher.h" #include "../rbackend/rcommand.h" #include "../misc/rkcommonfunctions.h" #include "../rkglobals.h" #include "../rkward.h" #include "../debug.h" // static RKWorkplace *RKWorkplace::main_workplace = 0; RKWorkplace::RKWorkplace (QWidget *parent) : QWidget (parent) { RK_TRACE (APP); RK_ASSERT (main_workplace == 0); main_workplace = this; _workspace_config = 0; window_placement_override = RKMDIWindow::AnyWindowState; // message area message_area = new QWidget (this); QVBoxLayout *message_layout = new QVBoxLayout (message_area); message_layout->setContentsMargins (0, 0, 0, 0); QVBoxLayout *vbox = new QVBoxLayout (this); vbox->setContentsMargins (0, 0, 0, 0); vbox->setSpacing (0); vbox->addWidget (message_area); tool_window_bars[RKToolWindowList::Top] = new RKToolWindowBar (KMultiTabBar::Top, this); vert_splitter = new QSplitter (Qt::Vertical, this); tool_window_bars[RKToolWindowList::Top]->setSplitter (vert_splitter); QWidget *harea = new QWidget (vert_splitter); QHBoxLayout *hbox = new QHBoxLayout (harea); hbox->setContentsMargins (0, 0, 0, 0); hbox->setSpacing (0); vert_splitter->setCollapsible (vert_splitter->indexOf (harea), false); vert_splitter->setStretchFactor (vert_splitter->indexOf (harea), 1); tool_window_bars[RKToolWindowList::Left] = new RKToolWindowBar (KMultiTabBar::Left, harea); horiz_splitter = new QSplitter (Qt::Horizontal, harea); tool_window_bars[RKToolWindowList::Left]->setSplitter (horiz_splitter); wview = new RKWorkplaceView (horiz_splitter); horiz_splitter->setCollapsible (horiz_splitter->indexOf (wview), false); horiz_splitter->setStretchFactor(horiz_splitter->indexOf (wview), 1); tool_window_bars[RKToolWindowList::Right] = new RKToolWindowBar (KMultiTabBar::Right, harea); tool_window_bars[RKToolWindowList::Right]->setSplitter (horiz_splitter); hbox->addWidget (tool_window_bars[RKToolWindowList::Left]); hbox->addWidget (horiz_splitter); hbox->addWidget (tool_window_bars[RKToolWindowList::Right]); tool_window_bars[RKToolWindowList::Bottom] = new RKToolWindowBar (KMultiTabBar::Bottom, this); tool_window_bars[RKToolWindowList::Bottom]->setSplitter (vert_splitter); vbox->addWidget (tool_window_bars[RKToolWindowList::Top]); vbox->addWidget (vert_splitter); vbox->addWidget (tool_window_bars[RKToolWindowList::Bottom]); KConfigGroup toolbar_config = KSharedConfig::openConfig ()->group ("ToolwindowBars"); for (int i = 0; i < TOOL_WINDOW_BAR_COUNT; ++i) tool_window_bars[i]->restoreSize (toolbar_config); history = new RKMDIWindowHistory (this); connect (RKWardMainWindow::getMain (), &RKWardMainWindow::aboutToQuitRKWard, this, &RKWorkplace::saveSettings); } RKWorkplace::~RKWorkplace () { RK_TRACE (APP); delete _workspace_config; // closeAll (); // not needed, as the windows will autodelete themselves using QObject mechanism. Of course, closeAll () should be called *before* quitting. for (int i = 0; i < windows.size (); ++i) { disconnect (windows[i], 0, this, 0); } } void RKWorkplace::addMessageWidget (KMessageWidget* message) { RK_TRACE (APP); message_area->layout ()->addWidget (message); topLevelWidget ()->show (); topLevelWidget ()->raise (); } QString workspaceConfigFileName (const QUrl &url) { QString base_name = QString (QCryptographicHash::hash (url.toDisplayString ().toUtf8 (), QCryptographicHash::Md5).toHex()); QDir dir (QStandardPaths::writableLocation (QStandardPaths::GenericDataLocation)); dir.mkpath ("rkward"); return (dir.absoluteFilePath ("rkward/workspace_config_" + base_name)); } KConfigBase *RKWorkplace::workspaceConfig () { if (!_workspace_config) { RK_TRACE (APP); _workspace_config = new KConfig (workspaceConfigFileName (workspaceURL ())); } return _workspace_config; } QString RKWorkplace::portableUrl (const QUrl &url) { QUrl ret = url; if (url.scheme () == workspaceURL ().scheme () && url.authority () == workspaceURL ().authority ()) { QString relative = QDir (workspaceURL ().path ()).relativeFilePath (url.path ()); ret = QUrl (relative); } // QUrl::toDisplayString () used here, for the side effect of stripping credentials return QUrl (ret).adjusted (QUrl::NormalizePathSegments).toDisplayString (); } void RKWorkplace::setWorkspaceURL (const QUrl &url, bool keep_config) { RK_TRACE (APP); if (url != current_url) { current_url = url; if (keep_config && _workspace_config) { KConfig * _new_config = _workspace_config->copyTo (workspaceConfigFileName (workspaceURL ())); delete _workspace_config; _workspace_config = _new_config; } else { delete _workspace_config; _workspace_config = 0; } emit (workspaceUrlChanged (url)); } } void RKWorkplace::saveSettings () { RK_TRACE (APP); KConfigGroup toolbar_config = KSharedConfig::openConfig ()->group ("ToolwindowBars"); for (int i = 0; i < TOOL_WINDOW_BAR_COUNT; ++i) tool_window_bars[i]->saveSize (toolbar_config); } void RKWorkplace::initActions (KActionCollection *ac) { RK_TRACE (APP); wview->initActions (ac); } void RKWorkplace::attachWindow (RKMDIWindow *window) { RK_TRACE (APP); RK_ASSERT (windows.contains (window)); // This should not happen for now. if (!window->isAttached ()) { QWidget *old_parent = window->parentWidget (); window->prepareToBeAttached (); if (old_parent && qobject_cast (old_parent)) { old_parent->deleteLater (); } } // all the rest is done, even if the window was previously "Attached", as this may also mean it was freshly created window->state = RKMDIWindow::Attached; if (window->isToolWindow ()) { if (!window->tool_window_bar) placeInToolWindowBar (window, RKToolWindowList::Bottom); else window->tool_window_bar->reclaimDetached (window); } else { view ()->addWindow (window); view ()->topLevelWidget ()->raise (); view ()->topLevelWidget ()->activateWindow (); } RK_ASSERT (window->getPart ()); RKWardMainWindow::getMain ()->partManager ()->addPart (window->getPart ()); } void RKWorkplace::detachWindow (RKMDIWindow *window, bool was_attached) { RK_TRACE (APP); if (!window) return; RK_ASSERT (windows.contains (window)); // Can't detach a window that is not registered window->prepareToBeDetached (); window->state = RKMDIWindow::Detached; RK_ASSERT (window->getPart ()); if (was_attached) { RKWardMainWindow::getMain ()->partManager ()->removePart (window->getPart ()); if (!window->isToolWindow ()) view ()->removeWindow (window); } DetachedWindowContainer *detached = new DetachedWindowContainer (window, was_attached); detached->show (); if (!was_attached) window->activate (); } void RKWorkplace::addWindow (RKMDIWindow *window, bool attached) { RK_TRACE (APP); // first handle placement overrides if (window_placement_override == RKMDIWindow::Attached) { if (!attached) window->state = RKMDIWindow::Detached; // Ok, yeah. BAD style. But windows that would go to detached by default would not prepareToBeAttached(), without this. // TODO: Create third state: NotManaged attached = true; } else if (window_placement_override == RKMDIWindow::Detached) { attached = false; } // style override. Windows may or may not handle this if (!window_style_override.isEmpty ()) window->setWindowStyleHint (window_style_override); // next handle name overrides, if any if (!window_name_override.isEmpty ()) { int pos = -1; for (int i = 0; i < named_windows.size (); ++i) { if (named_windows[i].id == window_name_override) { pos = i; break; } } if (pos < 0) { // not yet known: implicit registration -> create corresponding named_window_spec on the fly. registerNamedWindow (window_name_override, 0, attached ? RKWardMainWindow::getMain () : 0); pos = named_windows.size () - 1; } NamedWindow &nw = named_windows[pos]; if (nw.window != window) { if (nw.window) { // kill existing window (going to be replaced) // TODO: this is not really elegant, yet, as it will change tab-placement (for attached windows), and discard / recreate container (for detached windows) disconnect (nw.window, &QObject::destroyed, this, &RKWorkplace::namedWindowDestroyed); nw.window->deleteLater (); } nw.window = window; connect (nw.window, &QObject::destroyed, this, &RKWorkplace::namedWindowDestroyed); } named_windows[pos] = nw; // add window in the correct area if (nw.parent == RKWardMainWindow::getMain ()) attached = true; else if (nw.parent == 0) attached = false; else { // custom parent window->prepareToBeAttached (); window->setParent (nw.parent); // TODO: do this is somewhat inconsistent. But such windows are not attached to the main workplace view, which makes them rather behave detached. window->state = RKMDIWindow::Detached; // NOTE: The window is _not_ added to the window list/window history in this case. return; } } windows.append (window); connect (window, &QObject::destroyed, this, &RKWorkplace::removeWindow); connect (window, &RKMDIWindow::windowActivated, history, &RKMDIWindowHistory::windowActivated); if (window->isToolWindow () && !window->tool_window_bar) return; if (attached) attachWindow (window); else detachWindow (window, false); } void RKWorkplace::placeToolWindows() { RK_TRACE (APP); foreach (const RKToolWindowList::ToolWindowRepresentation& rep, RKToolWindowList::registeredToolWindows ()) { placeInToolWindowBar (rep.window, rep.default_placement); getHistory ()->popLastWindow (rep.window); // windows send a spurious activation signal triggered from KPartsManager::addPart(), so we pop them, again } } void RKWorkplace::placeInToolWindowBar (RKMDIWindow *window, int position) { RK_TRACE (APP); RK_ASSERT (window->isToolWindow ()); bool needs_registration = (!window->tool_window_bar && (position != RKToolWindowList::Nowhere)); if ((position < 0) || (position >= TOOL_WINDOW_BAR_COUNT)) { RK_ASSERT (position == RKToolWindowList::Nowhere); // should never happen... position = RKToolWindowList::Nowhere; // ... but let's set this explicitly, in case of a broken workplace representation } if (position == RKToolWindowList::Nowhere) { if (window->tool_window_bar) window->tool_window_bar->removeWidget (window); } else { tool_window_bars[position]->addWidget (window); } if (!windows.contains (window)) addWindow (window, true); // first time we see this window else if (needs_registration) attachWindow (window); } void RKWorkplace::setWindowPlacementOverrides(const QString& placement, const QString& name, const QString& style) { RK_TRACE (APP); if (placement == "attached") window_placement_override = RKMDIWindow::Attached; else if (placement == "detached") window_placement_override = RKMDIWindow::Detached; else { RK_ASSERT (placement.isEmpty ()); window_placement_override = RKMDIWindow::AnyWindowState; } window_name_override = name; window_style_override = style; } void RKWorkplace::registerNamedWindow (const QString& id, QObject* owner, QWidget* parent, RKMDIWindow* window) { RK_TRACE (APP); NamedWindow nw; nw.id = id; nw.owner = owner; nw.parent = parent; nw.window = window; for (int i = 0; i < named_windows.size (); ++i) { RK_ASSERT (named_windows[i].id != id); } named_windows.append (nw); if (owner) connect (owner, &QObject::destroyed, this, &RKWorkplace::namedWindowOwnerDestroyed); if (window) connect (window, &QObject::destroyed, this, &RKWorkplace::namedWindowOwnerDestroyed); } RKMDIWindow* RKWorkplace::getNamedWindow (const QString& id) { RK_TRACE (APP); for (int i = 0; i < named_windows.size (); ++i) { if (named_windows[i].id == id) { return named_windows[i].window; } } return 0; } void RKWorkplace::namedWindowDestroyed (QObject* window) { RK_TRACE (APP); for (int i = 0; i < named_windows.size (); ++i) { if (named_windows[i].window == window) { if (!named_windows[i].owner) { named_windows.removeAt (i); return; } else { named_windows[i].window = 0; } } } } void RKWorkplace::namedWindowOwnerDestroyed (QObject* owner) { RK_TRACE (APP); for (int i = 0; i < named_windows.size (); ++i) { if (named_windows[i].owner == owner) { if (named_windows[i].window) { named_windows[i].window->deleteLater (); } named_windows.removeAt (i); return; } } } bool RKWorkplace::openAnyUrl (const QUrl &url, const QString &known_mimetype, bool force_external) { RK_TRACE (APP); if (url.scheme () == "rkward") { if (RKHTMLWindow::handleRKWardURL (url)) return true; } QMimeDatabase mdb; QMimeType mimetype; if (!known_mimetype.isEmpty ()) mimetype = mdb.mimeTypeForName (known_mimetype); else mimetype = mdb.mimeTypeForUrl (url); if (!force_external) { // NOTE: Currently a known mimetype implies that the URL is local or served from the local machine. // Thus, external web pages are *not* opened, here. Which is the behavior we want, although the implementation is ugly if (mimetype.inherits ("text/html")) { openHelpWindow (url, true); return true; // TODO } if (url.fileName ().toLower ().endsWith (QLatin1String (".rdata")) || url.fileName ().toLower ().endsWith (QLatin1String (".rda"))) { RKWardMainWindow::getMain ()->askOpenWorkspace (url); return true; // TODO } if (mimetype.inherits ("text/plain")) { return (openScriptEditor (url, QString ())); } RK_DEBUG (APP, DL_INFO, "Don't know how to handle mimetype %s.", qPrintable (mimetype.name ())); } if (KMessageBox::questionYesNo (this, i18n ("The url you are trying to open ('%1') is not a local file or the filetype is not supported by RKWard. Do you want to open the url in the default application?", url.toDisplayString ()), i18n ("Open in default application?")) != KMessageBox::Yes) { return false; } KRun *runner = new KRun (url, topLevelWidget()); // according to KRun-documentation, KRun will self-destruct when done. runner->setRunExecutables (false); return false; } RKMDIWindow* RKWorkplace::openScriptEditor (const QUrl &url, const QString& encoding, int flags, const QString &force_caption) { RK_TRACE (APP); // is this url already opened? if (!url.isEmpty ()) { RKWorkplaceObjectList script_windows = getObjectList (RKMDIWindow::CommandEditorWindow, RKMDIWindow::AnyWindowState); for (int i = 0; i < script_windows.size (); ++i) { QUrl ourl = static_cast (script_windows[i])->url (); if (url == ourl) { if (view ()->windowInActivePane (script_windows[i])) { script_windows[i]->activate (); return (script_windows[i]); } } } } RKCommandEditorWindow *editor = new RKCommandEditorWindow (view (), url, encoding, flags); if (!force_caption.isEmpty ()) editor->setCaption (force_caption); addWindow (editor); return (editor); } RKMDIWindow* RKWorkplace::openHelpWindow (const QUrl &url, bool only_once) { RK_TRACE (APP); if (url.isEmpty ()) { RK_ASSERT (false); return 0; } if (only_once) { RKWorkplaceObjectList help_windows = getObjectList (RKMDIWindow::HelpWindow, RKMDIWindow::AnyWindowState); for (int i = 0; i < help_windows.size (); ++i) { if (static_cast (help_windows[i])->url ().matches (url, QUrl::StripTrailingSlash | QUrl::NormalizePathSegments)) { if (view ()->windowInActivePane (help_windows[i])) { help_windows[i]->activate (); return (help_windows[i]); } } } } // if we're working with a window hint, try to _reuse_ the existing window, even if it did not get found, above if (!window_name_override.isEmpty ()) { int pos = -1; for (int i = 0; i < named_windows.size (); ++i) { if (named_windows[i].id == window_name_override) { RKHTMLWindow *w = dynamic_cast (named_windows[i].window); if (w) { w->openURL (url); // w->activate (); // HACK: Keep preview windows from stealing focus return w; } break; } } } RKHTMLWindow *hw = new RKHTMLWindow (view (), RKHTMLWindow::HTMLHelpWindow); hw->openURL (url); addWindow (hw); return (hw); } RKMDIWindow* RKWorkplace::openOutputWindow (const QUrl &url) { RK_TRACE (APP); QList owins = RKOutputWindowManager::self ()->existingOutputWindows (); for (int i = 0; i < owins.size (); ++i) { if (view ()->windowInActivePane (owins[i])) { owins[i]->activate (); return (owins[i]); } } RKHTMLWindow* ret = RKOutputWindowManager::self ()->newOutputWindow (); addWindow (ret); return (ret); } void RKWorkplace::newX11Window (QWindow* window_to_embed, int device_number) { RK_TRACE (APP); RKCaughtX11Window *window = new RKCaughtX11Window (window_to_embed, device_number); window->state = RKMDIWindow::Detached; addWindow (window, false); } void RKWorkplace::newRKWardGraphisWindow (RKGraphicsDevice* dev, int device_number) { RK_TRACE (APP); RKCaughtX11Window *window = new RKCaughtX11Window (dev, device_number); window->state = RKMDIWindow::Detached; addWindow (window, false); } RKMDIWindow* RKWorkplace::newObjectViewer (RObject *object) { RK_TRACE (APP); RK_ASSERT (object); RKWorkplaceObjectList object_windows = getObjectList (RKMDIWindow::ObjectWindow, RKMDIWindow::AnyWindowState); for (int i = 0; i < object_windows.size (); ++i) { RObjectViewer *viewer = static_cast (object_windows[i]); if (viewer->object () == object) { if (view ()->windowInActivePane (viewer)) { viewer->activate (); return viewer; } } } RObjectViewer *ov = new RObjectViewer (view (), object); addWindow (ov); return ov; } bool RKWorkplace::canEditObject (RObject *object) { RK_TRACE (APP); if (object->isDataFrame ()) { return true; } else if (object->isVariable () && object->parentObject ()->isDataFrame ()) { return true; } return false; } RKEditor* RKWorkplace::editNewDataFrame (const QString &name) { RK_TRACE (APP); RKEditorDataFrame* ed = new RKEditorDataFrame (name, 0); addWindow (ed); ed->activate (); return ed; } RKEditor *RKWorkplace::editObject (RObject *object) { RK_TRACE (APP); RK_ASSERT (object); RObject *iobj = object; if (!iobj->isDataFrame ()) { if (iobj->isVariable () && iobj->parentObject ()->isDataFrame ()) { iobj = iobj->parentObject (); } else { return 0; } } RKEditor *ed = 0; QList existing_editors = object->editors (); for (int i = 0; i < existing_editors.size (); ++i) { RObject *eobj = existing_editors[i]->getObject (); if (eobj == iobj) { if (view ()->windowInActivePane (existing_editors[i])) { ed = existing_editors[i]; break; } } } if (!ed) { unsigned long size = 1; foreach (int dim, iobj->getDimensions ()) { size *= dim; } if ((RKSettingsModuleGeneral::warnLargeObjectThreshold () != 0) && (size > RKSettingsModuleGeneral::warnLargeObjectThreshold ())) { if (KMessageBox::warningContinueCancel (view (), i18n ("You are about to edit object \"%1\", which is very large (%2 fields). RKWard is not optimized to handle very large objects in the built in data editor. This will use a lot of memory, and - depending on your system - might be very slow. For large objects it is generally recommended to edit using command line means or to split into smaller chunks before editing. On the other hand, if you have enough memory, or the data is simple enough (numeric data is easier to handle, than factor), editing may not be a problem at all. You can configure this warning (or turn it off entirely) under Settings->Configure RKWard->General.\nReally edit object?", iobj->getFullName (), size), i18n ("About to edit very large object")) != KMessageBox::Continue) { return 0; } } ed = new RKEditorDataFrame (static_cast (iobj), 0); addWindow (ed); } ed->activate (); return ed; } void RKWorkplace::flushAllData () { RK_TRACE (APP); for (RKWorkplaceObjectList::const_iterator it = windows.constBegin (); it != windows.constEnd (); ++it) { if ((*it)->type == RKMDIWindow::DataEditorWindow) { static_cast (*it)->flushChanges (); } } } bool RKWorkplace::closeWindow (RKMDIWindow *window) { RK_TRACE (APP); RK_ASSERT (windows.contains (window)); bool tool_window = window->isToolWindow (); bool closed = window->close (true); // all the rest should happen in removeWindow () if (closed && tool_window) windowRemoved (); // for regular windows, this happens in removeWindow(), already return closed; } void RKWorkplace::closeActiveWindow () { RK_TRACE (APP); RKMDIWindow *w = activeWindow (RKMDIWindow::Attached); if (w) closeWindow (w); } RKWorkplace::RKWorkplaceObjectList RKWorkplace::getObjectList (int type, int state) { RK_TRACE (APP); RKWorkplaceObjectList ret; for (RKWorkplaceObjectList::const_iterator it = windows.constBegin (); it != windows.constEnd (); ++it) { if (((*it)->type & type) && ((*it)->state & state)) { ret.append ((*it)); } } return ret; } void RKWorkplace::closeAll (int type, int state) { RK_TRACE (APP); closeWindows (getObjectList (type, state)); } bool RKWorkplace::closeWindows (QList windows) { RK_TRACE (APP); bool allclosed = true; RKWardMainWindow::getMain ()->lockGUIRebuild (true); for (int i = windows.size () - 1; i >= 0; --i) { if (!closeWindow (windows[i])) allclosed = false; } RKWardMainWindow::getMain ()->lockGUIRebuild (false); return allclosed; } void RKWorkplace::removeWindow (QObject *object) { RK_TRACE (APP); RKMDIWindow *window = static_cast (object); // remove from history first (otherwise, we might find it there, when trying to activate a new window) history->removeWindow (window); // WARNING: the window is dead. Don't call any functions on it. RK_ASSERT (windows.contains (window)); windows.removeAll (window); // do this first! view()->removeWindow will call activePage() indirectly from setCaption, causing us to iterate over all known windows! if (view ()->hasWindow (window)) view ()->removeWindow (window, true); windowRemoved (); } void RKWorkplace::windowRemoved () { RK_TRACE (APP); if (activeWindow (RKMDIWindow::AnyWindowState) != 0) return; // some RKMDIWindow is already active QWidget *appWin = QApplication::activeWindow (); if (appWin && appWin != RKWardMainWindow::getMain () && !qobject_cast (appWin)) return; // a dialog window or the like is active // try to activate an attached document window, first RKMDIWindow *window = view ()->activePage (); if (window) { window->activate (true); return; } // some document window in the history? Try that. window = history->previousDocumentWindow (); if (window) { window->activate (true); return; } // now try to activate an attached (tool) window, if one is visible for (RKWorkplaceObjectList::const_iterator it = windows.constBegin (); it != windows.constEnd (); ++it) { if ((*it)->isAttached ()) { if ((*it)->isVisible ()) { (*it)->activate (true); return; } } } // nothing, yet? Try *any* visible window for (RKWorkplaceObjectList::const_iterator it = windows.constBegin (); it != windows.constEnd (); ++it) { if ((*it)->isVisible ()) { (*it)->activate (true); return; } } // give up } RKMDIWindow *RKWorkplace::activeWindow (RKMDIWindow::State state) { RK_TRACE (APP); RKMDIWindow *ret = 0; for (RKWorkplaceObjectList::const_iterator it = windows.constBegin (); it != windows.constEnd (); ++it) { if (!(state & ((*it)->state))) continue; if ((*it)->isActive ()) { ret = *it; break; } } return (ret); } QUrl checkAdjustRestoredUrl (const QString &_url, const QString old_base) { QUrl url = QUrl::fromUserInput (_url, QString (), QUrl::AssumeLocalFile); if (old_base.isEmpty ()) return (url); QUrl new_base_url = RKWorkplace::mainWorkplace ()->workspaceURL ().adjusted (QUrl::RemoveFilename | QUrl::NormalizePathSegments); if (new_base_url.isEmpty ()) return (url); QUrl old_base_url (old_base); if (old_base_url == new_base_url) return (url); // TODO: Should we also care about non-local files? In theory: yes, but stat'ing remote files for existence can take a long time. if (!(old_base_url.isLocalFile () && new_base_url.isLocalFile () && url.isLocalFile ())) return (url); // if the file exists, unadjusted, return it. if (QFileInfo (url.toLocalFile ()).exists ()) return (url); // check whether a file exists for the adjusted url QString relative = QDir (new_base_url.path ()).absoluteFilePath (QDir (old_base_url.path ()).relativeFilePath (url.path ())); if (QFileInfo (relative).exists ()) return (QUrl::fromLocalFile (relative)); return (url); } QString RKWorkplace::makeItemDescription (RKMDIWindow *win) const { QString type, specification; QStringList params; if (win->isType (RKMDIWindow::DataEditorWindow)) { type = "data"; specification = static_cast (win)->getObject ()->getFullName (); } else if (win->isType (RKMDIWindow::CommandEditorWindow)) { type = "script"; specification = static_cast (win)->url ().url (); if (specification.isEmpty ()) specification = static_cast (win)->id (); } else if (win->isType (RKMDIWindow::OutputWindow)) { type = "output"; specification = static_cast (win)->url ().url (); } else if (win->isType (RKMDIWindow::HelpWindow)) { type = "help"; specification = static_cast (win)->restorableUrl ().url (); } else if (win->isToolWindow ()) { type = RKToolWindowList::idOfWindow (win); } else if (win->isType (RKMDIWindow::ObjectWindow)) { type = "object"; specification = static_cast (win)->object ()->getFullName (); } if (!type.isEmpty ()) { if (!win->isAttached ()) { params.append (QString ("detached,") + QString::number (win->x ()) + ',' + QString::number (win->y ()) + ',' + QString::number (win->width ()) + ',' + QString::number (win->height ())); } if (win->isToolWindow ()) { int sidebar = RKToolWindowList::Nowhere; for (int i = 0; i < TOOL_WINDOW_BAR_COUNT; ++i) { if (win->tool_window_bar == tool_window_bars[i]) { sidebar = i; break; } } params.append (QString ("sidebar,") + QString::number (sidebar)); } return (type + "::" + params.join (":") + "::" + specification); } return QString (); } struct ItemSpecification { QString type; QString specification; QStringList params; }; ItemSpecification parseItemDescription (const QString &description) { ItemSpecification ret; // Item format for rkward <= 0.5.4: "type:specification" // Item format for rkward <= 0.5.5: "type::[optional_params1[:optional_params2[:...]]]::specification" int typeend = description.indexOf (':'); if ((typeend < 0) || (typeend >= (description.size () - 1))) { RK_ASSERT (false); return ret; } ret.type = description.left (typeend); if (description.at (typeend + 1) == ':') { // rkward 0.5.5 or later int specstart = description.indexOf ("::", typeend + 2); if (specstart < typeend) { RK_ASSERT (false); return ret; } ret.params = description.mid (typeend + 2, specstart - typeend - 2).split (':', QString::SkipEmptyParts); ret.specification = description.mid (specstart + 2); } else { ret.specification = description.mid (typeend + 1); } return ret; } RKMDIWindow* restoreDocumentWindowInternal (RKWorkplace* wp, ItemSpecification spec, const QString &base) { RK_TRACE (APP); RKMDIWindow *win = 0; if (spec.type == "data") { RObject *object = RObjectList::getObjectList ()->findObject (spec.specification); if (object) win = wp->editObject (object); } else if (spec.type == "script") { QUrl url = checkAdjustRestoredUrl (spec.specification, base); win = wp->openScriptEditor (url, QString ()); } else if (spec.type == "output") { win = wp->openOutputWindow (checkAdjustRestoredUrl (spec.specification, base)); } else if (spec.type == "help") { win = wp->openHelpWindow (checkAdjustRestoredUrl (spec.specification, base), true); } else if (spec.type == "object") { RObject *object = RObjectList::getObjectList ()->findObject (spec.specification); if (object) win = wp->newObjectViewer (object); } return win; } bool RKWorkplace::restoreDocumentWindow (const QString &description, const QString &base) { RK_TRACE (APP); return (restoreDocumentWindowInternal (this, parseItemDescription (description), base) != 0); } QStringList RKWorkplace::makeWorkplaceDescription () { RK_TRACE (APP); QStringList workplace_description; // first, save the base directory of the workplace. This allows us to cope better with moved workspaces while restoring. QUrl base_url = workspaceURL ().adjusted (QUrl::RemoveFilename); if (base_url.isLocalFile () && !base_url.isEmpty ()) workplace_description.append ("base::::" + base_url.url ()); // window order in the workplace view may have changed with respect to our list. Thus we first generate a properly sorted list RKWorkplaceObjectList list = getObjectList (RKMDIWindow::DocumentWindow, RKMDIWindow::Detached); foreach (RKMDIWindow *win, list) { QString desc = makeItemDescription (win); if (!desc.isEmpty ()) workplace_description.append (desc); } workplace_description.append (QStringLiteral ("layout::::") + wview->listLayout ()); workplace_description.append (wview->listContents ()); list = getObjectList (RKMDIWindow::ToolWindow, RKMDIWindow::AnyWindowState); foreach (RKMDIWindow *win, list) { QString desc = makeItemDescription (win); if (!desc.isEmpty ()) workplace_description.append (desc); } return workplace_description; } void RKWorkplace::saveWorkplace (RCommandChain *chain) { RK_TRACE (APP); // TODO: This is still a mess. All workplace-related settings, including the workspaceConfig(), should be saved to a single place, and in // standard KConfig format. if (RKSettingsModuleGeneral::workplaceSaveMode () != RKSettingsModuleGeneral::SaveWorkplaceWithWorkspace) return; RKGlobals::rInterface ()->issueCommand ("rk.save.workplace(description=" + RObject::rQuote (makeWorkplaceDescription().join ("\n")) + ')', RCommand::App, i18n ("Save Workplace layout"), 0, 0, chain); } void RKWorkplace::restoreWorkplace (RCommandChain *chain, bool merge) { RK_TRACE (APP); if (RKSettingsModuleGeneral::workplaceSaveMode () != RKSettingsModuleGeneral::SaveWorkplaceWithWorkspace) return; QString no_close_windows; if (merge) no_close_windows = "close.windows = FALSE"; RKGlobals::rInterface ()->issueCommand ("rk.restore.workplace(" + no_close_windows + ')', RCommand::App, i18n ("Restore Workplace layout"), 0, 0, chain); } void RKWorkplace::restoreWorkplace (const QStringList &description) { RK_TRACE (APP); RKWardMainWindow::getMain ()->lockGUIRebuild (true); QString base; for (int i = 0; i < description.size (); ++i) { ItemSpecification spec = parseItemDescription (description[i]); RKMDIWindow *win = 0; if (spec.type == "base") { RK_ASSERT (i == 0); base = spec.specification; } else if (restoreDocumentWindowInternal (this, spec, base)) { // it was restored. nothing else to do } else if (spec.type == "layout") { view ()->restoreLayout (spec.specification); } else if (spec.type == "pane_end") { view ()->nextPane (); } else { win = RKToolWindowList::findToolWindowById (spec.type); RK_ASSERT (win); } // apply generic window parameters if (win) { for (int p = 0; p < spec.params.size (); ++p) { if (spec.params[p].startsWith (QLatin1String ("sidebar"))) { int position = spec.params[p].section (',', 1).toInt (); placeInToolWindowBar (win, position); } if (spec.params[p].startsWith (QLatin1String ("detached"))) { QStringList geom = spec.params[p].split (','); win->hide (); win->setGeometry (geom.value (1).toInt (), geom.value (2).toInt (), geom.value (3).toInt (), geom.value (4).toInt ()); detachWindow (win); } } } } view ()->purgeEmptyPanes (); RKWardMainWindow::getMain ()->lockGUIRebuild (false); } void RKWorkplace::splitAndAttachWindow (RKMDIWindow* source) { RK_TRACE (APP); RK_ASSERT (source); if (source->isType (RKMDIWindow::CommandEditorWindow)) { QUrl url = static_cast (source)->url (); openScriptEditor (url, QString ()); } else if (source->isType (RKMDIWindow::HelpWindow)) { openHelpWindow (static_cast (source)->url ()); } else if (source->isType (RKMDIWindow::OutputWindow)) { openOutputWindow (static_cast (source)->url ()); } else if (source->isType (RKMDIWindow::DataEditorWindow)) { editObject (static_cast (source)->getObject ()); } else if (source->isType (RKMDIWindow::ObjectWindow)) { newObjectViewer (static_cast (source)->object ()); } else { openHelpWindow (QUrl ("rkward://page/rkward_split_views")); } } ///////////////////////// END RKWorkplace //////////////////////////// ///////////////////// BEGIN RKMDIWindowHistory /////////////////////// #include "../misc/rkstandardicons.h" #include class RKMDIWindowHistoryWidget : public QListWidget { public: RKMDIWindowHistoryWidget () : QListWidget (0) { RK_TRACE (APP); current = 0; setFocusPolicy (Qt::StrongFocus); setWindowFlags (Qt::Popup); } ~RKMDIWindowHistoryWidget () { RK_TRACE (APP); } void update (const QList windows) { RK_TRACE (APP); clear (); _windows = windows; for (int i = windows.count () - 1; i >= 0; --i) { // most recent top RKMDIWindow *win = windows[i]; QListWidgetItem *item = new QListWidgetItem (this); item->setIcon (RKStandardIcons::iconForWindow (win)); item->setText (win->windowTitle ()); } if (current >= count ()) current = count () - 1; if (current < 0) { hide (); return; } setCurrentRow (current); } void next () { RK_TRACE (APP); if (--current < 0) current = count () - 1; setCurrentRow (current); } void prev () { RK_TRACE (APP); if (++current >= count ()) current = 0; setCurrentRow (current); } private: void focusOutEvent (QFocusEvent *) override { RK_TRACE (APP); deleteLater (); } void keyReleaseEvent (QKeyEvent *ev) override { RK_TRACE (APP); if (ev->modifiers () == Qt::NoModifier) { commit (); } } void mouseReleaseEvent (QMouseEvent *ev) override { RK_TRACE (APP); // HACK to get by without slots, and the associated moc'ing QListWidget::mouseReleaseEvent (ev); commit (); } void commit () { RK_TRACE (APP); current = currentRow (); if ((current > 0) && (current < count ())) { RKMDIWindow *win = _windows.value (count () - 1 - current); RK_ASSERT (win); win->activate (true); } deleteLater (); } int current; QList _windows; }; RKMDIWindowHistory::RKMDIWindowHistory (QObject *parent) : QObject (parent) { RK_TRACE (APP); switcher = 0; } RKMDIWindowHistory::~RKMDIWindowHistory () { RK_TRACE (APP); RK_DEBUG (APP, DL_DEBUG, "Remaining windows in history: %d", recent_windows.count ()); } void RKMDIWindowHistory::windowActivated (RKMDIWindow *window) { RK_TRACE (APP); if (!window) return; if (!recent_windows.isEmpty () && (window == recent_windows.last ())) return; // update lists recent_windows.removeAll (window); // remove dupes recent_windows.append (window); updateSwitcher (); + + emit activeWindowChanged (window); } void RKMDIWindowHistory::next (QAction* prev_action, QAction *next_action) { RK_TRACE (APP); if (recent_windows.isEmpty ()) return; getSwitcher (prev_action, next_action)->next (); } void RKMDIWindowHistory::prev (QAction* prev_action, QAction *next_action) { RK_TRACE (APP); if (recent_windows.isEmpty ()) return; getSwitcher (prev_action, next_action)->prev (); } RKMDIWindow* RKMDIWindowHistory::previousDocumentWindow () { RK_TRACE (APP); for (int i = recent_windows.count () - 1; i >= 0; --i) { if (!recent_windows[i]->isToolWindow ()) return (recent_windows[i]); } return 0; } void RKMDIWindowHistory::updateSwitcher () { RK_TRACE (APP); if (switcher) switcher->update (recent_windows); } void RKMDIWindowHistory::removeWindow (RKMDIWindow *window) { RK_TRACE (APP); recent_windows.removeAll (window); updateSwitcher (); } RKMDIWindowHistoryWidget* RKMDIWindowHistory::getSwitcher (QAction* prev_action, QAction *next_action) { RK_TRACE (APP); if (switcher) return switcher; switcher = new RKMDIWindowHistoryWidget (); connect (switcher, &QObject::destroyed, this, &RKMDIWindowHistory::switcherDestroyed); switcher->addAction (prev_action); switcher->addAction (next_action); switcher->update (recent_windows); switcher->show (); QWidget *act = QApplication::activeWindow (); if (act) { int center_x = act->x () + act->width () / 2; int center_y = act->y () + act->height () / 2; switcher->move (center_x - switcher->width () / 2, center_y - switcher->height () / 2); } else { RK_ASSERT (false); } switcher->setFocus (); return switcher; } void RKMDIWindowHistory::switcherDestroyed () { RK_TRACE (APP); RK_ASSERT (switcher); switcher = 0; } void RKMDIWindowHistory::popLastWindow (RKMDIWindow* match) { RK_TRACE (APP); if (recent_windows.isEmpty ()) return; else if (recent_windows.last () == match) recent_windows.removeLast (); updateSwitcher (); } diff --git a/rkward/windows/rkworkplace.h b/rkward/windows/rkworkplace.h index 25e5e7c6..52ec1c53 100644 --- a/rkward/windows/rkworkplace.h +++ b/rkward/windows/rkworkplace.h @@ -1,258 +1,260 @@ /*************************************************************************** rkworkplace - description ------------------- begin : Thu Sep 21 2006 copyright : (C) 2006-2020 by Thomas Friedrichsmeier email : thomas.friedrichsmeier@kdemail.net ***************************************************************************/ /*************************************************************************** * * * 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. * * * ***************************************************************************/ #ifndef RKWORKPLACE_H #define RKWORKPLACE_H #include #include #include #include #include #include #include #include "rkmdiwindow.h" #include "rktoolwindowlist.h" class RObject; class RCommandChain; class RKWorkplaceView; class RKEditor; class KActionCollection; class QAction; class RKToolWindowBar; class RKMDIWindowHistoryWidget; class RKGraphicsDevice; class KMessageWidget; class QWindow; #define TOOL_WINDOW_BAR_COUNT 4 /** Simple class to store the history of recently used RKMDIWindow */ class RKMDIWindowHistory : public QObject { Q_OBJECT public: explicit RKMDIWindowHistory (QObject *parent); ~RKMDIWindowHistory (); void removeWindow (RKMDIWindow *window); /** pops the last window from the list, if it matches the given pointer */ void popLastWindow (RKMDIWindow *match); RKMDIWindow *previousDocumentWindow (); void next (QAction *prev_action, QAction *next_action); void prev (QAction *prev_action, QAction *next_action); public slots: void windowActivated (RKMDIWindow *window); +signals: + void activeWindowChanged (RKMDIWindow *window); private slots: void switcherDestroyed (); private: void updateSwitcher (); QList recent_windows; RKMDIWindowHistoryWidget *switcher; RKMDIWindowHistoryWidget *getSwitcher (QAction *prev_action, QAction *next_action); }; struct RKCommandEditorFlags { /** Or'able enum of flags to pass the RKCommandEditorWindow c'tor. Logically, these would belong to the RKCommandEditor-class, but are defined, here for technical reasons (trouble including texteditor includes from some places). */ enum Flags { DefaultToRHighlighting = 1, ///< Apply R highlighting, also if the url is empty ForceRHighlighting = 1 << 1,///< Apply R highlighting, even if the url does not match R script extension UseCodeHinting = 1 << 2, ///< The file is (probably) an editable R script file, and should show code hints ReadOnly = 1 << 3, ///< Open the file in read-only mode DeleteOnClose = 1 << 4, ///< The file to show should be deleted when closing the window. Only respected with read_only=true VisibleToKTextEditorPlugins = 1 << 5, DefaultFlags = DefaultToRHighlighting | UseCodeHinting | VisibleToKTextEditorPlugins }; }; /** This class (only one instance will probably be around) keeps track of which windows are opened in the workplace, which are detached, etc. Also it is responsible for creating and manipulating those windows. It also provides a QWidget (RKWorkplace::view ()), which actually manages the document windows (only those, so far. I.e. this is a half-replacement for KMdi, which will be gone in KDE 4). Currently layout of the document windows is always tabbed. */ class RKWorkplace : public QWidget { Q_OBJECT public: /** ctor. @param parent: The parent widget for the workspace view (see view ()) */ explicit RKWorkplace (QWidget *parent); ~RKWorkplace (); void initActions (KActionCollection *ac); /** @returns a pointer to the view of the workplace. Since possibly the workplace layout might change, better not rely on this pointer being valid for long */ RKWorkplaceView *view () { return wview; }; /** convenience typedef: A list of RKMDIWindow s */ typedef QList RKWorkplaceObjectList; /** Returns a list of all windows in the workplace. */ RKWorkplaceObjectList getObjectList () { return windows; }; /** Returns a list of all windows with a given type and state */ RKWorkplaceObjectList getObjectList (int type, int state=RKMDIWindow::AnyWindowState); /** Attach an already created window. */ void attachWindow (RKMDIWindow *window); /** Dettach a window (it is removed from the view (), and placed in a top-level DetachedWindowContainer instead. */ void detachWindow (RKMDIWindow *window, bool was_attached=true); /** @returns a pointer to the current window. state specifies, which windows should be considered. */ RKMDIWindow *activeWindow (RKMDIWindow::State state); /** Opens the given url in the appropriate way. */ bool openAnyUrl (const QUrl &url, const QString &known_mimetype = QString (), bool force_external=false); /** Opens a new script editor @param url URL to load. Default option is to open an empty document @param encoding encoding to use. If QString (), the default encoding is used. @param use_r_highlighting Force R highlighting mode? Default is no @param read_only Open the document read only? Default is false, i.e. Read-write @param force_caption Usually the caption is determined from the url of the file. If you specify a non-empty string here, that is used instead. @returns false if a local url could not be opened, true for all remote urls, and on success */ RKMDIWindow* openScriptEditor (const QUrl &url=QUrl (), const QString& encoding=QString (), int flags = RKCommandEditorFlags::DefaultFlags, const QString &force_caption = QString ()); /** Opens a new help window, starting at the given url @param url URL to open @param only_once if true, checks whether any help window already shows this URL. If so, raise it, but do not open a new window. Else show the new window */ RKMDIWindow* openHelpWindow (const QUrl &url=QUrl (), bool only_once=false); /** Opens a new output window, or raise / refresh the current output window. @param url currently ignored! */ RKMDIWindow* openOutputWindow (const QUrl &url=QUrl ()); void newX11Window (QWindow* window_to_embed, int device_number); void newRKWardGraphisWindow (RKGraphicsDevice *dev, int device_number); RKMDIWindow* newObjectViewer (RObject *object); /** @returns true if there is a known editor for this type of object, false otherwise */ bool canEditObject (RObject *object); /** Creates a new editor of an appropriate type, and loads the given object into the editor @param object object to edit @returns a pointer to the editor */ RKEditor* editObject (RObject *object); /** Creates a new data.frame with the given name, and loads it in an editor. @see editObject() @param name Name of the data.frame to create @returns a pointer to the editor */ RKEditor* editNewDataFrame (const QString& name); /** tell all DataEditorWindow s to synchronize changes to the R backend // TODO: add RCommandChain parameter */ void flushAllData (); /** Close the active (attached) window. Safe to call even if there is no current active window (no effect in that case) */ void closeActiveWindow (); /** Close the given window, whether it is attached or detached. @param window window to close @returns true, if the window was actually closed (not cancelled) */ bool closeWindow (RKMDIWindow *window); /** Close the given windows, whether they are attached or detached. TODO: Be smart about asking what to save. @param windows list windows to close @returns true, if _all_ windows were actually closed. */ bool closeWindows (QList windows); /** Closes all windows of the given type(s). Default call (no arguments) closes all windows @param type: A bitwise OR of RKWorkplaceObjectType @param state: A bitwise OR of RKWorkplaceObjectState */ void closeAll (int type=RKMDIWindow::AnyType, int state=RKMDIWindow::AnyWindowState); /** Write a description of all current windows to the R backend. This can later be read by restoreWorkplace (). Has no effect, if RKSettingsModuleGeneral::workplaceSaveMode () != RKSettingsModuleGeneral::SaveWorkplaceWithWorkspace @param chain command chain to place the command in */ void saveWorkplace (RCommandChain *chain=0); /** Load a description of windows from the R backend (created by saveWorkplace ()), and (try to) restore all windows accordingly Has no effect, if RKSettingsModuleGeneral::workplaceSaveMode () != RKSettingsModuleGeneral::SaveWorkplaceWithWorkspace @param chain command chain to place the command in */ void restoreWorkplace (RCommandChain *chain=0, bool merge=false); /** Like the other restoreWorkplace (), but takes the description as a parameter rather than reading from the R workspace. To be used, when RKSettingsModuleGeneral::workplaceSaveMode () == RKSettingsModuleGeneral::SaveWorkplaceWithSeesion @param description workplace description */ void restoreWorkplace (const QStringList &description); QStringList makeWorkplaceDescription (); QString makeItemDescription (RKMDIWindow *) const; /** Restore a document window given its description. Returns true, if a window was restored, false otherwise (e.g. invalid/unsupported description). */ bool restoreDocumentWindow (const QString &description, const QString &base=QString ()); /** In the current design there is only ever one workplace. Use this static function to reference it. @returns a pointer to the workplace */ static RKWorkplace *mainWorkplace () { return main_workplace; }; static RKMDIWindowHistory *getHistory () { return main_workplace->history; }; void placeToolWindows (); void setWorkspaceURL (const QUrl &url, bool keep_config=false); QUrl workspaceURL () const { return current_url; }; KConfigBase *workspaceConfig (); QString portableUrl (const QUrl &url); /** Register a named area where to place MDI windows. For directing preview windows to a specific location. */ void registerNamedWindow (const QString& id, QObject *owner, QWidget* parent, RKMDIWindow *window=0); /** Return the window in the specified named area (can be 0). */ RKMDIWindow *getNamedWindow (const QString& id); /** Make the next window to be created appear in a specific location (can be a named window). * @note It is the caller's responsibility to clear the override (by calling setWindowPlacementOverride ()) after the window in question has been created. */ void setWindowPlacementOverrides (const QString& placement=QString (), const QString& name=QString (), const QString& style=QString ()); /** Inserts the given message widget above the central area. While technically, the workplace becomes the parent widget of the message widget, it is the caller's responsibility to * delete the widget, when appropriate. */ void addMessageWidget (KMessageWidget *message); /** For window splitting: Copy the given window (or, if that is not possible, create a placeholder window), and attach it to the main view. */ void splitAndAttachWindow (RKMDIWindow *source); signals: /** emitted when the workspace Url has changed */ void workspaceUrlChanged (const QUrl &url); public slots: /** When windows are attached to the workplace, their QObject::destroyed () signal is connected to this slot. Thereby deleted objects are removed from the workplace automatically */ void removeWindow (QObject *window); void saveSettings (); private slots: void namedWindowDestroyed (QObject *); void namedWindowOwnerDestroyed (QObject *); private: QUrl current_url; KConfig *_workspace_config; /** current list of windows. @See getObjectList () */ RKWorkplaceObjectList windows; /** the view. @See view () */ RKWorkplaceView *wview; /** Internal function to add an existing window to the list of mdi windows */ void addWindow (RKMDIWindow *window, bool attached=true); /** static pointer to the workplace. @See mainWorkplace () */ static RKWorkplace *main_workplace; /** a window was removed. Try to activate some other window. */ void windowRemoved (); RKMDIWindowHistory *history; QSplitter *horiz_splitter; QSplitter *vert_splitter; QWidget *message_area; RKToolWindowBar* tool_window_bars[TOOL_WINDOW_BAR_COUNT]; friend class RKToolWindowBar; friend class KatePluginIntegrationWindow; void placeInToolWindowBar (RKMDIWindow *window, int position); /** Control placement of windows from R */ struct NamedWindow { /** "Owner" of this named window. If non-0, and the owner gets destroyed, the named window record gets closed, too. */ QObject *owner; /** Where to place window, if it does not exist, yet. Can be one of 0: detached, RKWardMainWindow::getMain(): attached, any other: docked in a special region */ QPointer parent; /** Pointer to window, if it exists, already */ RKMDIWindow* window; /** Identifier string */ QString id; }; QList named_windows; RKMDIWindow::State window_placement_override; QString window_name_override; QString window_style_override; }; #endif