diff --git a/rkward/rbackend/rkrinterface.cpp b/rkward/rbackend/rkrinterface.cpp index 1de01c4c..877b9d0d 100644 --- a/rkward/rbackend/rkrinterface.cpp +++ b/rkward/rbackend/rkrinterface.cpp @@ -1,864 +1,863 @@ /*************************************************************************** rkrinterface.cpp - description ------------------- begin : Fri Nov 1 2002 copyright : (C) 2002-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 "rkrinterface.h" #include "rcommandstack.h" #include "rkrbackendprotocol_frontend.h" #include "../rkward.h" #include "../rkconsole.h" #include "../settings/rksettingsmoduler.h" #include "../settings/rksettingsmodulegeneral.h" #include "../settings/rksettingsmoduleoutput.h" #include "../settings/rksettingsmodulegraphics.h" #include "../settings/rksettingsmoduleplugins.h" #include "../core/robjectlist.h" #include "../core/renvironmentobject.h" #include "../core/rkmodificationtracker.h" #include "../dialogs/rkloadlibsdialog.h" #include "../dialogs/rkselectlistdialog.h" #include "../dialogs/rkreadlinedialog.h" #include "../dialogs/rkerrordialog.h" #include "../agents/showedittextfileagent.h" #include "../agents/rkeditobjectagent.h" #include "../agents/rkprintagent.h" #include "../agents/rkdebughandler.h" #include "../windows/rcontrolwindow.h" #include "../windows/rkworkplace.h" #include "../windows/rkcommandlog.h" #include "../windows/rkhtmlwindow.h" #include "../plugin/rkcomponentmap.h" #include "../misc/rkcommonfunctions.h" #include "../misc/rkmessagecatalog.h" #include "rksessionvars.h" #include "../windows/rkwindowcatcher.h" #include "../rkglobals.h" #include "../version.h" #include "../debug.h" #include #include #include #include #include #include #include #include // flush new pieces of output after this period of time: #define FLUSH_INTERVAL 100 #define GET_LIB_PATHS 1 #define GET_HELP_BASE 2 #define SET_RUNTIME_OPTS 3 #define STARTUP_PHASE2_COMPLETE 4 #define GET_R_VERSION 5 #define RSTARTUP_COMPLETE 6 // statics double RInterface::na_real; int RInterface::na_int; RInterface::RInterface () { RK_TRACE (RBACKEND); new RCommandStackModel (this); RCommandStack::regular_stack = new RCommandStack (); startup_phase2_error = false; command_logfile_mode = NotRecordingCommands; previously_idle = false; locked = 0; backend_dead = false; flush_timer_id = 0; dummy_command_on_stack = 0; // create a fake init command RCommand *fake = new RCommand (i18n ("R Startup"), RCommand::App | RCommand::Sync | RCommand::ObjectListUpdate, i18n ("R Startup"), this, STARTUP_PHASE2_COMPLETE); issueCommand (fake); new RKSessionVars (this); new RKDebugHandler (this); new RKRBackendProtocolFrontend (this); RKRBackendProtocolFrontend::instance ()->setupBackend (); /////// Further initialization commands, which do not necessarily have to run before everything else can be queued, here. /////// // NOTE: will receive the list as a call plain generic request from the backend ("updateInstalledPackagesList") issueCommand (".rk.get.installed.packages()", RCommand::App | RCommand::Sync); issueCommand (new RCommand (QString (), RCommand::App | RCommand::Sync | RCommand::EmptyCommand, QString (), this, RSTARTUP_COMPLETE)); } void RInterface::issueCommand (const QString &command, int type, const QString &rk_equiv, RCommandReceiver *receiver, int flags, RCommandChain *chain) { RK_TRACE (RBACKEND); issueCommand (new RCommand (command, type, rk_equiv, receiver, flags), chain); } RInterface::~RInterface(){ RK_TRACE (RBACKEND); RKWindowCatcher::discardInstance (); } bool RInterface::backendIsIdle () { RK_TRACE (RBACKEND); return (RCommandStack::regular_stack->isEmpty() && (!runningCommand())); } RCommand *RInterface::popPreviousCommand (int id) { RK_TRACE (RBACKEND); RK_ASSERT (!all_current_commands.isEmpty ()); for (int i = all_current_commands.size () - 1; i >= 0; --i) { RCommand *ret = all_current_commands[i]; if (ret->id () == id) { RCommandStack::pop (ret); all_current_commands.removeAt (i); return ret; } } RK_ASSERT (false); return 0; } RCommandChain* RInterface::openSubcommandChain (RCommand* parent_command) { RK_TRACE (RBACKEND); current_commands_with_subcommands.append (parent_command); return RCommandStack::startChain (parent_command); } void RInterface::closeSubcommandChain (RCommand* parent_command) { RK_TRACE (RBACKEND); if (current_commands_with_subcommands.contains (parent_command)) { current_commands_with_subcommands.removeAll (parent_command); doNextCommand (0); } if (parent_command && (parent_command == dummy_command_on_stack)) { all_current_commands.removeAll (dummy_command_on_stack); RCommandStack::pop (dummy_command_on_stack); handleCommandOut (dummy_command_on_stack); dummy_command_on_stack = 0; } } void RInterface::tryNextCommand () { RK_TRACE (RBACKEND); RCommand *command = RCommandStack::currentCommand (); if (command_requests.isEmpty ()) { // if the backend is not requesting anything, only priority commands will be pushed if (!command) return; if (!(command->type () & RCommand::PriorityCommand)) return; if (all_current_commands.contains (command)) return; } bool priority = command && (command->type () & RCommand::PriorityCommand); bool on_top_level = all_current_commands.isEmpty (); if (!(on_top_level && locked && !(priority))) { // do not respect locks for sub-commands if ((!on_top_level) && all_current_commands.contains (command)) { // all sub-commands of the current command have finished, it became the top-most item of the RCommandStack, again closeSubcommandChain (command); return; } if (command) { all_current_commands.append (command); if (command->status & RCommand::Canceled) { // avoid passing cancelled commands to R command->status |= RCommand::Failed; // notify ourselves... RCommand* dummy = popPreviousCommand (command->id ()); RK_ASSERT (dummy == command); handleCommandOut (command); return; } if (previously_idle) RKWardMainWindow::getMain ()->setRStatus (RKWardMainWindow::Busy); previously_idle = false; doNextCommand (command); return; } } if (on_top_level) { if (!previously_idle) RKWardMainWindow::getMain ()->setRStatus (RKWardMainWindow::Idle); previously_idle = true; } } void RInterface::handleCommandOut (RCommand *command) { RK_TRACE (RBACKEND); RK_ASSERT (command); #ifdef RKWARD_DEBUG int dl = DL_WARNING; // failed application commands are an issue worth reporting, failed user commands are not if (command->type () & RCommand::User) dl = DL_DEBUG; if (command->failed ()) { command->status |= RCommand::WasTried | RCommand::Failed; if (command->status & RCommand::ErrorIncomplete) { RK_DEBUG (RBACKEND, dl, "Command failed (incomplete)"); } else if (command->status & RCommand::ErrorSyntax) { RK_DEBUG (RBACKEND, dl, "Command failed (syntax)"); } else if (command->status & RCommand::Canceled) { RK_DEBUG (RBACKEND, dl, "Command failed (interrupted)"); } else { RK_DEBUG (RBACKEND, dl, "Command failed (other)"); } RK_DEBUG (RBACKEND, dl, "failed command was: '%s'", qPrintable (command->command ())); RK_DEBUG (RBACKEND, dl, "- error message was: '%s'", qPrintable (command->error ())); } #endif if (command->status & RCommand::Canceled) { command->status |= RCommand::HasError; ROutput *out = new ROutput; out->type = ROutput::Error; out->output = ("--- interrupted ---"); command->output_list.append (out); command->newOutput (out); } command->finished (); delete command; } void RInterface::doNextCommand (RCommand *command) { RK_TRACE (RBACKEND); RBackendRequest* command_request = currentCommandRequest (); if (!command_request) { if (!(command && (command->type () & RCommand::PriorityCommand))) return; } // importantly, this point is not reached for the fake startup command if (RK_Debug::RK_Debug_CommandStep) { QTime t; t.start (); while (t.elapsed () < RK_Debug::RK_Debug_CommandStep) {} } flushOutput (true); RCommandProxy *proxy = 0; if (command) { RKWardMainWindow::getMain ()->setWorkspaceMightBeModified (true); proxy = command->makeProxy (); RK_DEBUG (RBACKEND, DL_DEBUG, "running command: %s", command->command ().toLatin1().data ()); command->status |= RCommand::Running; RCommandStackModel::getModel ()->itemChange (command); RKCommandLog::getLog ()->addInput (command); if (command_logfile_mode != NotRecordingCommands) { bool record = true; if (command_logfile_mode != RecordingCommandsUnfiltered) { if (command->type () & (RCommand::Silent | RCommand::Sync)) record = false; } if (record) { command_logfile.write (command->command ().toUtf8 ()); command_logfile.write ("\n"); } } } if (command && (command->type () & RCommand::PriorityCommand)) { RKRBackendProtocolFrontend::sendPriorityCommand (proxy); } else { RK_ASSERT (command_request); command_request->command = proxy; RKRBackendProtocolFrontend::setRequestCompleted (command_request); command_requests.pop_back (); } } void RInterface::rCommandDone (RCommand *command) { RK_TRACE (RBACKEND); if (command->failed ()) { startup_phase2_error = true; return; } if (command->getFlags () == GET_LIB_PATHS) { RK_ASSERT (command->getDataType () == RData::StringVector); RKSettingsModuleRPackages::r_libs_user = command->stringVector ().value (0); RKSettingsModuleRPackages::defaultliblocs += command->stringVector ().mid (1); RCommandChain *chain = command->parent; RK_ASSERT (chain); RK_ASSERT (!chain->isClosed ()); // apply user configurable run time options QStringList commands = RKSettingsModuleR::makeRRunTimeOptionCommands () + RKSettingsModuleRPackages::makeRRunTimeOptionCommands () + RKSettingsModuleOutput::makeRRunTimeOptionCommands () + RKSettingsModuleGraphics::makeRRunTimeOptionCommands (); for (QStringList::const_iterator it = commands.begin (); it != commands.end (); ++it) { issueCommand (*it, RCommand::App | RCommand::Sync, QString (), this, SET_RUNTIME_OPTS, chain); } // initialize output file issueCommand ("rk.set.output.html.file (\"" + RKSettingsModuleGeneral::filesPath () + "/rk_out.html\")\n", RCommand::App | RCommand::Sync, QString (), this, SET_RUNTIME_OPTS, chain); #ifdef Q_OS_MACOS // On MacOS, the backend is started from inside R home to allow resolution of dynamic libs. Re-set to frontend wd, here. issueCommand ("setwd (" + RKRSharedFunctionality::quote (QDir::currentPath ()) + ")\n", RCommand::App | RCommand::Sync, QString (), this, SET_RUNTIME_OPTS, chain); #endif closeChain (chain); } else if (command->getFlags () == GET_R_VERSION) { RK_ASSERT (command->getDataType () == RData::StringVector); RK_ASSERT (command->getDataLength () == 1); RKSessionVars::setRVersion (command->stringVector ().value (0)); } else if (command->getFlags () == GET_HELP_BASE) { RK_ASSERT (command->getDataType () == RData::StringVector); RK_ASSERT (command->getDataLength () == 1); RKSettingsModuleR::help_base_url = command->stringVector ().value (0); } else if (command->getFlags () == SET_RUNTIME_OPTS) { // no special handling. In case of failures, staturt_fail was set to true, above. } else if (command->getFlags () == STARTUP_PHASE2_COMPLETE) { QString message = startup_errors; if (startup_phase2_error) message.append (i18n ("

\t-An unspecified error occurred that is not yet handled by RKWard. Likely RKWard will not function properly. Please check your setup.

\n")); if (!message.isEmpty ()) { message.prepend (i18n ("

There was a problem starting the R backend. The following error(s) occurred:

\n")); QString details = command->fullOutput().replace('<', "<").replace('\n', "
"); if (!details.isEmpty ()) { // WORKAROUND for stupid KMessageBox behavior. (kdelibs 4.2.3) // If length of details <= 512, it tries to show the details as a QLabel. details = details.leftJustified (513); } KMessageBox::detailedError (0, message, details, i18n ("Error starting R"), KMessageBox::Notify | KMessageBox::AllowLink); } startup_errors.clear (); } else if (command->getFlags () == RSTARTUP_COMPLETE) { RKSettings::validateSettingsInteractive (); } } void RInterface::handleRequest (RBackendRequest* request) { RK_TRACE (RBACKEND); if (request->type == RBackendRequest::OutputStartedNotification) { RK_ASSERT (flush_timer_id == 0); flush_timer_id = startTimer (FLUSH_INTERVAL); // calls flushOutput (false); see timerEvent () RKRBackendProtocolFrontend::setRequestCompleted (request); return; } flushOutput (true); if (request->type == RBackendRequest::CommandOut) { RCommandProxy *cproxy = request->takeCommand (); if (cproxy) { RK_DEBUG (RBACKEND, DL_DEBUG, "Command out \"%s\", id %d", qPrintable (cproxy->command), cproxy->id); } else { RK_DEBUG (RBACKEND, DL_DEBUG, "Fake command out"); } RCommand *command = 0; // NOTE: the order of processing is: first try to submit the next command, then handle the old command. // The reason for doing it this way, instead of the reverse, is that this allows the backend thread / process to continue working, concurrently // NOTE: cproxy should only ever be 0 in the very first cycle if (cproxy) command = popPreviousCommand (cproxy->id); if (request->synchronous) command_requests.append (request); tryNextCommand (); if (cproxy) { RK_ASSERT (command); command->mergeAndDeleteProxy (cproxy); handleCommandOut (command); } tryNextCommand (); } else if (request->type == RBackendRequest::HistoricalSubstackRequest) { RCommandProxy *cproxy = request->command; RCommand *parent = 0; for (int i = all_current_commands.size () - 1; i >= 0; --i) { if (all_current_commands[i]->id () == cproxy->id) { parent = all_current_commands[i]; break; } } command_requests.append (request); processHistoricalSubstackRequest (request->params["call"].toStringList (), parent); } else if (request->type == RBackendRequest::PlainGenericRequest) { request->params["return"] = QVariant (processPlainGenericRequest (request->params["call"].toStringList ())); RKRBackendProtocolFrontend::setRequestCompleted (request); } else if (request->type == RBackendRequest::Started) { // The backend thread has finished basic initialization, but we still have more to do... startup_errors = request->params["message"].toString (); command_requests.append (request); RCommandChain *chain = openSubcommandChain (runningCommand ()); issueCommand ("paste (R.version[c (\"major\", \"minor\")], collapse=\".\")\n", RCommand::GetStringVector | RCommand::App | RCommand::Sync, QString (), this, GET_R_VERSION, chain); // find out about standard library locations issueCommand ("c(path.expand(Sys.getenv(\"R_LIBS_USER\")), .libPaths())\n", RCommand::GetStringVector | RCommand::App | RCommand::Sync, QString (), this, GET_LIB_PATHS, chain); // start help server / determined help base url issueCommand (".rk.getHelpBaseUrl ()\n", RCommand::GetStringVector | RCommand::App | RCommand::Sync, QString (), this, GET_HELP_BASE, chain); // NOTE: more initialization commands get run *after* we have determined the standard library locations (see rCommandDone()) } else { processRBackendRequest (request); } } void RInterface::timerEvent (QTimerEvent *) { // do not trace. called periodically flushOutput (false); } void RInterface::flushOutput (bool forced) { // do not trace. called periodically // RK_TRACE (RBACKEND); ROutputList list = RKRBackendProtocolFrontend::instance ()->flushOutput (forced); // this must come _after_ the output has been flushed. if (forced || !list.isEmpty ()) { if (flush_timer_id != 0) { killTimer (flush_timer_id); flush_timer_id = 0; } } foreach (ROutput *output, list) { if (all_current_commands.isEmpty ()) { RK_DEBUG (RBACKEND, DL_WARNING, "output without receiver'%s'", qPrintable (output->output)); delete output; continue; // to delete the other output pointers, too } else { RK_DEBUG (RBACKEND, DL_DEBUG, "output '%s'", qPrintable (output->output)); } bool first = true; foreach (RCommand* command, all_current_commands) { ROutput *coutput = output; if (!first) { // this output belongs to several commands at once. So we need to copy it. coutput = new ROutput; coutput->type = output->type; coutput->output = output->output; } first = false; if (coutput->type == ROutput::Output) { command->status |= RCommand::HasOutput; command->output_list.append (coutput); } else if (coutput->type == ROutput::Warning) { command->status |= RCommand::HasWarnings; command->output_list.append (coutput); } else if (coutput->type == ROutput::Error) { command->status |= RCommand::HasError; // An error output is typically just the copy of the previous output, so merge if possible if (command->output_list.isEmpty ()) { command->output_list.append (coutput); } if (command->output_list.last ()->output == coutput->output) { command->output_list.last ()->type = ROutput::Error; continue; // don't call command->newOutput(), again! } } command->newOutput (coutput); } } } void RInterface::issueCommand (RCommand *command, RCommandChain *chain) { RK_TRACE (RBACKEND); if (command->command ().isEmpty ()) command->_type |= RCommand::EmptyCommand; if (RKCarbonCopySettings::shouldCarbonCopyCommand (command)) { command->_type |= RCommand::CCCommand; if (RKCarbonCopySettings::includeOutputInCarbonCopy ()) command->_type |= RCommand::CCOutput; } RCommandStack::issueCommand (command, chain); tryNextCommand (); } RCommandChain *RInterface::startChain (RCommandChain *parent) { RK_TRACE (RBACKEND); return RCommandStack::startChain (parent); }; void RInterface::closeChain (RCommandChain *chain) { RK_TRACE (RBACKEND); RCommandStack::closeChain (chain); tryNextCommand (); }; void RInterface::cancelAll () { RK_TRACE (RBACKEND); QList all_commands = RCommandStack::regular_stack->allCommands (); foreach (RCommand* command, all_commands) cancelCommand (command); } bool RInterface::softCancelCommand (RCommand* command) { RK_TRACE (RBACKEND); if (!(command->type () & RCommand::Running)) { cancelCommand (command); } return command->status & RCommand::Canceled; } void RInterface::cancelCommand (RCommand *command) { RK_TRACE (RBACKEND); if (!(command->type () & RCommand::Sync)) { command->status |= RCommand::Canceled; if (command->type () & RCommand::Running) { if ((RKDebugHandler::instance ()->state () == RKDebugHandler::InDebugPrompt) && (command == RKDebugHandler::instance ()->command ())) { RKDebugHandler::instance ()->sendCancel (); } else { RKRBackendProtocolFrontend::instance ()->interruptCommand (command->id ()); } } RCommandStackModel::getModel ()->itemChange (command); } else { RK_ASSERT (false); } } void RInterface::pauseProcessing (bool pause) { RK_TRACE (RBACKEND); if (pause) locked |= User; else locked -= locked & User; } QStringList RInterface::processPlainGenericRequest (const QStringList &calllist) { RK_TRACE (RBACKEND); QString call = calllist.value (0); if (call == "get.tempfile.name") { RK_ASSERT (calllist.count () == 3); return (QStringList (RKCommonFunctions::getUseableRKWardSavefileName (calllist.value (1), calllist.value (2)))); } else if (call == "set.output.file") { RK_ASSERT (calllist.count () == 2); RKOutputWindowManager::self ()->setCurrentOutputPath (calllist.value (1)); } else if (call == "wdChange") { // in case of separate processes, apply new working directory in frontend, too. QDir::setCurrent (calllist.value (1)); emit (backendWorkdirChanged()); } else if (call == "highlightRCode") { return (QStringList (RKCommandHighlighter::commandToHTML (calllist.value (1)))); } else if (call == "quit") { RKWardMainWindow::getMain ()->close (); // if we're still alive, quitting was cancelled return (QStringList ("FALSE")); } else if (call == "preLocaleChange") { int res = KMessageBox::warningContinueCancel (0, i18n ("A command in the R backend is trying to change the character encoding. While RKWard offers support for this, and will try to adjust to the new locale, this operation may cause subtle bugs, if data windows are currently open. Also the feature is not well tested, yet, and it may be advisable to save your workspace before proceeding.\nIf you have any data editor opened, or in any doubt, it is recommended to close those first (this will probably be auto-detected in later versions of RKWard). In this case, please choose 'Cancel' now, then close the data windows, save, and retry."), i18n ("Locale change")); if (res != KMessageBox::Continue) return (QStringList ("FALSE")); } else if (call == "listPlugins") { RK_ASSERT (calllist.count () == 1); return RKComponentMap::getMap ()->listPlugins (); } else if (call == "setPluginStatus") { QStringList params = calllist.mid (1); RK_ASSERT ((params.size () % 3) == 0); const int rows = params.size () / 3; QStringList ids = params.mid (0, rows); QStringList contexts = params.mid (rows, rows); QStringList visible = params.mid (rows*2, rows); RKComponentMap::getMap ()->setPluginStatus (ids, contexts, visible); } else if (call == "loadPluginMaps") { bool force = (calllist.value (1) == "force"); bool reload = (calllist.value (2) == "reload"); RKSettingsModulePlugins::registerPluginMaps (calllist.mid (3), force, reload); } else if (call == "updateInstalledPackagesList") { RKSessionVars::instance ()->setInstalledPackages (calllist.mid (1)); } else if (call == "showHTML") { RK_ASSERT (calllist.count () == 2); RKWorkplace::mainWorkplace ()->openHelpWindow (QUrl::fromUserInput (calllist.value (1), QDir::currentPath (), QUrl::AssumeLocalFile)); } else if (call == "select.list") { QString title = calllist.value (1); bool multiple = (calllist.value (2) == "multi"); int num_preselects = calllist.value (3).toInt (); QStringList preselects = calllist.mid (4, num_preselects); QStringList choices = calllist.mid (4 + num_preselects); QStringList results = RKSelectListDialog::doSelect (QApplication::activeWindow(), title, choices, preselects, multiple); if (results.isEmpty ()) results.append (""); // R wants to have it that way return (results); } else if (call == "commandHistory") { if (calllist.value (1) == "get") { return (RKConsole::mainConsole ()->commandHistory ()); } else { RKConsole::mainConsole ()->setCommandHistory (calllist.mid (2), calllist.value (1) == "append"); } } else if (call == "getWorkspaceUrl") { QUrl url = RKWorkplace::mainWorkplace ()->workspaceURL (); if (!url.isEmpty ()) return (QStringList (url.url ())); } else if (call == "workplace.layout") { if (calllist.value (1) == "set") { if (calllist.value (2) == "close") RKWorkplace::mainWorkplace ()->closeAll (); QStringList list = calllist.mid (3); RKWorkplace::mainWorkplace ()->restoreWorkplace (list); } else { RK_ASSERT (calllist.value (1) == "get"); return (RKWorkplace::mainWorkplace ()->makeWorkplaceDescription ()); } } else if (call == "set.window.placement.hint") { RKWorkplace::mainWorkplace ()->setWindowPlacementOverrides (calllist.value (1), calllist.value (2), calllist.value (3)); } else if (call == "getSessionInfo") { // Non-translatable on purpose. This is meant for posting to the bug tracker, mostly. QStringList lines ("-- Frontend --"); lines.append (RKSessionVars::frontendSessionInfo ()); lines.append (QString ()); lines.append ("-- Backend --"); lines.append ("Debug message file (this may contain relevant diagnostic output in case of trouble):"); lines.append (calllist.value (1)); lines.append (QString ()); lines.append ("R version (compile time): " + calllist.value (2)); return (lines); } else if (call == "recordCommands") { RK_ASSERT (calllist.count () == 3); QString filename = calllist.value (1); bool unfiltered = (calllist.value (2) == "include.all"); if (filename.isEmpty ()) { command_logfile_mode = NotRecordingCommands; command_logfile.close (); } else { if (command_logfile_mode != NotRecordingCommands) { return (QStringList ("Attempt to start recording, while already recording commands. Ignoring.)")); } else { command_logfile.setFileName (filename); bool ok = command_logfile.open (QIODevice::WriteOnly | QIODevice::Truncate); if (ok) { if (unfiltered) command_logfile_mode = RecordingCommandsUnfiltered; else command_logfile_mode = RecordingCommands; } else { return (QStringList ("Could not open file for writing. Not recording commands")); } } } } else if (call == "printPreview") { RKPrintAgent::printPostscript (calllist.value (1), true); } else if (call == "endBrowserContext") { RKDebugHandler::instance ()->endDebug (); } else if (call == "switchLanguage") { RKMessageCatalog::switchLanguage (calllist.value (1)); } else { return (QStringList ("Error: unrecognized request '" + call + "'.")); } // for those calls which were recognized, but do not return anything return QStringList (); } void RInterface::processHistoricalSubstackRequest (const QStringList &calllist, RCommand *parent_command) { RK_TRACE (RBACKEND); RCommandChain *in_chain; if (!parent_command) { // This can happen for Tcl events. Create a dummy command on the stack to keep things looping. parent_command = new RCommand (QString (), RCommand::App | RCommand::EmptyCommand | RCommand::Sync); RCommandStack::issueCommand (parent_command, 0); all_current_commands.append (parent_command); dummy_command_on_stack = parent_command; // so we can get rid of it again, after it's sub-commands have finished } in_chain = openSubcommandChain (parent_command); RK_DEBUG (RBACKEND, DL_DEBUG, "started sub-command chain (%p) for command %s", in_chain, qPrintable (parent_command->command ())); QString call = calllist.value (0); if (call == "sync") { RK_ASSERT (calllist.count () >= 2); for (int i = 1; i < calllist.count (); ++i) { QString object_name = calllist[i]; RObject *obj = RObjectList::getObjectList ()->findObject (object_name); if (obj) { RK_DEBUG (RBACKEND, DL_DEBUG, "triggering update for symbol %s", object_name.toLatin1 ().data()); obj->markDataDirty (); obj->updateFromR (in_chain); } else { RK_DEBUG (RBACKEND, DL_WARNING, "lookup failed for changed symbol %s", object_name.toLatin1 ().data()); } } } else if (call == "syncenvs") { RK_DEBUG (RBACKEND, DL_DEBUG, "triggering update of object list"); int search_len = calllist.value (1).toInt (); RObjectList::getObjectList ()->updateFromR (in_chain, calllist.mid (2, search_len), calllist.mid (2 + search_len)); } else if (call == "syncglobal") { RK_DEBUG (RBACKEND, DL_DEBUG, "triggering update of globalenv"); RObjectList::getGlobalEnv ()->updateFromR (in_chain, calllist.mid (1)); #ifndef DISABLE_RKWINDOWCATCHER // NOTE: WARNING: When converting these to PlainGenericRequests, the occasional "error, figure margins too large" starts coming up, again. Not sure, why. } else if (call == "startOpenX11") { RK_ASSERT (calllist.count () == 2); RKWindowCatcher::instance ()->start (calllist.value (1).toInt ()); } else if (call == "endOpenX11") { RK_ASSERT (calllist.count () == 2); RKWindowCatcher::instance ()->stop (calllist.value (1).toInt ()); } else if (call == "updateDeviceHistory") { if (calllist.count () >= 2) { RKWindowCatcher::instance ()->updateHistory (calllist.mid (1)); } } else if (call == "killDevice") { RK_ASSERT (calllist.count () == 2); RKWindowCatcher::instance ()->killDevice (calllist.value (1).toInt ()); #endif // DISABLE_RKWINDOWCATCHER } else if (call == "edit") { RK_ASSERT (calllist.count () >= 2); QStringList object_list = calllist.mid (1); new RKEditObjectAgent (object_list, in_chain); } else if (call == "require") { if (calllist.count () >= 2) { QString lib_name = calllist[1]; KMessageBox::information (0, i18n ("The R-backend has indicated that in order to carry out the current task it needs the package '%1', which is not currently installed. We will open the package-management tool, and there you can try to locate and install the needed package.", lib_name), i18n ("Require package '%1'", lib_name)); RKLoadLibsDialog::showInstallPackagesModal (0, in_chain, lib_name); issueCommand (".rk.set.reply (\"\")", RCommand::App | RCommand::Sync, QString (), 0, 0, in_chain); } else { issueCommand (".rk.set.reply (\"Too few arguments in call to require.\")", RCommand::App | RCommand::Sync, QString (), 0, 0, in_chain); } } else if (call == "doPlugin") { if (calllist.count () >= 3) { QString message; bool ok; RKComponentMap::ComponentInvocationMode mode = RKComponentMap::ManualSubmit; if (calllist[2] == "auto") mode = RKComponentMap::AutoSubmit; else if (calllist[2] == "submit") mode = RKComponentMap::AutoSubmitOrFail; ok = RKComponentMap::invokeComponent (calllist[1], calllist.mid (3), mode, &message, in_chain); if (!message.isEmpty ()) { QString type = "warning"; if (!ok) type = "error"; issueCommand (".rk.set.reply (list (type=\"" + type + "\", message=\"" + RKCommonFunctions::escape (message) + "\"))", RCommand::App | RCommand::Sync, QString (), 0, 0, in_chain); } } else { RK_ASSERT (false); } } else { issueCommand ("stop (\"Unrecognized call '" + call + "'. Ignoring\")", RCommand::App | RCommand::Sync, QString (), 0, 0, in_chain); } closeChain (in_chain); } int addButtonToBox (QDialog *dialog, QDialogButtonBox *box, QDialogButtonBox::StandardButton which, const QString &text, const QString &def_text, bool is_default) { if (text.isEmpty ()) return 0; QPushButton *button = box->addButton (which); if (text != def_text) button->setText (text); if (is_default) button->setDefault (true); QObject::connect (button, &QPushButton::clicked, [dialog, which]() { dialog->done (which); }); return 1; } void RInterface::processRBackendRequest (RBackendRequest *request) { RK_TRACE (RBACKEND); // first, copy out the type. Allows for easier typing below RBackendRequest::RCallbackType type = request->type; if (type == RBackendRequest::CommandLineIn) { int id = request->params["commandid"].toInt (); RCommand *command = all_current_commands.value (0, 0); // User command will always be the first. if ((command == 0) || (command->id () != id)) { RK_ASSERT (false); } else { command->commandLineIn (); } } else if (type == RBackendRequest::ShowMessage) { QString caption = request->params["caption"].toString (); QString message = request->params["message"].toString (); QString button_yes = request->params["button_yes"].toString (); QString button_no = request->params["button_no"].toString (); QString button_cancel = request->params["button_cancel"].toString (); QString def_button = request->params["default"].toString (); // NOTE: In order to support non-modal (information) dialogs, we cannot use KMessageBox or QMessgaeBox, below. QDialog* dialog = new QDialog (); dialog->setResult (-1); // We use this to stand for cancelled QDialogButtonBox *button_box = new QDialogButtonBox (dialog); - QPushButton *button; int button_count = 0; button_count += addButtonToBox (dialog, button_box, QDialogButtonBox::Yes, button_yes, "yes", def_button == button_yes); button_count += addButtonToBox (dialog, button_box, QDialogButtonBox::No, button_no, "no", def_button == button_no); button_count += addButtonToBox (dialog, button_box, QDialogButtonBox::Cancel, button_cancel, "cancel", def_button == button_cancel); if (!button_count) { // cannot have no button defined at all button_count += addButtonToBox (dialog, button_box, QDialogButtonBox::Ok, "ok", "ok", true); } bool synchronous = request->synchronous || (button_count > 1); KMessageBox::createKMessageBox (dialog, button_box, button_count < 2 ? QMessageBox::Information : QMessageBox::Question, message, QStringList (), QString (), 0, KMessageBox::Notify | KMessageBox::NoExec); dialog->setWindowTitle (caption); if (!synchronous) { dialog->setAttribute (Qt::WA_DeleteOnClose); dialog->show(); RKRBackendProtocolFrontend::setRequestCompleted (request); return; } else { int result = dialog->exec (); QString result_string; if (result == QDialogButtonBox::Yes || result == QDialogButtonBox::Ok) result_string = "yes"; else if (result == QDialogButtonBox::No) result_string = "no"; else result_string = "cancel"; request->params["result"] = result_string; delete dialog; } } else if (type == RBackendRequest::ReadLine) { QString result; // yes, readline *can* be called outside of a current command (e.g. from tcl/tk) bool dummy_command = false; RCommand *command = runningCommand (); if (!command) { command = new RCommand ("", RCommand::EmptyCommand); dummy_command = true; } bool ok = RKReadLineDialog::readLine (0, i18n ("R backend requests information"), request->params["prompt"].toString (), command, &result); request->params["result"] = QVariant (result); if (dummy_command) delete command; if (!ok) request->params["cancelled"] = QVariant (true); } else if (type == RBackendRequest::Debugger) { RKDebugHandler::instance ()->debugCall (request, runningCommand ()); return; // request will be closed by the debug handler } else if ((type == RBackendRequest::ShowFiles) || (type == RBackendRequest::EditFiles)) { ShowEditTextFileAgent::showEditFiles (request); return; // we are not done, yet! } else if (type == RBackendRequest::ChooseFile) { QString filename; if (request->params["new"].toBool ()) { filename = QFileDialog::getSaveFileName (); } else { filename = QFileDialog::getOpenFileName (); } request->params["result"] = QVariant (filename); } else if (type == RBackendRequest::SetParamsFromBackend) { na_real = request->params["na_real"].toDouble (); na_int = request->params["na_int"].toInt (); } else if (type == RBackendRequest::BackendExit) { if (request->params.value ("regular", QVariant (false)).toBool ()) backend_dead = true; // regular exit via QuitCommand if (!backend_dead) { backend_dead = true; QString message = request->params["message"].toString (); message += i18n ("\nThe R backend will be shut down immediately. This means, you can not use any more functions that rely on it. I.e. you can do hardly anything at all, not even save the workspace (but if you're lucky, R already did that). What you can do, however, is save any open command-files, the output, or copy data out of open data editors. Quit RKWard after that. Sorry!"); RKErrorDialog::reportableErrorMessage (0, message, QString (), i18n ("R engine has died"), "r_engine_has_died"); } } else { RK_ASSERT (false); } RKRBackendProtocolFrontend::setRequestCompleted (request); } diff --git a/rkward/windows/rkworkplace.cpp b/rkward/windows/rkworkplace.cpp index 8614c3e7..657d96d9 100644 --- a/rkward/windows/rkworkplace.cpp +++ b/rkward/windows/rkworkplace.cpp @@ -1,1177 +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 #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 (); }