diff --git a/rkward/settings/rksettingsmodulecommandeditor.cpp b/rkward/settings/rksettingsmodulecommandeditor.cpp index c624843d..8f6d5c82 100644 --- a/rkward/settings/rksettingsmodulecommandeditor.cpp +++ b/rkward/settings/rksettingsmodulecommandeditor.cpp @@ -1,225 +1,259 @@ /*************************************************************************** rksettingsmodulecommandeditor - description ------------------- begin : Tue Oct 23 2007 - copyright : (C) 2007, 2010, 2011 by Thomas Friedrichsmeier + copyright : (C) 2007-2019 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 "rksettingsmodulecommandeditor.h" #include #include #include #include #include #include #include #include #include +#include #include "../misc/rkspinbox.h" #include "../misc/rkcommonfunctions.h" +#include "../core/robject.h" #include "../rkglobals.h" #include "../debug.h" // static members int RKSettingsModuleCommandEditor::completion_min_chars; int RKSettingsModuleCommandEditor::completion_timeout; bool RKSettingsModuleCommandEditor::completion_enabled; +int RKSettingsModuleCommandEditor::completion_options; bool RKSettingsModuleCommandEditor::arghinting_enabled; bool RKSettingsModuleCommandEditor::autosave_enabled; bool RKSettingsModuleCommandEditor::autosave_keep; int RKSettingsModuleCommandEditor::autosave_interval; int RKSettingsModuleCommandEditor::num_recent_files; QString RKSettingsModuleCommandEditor::script_file_filter; RKSettingsModuleCommandEditor::RKSettingsModuleCommandEditor (RKSettings *gui, QWidget *parent) : RKSettingsModule (gui, parent) { RK_TRACE (SETTINGS); QVBoxLayout* main_vbox = new QVBoxLayout (this); QLabel *label = new QLabel (i18n ("Settings marked with (*) do not take effect until you restart RKWard"), this); label->setWordWrap (true); main_vbox->addWidget (label); main_vbox->addSpacing (2 * RKGlobals::spacingHint ()); QGroupBox* group = new QGroupBox (i18n ("Code Completion"), this); QVBoxLayout* box_layout = new QVBoxLayout (group); completion_enabled_box = new QCheckBox (i18n ("Enable code completion"), group); completion_enabled_box->setChecked (completion_enabled); connect (completion_enabled_box, &QCheckBox::stateChanged, this, &RKSettingsModuleCommandEditor::settingChanged); box_layout->addWidget (completion_enabled_box); box_layout->addSpacing (RKGlobals::spacingHint ()); label = new QLabel (i18n ("Minimum number of characters before completion is attempted"), group); label->setWordWrap (true); completion_min_chars_box = new RKSpinBox (group); completion_min_chars_box->setIntMode (1, INT_MAX, completion_min_chars); completion_min_chars_box->setEnabled (completion_enabled); connect (completion_min_chars_box, static_cast(&QSpinBox::valueChanged), this, &RKSettingsModuleCommandEditor::settingChanged); box_layout->addWidget (label); box_layout->addWidget (completion_min_chars_box); main_vbox->addSpacing (RKGlobals::spacingHint ()); label = new QLabel (i18n ("Timeout (milliseconds) before completion is attempted"), group); label->setWordWrap (true); completion_timeout_box = new RKSpinBox (group); completion_timeout_box->setIntMode (0, INT_MAX, completion_timeout); completion_timeout_box->setEnabled (completion_enabled); connect (completion_timeout_box, static_cast(&QSpinBox::valueChanged), this, &RKSettingsModuleCommandEditor::settingChanged); box_layout->addWidget (label); box_layout->addWidget (completion_timeout_box); + label = new QLabel (i18nc ("Note: list() and data.frame() are programming terms in R, and should not be translated, here", "Operator for access to members of list() and data.frame() objects")); + label->setWordWrap (true); + completion_list_member_operator_box = new QComboBox (group); + completion_list_member_operator_box->addItem (i18n ("'$'-operator (list$member)")); + completion_list_member_operator_box->addItem (i18n ("'[['-operator (list[[\"member\"]])")); + completion_list_member_operator_box->setCurrentIndex ((completion_options & RObject::DollarExpansion) ? 0 : 1); + connect (completion_list_member_operator_box, static_cast(&QComboBox::currentIndexChanged), this, &RKSettingsModuleCommandEditor::settingChanged); + box_layout->addWidget (label); + box_layout->addWidget (completion_list_member_operator_box); + + label = new QLabel (i18n ("Include environment for objects on the search path:")); + label->setWordWrap (true); + completion_object_qualification_box = new QComboBox (group); + completion_object_qualification_box->addItem (i18n ("For masked objects, only")); + completion_object_qualification_box->addItem (i18n ("For objects outside of .GlobalEnv, only")); + completion_object_qualification_box->addItem (i18n ("Always")); + if (completion_options & (RObject::IncludeEnvirIfNotGlobalEnv)) { + if (completion_options & (RObject::IncludeEnvirIfNotGlobalEnv)) completion_object_qualification_box->setCurrentIndex (2); + else completion_object_qualification_box->setCurrentIndex (1); + } + connect (completion_object_qualification_box, static_cast(&QComboBox::currentIndexChanged), this, &RKSettingsModuleCommandEditor::settingChanged); + box_layout->addWidget (label); + box_layout->addWidget (completion_object_qualification_box); + main_vbox->addWidget (group); arghinting_enabled_box = new QCheckBox (i18n ("Enable function argument hinting"), group); arghinting_enabled_box->setChecked (arghinting_enabled); connect (arghinting_enabled_box, &QCheckBox::stateChanged, this, &RKSettingsModuleCommandEditor::settingChanged); main_vbox->addWidget (arghinting_enabled_box); main_vbox->addSpacing (2 * RKGlobals::spacingHint ()); group = autosave_enabled_box = new QGroupBox (i18n ("Autosaves"), this); autosave_enabled_box->setCheckable (true); autosave_enabled_box->setChecked (autosave_enabled); connect (autosave_enabled_box, &QGroupBox::toggled, this, &RKSettingsModuleCommandEditor::settingChanged); box_layout = new QVBoxLayout (group); label = new QLabel (i18n ("Autosave interval (minutes)"), group); autosave_interval_box = new RKSpinBox (group); autosave_interval_box->setIntMode (1, INT_MAX, autosave_interval); connect (autosave_interval_box, static_cast(&QSpinBox::valueChanged), this, &RKSettingsModuleCommandEditor::settingChanged); box_layout->addWidget (label); box_layout->addWidget (autosave_interval_box); box_layout->addSpacing (RKGlobals::spacingHint ()); autosave_keep_box = new QCheckBox (i18n ("Keep autosave file after manual save"), group); autosave_keep_box->setChecked (autosave_keep); connect (autosave_keep_box, &QCheckBox::stateChanged, this, &RKSettingsModuleCommandEditor::settingChanged); box_layout->addWidget (autosave_keep_box); main_vbox->addWidget (group); main_vbox->addSpacing (2 * RKGlobals::spacingHint ()); group = new QGroupBox (i18n ("Opening script files"), this); box_layout = new QVBoxLayout (group); label = new QLabel (i18n ("Number of scripts in recent file lists (*)"), group); num_recent_files_box = new RKSpinBox (group); num_recent_files_box->setIntMode (1, INT_MAX, num_recent_files); RKCommonFunctions::setTips (i18n ("

The number of recent files to remember (in the Open Recent R Script File menu).

") + RKCommonFunctions::noteSettingsTakesEffectAfterRestart (), num_recent_files_box, label); connect (num_recent_files_box, static_cast(&QSpinBox::valueChanged), this, &RKSettingsModuleCommandEditor::settingChanged); box_layout->addWidget (label); box_layout->addWidget (num_recent_files_box); box_layout->addSpacing (RKGlobals::spacingHint ()); label = new QLabel (i18n ("R script file filters (separated by spaces)"), group); script_file_filter_box = new QLineEdit (group); script_file_filter_box->setText (script_file_filter); RKCommonFunctions::setTips (i18n ("

A list of filters (file name extensions) that should be treated as R script files. Most importantly, files matching one of these filters will always be opened with R syntax highlighting.

Filters are case insensitive.

"), script_file_filter_box, label); connect (script_file_filter_box, &QLineEdit::textChanged, this, &RKSettingsModuleCommandEditor::settingChanged); box_layout->addWidget (label); box_layout->addWidget (script_file_filter_box); box_layout->addSpacing (RKGlobals::spacingHint ()); main_vbox->addWidget (group); main_vbox->addStretch (); } RKSettingsModuleCommandEditor::~RKSettingsModuleCommandEditor () { RK_TRACE (SETTINGS); } void RKSettingsModuleCommandEditor::settingChanged () { RK_TRACE (SETTINGS); change (); completion_timeout_box->setEnabled (completion_enabled_box->isChecked ()); completion_min_chars_box->setEnabled (completion_enabled_box->isChecked ()); } QString RKSettingsModuleCommandEditor::caption () { RK_TRACE (SETTINGS); return (i18n ("Script editor")); } void RKSettingsModuleCommandEditor::applyChanges () { RK_TRACE (SETTINGS); completion_enabled = completion_enabled_box->isChecked (); completion_min_chars = completion_min_chars_box->intValue (); completion_timeout = completion_timeout_box->intValue (); arghinting_enabled = arghinting_enabled_box->isChecked (); + completion_options = 0; + if (completion_list_member_operator_box->currentIndex () == 0) completion_options += RObject::DollarExpansion; + if (completion_object_qualification_box->currentIndex () == 2) completion_options += RObject::IncludeEnvirForGlobalEnv | RObject::IncludeEnvirIfNotGlobalEnv; + else if (completion_object_qualification_box->currentIndex () == 1) completion_options += RObject::IncludeEnvirIfNotGlobalEnv; + else completion_options += RObject::IncludeEnvirIfMasked; autosave_enabled = autosave_enabled_box->isChecked (); autosave_keep = autosave_keep_box->isChecked (); autosave_interval = autosave_interval_box->intValue (); num_recent_files = num_recent_files_box->intValue (); script_file_filter = script_file_filter_box->text (); } void RKSettingsModuleCommandEditor::save (KConfig *config) { RK_TRACE (SETTINGS); saveSettings (config); } void RKSettingsModuleCommandEditor::saveSettings (KConfig *config) { RK_TRACE (SETTINGS); KConfigGroup cg = config->group ("Command Editor Windows"); cg.writeEntry ("Completion enabled", completion_enabled); cg.writeEntry ("Completion min chars", completion_min_chars); cg.writeEntry ("Completion timeout", completion_timeout); + cg.writeEntry ("Completion option flags", completion_options); cg.writeEntry ("Argument hinting enabled", arghinting_enabled); cg.writeEntry ("Autosave enabled", autosave_enabled); cg.writeEntry ("Autosave keep saves", autosave_keep); cg.writeEntry ("Autosave interval", autosave_interval); cg.writeEntry ("Max number of recent files", num_recent_files); cg.writeEntry ("Script file filter", script_file_filter); } void RKSettingsModuleCommandEditor::loadSettings (KConfig *config) { RK_TRACE (SETTINGS); KConfigGroup cg = config->group ("Command Editor Windows"); completion_enabled = cg.readEntry ("Completion enabled", true); completion_min_chars = cg.readEntry ("Completion min chars", 2); completion_timeout = cg.readEntry ("Completion timeout", 500); + completion_options = cg.readEntry ("Completion option flags", RObject::DollarExpansion | RObject::IncludeEnvirIfMasked); arghinting_enabled = cg.readEntry ("Argument hinting enabled", true); autosave_enabled = cg.readEntry ("Autosave enabled", true); autosave_keep = cg.readEntry ("Autosave keep saves", false); autosave_interval = cg.readEntry ("Autosave interval", 5); num_recent_files = cg.readEntry ("Max number of recent files", 10); script_file_filter = cg.readEntry ("Script file filter", "*.R *.S *.q *.Rhistory"); } // static bool RKSettingsModuleCommandEditor::matchesScriptFileFilter (const QString &filename) { RK_TRACE (SETTINGS); const QStringList exts = script_file_filter.split (' '); foreach (const QString& ext, exts) { QRegExp reg (ext, Qt::CaseInsensitive, QRegExp::Wildcard); if (reg.exactMatch (filename)) return true; } return false; } diff --git a/rkward/settings/rksettingsmodulecommandeditor.h b/rkward/settings/rksettingsmodulecommandeditor.h index bed059df..9cdbfa33 100644 --- a/rkward/settings/rksettingsmodulecommandeditor.h +++ b/rkward/settings/rksettingsmodulecommandeditor.h @@ -1,88 +1,93 @@ /*************************************************************************** rksettingsmodulecommandeditor - description ------------------- begin : Tue Oct 23 2007 - copyright : (C) 2007-2018 by Thomas Friedrichsmeier + copyright : (C) 2007-2019 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 RKSETTINGSMODULECOMMANDEDITOR_H #define RKSETTINGSMODULECOMMANDEDITOR_H #include "rksettingsmodule.h" class RKSpinBox; class QCheckBox; class QLineEdit; class QGroupBox; +class QComboBox; /** configuration for the Command Editor windows @author Thomas Friedrichsmeier */ class RKSettingsModuleCommandEditor : public RKSettingsModule { Q_OBJECT public: RKSettingsModuleCommandEditor (RKSettings *gui, QWidget *parent); ~RKSettingsModuleCommandEditor (); void applyChanges () override; void save (KConfig *config) override; static void saveSettings (KConfig *config); static void loadSettings (KConfig *config); static void validateSettingsInteractive (QList*) {}; QString caption () override; /// min number of character to try code completion static int completionMinChars () { return completion_min_chars; }; static int completionTimeout () { return completion_timeout; }; static bool completionEnabled () { return completion_enabled; }; static bool argHintingEnabled () { return arghinting_enabled; }; + static int completionOptions () { return completion_options; }; static bool autosaveEnabled () { return autosave_enabled; }; static bool autosaveKeep () { return autosave_keep; }; static int autosaveInterval () { return autosave_interval; }; static QString autosaveSuffix () { return ".rkward_autosave"; }; static int maxNumRecentFiles () { return num_recent_files; }; static QString scriptFileFilter () { return script_file_filter; }; static bool matchesScriptFileFilter (const QString &filename); public slots: void settingChanged (); private: static int completion_min_chars; static int completion_timeout; static bool completion_enabled; static bool arghinting_enabled; + static int completion_options; RKSpinBox* completion_min_chars_box; RKSpinBox* completion_timeout_box; QCheckBox* completion_enabled_box; QCheckBox* arghinting_enabled_box; + QComboBox* completion_list_member_operator_box; + QComboBox* completion_object_qualification_box; static bool autosave_enabled; static bool autosave_keep; static int autosave_interval; QGroupBox* autosave_enabled_box; QCheckBox* autosave_keep_box; RKSpinBox* autosave_interval_box; RKSpinBox* num_recent_files_box; QLineEdit* script_file_filter_box; static int num_recent_files; static QString script_file_filter; }; #endif diff --git a/rkward/windows/rkcommandeditorwindow.cpp b/rkward/windows/rkcommandeditorwindow.cpp index 577fabfc..218f85cc 100644 --- a/rkward/windows/rkcommandeditorwindow.cpp +++ b/rkward/windows/rkcommandeditorwindow.cpp @@ -1,1535 +1,1568 @@ /*************************************************************************** rkcommandeditorwindow - description ------------------- begin : Mon Aug 30 2004 copyright : (C) 2004-2019 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 "../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 "../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; RKCommandEditorWindow::RKCommandEditorWindow (QWidget *parent, const QUrl _url, const QString& encoding, bool use_r_highlighting, bool use_codehinting, bool read_only, bool delete_on_close) : 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; // 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 = editor->createDocument (RKWardMainWindow::getMain ()); _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 = editor->createDocument (RKWardMainWindow::getMain ()); 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 decoding. 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 = editor->createDocument (RKWardMainWindow::getMain ()); // 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 (!delete_on_close) { // 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 (read_only); if (delete_on_close) { if (read_only) { 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); 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); completion_model = 0; cc_iface = 0; hinter = 0; if (use_r_highlighting) { RKCommandHighlighter::setHighlighting (m_doc, RKCommandHighlighter::RScript); if (use_codehinting) { cc_iface = qobject_cast (m_view); if (cc_iface) { cc_iface->setAutomaticInvocationEnabled (true); completion_model = new RKCodeCompletionModel (this); completion_timer = new QTimer (this); completion_timer->setSingleShot (true); connect (completion_timer, &QTimer::timeout, this, &RKCommandEditorWindow::tryCompletion); connect (m_doc, &KTextEditor::Document::textChanged, this, &RKCommandEditorWindow::tryCompletionProxy); } else { RK_ASSERT (false); } 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 ()) { delete 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 ()); } void RKCommandEditorWindow::tryCompletionProxy (KTextEditor::Document*) { if (RKSettingsModuleCommandEditor::completionEnabled ()) { if (cc_iface && cc_iface->isCompletionActive ()) { tryCompletion (); } else if (cc_iface) { completion_timer->start (RKSettingsModuleCommandEditor::completionTimeout ()); } } } QString RKCommandEditorWindow::currentCompletionWord () const { RK_TRACE (COMMANDEDITOR); // KDE4 TODO: This may no longer be needed, if the katepart gets fixed not to abort completions when the range // contains dots or other special characters KTextEditor::Cursor c = m_view->cursorPosition(); if (!c.isValid ()) return QString (); uint para=c.line(); uint cursor_pos=c.column(); QString current_line = m_doc->line (para); if (current_line.lastIndexOf ("#", cursor_pos) >= 0) return QString (); // do not hint while in comments return RKCommonFunctions::getCurrentSymbol (current_line, cursor_pos, false); } KTextEditor::Range RKCodeCompletionModel::completionRange (KTextEditor::View *view, const KTextEditor::Cursor &position) { if (!position.isValid ()) return KTextEditor::Range (); QString current_line = view->document ()->line (position.line ()); int start; int end; RKCommonFunctions::getCurrentSymbolOffset (current_line, position.column (), false, &start, &end); return KTextEditor::Range (position.line (), start, position.line (), end); } void RKCommandEditorWindow::tryCompletion () { // TODO: merge this with RKConsole::doTabCompletion () somehow RK_TRACE (COMMANDEDITOR); if ((!cc_iface) || (!completion_model)) { RK_ASSERT (false); return; } KTextEditor::Cursor c = m_view->cursorPosition(); uint para=c.line(); int cursor_pos=c.column(); QString current_line = m_doc->line (para); int start; int end; RKCommonFunctions::getCurrentSymbolOffset (current_line, cursor_pos, false, &start, &end); if (end > cursor_pos) return; // Only hint when at the end of a word/symbol: https://mail.kde.org/pipermail/rkward-devel/2015-April/004122.html KTextEditor::Range range = KTextEditor::Range (para, start, para, end); QString word; if (range.isValid ()) word = m_doc->text (range); if (current_line.lastIndexOf ("#", cursor_pos) >= 0) word.clear (); // do not hint while in comments if (word.length () >= RKSettingsModuleCommandEditor::completionMinChars ()) { completion_model->updateCompletionList (word); if (completion_model->isEmpty ()) { cc_iface->abortCompletion (); } else { if (!cc_iface->isCompletionActive ()) { cc_iface->startCompletion (range, completion_model); } } } else { cc_iface->abortCompletion (); } } 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 1: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; } //////////////////////// RKCodeCompletionModel //////////////////// RKCodeCompletionModel::RKCodeCompletionModel (RKCommandEditorWindow *parent) : KTextEditor::CodeCompletionModel (parent) { RK_TRACE (COMMANDEDITOR); setRowCount (0); command_editor = parent; + setHasGroups (true); } RKCodeCompletionModel::~RKCodeCompletionModel () { RK_TRACE (COMMANDEDITOR); } void RKCodeCompletionModel::updateCompletionList (const QString& symbol) { RK_TRACE (COMMANDEDITOR); if (current_symbol == symbol) return; // already up to date beginResetModel (); RObject::ObjectList matches; QStringList objectpath = RObject::parseObjectPath (symbol); if (!objectpath.isEmpty () && !objectpath[0].isEmpty ()) { // Skip completion, if the current symbol is '""' (or another empty quote), for instance matches = RObjectList::getObjectList ()->findObjectsMatching (symbol); } // copy the map to two lists. For one thing, we need an int indexable storage, for another, caching this information is safer // in case objects are removed while the completion mode is active. int count = matches.size (); icons.clear (); icons.reserve (count); - names = RObject::getFullNames (matches, RObject::IncludeEnvirIfMasked); + names = RObject::getFullNames (matches, RKSettingsModuleCommandEditor::completionOptions()); for (int i = 0; i < count; ++i) { icons.append (RKStandardIcons::iconForObject (matches[i])); } - setRowCount (count); current_symbol = symbol; endResetModel (); } void RKCodeCompletionModel::completionInvoked (KTextEditor::View*, const KTextEditor::Range&, InvocationType) { RK_TRACE (COMMANDEDITOR); // we totally ignore whichever range the katepart thinks we should offer a completion on. // it is often wrong, esp, when there are dots in the symbol // KDE4 TODO: This may no longer be needed, if the katepart gets fixed not to abort completions when the range // contains dots or other special characters updateCompletionList (command_editor->currentCompletionWord ()); } void RKCodeCompletionModel::executeCompletionItem (KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const { RK_TRACE (COMMANDEDITOR); RK_ASSERT (names.size () > index.row ()); view->document ()->replaceText (word, names[index.row ()]); } +QModelIndex RKCodeCompletionModel::index (int row, int column, const QModelIndex& parent) const { + if (!parent.isValid ()) { // header item + if (row == 0) return createIndex (row, column, quintptr (0)); + return QModelIndex (); + } else if (parent.parent ().isValid ()) { + return QModelIndex (); + } + + if (row < 0 || row >= names.count () || column < 0 || column >= ColumnCount) { + return QModelIndex (); + } + + return createIndex (row, column, 1); // regular item +} + +QModelIndex RKCodeCompletionModel::parent (const QModelIndex& index) const { + if (index.internalId ()) return createIndex (0, 0, quintptr (0)); + return QModelIndex (); +} + +int RKCodeCompletionModel::rowCount (const QModelIndex& parent) const { + if (parent.parent ().isValid ()) return 0; // no children on completion entries + if (parent.isValid ()) return names.count (); + return (!names.isEmpty ()); // header item, if list not empty +} + QVariant RKCodeCompletionModel::data (const QModelIndex& index, int role) const { + if (!index.parent ().isValid ()) { // group header + if (role == Qt::DisplayRole) return i18n ("Objects on search path"); + if (role == GroupRole) return Qt::DisplayRole; + return QVariant (); + } int col = index.column (); int row = index.row (); - if (index.parent ().isValid ()) return QVariant (); - - if ((role == Qt::DisplayRole) || (role==KTextEditor::CodeCompletionModel::CompletionRole)) { + if ((role == Qt::DisplayRole) || (role == KTextEditor::CodeCompletionModel::CompletionRole)) { if (col == KTextEditor::CodeCompletionModel::Name) { return (names.value (row)); } } else if (role == Qt::DecorationRole) { if (col == KTextEditor::CodeCompletionModel::Icon) { return (icons.value (row)); } + } else if (role == KTextEditor::CodeCompletionModel::InheritanceDepth) { + return (row); // disable sorting + } else if (role == KTextEditor::CodeCompletionModel::MatchQuality) { + return (10); } return QVariant (); } // 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/rkcommandeditorwindow.h b/rkward/windows/rkcommandeditorwindow.h index b1c47063..89ebe070 100644 --- a/rkward/windows/rkcommandeditorwindow.h +++ b/rkward/windows/rkcommandeditorwindow.h @@ -1,317 +1,320 @@ /*************************************************************************** rkcommandeditorwindow - description ------------------- begin : Mon Aug 30 2004 - copyright : (C) 2004-2018 by Thomas Friedrichsmeier + copyright : (C) 2004-2019 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 RKCOMMANDEDITORWINDOW_H #define RKCOMMANDEDITORWINDOW_H #include #include #include #include #include #include #include #include #include #include #include #include #include "../windows/rkmdiwindow.h" class QEvent; class QCloseEvent; class QFrame; class QLabel; class QAction; class QActionGroup; class QTemporaryDir; class QFile; class KActionMenu; class RKCommandEditorWindow; class KActionCollection; /** This class provides a KPart interface to RKCommandEditorWindow. The reason to use this, is so the required menus/menu-items can be merged in on the fly. @author Thomas Friedrichsmeier */ class RKCommandEditorWindowPart : public KParts::Part { protected: friend class RKCommandEditorWindow; RKCommandEditorWindowPart (QWidget *parent); ~RKCommandEditorWindowPart (); }; /** classes wishing to use RKFunctionArgHinter should derive from this, and implement provideContext () */ class RKScriptContextProvider { public: RKScriptContextProvider () {}; virtual ~RKScriptContextProvider () {}; /** to be implemented in subclasses. Provide some context, i.e. text *preceding* the cursor position (probably a line, but you may provide chunks in arbitrary size). If line_rev is 0, provide the line, the cursor is in. If line_rev is greater than 0, provide context before that. @param context Place the context here @returns a chunk of context. A null QString(), if no context was available. */ virtual QString provideContext (int line_rev) { Q_UNUSED (line_rev); return QString (); }; /** to be implemented in subclasses. Provide current context for help searches (based on current selection / current cursor position). If not package information is known, leave that empty. */ virtual void currentHelpContext (QString *symbol, QString *package) = 0; }; class RObject; /** function argument hinting for RKCommandEditorWindow and RKConsole */ class RKFunctionArgHinter : public QObject { Q_OBJECT public: RKFunctionArgHinter (RKScriptContextProvider *provider, KTextEditor::View* view); ~RKFunctionArgHinter (); /** Try to show an arg hint now */ void tryArgHint (); /** Hide the arg hint (if shown) */ void hideArgHint (); public slots: /** Internal worker function for tryArgHint () */ void tryArgHintNow (); void updateArgHintWindow (); protected: /** The (keypress) events of the view are filtered to determine, when to show / hide an argument hint */ bool eventFilter (QObject *, QEvent *e) override; private: RKScriptContextProvider *provider; KTextEditor::View *view; /** A timer to refresh the hint window periodically. This is a bit sorry, but it's really hard to find out, when the view has been moved, or gains/loses focus. While possible, this approach uses much less code. */ QTimer updater; bool active; QLabel *arghints_popup; }; class RKCodeCompletionModel : public KTextEditor::CodeCompletionModel, public KTextEditor::CodeCompletionModelControllerInterface { Q_OBJECT Q_INTERFACES(KTextEditor::CodeCompletionModelControllerInterface) public: explicit RKCodeCompletionModel (RKCommandEditorWindow* parent); ~RKCodeCompletionModel (); KTextEditor::Range completionRange (KTextEditor::View *view, const KTextEditor::Cursor &position) override; QString filterString (KTextEditor::View *, const KTextEditor::Range &, const KTextEditor::Cursor &) override { return QString (); }; void updateCompletionList (const QString& symbol); void completionInvoked (KTextEditor::View *, const KTextEditor::Range &, InvocationType) override; void executeCompletionItem (KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const override; QVariant data (const QModelIndex& index, int role=Qt::DisplayRole) const override; + int rowCount (const QModelIndex &parent) const override; + QModelIndex index (int row, int column, const QModelIndex &parent = QModelIndex ()) const override; + QModelIndex parent (const QModelIndex &index) const override; bool isEmpty () const { return names.isEmpty (); }; private: QList icons; QStringList names; QString current_symbol; RKCommandEditorWindow *command_editor; }; class RKJobSequence; class RKXMLGUIPreviewArea; class RKPreviewManager; /** \brief Provides an editor window for R-commands, as well as a text-editor window in general. While being called RKCommandEditorWindow, this class handles all sorts of text-files, both read/write and read-only. It is an MDI window that is added to the main window, based on KatePart. @author Pierre Ecochard */ class RKCommandEditorWindow : public RKMDIWindow, public RKScriptContextProvider { // we need the Q_OBJECT thing for some inherits ("RKCommandEditorWindow")-calls in rkward.cpp. Q_OBJECT public: /** constructor @param encoding encoding to use. If QString (), the default encoding is used. @param read_only Open the file in read-only mode @param delete_on_close File should be deleted when closing the window. Only respected with read_only=true. @param use_r_highlighting Initialize the view to use R syntax highlighting. Use, if you're going to edit an R syntax file */ explicit RKCommandEditorWindow (QWidget *parent, const QUrl url, const QString& encoding=QString (), bool use_r_highlighting=true, bool use_codehinting=true, bool read_only=false, bool delete_on_close=false); /** destructor */ ~RKCommandEditorWindow (); /** returns, whether the document was modified since the last save */ bool isModified () override; /** insert the given text into the document at the current cursor position. Additionally, focuses the view */ void insertText (const QString &text); /** set the current text (clear all previous text, and sets new text) */ void setText (const QString &text); /** @see restoreScrollPosition (). Note: Currently this saves/restored the cursor position, not necessarily the scroll position. */ void saveScrollPosition (); /** @see saveScrollPosition (). Note: Currently this saves/restored the cursor position, not necessarily the scroll position. */ void restoreScrollPosition (); /** copy current selection. Wrapper for use by external classes */ void copy (); /** reimplemented from RKMDIWindow to return full path of file (if any) */ QString fullCaption () override; void setReadOnly (bool ro); /** Return current url */ QUrl url () const; /** Returns an id string for this document. Meaningful, only when url is empty. For keeping track of split views on unnamed/unsaved windows */ QString id () const { return _id; }; QString provideContext (int line_rev) override; void currentHelpContext (QString* symbol, QString* package) override; QString currentCompletionWord () const; void highlightLine (int linenum); public slots: /** update Tab caption according to the current url. Display the filename-component of the URL, or - if not available - a more elaborate description of the url. Also appends a "[modified]" if appropriate */ void updateCaption (); /** called whenever it might be appropriate to show a code completion box. The box is not shown immediately, but only after a timeout (if at all) */ void tryCompletionProxy (KTextEditor::Document*); /** show a code completion box if appropriate. Use tryCompletionProxy () instead, which will call this function after a timeout */ void tryCompletion (); void setPopupMenu (); void focusIn (KTextEditor::View *); /** run the currently selected command(s) or line */ void runCurrent (); /** run the entire script */ void runAll (); /** insert line break and run the (previous) line */ void enterAndSubmit (); void copyLinesToOutput (); /** selection has changed. Enable / disable actions accordingly */ void selectionChanged (KTextEditor::View* view); /** change to the directory of the current script */ void setWDToScript (); /** paste the given text at the current cursor position */ void paste (const QString &text); /** apply our customizations to the katepart GUI */ void fixupPartGUI (); QAction* fileSaveAction () { return file_save; }; QAction* fileSaveAsAction () { return file_save_as; }; protected: /** reimplemented from RKMDIWindow: give the editor window a chance to object to being closed (if unsaved) */ void closeEvent (QCloseEvent *e) override; void setWindowStyleHint (const QString& hint) override; private slots: /** mark current selection as a block */ void markBlock (); /** unmark a block */ void unmarkBlock (); /** run a block */ void runBlock (); void clearUnusedBlocks (); /** handler to control when autosaves should be created, preview should be updated */ void textChanged (); /** Render the (.Rmd) current script */ void doRenderPreview (); /** creates an autosave file */ void doAutoSave (); /** handler to control when autosaves should be created */ void autoSaveHandlerModifiedChanged (); /** handle any errors during auto-saving */ void autoSaveHandlerJobFinished (RKJobSequence* seq); /** document was saved. Update preview, if appropriate */ void documentSaved (); private: KTextEditor::Cursor saved_scroll_position; KTextEditor::Document *m_doc; KTextEditor::View *m_view; KTextEditor::CodeCompletionInterface *cc_iface; KTextEditor::MovingInterface *smart_iface; RKFunctionArgHinter *hinter; RKCodeCompletionModel *completion_model; QTimer *completion_timer; void initializeActions (KActionCollection* ac); struct BlockRecord { KTextEditor::MovingRange* range; bool active; KTextEditor::Attribute::Ptr attribute; QAction* mark; QAction* unmark; QAction* run; }; QVector block_records; void initBlocks (); void addBlock (int index, const KTextEditor::Range& range); void removeBlock (int index, bool was_deleted=false); QAction *file_save, *file_save_as; KActionMenu* actionmenu_mark_block; KActionMenu* actionmenu_unmark_block; KActionMenu* actionmenu_run_block; QAction* action_run_all; QAction* action_run_current; QActionGroup* preview_modes; QAction* action_no_preview; QAction* action_preview_as_you_type; enum PreviewMode { NoPreview, RMarkdownPreview, RKOutputPreview, GraphPreview, ConsolePreview, }; QAction* action_setwd_to_script; QUrl previous_autosave_url; QTimer autosave_timer; QUrl delete_on_close; QString _id; static QMap unnamed_documents; RKXMLGUIPreviewArea *preview; QTimer preview_timer; RKPreviewManager *preview_manager; QTemporaryDir *preview_dir; QFile *preview_input_file; void changePreviewMode (QAction* mode); void discardPreview (); }; /** Simple class to provide HTML highlighting for arbitrary R code. */ class RKCommandHighlighter { public: enum HighlightingMode { RInteractiveSession, RScript, Automatic }; static void copyLinesToOutput (KTextEditor::View *view, HighlightingMode mode); static void setHighlighting (KTextEditor::Document *doc, HighlightingMode mode); static QString commandToHTML (const QString r_command, HighlightingMode mode=RScript); private: static KTextEditor::Document* getDoc (); static KTextEditor::Document* _doc; static KTextEditor::View* getView (); static KTextEditor::View* _view; }; #endif