No OneTemporary

File Metadata

Created
Sat, May 4, 11:56 PM
diff --git a/src/backends/maxima/maximaexpression.cpp b/src/backends/maxima/maximaexpression.cpp
index bd2dc65b..549c02a3 100644
--- a/src/backends/maxima/maximaexpression.cpp
+++ b/src/backends/maxima/maximaexpression.cpp
@@ -1,445 +1,445 @@
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2009-2012 Alexander Rieder <alexanderrieder@gmail.com>
*/
#include "maximaexpression.h"
#include <config-cantorlib.h>
#include "maximasession.h"
#include "textresult.h"
#include "epsresult.h"
#include "imageresult.h"
#include "helpresult.h"
#include "latexresult.h"
#include "settings.h"
#include <QDir>
#include <QTemporaryFile>
#include <KLocalizedString>
#include <QDebug>
#include <QTimer>
#include <QRegExp>
#include <QChar>
#include <QUrl>
// MaximaExpression use real id from Maxima as expression id, so we don't know id before executing
MaximaExpression::MaximaExpression( Cantor::Session* session, bool internal ) : Cantor::Expression(session, internal, -1),
m_tempFile(nullptr),
m_isHelpRequest(false),
m_isHelpRequestAdditional(false),
m_isPlot(false),
m_plotResult(nullptr),
m_plotResultIndex(-1),
m_gotErrorContent(false)
{
}
MaximaExpression::~MaximaExpression() {
if(m_tempFile)
delete m_tempFile;
}
void MaximaExpression::evaluate()
{
m_isHelpRequest=false;
m_gotErrorContent=false;
if(m_tempFile)
{
delete m_tempFile;
m_tempFile = nullptr;
m_isPlot = false;
m_plotResult = nullptr;
m_plotResultIndex = -1;
}
//check if this is a ?command
if(command().startsWith(QLatin1String("??"))
|| command().startsWith(QLatin1String("describe("))
|| command().startsWith(QLatin1String("example("))
|| command().startsWith(QLatin1String(":lisp(cl-info::info-exact")))
m_isHelpRequest=true;
if(command().contains(QRegExp(QLatin1String("(?:plot2d|plot3d|contour_plot)\\s*\\([^\\)]"))) && MaximaSettings::self()->integratePlots() && !command().contains(QLatin1String("ps_file")))
{
m_isPlot=true;
#ifdef WITH_EPS
m_tempFile=new QTemporaryFile(QDir::tempPath() + QLatin1String("/cantor_maxima-XXXXXX.eps" ));
#else
m_tempFile=new QTemporaryFile(QDir::tempPath() + QLatin1String("/cantor_maxima-XXXXXX.png"));
#endif
m_tempFile->open();
m_fileWatch.removePaths(m_fileWatch.files());
m_fileWatch.addPath(m_tempFile->fileName());
connect(&m_fileWatch, &QFileSystemWatcher::fileChanged, this, &MaximaExpression::imageChanged, Qt::UniqueConnection);
}
const QString& cmd=command();
bool isComment = true;
int commentLevel = 0;
bool inString = false;
for (int i = 0; i < cmd.size(); ++i) {
if (cmd[i] == QLatin1Char('\\')) {
++i; // skip the next character
if (commentLevel == 0 && !inString) {
isComment = false;
}
} else if (cmd[i] == QLatin1Char('"') && commentLevel == 0) {
inString = !inString;
isComment = false;
} else if (cmd.mid(i,2) == QLatin1String("/*") && !inString) {
++commentLevel;
++i;
} else if (cmd.mid(i,2) == QLatin1String("*/") && !inString) {
if (commentLevel == 0) {
qDebug() << "Comments mismatched!";
setErrorMessage(i18n("Error: Too many */"));
setStatus(Cantor::Expression::Error);
return;
}
++i;
--commentLevel;
} else if (isComment && commentLevel == 0 && !cmd[i].isSpace()) {
isComment = false;
}
}
if (commentLevel > 0) {
qDebug() << "Comments mismatched!";
setErrorMessage(i18n("Error: Too many /*"));
setStatus(Cantor::Expression::Error);
return;
}
if (inString) {
qDebug() << "String not closed";
setErrorMessage(i18n("Error: expected \" before ;"));
setStatus(Cantor::Expression::Error);
return;
}
if(isComment)
{
setStatus(Cantor::Expression::Done);
return;
}
session()->enqueueExpression(this);
}
void MaximaExpression::interrupt()
{
qDebug()<<"interrupting";
setStatus(Cantor::Expression::Interrupted);
}
QString MaximaExpression::internalCommand()
{
QString cmd=command();
if(m_isPlot)
{
if(!m_tempFile)
{
qDebug()<<"plotting without tempFile";
return QString();
}
QString fileName = m_tempFile->fileName();
#ifdef WITH_EPS
const QString psParam=QLatin1String("[gnuplot_ps_term_command, \"set size 1.0, 1.0; set term postscript eps color solid \"]");
const QString plotParameters = QLatin1String("[ps_file, \"")+ fileName+QLatin1String("\"],")+psParam;
#else
const QString plotParameters = QLatin1String("[gnuplot_term, \"png size 500,340\"], [gnuplot_out_file, \"")+fileName+QLatin1String("\"]");
#endif
cmd.replace(QRegExp(QLatin1String("((plot2d|plot3d|contour_plot)\\s*\\(.*)\\)([;\n$]|$)")), QLatin1String("\\1, ")+plotParameters+QLatin1String(");"));
}
if (!cmd.endsWith(QLatin1Char('$')))
{
if (!cmd.endsWith(QLatin1String(";")))
cmd+=QLatin1Char(';');
}
//replace all newlines with spaces, as maxima isn't sensitive about
//whitespaces, and without newlines the whole command
//is executed at once, without outputting an input
//prompt after each line
cmd.replace(QLatin1Char('\n'), QLatin1Char(' '));
//lisp-quiet doesn't print a prompt after the command
//is completed, which causes the parsing to hang.
//replace the command with the non-quiet version
cmd.replace(QRegExp(QLatin1String("^:lisp-quiet")), QLatin1String(":lisp"));
return cmd;
}
void MaximaExpression::forceDone()
{
qDebug()<<"forcing Expression state to DONE";
setResult(nullptr);
setStatus(Cantor::Expression::Done);
}
/*!
example output for the simple expression '5+5':
latex mode - "<cantor-result><cantor-text>\n(%o1) 10\n</cantor-text><cantor-latex>\\mbox{\\tt\\red(\\mathrm{\\%o1}) \\black}10</cantor-latex></cantor-result>\n<cantor-prompt>(%i2) </cantor-prompt>\n"
text mode - "<cantor-result><cantor-text>\n(%o1) 10\n</cantor-text></cantor-result>\n<cantor-prompt>(%i2) </cantor-prompt>\n"
*/
bool MaximaExpression::parseOutput(QString& out)
{
const int promptStart = out.indexOf(QLatin1String("<cantor-prompt>"));
const int promptEnd = out.indexOf(QLatin1String("</cantor-prompt>"));
const QString prompt = out.mid(promptStart + 15, promptEnd - promptStart - 15).simplified();
//check whether the result is part of the promt - this is the case when additional input is required from the user
if (prompt.contains(QLatin1String("<cantor-result>")))
{
//text part of the output
const int textContentStart = prompt.indexOf(QLatin1String("<cantor-text>"));
const int textContentEnd = prompt.indexOf(QLatin1String("</cantor-text>"));
QString textContent = prompt.mid(textContentStart + 13, textContentEnd - textContentStart - 13).trimmed();
qDebug()<<"asking for additional input for " << textContent;
emit needsAdditionalInformation(textContent);
return true;
}
qDebug()<<"new input label: " << prompt;
QString errorContent;
//parse the results
int resultStart = out.indexOf(QLatin1String("<cantor-result>"));
if (resultStart != -1)
errorContent += out.mid(0, resultStart);
while (resultStart != -1)
{
int resultEnd = out.indexOf(QLatin1String("</cantor-result>"), resultStart + 15);
const QString resultContent = out.mid(resultStart + 15, resultEnd - resultStart - 15);
parseResult(resultContent);
//search for the next openning <cantor-result> tag after the current closing </cantor-result> tag
resultStart = out.indexOf(QLatin1String("<cantor-result>"), resultEnd + 16);
}
//parse the error message, the part outside of the <cantor*> tags
int lastResultEnd = out.lastIndexOf(QLatin1String("</cantor-result>"));
if (lastResultEnd != -1)
lastResultEnd += 16;
else
lastResultEnd = 0;
errorContent += out.mid(lastResultEnd, promptStart - lastResultEnd).trimmed();
if (errorContent.isEmpty())
{
// For plots we set Done status in imageChanged
if (!m_isPlot || m_plotResult)
setStatus(Cantor::Expression::Done);
}
else
{
qDebug() << "error content: " << errorContent;
if (out.contains(QLatin1String("cantor-value-separator")))
{
//when fetching variables, in addition to the actual result with variable names and values,
//Maxima also writes out the names of the variables to the error buffer.
//we don't interpret this as an error.
setStatus(Cantor::Expression::Done);
}
else if(m_isHelpRequest || m_isHelpRequestAdditional) //help messages are also part of the error output
{
//we've got help result, but maybe additional input is required -> check this
const int index = MaximaSession::MaximaInputPrompt.indexIn(prompt.trimmed());
if (index == -1) {
// No input label found in the prompt -> additional info is required
qDebug()<<"asking for additional input for the help request" << prompt;
m_isHelpRequestAdditional = true;
emit needsAdditionalInformation(prompt);
}
//set the help result
errorContent.prepend(QLatin1Char(' '));
Cantor::HelpResult* result = new Cantor::HelpResult(errorContent);
setResult(result);
//if a new input prompt was found, no further input is expected and we're done
if (index != -1) {
m_isHelpRequestAdditional = false;
setStatus(Cantor::Expression::Done);
}
}
else
{
errorContent = errorContent.replace(QLatin1String("\n\n"), QLatin1String("\n"));
setErrorMessage(errorContent);
setStatus(Cantor::Expression::Error);
}
}
return true;
}
void MaximaExpression::parseResult(const QString& resultContent)
{
//in case we asked for additional input for the help request,
//no need to process the result - we're not done yet and maxima is waiting for further input
if (m_isHelpRequestAdditional)
return;
qDebug()<<"result content: " << resultContent;
//text part of the output
const int textContentStart = resultContent.indexOf(QLatin1String("<cantor-text>"));
const int textContentEnd = resultContent.indexOf(QLatin1String("</cantor-text>"));
QString textContent = resultContent.mid(textContentStart + 13, textContentEnd - textContentStart - 13).trimmed();
qDebug()<<"text content: " << textContent;
//output label can be a part of the text content -> determine it
const QRegExp regex = QRegExp(MaximaSession::MaximaOutputPrompt.pattern());
const int index = regex.indexIn(textContent);
QString outputLabel;
if (index != -1) // No match, so output don't contain output label
outputLabel = textContent.mid(index, regex.matchedLength()).trimmed();
qDebug()<<"output label: " << outputLabel;
//extract the expression id
bool ok;
QString idString = outputLabel.mid(3, outputLabel.length()-4);
int id = idString.toInt(&ok);
if (ok)
setId(id);
qDebug()<<"expression id: " << this->id();
//remove the output label from the text content
textContent = textContent.remove(outputLabel).trimmed();
//determine the actual result
Cantor::Result* result = nullptr;
const int latexContentStart = resultContent.indexOf(QLatin1String("<cantor-latex>"));
//Handle system maxima output for plotting commands
if (m_isPlot && textContent.endsWith(QString::fromLatin1("\"%1\"]").arg(m_tempFile->fileName())))
{
m_plotResultIndex = results().size();
// Gnuplot could generate plot before we parse text output from maxima and after
// If we already have plot result, just add it
// Else set info message, and replace it by real result in imageChanged function later
if (m_plotResult)
result = m_plotResult;
else
result = new Cantor::TextResult(i18n("Waiting for the plot result"));
}
else if (latexContentStart != -1)
{
//latex output is available
const int latexContentEnd = resultContent.indexOf(QLatin1String("</cantor-latex>"));
QString latexContent = resultContent.mid(latexContentStart + 14, latexContentEnd - latexContentStart - 14).trimmed();
qDebug()<<"latex content: " << latexContent;
Cantor::TextResult* textResult;
//replace the \mbox{} environment, if available, by the eqnarray environment
if (latexContent.indexOf(QLatin1String("\\mbox{")) != -1)
{
int i;
int pcount=0;
for(i = latexContent.indexOf(QLatin1String("\\mbox{"))+5; i < latexContent.size(); ++i)
{
if(latexContent[i]==QLatin1Char('{'))
pcount++;
else if(latexContent[i]==QLatin1Char('}'))
pcount--;
if(pcount==0)
break;
}
QString modifiedLatexContent = latexContent.mid(i+1);
if(modifiedLatexContent.trimmed().isEmpty())
{
//empty content in the \mbox{} environment (e.g. for print() outputs), use the latex string outside of the \mbox{} environment
modifiedLatexContent = latexContent.left(latexContent.indexOf(QLatin1String("\\mbox{")));
}
modifiedLatexContent.prepend(QLatin1String("\\begin{eqnarray*}"));
modifiedLatexContent.append(QLatin1String("\\end{eqnarray*}"));
textResult = new Cantor::TextResult(modifiedLatexContent, textContent);
qDebug()<<"modified latex content: " << modifiedLatexContent;
}
else
{
//no \mbox{} available, use what we've got.
textResult = new Cantor::TextResult(latexContent, textContent);
}
textResult->setFormat(Cantor::TextResult::LatexFormat);
result = textResult;
}
else
{
- //no latex output is availabe, the actual result is part of the textContent string
+ //no latex output is available, the actual result is part of the textContent string
result = new Cantor::TextResult(textContent);
}
addResult(result);
}
void MaximaExpression::parseError(const QString& out)
{
m_errorBuffer.append(out);
}
void MaximaExpression::addInformation(const QString& information)
{
qDebug()<<"adding information";
QString inf=information;
if(!inf.endsWith(QLatin1Char(';')))
inf+=QLatin1Char(';');
Cantor::Expression::addInformation(inf);
static_cast<MaximaSession*>(session())->sendInputToProcess(inf+QLatin1Char('\n'));
}
void MaximaExpression::imageChanged()
{
if(m_tempFile->size()>0)
{
#ifdef WITH_EPS
m_plotResult = new Cantor::EpsResult( QUrl::fromLocalFile(m_tempFile->fileName()) );
#else
m_plotResult = new Cantor::ImageResult( QUrl::fromLocalFile(m_tempFile->fileName()) );
#endif
// Check, that we already parse maxima output for this plot, and if not, keep it up to this moment
// If it's true, replace text info result by real plot and set status as Done
if (m_plotResultIndex != -1)
{
replaceResult(m_plotResultIndex, m_plotResult);
if (status() != Cantor::Expression::Error)
setStatus(Cantor::Expression::Done);
}
}
}
diff --git a/src/backends/maxima/testmaxima.h b/src/backends/maxima/testmaxima.h
index 766046f7..5fea65ca 100644
--- a/src/backends/maxima/testmaxima.h
+++ b/src/backends/maxima/testmaxima.h
@@ -1,72 +1,72 @@
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2009 Alexander Rieder <alexanderrieder@gmail.com>
*/
#ifndef _TESTMAXIMA_H
#define _TESTMAXIMA_H
#include "backendtest.h"
/** This class test some of the basic functions of the maxima backend
The different tests represent some general expressions, as well
as expressions, that are known to have caused problems in earlier
versions
**/
class TestMaxima : public BackendTest
{
Q_OBJECT
private Q_SLOTS:
//tests evaluating a simple command
void testSimpleCommand();
//tests a command, containing more than 1 line
void testMultilineCommand();
- //tests if the command queue works correcly
+ //tests if the command queue works correctly
void testCommandQueue();
//tests doing a plot
void testPlot();
void testPlotWithAnotherTextResults();
//tests a syntax error (not closing bracket)
void testInvalidSyntax();
//tests if the expression numbering works
void testExprNumbering();
//some tests to see if comments are working correctly
void testSimpleExpressionWithComment();
void testCommentExpression();
void testNestedComment();
void testUnmatchedComment();
void testInvalidAssignment();
void testInformationRequest();
void testHelpRequest();
void testSyntaxHelp();
void testCompletion();
void testVariableModel();
void testLoginLogout();
void testRestartWhileRunning();
private:
QString backendName() override;
};
#endif /* _TESTMAXIMA_H */
diff --git a/src/backends/octave/octavesession.cpp b/src/backends/octave/octavesession.cpp
index 6dee8e43..2c0e7923 100644
--- a/src/backends/octave/octavesession.cpp
+++ b/src/backends/octave/octavesession.cpp
@@ -1,340 +1,340 @@
/*
Copyright (C) 2010 Miha Čančula <miha.cancula@gmail.com>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "octavesession.h"
#include "octaveexpression.h"
#include "octavecompletionobject.h"
#include "octavesyntaxhelpobject.h"
#include "octavehighlighter.h"
#include "result.h"
#include "textresult.h"
#include "settings.h"
#include <KProcess>
#include <KDirWatch>
#include <KLocalizedString>
#include <QTimer>
#include <QFile>
#include <QDir>
#include <QStringRef>
#ifndef Q_OS_WIN
#include <signal.h>
#endif
#include "octavevariablemodel.h"
const QRegExp OctaveSession::PROMPT_UNCHANGEABLE_COMMAND = QRegExp(QLatin1String("(,|;)+"));
OctaveSession::OctaveSession ( Cantor::Backend* backend ) : Session ( backend),
m_process(nullptr),
m_prompt(QLatin1String("CANTOR_OCTAVE_BACKEND_PROMPT:([0-9]+)> ")),
m_subprompt(QLatin1String("CANTOR_OCTAVE_BACKEND_SUBPROMPT:([0-9]+)> ")),
m_previousPromptNumber(1),
m_syntaxError(false)
{
setVariableModel(new OctaveVariableModel(this));
}
OctaveSession::~OctaveSession()
{
if (m_process)
{
m_process->kill();
m_process->deleteLater();
m_process = nullptr;
}
}
void OctaveSession::login()
{
qDebug() << "login";
if (m_process)
return;
emit loginStarted();
m_process = new KProcess ( this );
QStringList args;
args << QLatin1String("--silent");
args << QLatin1String("--interactive");
args << QLatin1String("--persist");
// Setting prompt and subprompt
args << QLatin1String("--eval");
args << QLatin1String("PS1('CANTOR_OCTAVE_BACKEND_PROMPT:\\#> ');");
args << QLatin1String("--eval");
args << QLatin1String("PS2('CANTOR_OCTAVE_BACKEND_SUBPROMPT:\\#> ');");
// Add the cantor script directory to octave script search path
const QStringList& scriptDirs = locateAllCantorFiles(QLatin1String("octavebackend"), QStandardPaths::LocateDirectory);
if (scriptDirs.isEmpty())
qCritical() << "Octave script directory not found, needed for integrated plots";
else
{
for (const QString& dir : scriptDirs)
args << QLatin1String("--eval") << QString::fromLatin1("addpath \"%1\";").arg(dir);
}
// Do not show extra text in help commands
args << QLatin1String("--eval");
args << QLatin1String("suppress_verbose_help_message(1);");
if (OctaveSettings::integratePlots())
{
// Do not show the popup when plotting, rather only print to a file
args << QLatin1String("--eval");
args << QLatin1String("set (0, \"defaultfigurevisible\",\"off\");");
args << QLatin1String("--eval");
args << QLatin1String("graphics_toolkit gnuplot;");
}
else
{
args << QLatin1String("--eval");
args << QLatin1String("set (0, \"defaultfigurevisible\",\"on\");");
}
m_process->setProgram ( OctaveSettings::path().toLocalFile(), args );
qDebug() << "starting " << m_process->program();
m_process->setOutputChannelMode ( KProcess::SeparateChannels );
m_process->start();
m_process->waitForStarted();
connect ( m_process, SIGNAL (readyReadStandardOutput()), SLOT (readOutput()) );
connect ( m_process, SIGNAL (readyReadStandardError()), SLOT (readError()) );
connect ( m_process, SIGNAL (error(QProcess::ProcessError)), SLOT (processError()) );
if(!OctaveSettings::self()->autorunScripts().isEmpty()){
QString autorunScripts = OctaveSettings::self()->autorunScripts().join(QLatin1String("\n"));
evaluateExpression(autorunScripts, OctaveExpression::DeleteOnFinish, true);
updateVariables();
}
changeStatus(Cantor::Session::Done);
emit loginDone();
qDebug()<<"login done";
}
void OctaveSession::logout()
{
qDebug()<<"logout";
if(!m_process)
return;
disconnect(m_process, nullptr, this, nullptr);
if(status() == Cantor::Session::Running)
interrupt();
m_process->write("exit\n");
qDebug()<<"send exit command to octave";
if(!m_process->waitForFinished(1000))
{
m_process->kill();
qDebug()<<"octave still running, process kill enforced";
}
m_process->deleteLater();
m_process = nullptr;
expressionQueue().clear();
m_output.clear();
m_previousPromptNumber = 1;
Session::logout();
qDebug()<<"logout done";
}
void OctaveSession::interrupt()
{
qDebug() << expressionQueue().size();
if(!expressionQueue().isEmpty())
{
qDebug()<<"interrupting " << expressionQueue().first()->command();
if(m_process && m_process->state() != QProcess::NotRunning)
{
#ifndef Q_OS_WIN
const int pid=m_process->pid();
kill(pid, SIGINT);
#else
; //TODO: interrupt the process on windows
#endif
}
foreach (Cantor::Expression* expression, expressionQueue())
expression->setStatus(Cantor::Expression::Interrupted);
expressionQueue().clear();
// Cleanup inner state and call octave prompt printing
// If we move this code for interruption to Session, we need add function for
// cleaning before setting Done status
m_output.clear();
m_process->write("\n");
qDebug()<<"done interrupting";
}
changeStatus(Cantor::Session::Done);
}
void OctaveSession::processError()
{
qDebug() << "processError";
emit error(m_process->errorString());
}
Cantor::Expression* OctaveSession::evaluateExpression ( const QString& command, Cantor::Expression::FinishingBehavior finishingBehavior, bool internal )
{
qDebug() << "evaluating: " << command;
OctaveExpression* expression = new OctaveExpression ( this, internal);
expression->setCommand ( command );
expression->setFinishingBehavior ( finishingBehavior );
expression->evaluate();
return expression;
}
void OctaveSession::runFirstExpression()
{
OctaveExpression* expression = static_cast<OctaveExpression*>(expressionQueue().first());
connect(expression, SIGNAL(statusChanged(Cantor::Expression::Status)), this, SLOT(currentExpressionStatusChanged(Cantor::Expression::Status)));
QString command = expression->internalCommand();
expression->setStatus(Cantor::Expression::Computing);
if (isDoNothingCommand(command))
expression->setStatus(Cantor::Expression::Done);
else
{
m_process->write ( command.toLocal8Bit() );
}
}
void OctaveSession::readError()
{
qDebug() << "readError";
QString error = QString::fromLocal8Bit(m_process->readAllStandardError());
if (!expressionQueue().isEmpty() && !error.isEmpty())
{
OctaveExpression* const exp = static_cast<OctaveExpression*>(expressionQueue().first());
if (m_syntaxError)
{
m_syntaxError = false;
exp->parseError(i18n("Syntax Error"));
}
else
exp->parseError(error);
m_output.clear();
}
}
void OctaveSession::readOutput()
{
qDebug() << "readOutput";
while (m_process->bytesAvailable() > 0)
{
QString line = QString::fromLocal8Bit(m_process->readLine());
qDebug()<<"start parsing " << " " << line;
if (line.contains(m_prompt))
{
const int promptNumber = m_prompt.cap(1).toInt();
// Add all text before prompt, if exists
m_output += QStringRef(&line, 0, line.indexOf(m_prompt)).toString();
if (!expressionQueue().isEmpty())
{
const QString& command = expressionQueue().first()->command();
if (m_previousPromptNumber + 1 == promptNumber || isSpecialOctaveCommand(command))
{
if (!expressionQueue().isEmpty())
{
readError();
static_cast<OctaveExpression*>(expressionQueue().first())->parseOutput(m_output);
}
}
else
{
// Error command don't increase octave prompt number (usually, but not always)
readError();
}
}
m_previousPromptNumber = promptNumber;
m_output.clear();
}
else if (line.contains(m_subprompt) && m_subprompt.cap(1).toInt() == m_previousPromptNumber)
{
// User don't write finished octave statement (for example, write 'a = [1,2, ' only), so
// octave print subprompt and waits input finish.
m_syntaxError = true;
qDebug() << "subprompt catch";
- m_process->write(")]'\"\n"); // forse exit from subprompt
+ m_process->write(")]'\"\n"); // force exit from subprompt
m_output.clear();
}
else
m_output += line;
}
}
void OctaveSession::currentExpressionStatusChanged(Cantor::Expression::Status status)
{
qDebug() << "currentExpressionStatusChanged" << status << expressionQueue().first()->command();
switch (status)
{
case Cantor::Expression::Done:
case Cantor::Expression::Error:
finishFirstExpression();
break;
default:
break;
}
}
Cantor::CompletionObject* OctaveSession::completionFor ( const QString& cmd, int index )
{
return new OctaveCompletionObject ( cmd, index, this );
}
Cantor::SyntaxHelpObject* OctaveSession::syntaxHelpFor ( const QString& cmd )
{
return new OctaveSyntaxHelpObject ( cmd, this );
}
QSyntaxHighlighter* OctaveSession::syntaxHighlighter ( QObject* parent )
{
return new OctaveHighlighter ( parent, this );
}
void OctaveSession::runSpecificCommands()
{
m_process->write("figure(1,'visible','off')");
}
bool OctaveSession::isDoNothingCommand(const QString& command)
{
return PROMPT_UNCHANGEABLE_COMMAND.exactMatch(command) || command.isEmpty() || command == QLatin1String("\n");
}
bool OctaveSession::isSpecialOctaveCommand(const QString& command)
{
return command.contains(QLatin1String("completion_matches"));
}
diff --git a/src/backends/octave/testoctave.h b/src/backends/octave/testoctave.h
index 10de1888..8bf34c12 100644
--- a/src/backends/octave/testoctave.h
+++ b/src/backends/octave/testoctave.h
@@ -1,72 +1,72 @@
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2009 Alexander Rieder <alexanderrieder@gmail.com>
*/
#ifndef _TESTOCTAVE_H
#define _TESTOCTAVE_H
#include "backendtest.h"
/** This class test some of the basic functions of the Octave backend
The different tests represent some general expressions, as well
as expressions, that are known to have caused problems in earlier
versions
**/
class TestOctave : public BackendTest
{
Q_OBJECT
private Q_SLOTS:
//tests evaluating a simple command
void testSimpleCommand();
//tests a command, containing more than 1 line
void testMultilineCommand();
- //tests if the command queue works correcly
+ //tests if the command queue works correctly
void testCommandQueue();
void testVariableDefinition();
void testMatrixDefinition();
//some tests to see if comments are working correctly
void testSimpleExpressionWithComment();
void testCommentExpression();
void testMultilineCommandWithComment();
//tests a syntax error (not closing bracket)
void testInvalidSyntax();
void testCompletion();
void testHelpRequest();
void testSyntaxHelp();
//tests variable model
void testVariablesCreatingFromCode();
void testVariableCleanupAfterRestart();
void testVariableCreatingFromCodeWithPlot();
//tests doing a plot
void testPlot();
void testLoginLogout();
void testRestartWhileRunning();
private:
QString backendName() override;
};
#endif /* _TESTOCTAVE_H */
diff --git a/src/cantor.cpp b/src/cantor.cpp
index a1ea0e48..f7532e6e 100644
--- a/src/cantor.cpp
+++ b/src/cantor.cpp
@@ -1,715 +1,715 @@
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2009 Alexander Rieder <alexanderrieder@gmail.com>
*/
#include "cantor.h"
#include <KActionCollection>
#include <KConfigDialog>
#include <KConfigGroup>
#include <KMessageBox>
#include <KShortcutsDialog>
#include <KStandardAction>
#include <KNS3/DownloadDialog>
#include <KParts/ReadWritePart>
#include <KRecentFilesAction>
#include <QApplication>
#include <QCloseEvent>
#include <QDebug>
#include <QDockWidget>
#include <QDir>
#include <QFileDialog>
#include <QPushButton>
#include <QGraphicsView>
#include "lib/backend.h"
#include "lib/panelpluginhandler.h"
#include "lib/panelplugin.h"
#include "lib/worksheetaccess.h"
#include "settings.h"
#include "ui_settings.h"
#include "backendchoosedialog.h"
CantorShell::CantorShell() : KParts::MainWindow(), m_part(nullptr)
{
// set the shell's ui resource file
setXMLFile(QLatin1String("cantor_shell.rc"));
// then, setup our actions
setupActions();
createGUI(nullptr);
m_tabWidget=new QTabWidget(this);
m_tabWidget->setTabsClosable(true);
m_tabWidget->setMovable(true);
m_tabWidget->setDocumentMode(true);
setCentralWidget(m_tabWidget);
connect(m_tabWidget, SIGNAL(currentChanged(int)), this, SLOT(activateWorksheet(int)));
connect(m_tabWidget, SIGNAL(tabCloseRequested(int)), this, SLOT(closeTab(int)));
// apply the saved mainwindow settings, if any, and ask the mainwindow
// to automatically save settings if changed: window size, toolbar
// position, icon size, etc.
setAutoSaveSettings();
setDockOptions(QMainWindow::AnimatedDocks|QMainWindow::AllowTabbedDocks|QMainWindow::VerticalTabs);
updateNewSubmenu();
}
CantorShell::~CantorShell()
{
if (m_recentProjectsAction)
m_recentProjectsAction->saveEntries(KSharedConfig::openConfig()->group(QLatin1String("Recent Files")));
if (!m_newBackendActions.isEmpty())
{
unplugActionList(QLatin1String("new_worksheet_with_backend_list"));
qDeleteAll(m_newBackendActions);
m_newBackendActions.clear();
}
unplugActionList(QLatin1String("view_show_panel_list"));
qDeleteAll(m_panels);
m_panels.clear();
}
void CantorShell::load(const QUrl &url)
{
if (!m_part||!m_part->url().isEmpty() || m_part->isModified() )
{
addWorksheet(QLatin1String("null"));
m_tabWidget->setCurrentIndex(m_parts.size()-1);
}
if (!m_part->openUrl( url ))
closeTab(m_tabWidget->currentIndex());
if (m_recentProjectsAction)
m_recentProjectsAction->addUrl(url);
}
bool CantorShell::hasAvailableBackend()
{
bool hasBackend=false;
foreach(Cantor::Backend* b, Cantor::Backend::availableBackends())
{
if(b->isEnabled())
hasBackend=true;
}
return hasBackend;
}
void CantorShell::setupActions()
{
QAction* openNew = KStandardAction::openNew(this, SLOT(fileNew()), actionCollection());
openNew->setPriority(QAction::LowPriority);
QAction* open = KStandardAction::open(this, SLOT(fileOpen()), actionCollection());
open->setPriority(QAction::LowPriority);
m_recentProjectsAction = KStandardAction::openRecent(this, &CantorShell::load, actionCollection());
m_recentProjectsAction->setPriority(QAction::LowPriority);
m_recentProjectsAction->loadEntries(KSharedConfig::openConfig()->group(QLatin1String("Recent Files")));
KStandardAction::close (this, SLOT(closeTab()), actionCollection());
KStandardAction::quit(qApp, SLOT(closeAllWindows()), actionCollection());
createStandardStatusBarAction();
//setStandardToolBarMenuEnabled(true);
KStandardAction::keyBindings(this, SLOT(optionsConfigureKeys()), actionCollection());
KStandardAction::configureToolbars(this, SLOT(configureToolbars()), actionCollection());
KStandardAction::preferences(this, SLOT(showSettings()), actionCollection());
QAction * downloadExamples = new QAction(i18n("Download Example Worksheets"), actionCollection());
downloadExamples->setIcon(QIcon::fromTheme(QLatin1String("get-hot-new-stuff")));
actionCollection()->addAction(QLatin1String("file_example_download"), downloadExamples);
connect(downloadExamples, SIGNAL(triggered()), this, SLOT(downloadExamples()));
QAction * openExample =new QAction(i18n("&Open Example"), actionCollection());
openExample->setIcon(QIcon::fromTheme(QLatin1String("document-open")));
actionCollection()->addAction(QLatin1String("file_example_open"), openExample);
connect(openExample, SIGNAL(triggered()), this, SLOT(openExample()));
QAction* toPreviousTab = new QAction(i18n("Go to previous worksheet"), actionCollection());
actionCollection()->addAction(QLatin1String("go_to_previous_tab"), toPreviousTab);
actionCollection()->setDefaultShortcut(toPreviousTab, Qt::CTRL+Qt::Key_PageDown);
connect(toPreviousTab, &QAction::triggered, toPreviousTab, [this](){
const int index = m_tabWidget->currentIndex()-1;
if (index >= 0)
m_tabWidget->setCurrentIndex(index);
else
m_tabWidget->setCurrentIndex(m_tabWidget->count()-1);
});
addAction(toPreviousTab);
QAction* toNextTab = new QAction(i18n("Go to next worksheet"), actionCollection());
actionCollection()->addAction(QLatin1String("go_to_next_tab"), toNextTab);
actionCollection()->setDefaultShortcut(toNextTab, Qt::CTRL+Qt::Key_PageUp);
connect(toNextTab, &QAction::triggered, toNextTab, [this](){
const int index = m_tabWidget->currentIndex()+1;
if (index < m_tabWidget->count())
m_tabWidget->setCurrentIndex(index);
else
m_tabWidget->setCurrentIndex(0);
});
addAction(toNextTab);
}
void CantorShell::saveProperties(KConfigGroup & /*config*/)
{
// the 'config' object points to the session managed
// config file. anything you write here will be available
// later when this app is restored
}
void CantorShell::readProperties(const KConfigGroup & /*config*/)
{
// the 'config' object points to the session managed
// config file. this function is automatically called whenever
// the app is being restored. read in here whatever you wrote
// in 'saveProperties'
}
void CantorShell::fileNew()
{
if (sender()->inherits("QAction"))
{
QAction * a = qobject_cast<QAction*>(sender());
QString backendName = a->data().toString();
if (!backendName.isEmpty())
{
addWorksheet(backendName);
return;
}
}
addWorksheet();
}
void CantorShell::optionsConfigureKeys()
{
KShortcutsDialog dlg( KShortcutsEditor::AllActions, KShortcutsEditor::LetterShortcutsDisallowed, this );
dlg.addCollection( actionCollection(), i18n("Cantor") );
if (m_part)
dlg.addCollection( m_part->actionCollection(), i18n("Cantor") );
dlg.configure( true );
}
void CantorShell::fileOpen()
{
// this slot is called whenever the File->Open menu is selected,
// the Open shortcut is pressed (usually CTRL+O) or the Open toolbar
// button is clicked
static const QString& worksheetFilter = i18n("Cantor Worksheet (*.cws)");
static const QString& notebookFilter = i18n("Jupyter Notebook (*.ipynb)");
QString filter;
if (m_previousFilter == notebookFilter)
filter = notebookFilter + QLatin1String(";;") + worksheetFilter;
else
filter = worksheetFilter + QLatin1String(";;") + notebookFilter;
QUrl url = QFileDialog::getOpenFileUrl(this, i18n("Open file"), QUrl(), filter, &m_previousFilter);
if (url.isEmpty() == false)
{
// About this function, the style guide (
// http://developer.kde.org/documentation/standards/kde/style/basics/index.html )
// says that it should open a new window if the document is _not_
// in its initial state. This is what we do here..
/*if ( m_part->url().isEmpty() && ! m_part->isModified() )
{
// we open the file in this window...
load( url );
}
else
{
// we open the file in a new window...
CantorShell* newWin = new CantorShell;
newWin->load( url );
newWin->show();
}*/
load( url );
}
}
void CantorShell::addWorksheet()
{
if(hasAvailableBackend()) //There is no point in asking for the backend, if no one is available
{
QString backend = Settings::self()->defaultBackend();
if (backend.isEmpty())
{
QPointer<BackendChooseDialog> dlg=new BackendChooseDialog(this);
if(dlg->exec())
{
backend = dlg->backendName();
addWorksheet(backend);
}
delete dlg;
}
else
{
addWorksheet(backend);
}
}else
{
QTextBrowser *browser=new QTextBrowser(this);
QString backendList=QLatin1String("<ul>");
int backendListSize = 0;
foreach(Cantor::Backend* b, Cantor::Backend::availableBackends())
{
- if(!b->requirementsFullfilled()) //It's disabled because of misssing dependencies, not because of some other reason(like eg. nullbackend)
+ if(!b->requirementsFullfilled()) //It's disabled because of missing dependencies, not because of some other reason(like eg. nullbackend)
{
backendList+=QString::fromLatin1("<li>%1: <a href=\"%2\">%2</a></li>").arg(b->name(), b->url());
++backendListSize;
}
}
browser->setHtml(i18np("<h1>No Backend Found</h1>\n" \
"<div>You could try:\n" \
" <ul>" \
" <li>Changing the settings in the config dialog;</li>" \
" <li>Installing packages for the following program:</li>" \
" %2 " \
" </ul> " \
"</div> "
, "<h1>No Backend Found</h1>\n" \
"<div>You could try:\n" \
" <ul>" \
" <li>Changing the settings in the config dialog;</li>" \
" <li>Installing packages for one of the following programs:</li>" \
" %2 " \
" </ul> " \
"</div> "
, backendListSize, backendList
));
browser->setObjectName(QLatin1String("ErrorMessage"));
m_tabWidget->addTab(browser, i18n("Error"));
}
}
void CantorShell::addWorksheet(const QString& backendName)
{
static int sessionCount=1;
// this routine will find and load our Part. it finds the Part by
// name which is a bad idea usually.. but it's alright in this
// case since our Part is made for this Shell
KPluginLoader loader(QLatin1String("cantorpart"));
KPluginFactory* factory = loader.factory();
if (factory)
{
Cantor::Backend* backend = Cantor::Backend::getBackend(backendName);
if (backend)
{
if (backend->isEnabled() || backendName == QLatin1String("null"))
{
// now that the Part is loaded, we cast it to a Part to get our hands on it
KParts::ReadWritePart* part = factory->create<KParts::ReadWritePart>(m_tabWidget, QVariantList()<<backendName);
if (part)
{
connect(part, SIGNAL(setCaption(QString,QIcon)), this, SLOT(setTabCaption(QString,QIcon)));
connect(part, SIGNAL(worksheetSave(QUrl)), this, SLOT(onWorksheetSave(QUrl)));
m_parts.append(part);
int tab = m_tabWidget->addTab(part->widget(), QIcon::fromTheme(backend->icon()), i18n("Session %1", sessionCount++));
m_tabWidget->setCurrentIndex(tab);
// Setting focus on worksheet view, because Qt clear focus of added widget inside addTab
// This fix https://bugs.kde.org/show_bug.cgi?id=395976
part->widget()->findChild<QGraphicsView*>()->setFocus();
}
else
{
qDebug()<<"error creating part ";
}
}
else
{
KMessageBox::error(this, i18n("%1 backend installed, but inactive. Please check installation and Cantor settings", backendName), i18n("Cantor"));
}
}
else
KMessageBox::error(this, i18n("Backend %1 is not installed", backendName), i18n("Cantor"));
}
else
{
// if we couldn't find our Part, we exit since the Shell by
// itself can't do anything useful
KMessageBox::error(this, i18n("Failed to find the Cantor Part with error %1", loader.errorString()));
qApp->quit();
// we return here, cause qApp->quit() only means "exit the
// next time we enter the event loop...
return;
}
}
void CantorShell::activateWorksheet(int index)
{
QObject* pluginHandler=m_part->findChild<QObject*>(QLatin1String("PanelPluginHandler"));
if (pluginHandler)
disconnect(pluginHandler,SIGNAL(pluginsChanged()), this, SLOT(updatePanel()));
// Save part state before change worksheet
if (m_part)
{
QStringList visiblePanelNames;
foreach (QDockWidget* doc, m_panels)
{
if (doc->widget() && doc->widget()->isVisible())
visiblePanelNames << doc->objectName();
}
m_pluginsVisibility[m_part] = visiblePanelNames;
}
m_part=findPart(m_tabWidget->widget(index));
if(m_part)
{
createGUI(m_part);
QObject* pluginHandler=m_part->findChild<QObject*>(QLatin1String("PanelPluginHandler"));
connect(pluginHandler, SIGNAL(pluginsChanged()), this, SLOT(updatePanel()));
updatePanel();
}
else
qDebug()<<"selected part doesn't exist";
m_tabWidget->setCurrentIndex(index);
}
void CantorShell::setTabCaption(const QString& caption, const QIcon& icon)
{
if (caption.isEmpty()) return;
KParts::ReadWritePart* part=dynamic_cast<KParts::ReadWritePart*>(sender());
if (part)
{
m_tabWidget->setTabText(m_tabWidget->indexOf(part->widget()), caption);
m_tabWidget->setTabIcon(m_tabWidget->indexOf(part->widget()), icon);
}
}
void CantorShell::closeTab(int index)
{
if (!reallyClose(false))
{
return;
}
QWidget* widget = nullptr;
if (index >= 0)
{
widget = m_tabWidget->widget(index);
}
else if (m_part)
{
widget = m_part->widget();
}
if (!widget)
{
qWarning() << "Could not find widget by tab index" << index;
return;
}
m_tabWidget->removeTab(index);
if(widget->objectName()==QLatin1String("ErrorMessage"))
{
widget->deleteLater();
}else
{
KParts::ReadWritePart* part= findPart(widget);
if(part)
{
m_parts.removeAll(part);
m_pluginsVisibility.remove(part);
delete part;
}
}
if (m_tabWidget->count() == 0)
setCaption(QString());
updatePanel();
}
bool CantorShell::reallyClose(bool checkAllParts) {
if(checkAllParts && m_parts.count() > 1) {
bool modified = false;
foreach( KParts::ReadWritePart* const part, m_parts)
{
if(part->isModified()) {
modified = true;
break;
}
}
if(!modified) return true;
int want_save = KMessageBox::warningYesNo( this,
i18n("Multiple unsaved Worksheets are opened. Do you want to close them?"),
i18n("Close Cantor"));
switch (want_save) {
case KMessageBox::Yes:
return true;
case KMessageBox::No:
return false;
}
}
if (m_part && m_part->isModified() ) {
int want_save = KMessageBox::warningYesNoCancel( this,
i18n("The current project has been modified. Do you want to save it?"),
i18n("Save Project"));
switch (want_save) {
case KMessageBox::Yes:
m_part->save();
if(m_part->waitSaveComplete()) {
return true;
} else {
m_part->setModified(true);
return false;
}
case KMessageBox::Cancel:
return false;
case KMessageBox::No:
return true;
}
}
return true;
}
void CantorShell::closeEvent(QCloseEvent* event) {
if(!reallyClose()) {
event->ignore();
} else {
KParts::MainWindow::closeEvent(event);
}
}
void CantorShell::showSettings()
{
KConfigDialog *dialog = new KConfigDialog(this, QLatin1String("settings"), Settings::self());
QWidget *generalSettings = new QWidget;
Ui::SettingsBase base;
base.setupUi(generalSettings);
base.kcfg_DefaultBackend->addItems(Cantor::Backend::listAvailableBackends());
dialog->addPage(generalSettings, i18n("General"), QLatin1String("preferences-other"));
foreach(Cantor::Backend* backend, Cantor::Backend::availableBackends())
{
if (backend->config()) //It has something to configure, so add it to the dialog
dialog->addPage(backend->settingsWidget(dialog), backend->config(), backend->name(), backend->icon());
}
dialog->show();
}
void CantorShell::downloadExamples()
{
KNS3::DownloadDialog dialog;
dialog.exec();
foreach (const KNS3::Entry& e, dialog.changedEntries())
{
qDebug() << "Changed Entry: " << e.name();
}
}
void CantorShell::openExample()
{
QString dir = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1String("/examples");
if (dir.isEmpty()) return;
QDir().mkpath(dir);
QStringList files=QDir(dir).entryList(QDir::Files);
QPointer<QDialog> dlg=new QDialog(this);
QListWidget* list=new QListWidget(dlg);
foreach(const QString& file, files)
{
QString name=file;
name.remove(QRegExp(QLatin1String("-.*\\.hotstuff-access$")));
list->addItem(name);
}
QVBoxLayout *mainLayout = new QVBoxLayout;
dlg->setLayout(mainLayout);
mainLayout->addWidget(list);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
mainLayout->addWidget(buttonBox);
buttonBox->button(QDialogButtonBox::Ok)->setIcon(QApplication::style()->standardIcon(QStyle::SP_DialogOkButton));
buttonBox->button(QDialogButtonBox::Cancel)->setIcon(QApplication::style()->standardIcon(QStyle::SP_DialogCancelButton));
connect(buttonBox, SIGNAL(accepted()), dlg, SLOT(accept()) );
connect(buttonBox, SIGNAL(rejected()), dlg, SLOT(reject()) );
if (dlg->exec()==QDialog::Accepted&&list->currentRow()>=0)
{
const QString& selectedFile=files[list->currentRow()];
QUrl url = QUrl::fromLocalFile(QDir(dir).absoluteFilePath(selectedFile));
qDebug()<<"loading file "<<url;
load(url);
}
delete dlg;
}
KParts::ReadWritePart* CantorShell::findPart(QWidget* widget)
{
foreach( KParts::ReadWritePart* const part, m_parts)
{
if(part->widget()==widget)
return part;
}
return nullptr;
}
void CantorShell::updatePanel()
{
unplugActionList(QLatin1String("view_show_panel_list"));
//remove all of the previous panels (but do not delete the widgets)
foreach(QDockWidget* dock, m_panels)
{
QWidget* widget=dock->widget();
if(widget!=nullptr)
{
widget->setParent(this);
widget->hide();
}
dock->deleteLater();
}
m_panels.clear();
QList<QAction*> panelActions;
Cantor::PanelPluginHandler* handler=m_part->findChild<Cantor::PanelPluginHandler*>(QLatin1String("PanelPluginHandler"));
if(!handler)
{
qDebug()<<"no PanelPluginHandle found for this part";
return;
}
QDockWidget* last=nullptr;
QList<Cantor::PanelPlugin*> plugins=handler->plugins();
const bool isNewWorksheet = !m_pluginsVisibility.contains(m_part);
foreach(Cantor::PanelPlugin* plugin, plugins)
{
if(plugin==nullptr)
{
qDebug()<<"somethings wrong";
continue;
}
qDebug()<<"adding panel for "<<plugin->name();
plugin->setParentWidget(this);
QDockWidget* docker=new QDockWidget(plugin->name(), this);
docker->setObjectName(plugin->name());
docker->setWidget(plugin->widget());
addDockWidget ( Qt::RightDockWidgetArea, docker );
// Set visibility for dock from saved info
if (isNewWorksheet)
{
if (plugin->showOnStartup())
docker->show();
else
docker->hide();
}
else
{
if (m_pluginsVisibility[m_part].contains(plugin->name()))
docker->show();
else
docker->hide();
}
if(last!=nullptr)
tabifyDockWidget(last, docker);
last=docker;
connect(plugin, &Cantor::PanelPlugin::visibilityRequested, this, &CantorShell::pluginVisibilityRequested);
m_panels.append(docker);
//Create the action to show/hide this panel
panelActions<<docker->toggleViewAction();
}
plugActionList(QLatin1String("view_show_panel_list"), panelActions);
updateNewSubmenu();
}
void CantorShell::updateNewSubmenu()
{
unplugActionList(QLatin1String("new_worksheet_with_backend_list"));
qDeleteAll(m_newBackendActions);
m_newBackendActions.clear();
foreach (Cantor::Backend* backend, Cantor::Backend::availableBackends())
{
if (!backend->isEnabled())
continue;
QAction * action = new QAction(QIcon::fromTheme(backend->icon()), backend->name(), nullptr);
action->setData(backend->name());
connect(action, SIGNAL(triggered()), this, SLOT(fileNew()));
m_newBackendActions << action;
}
plugActionList(QLatin1String("new_worksheet_with_backend_list"), m_newBackendActions);
}
Cantor::WorksheetAccessInterface* CantorShell::currentWorksheetAccessInterface()
{
Cantor::WorksheetAccessInterface* wa=m_part->findChild<Cantor::WorksheetAccessInterface*>(Cantor::WorksheetAccessInterface::Name);
if (!wa)
qDebug()<<"failed to access worksheet access interface for current part";
return wa;
}
void CantorShell::pluginVisibilityRequested()
{
Cantor::PanelPlugin* plugin = static_cast<Cantor::PanelPlugin*>(sender());
for (QDockWidget* docker: m_panels)
{
if (plugin->name() == docker->windowTitle())
{
if (docker->isHidden())
docker->show();
docker->raise();
}
}
}
void CantorShell::onWorksheetSave(const QUrl& url)
{
if (m_recentProjectsAction)
m_recentProjectsAction->addUrl(url);
}
diff --git a/src/lib/backend.h b/src/lib/backend.h
index ce1c8d75..1904cd53 100644
--- a/src/lib/backend.h
+++ b/src/lib/backend.h
@@ -1,219 +1,219 @@
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2009 Alexander Rieder <alexanderrieder@gmail.com>
*/
#ifndef _BACKEND_H
#define _BACKEND_H
#include <QObject>
#include <QVariant>
#include <KXMLGUIClient>
#include <KPluginInfo>
#include "cantor_export.h"
class KConfigSkeleton;
class QWidget;
/**
* Namespace collecting all Classes of the Cantor Libraries
*/
namespace Cantor
{
class Session;
class Extension;
class BackendPrivate;
/**
* The Backend class provides access to information about the backend.
* It provides access to what features are supported by the backend, and
* a factory method to create a new Session
* It needs to be subclassed by all Backends.
*
* @author Alexander Rieder
*/
class CANTOR_EXPORT Backend : public QObject, public KXMLGUIClient
{
Q_OBJECT
public:
/**
* This enum is used to specify the Features, supported by a backend.
*/
enum Capability{
Nothing = 0x0, ///< the Backend doesn't support any of the optional features
LaTexOutput = 0x1, ///< it can output results as LaTeX code
InteractiveMode = 0x2, /**< it supports an interactive workflow. (It means a command
can ask for additional input while running
*/
SyntaxHighlighting = 0x4, ///< it offers a custom Syntax Highlighter
Completion = 0x8, ///< it offers completion of partially typed commands
SyntaxHelp = 0x10, /**< it offers help about a commands syntax, that will
be shown in a tooltip
*/
VariableManagement= 0x20 ///< it offers access to the variables (for variable management panel)
};
Q_DECLARE_FLAGS(Capabilities, Capability)
protected:
/**
* Create a new Backend. Normally the static createBackend factory method
* should be used.
* @param parent the Parent object
* @param args optional arguments (not used)
*/
explicit Backend( QObject* parent = nullptr,const QList<QVariant>& args=QList<QVariant>() );
/**
* Destructor. Doesn't anything.
*/
~Backend() override;
public:
/**
* Creates a new Session. It is the way to go to create a Session,
* don't call new Session on your own.
* @return a new Session of this Backend, or 0 if creating failed
*/
virtual Session* createSession() = 0;
/**
* Returns list of the supported optional features
* @return a list of features, containing items of the Capability enum, ORed together
*/
virtual Capabilities capabilities() const = 0;
/**
* Returns whether all of this backends requirements are fulfilled, or if some are missing.
* @param reason if set, backend write information about missing requirements, if they exists
* @return @c true if all the requirements needed to use this Backend are fulfilled
* @return @c false some requirements are missing. e.g. the maxima executable can not be found
* @see Capability
*/
virtual bool requirementsFullfilled(QString* const reason = nullptr) const;
/**
* Returns a unique string to identify this backend.
* In contrast to name() this string isn't translated
* @return string to identify backend
*/
virtual QString id() const = 0;
/**
* Returns the recommended version of the backend supported by Cantor
* @return the recommended version of the backend
*/
virtual QString version() const;
//Stuff extracted from the .desktop file
/**
* Returns the name of the backend
* @return the backends name
*/
QString name() const;
/**
* Returns a short comment about the backend.
* @return comment about the backend
*/
QString comment() const;
/**
* Returns the icon to use with this backend
* @return name of the icon
*/
QString icon() const;
/**
* Returns the Url of the Homepage for the Backend
* @return the url
*/
QString url() const;
/**
* Returns an Url pointing to the Help of the Backend
* The method should be overwritten by all Backends(who have an online help)
- * You should make the returned Url translateble, e.g. by doing something like:
+ * You should make the returned Url translatable, e.g. by doing something like:
* return i18nc("the url to the documentation of KAlgebra, please check if there is a translated version and use the correct url",
* "http://docs.kde.org/stable/en/kdeedu/kalgebra/");
* @return Url of the help
*/
virtual QUrl helpUrl() const;
/**
* Returns if the backend should be enabled (shown in the Backend dialog)
* @return @c true, if the enabled flag is set to true, and the requirements are fulfilled
- * @return @c false, if the backend was purposly disabled, or requirements are missing
+ * @return @c false, if the backend was purposely disabled, or requirements are missing
* @see requirementsFullfilled()
*/
bool isEnabled() const;
/**
* Enables/disables this backend
* @param enabled true to enable backend false to disable
*/
void setEnabled(bool enabled);
/**
* Returns a longer description of the Backend, e.g. purpose, strengths etc.
* It should help the user to decide between the different Backends
* @return a description of the backend. It can contain html
*/
virtual QString description() const;
/**
* Returns a Widget for configuring this backend
* @return Widget for usage in the Settings dialog
*/
virtual QWidget* settingsWidget(QWidget* parent) const;
/**
* Returns a KConfig object, containing all the settings,
* the backend might need
* @return a KConfigSkeleton object, for configuring this backend
*/
virtual KConfigSkeleton* config() const;
/**
* Returns a list of the names of all the Extensions supported by this backend
* @return a list of the names of all the Extensions supported by this backend
* @see extension(const QString& name)
*/
QStringList extensions() const;
/**
* Returns an Extension of this backend for the given name, or null
* if the Backend doesn't have an extension with this name.
* @return Pointer to the Extension object with the given name
*/
Extension * extension(const QString& name) const;
/**
* Returns a list of the names of all the installed and enabled backends
* @return a list of the names of all the installed and enabled backends
* @see isEnabled()
*/
static QStringList listAvailableBackends();
/**
* Returns Pointers to all the installed backends
* @return Pointers to all the installed backends
*/
static QList<Backend*> availableBackends();
/**
* Returns the backend with the given name, or null if it isn't found
* @return the backend with the given name, or null if it isn't found
*/
static Backend* getBackend(const QString& name);
private:
BackendPrivate* d;
};
Q_DECLARE_OPERATORS_FOR_FLAGS(Backend::Capabilities)
}
#endif /* _BACKEND_H */
diff --git a/src/lib/session.h b/src/lib/session.h
index c2a8bea2..f22e1851 100644
--- a/src/lib/session.h
+++ b/src/lib/session.h
@@ -1,273 +1,273 @@
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2009 Alexander Rieder <alexanderrieder@gmail.com>
*/
#ifndef _SESSION_H
#define _SESSION_H
#include <QObject>
#include <QStandardPaths>
#include "cantor_export.h"
#include "expression.h"
#include "defaultvariablemodel.h"
class QTextEdit;
class QSyntaxHighlighter;
class QAbstractItemModel;
/**
* Namespace collecting all Classes of the Cantor Libraries
*/
namespace Cantor
{
class Backend;
class SessionPrivate;
class CompletionObject;
class SyntaxHelpObject;
class DefaultVariableModel;
/**
* The Session object is the main class used to interact with a Backend.
* It is used to evaluate Expressions, get completions, syntax highlighting, etc.
*
* @author Alexander Rieder
*/
class CANTOR_EXPORT Session : public QObject
{
Q_OBJECT
public:
enum Status {
Running, ///< the session is busy, running some expression
Done, ///< the session has done all the jobs, and is now waiting for more
Disable ///< the session don't login yet, or already logout
};
/**
* Create a new Session. This should not yet set up the complete session,
* thats job of the login() function
* @see login()
*/
explicit Session( Backend* backend);
/**
* Similar to Session::Session, but also specify variable model for automatically handles model's updates
*/
explicit Session( Backend* backend, DefaultVariableModel* model);
/**
* Destructor
*/
~Session() override;
/**
* Login to the Session. In this function you should do anything needed to set up
* the session, and make it ready for usage. The method should be implemented non-blocking.
* Emit loginStarted() prior to connection to the actual backend in order to notify cantor_part about it.
* If the logging in is completed, the loginDone() signal must be emitted
*/
virtual void login() = 0;
/**
* Log out of the Session. Destroy everything specific to a single session, e.g.
* stop all the running processes etc. Also after logout session status must be Status::Disable
* Default implementation does basic operations for all sessions (for example, variable model cleanup)
* NOTE: restarting the session consists of first logout() and then login()
*/
virtual void logout();
/**
* Passes the given command to the backend and returns a Pointer
* to a new Expression object, which will emit the gotResult()
* signal as soon as the computation is done. The result will
* then be accessible by Expression::result()
* @param command the command that should be run by the backend.
* @param finishingBehavior the FinishingBehaviour that should be used for this command. @see Expression::FinishingBehaviour
* @param internal true, if it is an internal command @see Expression::Expression(Session*, bool)
* @return an Expression object, representing this command
*/
virtual Expression* evaluateExpression(const QString& command, Expression::FinishingBehavior finishingBehavior = Expression::FinishingBehavior::DoNotDelete, bool internal = false) = 0;
/**
* Append the expression to queue .
* @see expressionQueue() const
*/
void enqueueExpression(Expression*);
/**
* Interrupts all the running calculations in this session
* After this function expression queue must be clean
*/
virtual void interrupt() = 0;
/**
* Returns tab-completion, for this command/command-part.
* The return type is a CompletionObject. The fetching
* of the completions works asynchronously, you'll have to
* listen to the done() Signal of the returned object
* @param cmd The partial command that should be completed
* @param index The index (cursor position) at which completion
* was invoked. Defaults to -1, indicating the end of the string.
* @return a Completion object, representing this completion
* @see CompletionObject
*/
virtual CompletionObject* completionFor(const QString& cmd, int index = -1);
/**
* Returns Syntax help, for this command.
* It returns a SyntaxHelpObject, that will fetch the
* needed information asynchronously. You need to listen
* to the done() Signal of the Object
* @param cmd the command, syntax help is requested for
* @return SyntaxHelpObject, representing the help request
* @see SyntaxHelpObject
*/
virtual SyntaxHelpObject* syntaxHelpFor(const QString& cmd);
/**
* returns a syntax highlighter for this session
* @param parent QObject the Highlighter's parent
* @return QSyntaxHighlighter doing the highlighting for this Session
*/
virtual QSyntaxHighlighter* syntaxHighlighter(QObject* parent);
/**
* returns a Model to interact with the variables or nullptr, if
* this backend have a variable model, which not inherit from
* default variable model class (in this case @see variableDataModel())
* @return DefaultVariableModel to interact with the variables
*/
virtual DefaultVariableModel* variableModel() const;
/**
* returns QAbstractItemModel to interact with the variables
*/
virtual QAbstractItemModel* variableDataModel() const;
/**
* Enables/disables Typesetting for this session.
* For this setting to make effect, the Backend must support
* LaTeX typesetting (as indicated by the capabilities() flag.
* @param enable true to enable, false to disable typesetting
*/
virtual void setTypesettingEnabled(bool enable);
/**
* Updates the worksheet path in the session.
* This can be useful to set the path of the currently opened
* Cantor project file in the backend interpreter.
* Default implementation does nothing. Derived classes have
* to implement the proper logic if this feature is supported.
* @param path the new absolute path to the worksheet.
*/
virtual void setWorksheetPath(const QString& path);
/**
* Returns the Backend, this Session is for
* @return the Backend, this Session is for
*/
Backend* backend();
/**
* Returns the status this Session has
* @return the status this Session has
*/
Cantor::Session::Status status();
/**
* Returns whether typesetting is enabled or not
* @return whether typesetting is enabled or not
*/
bool isTypesettingEnabled();
/**
* Returns the next available Expression id
* It is basically a counter, incremented for
* each new Expression
* @return next Expression id
*/
int nextExpressionId();
protected:
/**
* Change the status of the Session. This will cause the
* stausChanged signal to be emitted
* @param newStatus the new status of the session
*/
void changeStatus(Cantor::Session::Status newStatus);
/**
* Session can process one single expression at one time.
* Any other expressions submitted by the user are queued first until they get processed.
* The expression queue implements the FIFO mechanism.
* The queud expression have the status \c Expression::Queued.
*/
QList<Expression*>& expressionQueue() const;
/**
* Execute first expression in expression queue.
* Also, this function changes the status from Queued to Computing.
* @see expressionQueue() const
*/
virtual void runFirstExpression();
/**
* This method dequeues the expression and goes to the next expression, if the queue is not empty.
* Also, this method updates the variable model, if needed.
* If the queue is empty, the session status is set to Done.
* @param setDoneAfterUpdate enable setting status to Done after variable update, if queue is empty
*/
virtual void finishFirstExpression(bool setDoneAfterUpdate = false);
/**
- * Starts variable update immideatly, useful for subclasses, which run internal command
+ * Starts variable update immedeatly, useful for subclasses, which run internal command
* which could change variables listen
*/
virtual void updateVariables();
/**
* Setting variable model, useful if model constructor requires functional session
*/
void setVariableModel(DefaultVariableModel* model);
/**
* Search file for session in AppDataLocation and in GenericDataLocation
*/
QString locateCantorFile(const QString& partialPath, QStandardPaths::LocateOptions options = QStandardPaths::LocateFile);
QStringList locateAllCantorFiles(const QString& partialPath, QStandardPaths::LocateOptions options = QStandardPaths::LocateFile);
/**
* Sometimes backend process/server could crash, stop responding, in other words, session can't
* continue to work without restart.
* This method will notify about session crashing with automatically logout
* and another actions, which needed to do in situations like that
*/
void reportSessionCrash(const QString& additionalInfo = QString());
Q_SIGNALS:
void statusChanged(Cantor::Session::Status newStatus);
void loginStarted();
void loginDone();
void error(const QString& msg);
private:
SessionPrivate* d;
};
}
#endif /* _SESSION_H */
diff --git a/src/loadedexpression.cpp b/src/loadedexpression.cpp
index 6cc44c1e..4a19de87 100644
--- a/src/loadedexpression.cpp
+++ b/src/loadedexpression.cpp
@@ -1,333 +1,333 @@
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2009 Alexander Rieder <alexanderrieder@gmail.com>
*/
#include "loadedexpression.h"
#include "lib/jupyterutils.h"
#include "lib/imageresult.h"
#include "lib/epsresult.h"
#include "lib/textresult.h"
#include "lib/latexresult.h"
#include "lib/animationresult.h"
#include "lib/latexrenderer.h"
#include "lib/mimeresult.h"
#include "lib/htmlresult.h"
#include <QDir>
#include <QStandardPaths>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonDocument>
#include <QDebug>
#include <QPixmap>
#include <QTemporaryFile>
LoadedExpression::LoadedExpression( Cantor::Session* session ) : Cantor::Expression( session, false, -1)
{
}
void LoadedExpression::interrupt()
{
//Do nothing
}
void LoadedExpression::evaluate()
{
//Do nothing
}
void LoadedExpression::loadFromXml(const QDomElement& xml, const KZip& file)
{
setCommand(xml.firstChildElement(QLatin1String("Command")).text());
const QDomNodeList& results = xml.elementsByTagName(QLatin1String("Result"));
for (int i = 0; i < results.size(); i++)
{
const QDomElement& resultElement = results.at(i).toElement();
const QString& type = resultElement.attribute(QLatin1String("type"));
qDebug() << "type" << type;
if ( type == QLatin1String("text"))
{
const QString& format = resultElement.attribute(QLatin1String("format"));
bool isStderr = resultElement.attribute(QLatin1String("stderr")).toInt();
Cantor::TextResult* result = new Cantor::TextResult(resultElement.text());
if (format == QLatin1String("latex"))
result->setFormat(Cantor::TextResult::LatexFormat);
result->setStdErr(isStderr);
addResult(result);
}
else if (type == QLatin1String("mime"))
{
const QDomElement& resultElement = results.at(i).toElement();
QJsonObject mimeBundle;
const QDomNodeList& contents = resultElement.elementsByTagName(QLatin1String("Content"));
for (int x = 0; x < contents.count(); x++)
{
const QDomElement& content = contents.at(x).toElement();
const QString& mimeType = content.attribute(QLatin1String("key"));
QJsonDocument jsonDoc = QJsonDocument::fromJson(content.text().toUtf8());;
const QJsonValue& value = jsonDoc.object().value(QLatin1String("content"));
mimeBundle.insert(mimeType, value);
}
addResult(new Cantor::MimeResult(mimeBundle));
}
else if (type == QLatin1String("html"))
{
const QString& formatString = resultElement.attribute(QLatin1String("showCode"));
Cantor::HtmlResult::Format format = Cantor::HtmlResult::Html;
if (formatString == QLatin1String("htmlSource"))
format = Cantor::HtmlResult::HtmlSource;
else if (formatString == QLatin1String("plain"))
format = Cantor::HtmlResult::PlainAlternative;
const QString& plain = resultElement.firstChildElement(QLatin1String("Plain")).text();
const QString& html = resultElement.firstChildElement(QLatin1String("Html")).text();
std::map<QString, QJsonValue> alternatives;
const QDomNodeList& alternativeElms = resultElement.elementsByTagName(QLatin1String("Alternative"));
for (int x = 0; x < alternativeElms.count(); x++)
{
const QDomElement& content = alternativeElms.at(x).toElement();
const QString& mimeType = content.attribute(QLatin1String("key"));
QJsonDocument jsonDoc = QJsonDocument::fromJson(content.text().toUtf8());;
const QJsonValue& value = jsonDoc.object().value(QLatin1String("root"));
alternatives[mimeType] = value;
}
Cantor::HtmlResult* result = new Cantor::HtmlResult(html, plain, alternatives);
result->setFormat(format);
addResult(result);
}
else if (type == QLatin1String("image") || type == QLatin1String("latex") || type == QLatin1String("animation") || type == QLatin1String("epsimage"))
{
const KArchiveEntry* imageEntry=file.directory()->entry(resultElement.attribute(QLatin1String("filename")));
if (imageEntry&&imageEntry->isFile())
{
const KArchiveFile* imageFile=static_cast<const KArchiveFile*>(imageEntry);
QString dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation);
imageFile->copyTo(dir);
QUrl imageUrl = QUrl::fromLocalFile(QDir(dir).absoluteFilePath(imageFile->name()));
if(type==QLatin1String("latex"))
{
const QByteArray& ba = QByteArray::fromBase64(resultElement.attribute(QLatin1String("image")).toLatin1());
QImage image;
image.loadFromData(ba);
addResult(new Cantor::LatexResult(resultElement.text(), imageUrl, QString(), image));
}
else if(type==QLatin1String("animation"))
{
addResult(new Cantor::AnimationResult(imageUrl));
}
else if(type==QLatin1String("epsimage"))
{
const QByteArray& ba = QByteArray::fromBase64(resultElement.attribute(QLatin1String("image")).toLatin1());
QImage image;
image.loadFromData(ba);
addResult(new Cantor::EpsResult(imageUrl, image));
}
else if(imageFile->name().endsWith(QLatin1String(".eps")))
{
const QByteArray& ba = QByteArray::fromBase64(resultElement.attribute(QLatin1String("image")).toLatin1());
QImage image;
image.loadFromData(ba);
addResult(new Cantor::EpsResult(imageUrl, image));
}
else
{
addResult(new Cantor::ImageResult(imageUrl, resultElement.text()));
}
}
}
}
const QDomElement& errElem = xml.firstChildElement(QLatin1String("Error"));
if (!errElem.isNull())
{
setErrorMessage(errElem.text());
setStatus(Error);
}
else
setStatus(Done);
}
void LoadedExpression::loadFromJupyter(const QJsonObject& cell)
{
setCommand(Cantor::JupyterUtils::getSource(cell));
const QJsonValue idObject = cell.value(QLatin1String("execution_count"));
if (!idObject.isUndefined() && !idObject.isNull())
setId(idObject.toInt());
const QJsonArray& outputs = cell.value(QLatin1String("outputs")).toArray();
for (QJsonArray::const_iterator iter = outputs.begin(); iter != outputs.end(); iter++)
{
if (!Cantor::JupyterUtils::isJupyterOutput(*iter))
continue;
const QJsonObject& output = iter->toObject();
const QString& outputType = Cantor::JupyterUtils::getOutputType(output);
if (Cantor::JupyterUtils::isJupyterTextOutput(output))
{
const QString& text = Cantor::JupyterUtils::fromJupyterMultiline(output.value(QLatin1String("text")));
bool isStderr = output.value(QLatin1String("name")).toString() == QLatin1String("stderr");
Cantor::TextResult* result = new Cantor::TextResult(text);
result->setStdErr(isStderr);
addResult(result);
}
else if (Cantor::JupyterUtils::isJupyterErrorOutput(output))
{
const QJsonArray& tracebackLineArray = output.value(QLatin1String("traceback")).toArray();
QString traceback;
// Looks like the traceback in Jupyter joined with '\n', no ''
// So, manually add it
for (const QJsonValue& line : tracebackLineArray)
traceback += line.toString() + QLatin1Char('\n');
traceback.chop(1);
// IPython returns error with terminal colors, we handle it here, but should we?
static const QChar ESC(0x1b);
traceback.remove(QRegExp(QString(ESC)+QLatin1String("\\[[0-9;]*m")));
setErrorMessage(traceback);
}
else if (Cantor::JupyterUtils::isJupyterDisplayOutput(output) || Cantor::JupyterUtils::isJupyterExecutionResult(output))
{
const QJsonObject& data = output.value(QLatin1String("data")).toObject();
QJsonObject metadata = Cantor::JupyterUtils::getMetadata(output);
const QString& text = Cantor::JupyterUtils::fromJupyterMultiline(data.value(Cantor::JupyterUtils::textMime));
const QString& mainKey = Cantor::JupyterUtils::mainBundleKey(data);
Cantor::Result* result = nullptr;
if (mainKey == Cantor::JupyterUtils::gifMime)
{
const QByteArray& bytes = QByteArray::fromBase64(data.value(mainKey).toString().toLatin1());
QTemporaryFile file;
file.setAutoRemove(false);
file.open();
file.write(bytes);
file.close();
result = new Cantor::AnimationResult(QUrl::fromLocalFile(file.fileName()), text);
}
else if (mainKey == Cantor::JupyterUtils::textMime)
{
result = new Cantor::TextResult(text);
}
else if (mainKey == Cantor::JupyterUtils::htmlMime)
{
const QString& html = Cantor::JupyterUtils::fromJupyterMultiline(data.value(Cantor::JupyterUtils::htmlMime));
// Some backends places gif animation in hmlt (img tag), for example, Sage
if (Cantor::JupyterUtils::isGifHtml(html))
{
result = new Cantor::AnimationResult(Cantor::JupyterUtils::loadGifHtml(html), text);
}
else
{
// Load alternative content types too
std::map<QString, QJsonValue> alternatives;
for (const QString& key : data.keys())
if (key != Cantor::JupyterUtils::htmlMime && key != Cantor::JupyterUtils::textMime)
alternatives[key] = data[key];
result = new Cantor::HtmlResult(html, text, alternatives);
}
}
else if (mainKey == Cantor::JupyterUtils::latexMime)
{
- // Some latex results contains alreadey rendered images, so use them, if presents
+ // Some latex results contains already rendered images, so use them, if presents
const QImage& image = Cantor::JupyterUtils::loadImage(data, Cantor::JupyterUtils::pngMime);
QString latex = Cantor::JupyterUtils::fromJupyterMultiline(data.value(mainKey));
QScopedPointer<Cantor::LatexRenderer> renderer(new Cantor::LatexRenderer(this));
renderer->setLatexCode(latex);
renderer->setEquationOnly(false);
renderer->setMethod(Cantor::LatexRenderer::LatexMethod);
renderer->renderBlocking();
result = new Cantor::LatexResult(latex, QUrl::fromLocalFile(renderer->imagePath()), text, image);
// If we have failed to render LaTeX i think Cantor should show the latex code at least
if (!renderer->renderingSuccessful())
static_cast<Cantor::LatexResult*>(result)->showCode();
}
// So this is image
else if (Cantor::JupyterUtils::imageKeys(data).contains(mainKey))
{
const QImage& image = Cantor::JupyterUtils::loadImage(data, mainKey);
result = new Cantor::ImageResult(image, text);
static_cast<Cantor::ImageResult*>(result)->setOriginalFormat(mainKey);
if (mainKey == Cantor::JupyterUtils::svgMime)
static_cast<Cantor::ImageResult*>(result)->setSvgContent(Cantor::JupyterUtils::fromJupyterMultiline(data[Cantor::JupyterUtils::svgMime]));
const QJsonValue size = metadata.value(mainKey);
if (size.isObject())
{
int w = size.toObject().value(QLatin1String("width")).toInt(-1);
int h = size.toObject().value(QLatin1String("height")).toInt(-1);
if (w != -1 && h != -1)
{
static_cast<Cantor::ImageResult*>(result)->setDisplaySize(QSize(w, h));
// Remove size information, because we don't need it after setting display size
// Also, we encode image to 'image/png' on saving as .ipynb, even original image don't png
// So, without removing the size info here, after loading for example 'image/tiff' to Cantor from .ipynb and saving the worksheet
// (which means, that the image will be saved as png and not as tiff).
// We will have outdated key 'image/tiff' in metadata.
metadata.remove(mainKey);
}
}
}
else if (data.keys().size() == 1 && data.keys()[0] == Cantor::JupyterUtils::textMime)
result = new Cantor::TextResult(text);
// Cantor don't know, how handle this, so pack into mime container result
else
{
qDebug() << "Found unsupported " << outputType << "result with mimes" << data.keys() << ", so add them to mime container result";
result = new Cantor::MimeResult(data);
}
if (result)
{
result->setJupyterMetadata(metadata);
int resultIndex = output.value(QLatin1String("execution_count")).toInt(-1);
if (resultIndex != -1)
result->setExecutionIndex(resultIndex);
addResult(result);
}
}
}
if (errorMessage().isEmpty())
setStatus(Done);
else
setStatus(Error);
}
diff --git a/src/markdownentry.cpp b/src/markdownentry.cpp
index bce431e0..19cd018f 100644
--- a/src/markdownentry.cpp
+++ b/src/markdownentry.cpp
@@ -1,718 +1,718 @@
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2018 Yifei Wu <kqwyfg@gmail.com>
*/
#include "markdownentry.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonValue>
#include <QImage>
#include <QImageReader>
#include <QBuffer>
#include <KLocalizedString>
#include <QDebug>
#include <QStandardPaths>
#include <QDir>
#include <QFileDialog>
#include <KMessageBox>
#include "jupyterutils.h"
#include "mathrender.h"
#include <config-cantor.h>
#include "settings.h"
#ifdef Discount_FOUND
extern "C" {
#include <mkdio.h>
}
#endif
MarkdownEntry::MarkdownEntry(Worksheet* worksheet) : WorksheetEntry(worksheet), m_textItem(new WorksheetTextItem(this, Qt::TextEditorInteraction)), rendered(false)
{
m_textItem->enableRichText(false);
m_textItem->setOpenExternalLinks(true);
m_textItem->installEventFilter(this);
connect(m_textItem, &WorksheetTextItem::moveToPrevious, this, &MarkdownEntry::moveToPreviousEntry);
connect(m_textItem, &WorksheetTextItem::moveToNext, this, &MarkdownEntry::moveToNextEntry);
connect(m_textItem, SIGNAL(execute()), this, SLOT(evaluate()));
}
void MarkdownEntry::populateMenu(QMenu* menu, QPointF pos)
{
if (!rendered)
menu->addAction(i18n("Insert Image Attachment"), this, &MarkdownEntry::insertImage);
if (attachedImages.size() != 0)
menu->addAction(i18n("Clear Attachments"), this, &MarkdownEntry::clearAttachments);
WorksheetEntry::populateMenu(menu, pos);
}
bool MarkdownEntry::isEmpty()
{
return m_textItem->document()->isEmpty();
}
int MarkdownEntry::type() const
{
return Type;
}
bool MarkdownEntry::acceptRichText()
{
return false;
}
bool MarkdownEntry::focusEntry(int pos, qreal xCoord)
{
if (aboutToBeRemoved())
return false;
m_textItem->setFocusAt(pos, xCoord);
return true;
}
void MarkdownEntry::setContent(const QString& content)
{
rendered = false;
plain = content;
setPlainText(plain);
}
void MarkdownEntry::setContent(const QDomElement& content, const KZip& file)
{
rendered = content.attribute(QLatin1String("rendered"), QLatin1String("1")) == QLatin1String("1");
QDomElement htmlEl = content.firstChildElement(QLatin1String("HTML"));
if(!htmlEl.isNull())
html = htmlEl.text();
else
{
html = QLatin1String("");
rendered = false; // No html provided. Assume that it hasn't been rendered.
}
QDomElement plainEl = content.firstChildElement(QLatin1String("Plain"));
if(!plainEl.isNull())
plain = plainEl.text();
else
{
plain = QLatin1String("");
html = QLatin1String(""); // No plain text provided. The entry shouldn't render anything, or the user can't re-edit it.
}
const QDomNodeList& attachments = content.elementsByTagName(QLatin1String("Attachment"));
for (int x = 0; x < attachments.count(); x++)
{
const QDomElement& attachment = attachments.at(x).toElement();
QUrl url(attachment.attribute(QLatin1String("url")));
const QString& base64 = attachment.text();
QImage image;
image.loadFromData(QByteArray::fromBase64(base64.toLatin1()), "PNG");
attachedImages.push_back(std::make_pair(url, QLatin1String("image/png")));
m_textItem->document()->addResource(QTextDocument::ImageResource, url, QVariant(image));
}
if(rendered)
setRenderedHtml(html);
else
setPlainText(plain);
// Handle math after setting html
const QDomNodeList& maths = content.elementsByTagName(QLatin1String("EmbeddedMath"));
foundMath.clear();
for (int i = 0; i < maths.count(); i++)
{
const QDomElement& math = maths.at(i).toElement();
const QString mathCode = math.text();
foundMath.push_back(std::make_pair(mathCode, false));
}
if (rendered)
{
markUpMath();
for (int i = 0; i < maths.count(); i++)
{
const QDomElement& math = maths.at(i).toElement();
bool mathRendered = math.attribute(QLatin1String("rendered")).toInt();
const QString mathCode = math.text();
if (mathRendered)
{
const KArchiveEntry* imageEntry=file.directory()->entry(math.attribute(QLatin1String("path")));
if (imageEntry && imageEntry->isFile())
{
const KArchiveFile* imageFile=static_cast<const KArchiveFile*>(imageEntry);
const QString& dir=QStandardPaths::writableLocation(QStandardPaths::TempLocation);
imageFile->copyTo(dir);
const QString& pdfPath = dir + QDir::separator() + imageFile->name();
QString latex;
Cantor::LatexRenderer::EquationType type;
std::tie(latex, type) = parseMathCode(mathCode);
// Get uuid by removing 'cantor_' and '.pdf' extension
// len('cantor_') == 7, len('.pdf') == 4
QString uuid = pdfPath;
uuid.remove(0, 7);
uuid.chop(4);
bool success;
const auto& data = worksheet()->mathRenderer()->renderExpressionFromPdf(pdfPath, uuid, latex, type, &success);
if (success)
{
QUrl internal;
internal.setScheme(QLatin1String("internal"));
internal.setPath(uuid);
setRenderedMath(i+1, data.first, internal, data.second);
}
}
else
renderMathExpression(i+1, mathCode);
}
}
}
// Because, all previous actions was on load stage,
// them shoudl unconverted by user
m_textItem->document()->clearUndoRedoStacks();
}
void MarkdownEntry::setContentFromJupyter(const QJsonObject& cell)
{
if (!Cantor::JupyterUtils::isMarkdownCell(cell))
return;
// https://nbformat.readthedocs.io/en/latest/format_description.html#cell-metadata
// There isn't Jupyter metadata for markdown cells, which could be handled by Cantor
// So we just store it
setJupyterMetadata(Cantor::JupyterUtils::getMetadata(cell));
const QJsonObject attachments = cell.value(QLatin1String("attachments")).toObject();
for (const QString& key : attachments.keys())
{
const QJsonValue& attachment = attachments.value(key);
const QString& mimeKey = Cantor::JupyterUtils::firstImageKey(attachment);
if (!mimeKey.isEmpty())
{
const QImage& image = Cantor::JupyterUtils::loadImage(attachment, mimeKey);
QUrl resourceUrl;
resourceUrl.setUrl(QLatin1String("attachment:")+key);
attachedImages.push_back(std::make_pair(resourceUrl, mimeKey));
m_textItem->document()->addResource(QTextDocument::ImageResource, resourceUrl, QVariant(image));
}
}
setPlainText(Cantor::JupyterUtils::getSource(cell));
m_textItem->document()->clearUndoRedoStacks();
}
QDomElement MarkdownEntry::toXml(QDomDocument& doc, KZip* archive)
{
if(!rendered)
plain = m_textItem->toPlainText();
QDomElement el = doc.createElement(QLatin1String("Markdown"));
el.setAttribute(QLatin1String("rendered"), (int)rendered);
QDomElement plainEl = doc.createElement(QLatin1String("Plain"));
plainEl.appendChild(doc.createTextNode(plain));
el.appendChild(plainEl);
QDomElement htmlEl = doc.createElement(QLatin1String("HTML"));
htmlEl.appendChild(doc.createTextNode(html));
el.appendChild(htmlEl);
QUrl url;
QString key;
for (const auto& data : attachedImages)
{
std::tie(url, key) = std::move(data);
QDomElement attachmentEl = doc.createElement(QLatin1String("Attachment"));
attachmentEl.setAttribute(QStringLiteral("url"), url.toString());
const QImage& image = m_textItem->document()->resource(QTextDocument::ImageResource, url).value<QImage>();
QByteArray ba;
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "PNG");
attachmentEl.appendChild(doc.createTextNode(QString::fromLatin1(ba.toBase64())));
el.appendChild(attachmentEl);
}
// If math rendered, then append result .pdf to archive
QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter));
for (const auto& data : foundMath)
{
QDomElement mathEl = doc.createElement(QLatin1String("EmbeddedMath"));
mathEl.setAttribute(QStringLiteral("rendered"), data.second);
mathEl.appendChild(doc.createTextNode(data.first));
if (data.second)
{
bool foundNeededImage = false;
while(!cursor.isNull() && !foundNeededImage)
{
QTextImageFormat format=cursor.charFormat().toImageFormat();
if (format.hasProperty(Cantor::Renderer::CantorFormula))
{
const QString& latex = format.property(Cantor::Renderer::Code).toString();
const QString& delimiter = format.property(Cantor::Renderer::Delimiter).toString();
const QString& code = delimiter + latex + delimiter;
if (code == data.first)
{
const QUrl& url = QUrl::fromLocalFile(format.property(Cantor::Renderer::ImagePath).toString());
archive->addLocalFile(url.toLocalFile(), url.fileName());
mathEl.setAttribute(QStringLiteral("path"), url.fileName());
foundNeededImage = true;
}
}
cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor);
}
}
el.appendChild(mathEl);
}
return el;
}
QJsonValue MarkdownEntry::toJupyterJson()
{
QJsonObject entry;
entry.insert(QLatin1String("cell_type"), QLatin1String("markdown"));
entry.insert(QLatin1String("metadata"), jupyterMetadata());
QJsonObject attachments;
QUrl url;
QString key;
for (const auto& data : attachedImages)
{
std::tie(url, key) = std::move(data);
const QImage& image = m_textItem->document()->resource(QTextDocument::ImageResource, url).value<QImage>();
QString attachmentKey = url.toString().remove(QLatin1String("attachment:"));
attachments.insert(attachmentKey, Cantor::JupyterUtils::packMimeBundle(image, key));
}
if (!attachments.isEmpty())
entry.insert(QLatin1String("attachments"), attachments);
Cantor::JupyterUtils::setSource(entry, plain);
return entry;
}
QString MarkdownEntry::toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq)
{
Q_UNUSED(commandSep);
if (commentStartingSeq.isEmpty())
return QString();
QString text(plain);
if (!commentEndingSeq.isEmpty())
return commentStartingSeq + text + commentEndingSeq + QLatin1String("\n");
return commentStartingSeq + text.replace(QLatin1String("\n"), QLatin1String("\n") + commentStartingSeq) + QLatin1String("\n");
}
void MarkdownEntry::interruptEvaluation()
{
}
bool MarkdownEntry::evaluate(EvaluationOption evalOp)
{
if(!rendered)
{
if (m_textItem->toPlainText() == plain && !html.isEmpty())
{
setRenderedHtml(html);
rendered = true;
for (auto iter = foundMath.begin(); iter != foundMath.end(); iter++)
iter->second = false;
markUpMath();
}
else
{
plain = m_textItem->toPlainText();
rendered = renderMarkdown(plain);
}
m_textItem->document()->clearUndoRedoStacks();
}
if (rendered && worksheet()->embeddedMathEnabled())
renderMath();
evaluateNext(evalOp);
return true;
}
bool MarkdownEntry::renderMarkdown(QString& plain)
{
#ifdef Discount_FOUND
QByteArray mdCharArray = plain.toUtf8();
MMIOT* mdHandle = mkd_string(mdCharArray.data(), mdCharArray.size()+1, 0);
if(!mkd_compile(mdHandle, MKD_LATEX | MKD_FENCEDCODE | MKD_GITHUBTAGS))
{
qDebug()<<"Failed to compile the markdown document";
mkd_cleanup(mdHandle);
return false;
}
char *htmlDocument;
int htmlSize = mkd_document(mdHandle, &htmlDocument);
html = QString::fromUtf8(htmlDocument, htmlSize);
char *latexData;
int latexDataSize = mkd_latextext(mdHandle, &latexData);
QStringList latexUnits = QString::fromUtf8(latexData, latexDataSize).split(QLatin1Char(31), QString::SkipEmptyParts);
foundMath.clear();
mkd_cleanup(mdHandle);
setRenderedHtml(html);
QTextCursor cursor(m_textItem->document());
for (const QString& latex : latexUnits)
foundMath.push_back(std::make_pair(latex, false));
markUpMath();
return true;
#else
Q_UNUSED(plain);
return false;
#endif
}
void MarkdownEntry::updateEntry()
{
QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter));
while(!cursor.isNull())
{
QTextImageFormat format=cursor.charFormat().toImageFormat();
if (format.hasProperty(Cantor::Renderer::CantorFormula))
worksheet()->mathRenderer()->rerender(m_textItem->document(), format);
cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor);
}
}
WorksheetCursor MarkdownEntry::search(const QString& pattern, unsigned flags,
QTextDocument::FindFlags qt_flags,
const WorksheetCursor& pos)
{
if (!(flags & WorksheetEntry::SearchText) ||
(pos.isValid() && pos.entry() != this))
return WorksheetCursor();
QTextCursor textCursor = m_textItem->search(pattern, qt_flags, pos);
if (textCursor.isNull())
return WorksheetCursor();
else
return WorksheetCursor(this, m_textItem, textCursor);
}
void MarkdownEntry::layOutForWidth(qreal w, bool force)
{
if (size().width() == w && !force)
return;
m_textItem->setGeometry(0, 0, w);
setSize(QSizeF(m_textItem->width(), m_textItem->height() + VerticalMargin));
}
bool MarkdownEntry::eventFilter(QObject* object, QEvent* event)
{
if(object == m_textItem)
{
if(event->type() == QEvent::GraphicsSceneMouseDoubleClick)
{
QGraphicsSceneMouseEvent* mouseEvent = dynamic_cast<QGraphicsSceneMouseEvent*>(event);
if(!mouseEvent) return false;
if(mouseEvent->button() == Qt::LeftButton)
{
if (rendered)
{
setPlainText(plain);
m_textItem->setCursorPosition(mouseEvent->pos());
m_textItem->textCursor().clearSelection();
rendered = false;
return true;
}
}
}
}
return false;
}
bool MarkdownEntry::wantToEvaluate()
{
return !rendered;
}
void MarkdownEntry::setRenderedHtml(const QString& html)
{
m_textItem->setHtml(html);
m_textItem->denyEditing();
}
void MarkdownEntry::setPlainText(const QString& plain)
{
QTextDocument* doc = m_textItem->document();
doc->setPlainText(plain);
m_textItem->setDocument(doc);
m_textItem->allowEditing();
}
void MarkdownEntry::renderMath()
{
QTextCursor cursor(m_textItem->document());
for (int i = 0; i < (int)foundMath.size(); i++)
if (foundMath[i].second == false)
renderMathExpression(i+1, foundMath[i].first);
}
void MarkdownEntry::handleMathRender(QSharedPointer<MathRenderResult> result)
{
if (!result->successful)
{
if (Settings::self()->showMathRenderError())
KMessageBox::error(worksheetView(), result->errorMessage, i18n("Cantor Math Error"));
else
qDebug() << "MarkdownEntry: math render failed with message" << result->errorMessage;
return;
}
setRenderedMath(result->jobId, result->renderedMath, result->uniqueUrl, result->image);
}
void MarkdownEntry::renderMathExpression(int jobId, QString mathCode)
{
QString latex;
Cantor::LatexRenderer::EquationType type;
std::tie(latex, type) = parseMathCode(mathCode);
if (!latex.isNull())
worksheet()->mathRenderer()->renderExpression(jobId, latex, type, this, SLOT(handleMathRender(QSharedPointer<MathRenderResult>)));
}
std::pair<QString, Cantor::LatexRenderer::EquationType> MarkdownEntry::parseMathCode(QString mathCode)
{
static const QLatin1String inlineDelimiter("$");
static const QLatin1String displayedDelimiter("$$");
if (mathCode.startsWith(displayedDelimiter) && mathCode.endsWith(displayedDelimiter))
{
mathCode.remove(0, 2);
mathCode.chop(2);
if (mathCode[0] == QChar(6))
mathCode.remove(0, 1);
return std::make_pair(mathCode, Cantor::LatexRenderer::FullEquation);
}
else if (mathCode.startsWith(inlineDelimiter) && mathCode.endsWith(inlineDelimiter))
{
mathCode.remove(0, 1);
mathCode.chop(1);
if (mathCode[0] == QChar(6))
mathCode.remove(0, 1);
return std::make_pair(mathCode, Cantor::LatexRenderer::InlineEquation);
}
else if (mathCode.startsWith(QString::fromUtf8("\\begin{")) && mathCode.endsWith(QLatin1Char('}')))
{
if (mathCode[1] == QChar(6))
mathCode.remove(1, 1);
return std::make_pair(mathCode, Cantor::LatexRenderer::CustomEquation);
}
else
return std::make_pair(QString(), Cantor::LatexRenderer::InlineEquation);
}
void MarkdownEntry::setRenderedMath(int jobId, const QTextImageFormat& format, const QUrl& internal, const QImage& image)
{
if ((int)foundMath.size() < jobId)
return;
const auto& iter = foundMath.begin() + jobId-1;
QTextCursor cursor = findMath(jobId);
const QString delimiter = format.property(Cantor::Renderer::Delimiter).toString();
QString searchText = delimiter + format.property(Cantor::Renderer::Code).toString() + delimiter;
Cantor::LatexRenderer::EquationType type
= (Cantor::LatexRenderer::EquationType)format.intProperty(Cantor::Renderer::CantorFormula);
// From findMath we will be first symbol of math expression
// So in order to select all symbols of the expression, we need to go to previous symbol first
// But it working strange sometimes: some times we need to go to previous character, sometimes not
// So the code tests that we on '$' symbol and if it isn't true, then we revert back
cursor.movePosition(QTextCursor::PreviousCharacter);
bool withDollarDelimiter = type == Cantor::LatexRenderer::InlineEquation || type == Cantor::LatexRenderer::FullEquation;
if (withDollarDelimiter && m_textItem->document()->characterAt(cursor.position()) != QLatin1Char('$'))
cursor.movePosition(QTextCursor::NextCharacter);
else if (type == Cantor::LatexRenderer::CustomEquation && m_textItem->document()->characterAt(cursor.position()) != QLatin1Char('\\') )
cursor.movePosition(QTextCursor::NextCharacter);
cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, searchText.size());
if (!cursor.isNull())
{
m_textItem->document()->addResource(QTextDocument::ImageResource, internal, QVariant(image));
- // Dont add new line for $$...$$ on document's begin and end
+ // Don't add new line for $$...$$ on document's begin and end
// And if we in block, which haven't non-space characters except out math expression
// In another sitation, Cantor will move rendered image into another QTextBlock
QTextCursor prevSymCursor = m_textItem->document()->find(QRegExp(QLatin1String("[^\\s]")), cursor, QTextDocument::FindBackward);
if (type == Cantor::LatexRenderer::FullEquation
&& cursor.selectionStart() != 0
&& prevSymCursor.block() == cursor.block()
)
{
cursor.insertBlock();
cursor.setPosition(prevSymCursor.position()+2, QTextCursor::KeepAnchor);
cursor.removeSelectedText();
}
cursor.insertText(QString(QChar::ObjectReplacementCharacter), format);
bool atDocEnd = cursor.position() == m_textItem->document()->characterCount()-1;
QTextCursor nextSymCursor = m_textItem->document()->find(QRegExp(QLatin1String("[^\\s]")), cursor);
if (type == Cantor::LatexRenderer::FullEquation && !atDocEnd && nextSymCursor.block() == cursor.block())
{
cursor.setPosition(nextSymCursor.position()-1, QTextCursor::KeepAnchor);
cursor.removeSelectedText();
cursor.insertBlock();
}
// Set that the formulas is rendered
iter->second = true;
m_textItem->document()->clearUndoRedoStacks();
}
}
QTextCursor MarkdownEntry::findMath(int id)
{
QTextCursor cursor(m_textItem->document());
do
{
QTextCharFormat format = cursor.charFormat();
if (format.intProperty(JobProperty) == id)
break;
}
while (cursor.movePosition(QTextCursor::NextCharacter));
return cursor;
}
void MarkdownEntry::markUpMath()
{
QTextCursor cursor(m_textItem->document());
for (int i = 0; i < (int)foundMath.size(); i++)
{
if (foundMath[i].second)
continue;
QString searchText = foundMath[i].first;
searchText.replace(QRegExp(QLatin1String("\\s+")), QLatin1String(" "));
cursor = m_textItem->document()->find(searchText, cursor);
// Mark up founded math code
QTextCharFormat format = cursor.charFormat();
// Use index+1 in math array as property tag
format.setProperty(JobProperty, i+1);
// We found the math expression, so remove 'marker' (ACII symbol 'Acknowledgement')
// The marker have been placed after "$" or "$$"
// We remove the marker, only if it presents
QString codeWithoutMarker = foundMath[i].first;
if (searchText.startsWith(QLatin1String("$$")))
{
if (codeWithoutMarker[2] == QChar(6))
codeWithoutMarker.remove(2, 1);
}
else if (searchText.startsWith(QLatin1String("$")))
{
if (codeWithoutMarker[1] == QChar(6))
codeWithoutMarker.remove(1, 1);
}
else if (searchText.startsWith(QLatin1String("\\")))
{
if (codeWithoutMarker[1] == QChar(6))
codeWithoutMarker.remove(1, 1);
}
cursor.insertText(codeWithoutMarker, format);
}
}
void MarkdownEntry::insertImage()
{
const QString& filename = QFileDialog::getOpenFileName(worksheet()->worksheetView(), i18n("Choose Image"), QString(), i18n("Images (*.png *.bmp *.jpg *.svg)"));
if (!filename.isEmpty())
{
QImageReader reader(filename);
const QImage img = reader.read();
if (!img.isNull())
{
const QString& name = QFileInfo(filename).fileName();
QUrl url;
url.setScheme(QLatin1String("attachment"));
url.setPath(name);
attachedImages.push_back(std::make_pair(url, QLatin1String("image/png")));
m_textItem->document()->addResource(QTextDocument::ImageResource, url, QVariant(img));
QTextCursor cursor = m_textItem->textCursor();
cursor.insertText(QString::fromLatin1("![%1](attachment:%1)").arg(name));
animateSizeChange();
}
else
KMessageBox::error(worksheetView(), i18n("Cantor failed to read image with error \"%1\"", reader.errorString()), i18n("Cantor"));
}
}
void MarkdownEntry::clearAttachments()
{
for (auto& attachment: attachedImages)
{
const QUrl& url = attachment.first;
m_textItem->document()->addResource(QTextDocument::ImageResource, url, QVariant());
}
attachedImages.clear();
animateSizeChange();
}
diff --git a/src/mathrender.h b/src/mathrender.h
index 3ce10637..d2e05cdb 100644
--- a/src/mathrender.h
+++ b/src/mathrender.h
@@ -1,81 +1,81 @@
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2019 Sirgienko Nikita <warquark@gmail.com>
*/
#ifndef MATHRENDER_H
#define MATHRENDER_H
#include <QObject>
#include <QTextImageFormat>
#include <QMutex>
#include "lib/latexrenderer.h"
/**
- * Special class for renderning embedded math in MarkdownEntry and TextEntry
+ * Special class for rendering embedded math in MarkdownEntry and TextEntry
* Instead of LatexRenderer+EpsRenderer provide all needed functianality in one class
* Even if we add some speed optimization in future, API of the class probably won't change
*/
class MathRenderer : public QObject {
Q_OBJECT
public:
MathRenderer();
~MathRenderer();
static bool mathRenderAvailable();
// Resulution contol
void setScale(qreal scale);
qreal scale();
void useHighResolution(bool b);
/**
* This function will run render task in Qt thread pool and
* call resultHandler SLOT with MathRenderResult* argument on finish
* receiver will be managed about pointer, task only create it
*/
void renderExpression(
int jobId,
const QString& mathExpression,
Cantor::LatexRenderer::EquationType type,
const QObject *receiver,
const char *resultHandler);
/**
* Rerender renderer math expression in document
* Unlike MathRender::renderExpression this method isn't async, because
* rerender already rendered math is not long operation
*/
void rerender(QTextDocument* document, const QTextImageFormat& math);
/**
* Render math expression from existing .pdf
* Like MathRenderer::rerender is blocking
*/
std::pair<QTextImageFormat, QImage> renderExpressionFromPdf(
const QString& filename, const QString& uuid, const QString& code, Cantor::LatexRenderer::EquationType type, bool* success
);
private:
double m_scale;
bool m_useHighRes;
};
#endif /* MATHRENDER_H */
diff --git a/src/mathrendertask.cpp b/src/mathrendertask.cpp
index e37abc73..f63accdf 100644
--- a/src/mathrendertask.cpp
+++ b/src/mathrendertask.cpp
@@ -1,247 +1,247 @@
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2019 Sirgienko Nikita <warquark@gmail.com>
*/
#include "mathrendertask.h"
#include <QTemporaryFile>
#include <QStandardPaths>
#include <QUuid>
#include <QDir>
#include <KColorScheme>
#include <KProcess>
#include <QScopedPointer>
#include <QMutex>
#include <QApplication>
#include <QDebug>
#include "lib/renderer.h"
#include "lib/latexrenderer.h"
static const QLatin1String mathTex("\\documentclass%9{standalone}"\
"\\usepackage{amsfonts,amssymb}"\
"\\usepackage{amsmath}"\
"\\usepackage[utf8]{inputenc}"\
"\\usepackage{color}"\
/*
"\\setlength\\textwidth{5in}"\
"\\setlength{\\parindent}{0pt}"\
"\\pagestyle{empty}"\
*/
"\\begin{document}"\
"\\pagecolor[rgb]{%1,%2,%3}"\
"\\color[rgb]{%4,%5,%6}"\
"\\fontsize{%7}{%7}\\selectfont"\
"%8"\
"\\end{document}");
static const QLatin1String eqnHeader("$\\displaystyle %1$");
static const QLatin1String inlineEqnHeader("$%1$");
MathRenderTask::MathRenderTask(
int jobId,
const QString& code,
Cantor::LatexRenderer::EquationType type,
double scale,
bool highResolution
): m_jobId(jobId), m_code(code), m_type(type), m_scale(scale), m_highResolution(highResolution)
{}
void MathRenderTask::setHandler(const QObject* receiver, const char* resultHandler)
{
connect(this, SIGNAL(finish(QSharedPointer<MathRenderResult>)), receiver, resultHandler);
}
void MathRenderTask::run()
{
QSharedPointer<MathRenderResult> result(new MathRenderResult());
const QString& tempDir=QStandardPaths::writableLocation(QStandardPaths::TempLocation);
QTemporaryFile texFile(tempDir + QDir::separator() + QLatin1String("cantor_tex-XXXXXX.tex"));
texFile.open();
// Verify that standalone.cls available for rendering and could be founded
if (!tempDir.contains(QLatin1String("standalone.cls")))
{
QString file = QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String("latex/standalone.cls"));
if (file.isEmpty())
file = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("cantor/latex/standalone.cls"));
if (file.isEmpty())
{
result->successful = false;
result->errorMessage = QString::fromLatin1("needed for math render standalone.cls file not found in Cantor data directory");
finalize(result);
return;
}
else
QFile::copy(file, tempDir + QDir::separator() + QLatin1String("standalone.cls"));
}
QString expressionTex=mathTex;
KColorScheme scheme(QPalette::Active);
const QColor &backgroundColor=scheme.background().color();
const QColor &foregroundColor=scheme.foreground().color();
expressionTex=expressionTex
.arg(backgroundColor.redF()).arg(backgroundColor.greenF()).arg(backgroundColor.blueF())
.arg(foregroundColor.redF()).arg(foregroundColor.greenF()).arg(foregroundColor.blueF());
int fontPointSize = QApplication::font().pointSize();
expressionTex=expressionTex.arg(fontPointSize);
switch(m_type)
{
case Cantor::LatexRenderer::FullEquation:
expressionTex=expressionTex.arg(eqnHeader, QString());
break;
case Cantor::LatexRenderer::InlineEquation:
expressionTex=expressionTex.arg(inlineEqnHeader, QString());
break;
case Cantor::LatexRenderer::CustomEquation:
expressionTex=expressionTex.arg(QLatin1String("%1"), QLatin1String("[preview]"));
break;
}
QString latex = m_code;
// Looks hacky, but no sure, how do it better without overhead (like new latex type in lib/latexrender)
static const QString& equationBegin = QLatin1String("\\begin{equation}");
static const QString& equationEnd = QLatin1String("\\end{equation}");
if (latex.startsWith(equationBegin) && latex.endsWith(equationEnd))
{
latex.remove(0, equationBegin.size());
latex.chop(equationEnd.size());
latex = QLatin1String("\\begin{equation*}") + latex + QLatin1String("\\end{equation*}");
}
expressionTex=expressionTex.arg(latex);
texFile.write(expressionTex.toUtf8());
texFile.flush();
QProcess p;
p.setWorkingDirectory(tempDir);
// Create unique uuid for this job
// It will be used as pdf filename, for preventing names collisions
// And as internal url path too
const QString& uuid = Cantor::LatexRenderer::genUuid();
const QString& pdflatex = QStandardPaths::findExecutable(QLatin1String("pdflatex"));
p.setProgram(pdflatex);
p.setArguments({QStringLiteral("-jobname=cantor_") + uuid, QStringLiteral("-halt-on-error"), texFile.fileName()});
p.start();
p.waitForFinished();
if (p.exitCode() != 0)
{
// pdflatex render failed and we haven't pdf file
result->successful = false;
QString renderErrorText = QString::fromUtf8(p.readAllStandardOutput());
renderErrorText.remove(0, renderErrorText.indexOf(QLatin1Char('!')));
renderErrorText.remove(renderErrorText.indexOf(QLatin1String("! ==> Fatal error occurred")), renderErrorText.size());
renderErrorText = renderErrorText.trimmed();
result->errorMessage = renderErrorText;
finalize(result);
- texFile.setAutoRemove(false); //Usefull for debug
+ texFile.setAutoRemove(false); //Useful for debug
return;
}
//Clean up .aux and .log files
QString pathWithoutExtension = tempDir + QDir::separator() + QLatin1String("cantor_")+uuid;
QFile::remove(pathWithoutExtension + QLatin1String(".log"));
QFile::remove(pathWithoutExtension + QLatin1String(".aux"));
const QString& pdfFileName = pathWithoutExtension + QLatin1String(".pdf");
bool success; QString errorMessage; QSizeF size;
const auto& data = renderPdfToFormat(pdfFileName, m_code, uuid, m_type, m_scale, m_highResolution, &success, &errorMessage);
result->successful = success;
result->errorMessage = errorMessage;
if (success == false)
{
finalize(result);
return;
}
result->renderedMath = data.first;
result->image = data.second;
result->jobId = m_jobId;
QUrl internal;
internal.setScheme(QLatin1String("internal"));
internal.setPath(uuid);
result->uniqueUrl = internal;
finalize(result);
}
void MathRenderTask::finalize(QSharedPointer<MathRenderResult> result)
{
emit finish(result);
deleteLater();
}
std::pair<QTextImageFormat, QImage> MathRenderTask::renderPdfToFormat(const QString& filename, const QString& code, const QString uuid, Cantor::LatexRenderer::EquationType type, double scale, bool highResulution, bool* success, QString* errorReason)
{
QSizeF size;
const QImage& image = Cantor::Renderer::pdfRenderToImage(QUrl::fromLocalFile(filename), scale, highResulution, &size, errorReason);
if (success)
*success = image.isNull() == false;
if (success && *success == false)
return std::make_pair(QTextImageFormat(), QImage());
QTextImageFormat format;
QUrl internal;
internal.setScheme(QLatin1String("internal"));
internal.setPath(uuid);
format.setName(internal.url());
format.setWidth(size.width());
format.setHeight(size.height());
format.setProperty(Cantor::Renderer::CantorFormula, type);
format.setProperty(Cantor::Renderer::ImagePath, filename);
format.setProperty(Cantor::Renderer::Code, code);
format.setVerticalAlignment(QTextCharFormat::AlignBaseline);
switch(type)
{
case Cantor::LatexRenderer::FullEquation:
format.setProperty(Cantor::Renderer::Delimiter, QLatin1String("$$"));
break;
case Cantor::LatexRenderer::InlineEquation:
format.setProperty(Cantor::Renderer::Delimiter, QLatin1String("$"));
break;
case Cantor::LatexRenderer::CustomEquation:
format.setProperty(Cantor::Renderer::Delimiter, QLatin1String(""));
break;
}
return std::make_pair(std::move(format), std::move(image));
}
diff --git a/src/textentry.cpp b/src/textentry.cpp
index 88017397..0fb0903b 100644
--- a/src/textentry.cpp
+++ b/src/textentry.cpp
@@ -1,590 +1,590 @@
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2009 Alexander Rieder <alexanderrieder@gmail.com>
Copyright (C) 2012 Martin Kuettler <martin.kuettler@gmail.com>
*/
#include "textentry.h"
#include "worksheettextitem.h"
#include "lib/renderer.h"
#include "latexrenderer.h"
#include "lib/jupyterutils.h"
#include "mathrender.h"
#include "settings.h"
#include <QScopedPointer>
#include <QGraphicsLinearLayout>
#include <QJsonValue>
#include <QJsonObject>
#include <QJsonArray>
#include <QDebug>
#include <KLocalizedString>
#include <KColorScheme>
#include <QStringList>
#include <QInputDialog>
QStringList standartRawCellTargetNames = {QLatin1String("None"), QLatin1String("LaTeX"), QLatin1String("reST"), QLatin1String("HTML"), QLatin1String("Markdown")};
QStringList standartRawCellTargetMimes = {QString(), QLatin1String("text/latex"), QLatin1String("text/restructuredtext"), QLatin1String("text/html"), QLatin1String("text/markdown")};
TextEntry::TextEntry(Worksheet* worksheet) : WorksheetEntry(worksheet)
, m_rawCell(false)
, m_convertTarget()
, m_targetActionGroup(nullptr)
, m_ownTarget{nullptr}
, m_targetMenu(nullptr)
, m_textItem(new WorksheetTextItem(this, Qt::TextEditorInteraction))
{
m_textItem->enableRichText(true);
connect(m_textItem, &WorksheetTextItem::moveToPrevious, this, &TextEntry::moveToPreviousEntry);
connect(m_textItem, &WorksheetTextItem::moveToNext, this, &TextEntry::moveToNextEntry);
// Modern syntax of signal/stots don't work on this connection (arguments don't match)
connect(m_textItem, SIGNAL(execute()), this, SLOT(evaluate()));
connect(m_textItem, &WorksheetTextItem::doubleClick, this, &TextEntry::resolveImagesAtCursor);
// Init raw cell target menus
- // This used only for raw cells, but removing and creating this on convertation more complex
+ // This used only for raw cells, but removing and creating this on conversion more complex
// that just create them always
m_targetActionGroup= new QActionGroup(this);
m_targetActionGroup->setExclusive(true);
connect(m_targetActionGroup, &QActionGroup::triggered, this, &TextEntry::convertTargetChanged);
m_targetMenu = new QMenu(i18n("Raw Cell Targets"));
for (const QString& key : standartRawCellTargetNames)
{
QAction* action = new QAction(key, m_targetActionGroup);
action->setCheckable(true);
m_targetMenu->addAction(action);
}
m_ownTarget = new QAction(i18n("Add custom target"), m_targetActionGroup);
m_ownTarget->setCheckable(true);
m_targetMenu->addAction(m_ownTarget);
}
TextEntry::~TextEntry()
{
m_targetMenu->deleteLater();
}
void TextEntry::populateMenu(QMenu* menu, QPointF pos)
{
if (m_rawCell)
{
menu->addAction(i18n("Convert to Text Entry"), this, &TextEntry::convertToTextEntry);
menu->addMenu(m_targetMenu);
}
else
{
menu->addAction(i18n("Convert to Raw Cell"), this, &TextEntry::convertToRawCell);
bool imageSelected = false;
QTextCursor cursor = m_textItem->textCursor();
const QChar repl = QChar::ObjectReplacementCharacter;
if (cursor.hasSelection())
{
QString selection = m_textItem->textCursor().selectedText();
imageSelected = selection.contains(repl);
}
else
{
// we need to try both the current cursor and the one after the that
cursor = m_textItem->cursorForPosition(pos);
for (int i = 2; i; --i)
{
int p = cursor.position();
if (m_textItem->document()->characterAt(p-1) == repl &&
cursor.charFormat().hasProperty(Cantor::Renderer::CantorFormula))
{
m_textItem->setTextCursor(cursor);
imageSelected = true;
break;
}
cursor.movePosition(QTextCursor::NextCharacter);
}
}
if (imageSelected)
{
menu->addAction(i18n("Show LaTeX code"), this, SLOT(resolveImagesAtCursor()));
}
}
menu->addSeparator();
WorksheetEntry::populateMenu(menu, pos);
}
bool TextEntry::isEmpty()
{
return m_textItem->document()->isEmpty();
}
int TextEntry::type() const
{
return Type;
}
bool TextEntry::acceptRichText()
{
return true;
}
bool TextEntry::focusEntry(int pos, qreal xCoord)
{
if (aboutToBeRemoved())
return false;
m_textItem->setFocusAt(pos, xCoord);
return true;
}
void TextEntry::setContent(const QString& content)
{
m_textItem->setPlainText(content);
}
void TextEntry::setContent(const QDomElement& content, const KZip& file)
{
Q_UNUSED(file);
if(content.firstChildElement(QLatin1String("body")).isNull())
return;
if (content.hasAttribute(QLatin1String("convertTarget")))
{
convertToRawCell();
m_convertTarget = content.attribute(QLatin1String("convertTarget"));
// Set current action status
int idx = standartRawCellTargetMimes.indexOf(m_convertTarget);
if (idx != -1)
m_targetMenu->actions()[idx]->setChecked(true);
else
addNewTarget(m_convertTarget);
}
else
convertToTextEntry();
QDomDocument doc = QDomDocument();
QDomNode n = doc.importNode(content.firstChildElement(QLatin1String("body")), true);
doc.appendChild(n);
QString html = doc.toString();
m_textItem->setHtml(html);
}
void TextEntry::setContentFromJupyter(const QJsonObject& cell)
{
if (Cantor::JupyterUtils::isRawCell(cell))
{
convertToRawCell();
const QJsonObject& metadata = Cantor::JupyterUtils::getMetadata(cell);
QJsonValue format = metadata.value(QLatin1String("format"));
// Also checks "raw_mimetype", because raw cell don't corresponds Jupyter Notebook specification
// See https://github.com/jupyter/notebook/issues/4730
if (format.isUndefined())
format = metadata.value(QLatin1String("raw_mimetype"));
m_convertTarget = format.toString(QString());
// Set current action status
int idx = standartRawCellTargetMimes.indexOf(m_convertTarget);
if (idx != -1)
m_targetMenu->actions()[idx]->setChecked(true);
else
addNewTarget(m_convertTarget);
m_textItem->setPlainText(Cantor::JupyterUtils::getSource(cell));
setJupyterMetadata(metadata);
}
else if (Cantor::JupyterUtils::isMarkdownCell(cell))
{
convertToTextEntry();
QJsonObject cantorMetadata = Cantor::JupyterUtils::getCantorMetadata(cell);
m_textItem->setHtml(cantorMetadata.value(QLatin1String("text_entry_content")).toString());
}
}
QJsonValue TextEntry::toJupyterJson()
{
// Simple logic:
// If convertTarget is empty, it's user maded cell and we convert it to a markdown
- // If convertTarget setted, it's raw cell from Jupyter and we convert it to Jupyter cell
+ // If convertTarget set, it's raw cell from Jupyter and we convert it to Jupyter cell
QTextDocument* doc = m_textItem->document()->clone();
QTextCursor cursor = doc->find(QString(QChar::ObjectReplacementCharacter));
while(!cursor.isNull())
{
QTextCharFormat format = cursor.charFormat();
if (format.hasProperty(Cantor::Renderer::CantorFormula))
{
showLatexCode(cursor);
}
cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor);
}
QJsonObject metadata(jupyterMetadata());
QString entryData;
QString entryType;
if (!m_rawCell)
{
entryType = QLatin1String("markdown");
// Add raw text of entry to metadata, for situation when
// Cantor opens .ipynb converted from our .cws format
QJsonObject cantorMetadata;
if (Settings::storeTextEntryFormatting())
{
entryData = doc->toHtml();
// Remove DOCTYPE from html
entryData.remove(QRegExp(QLatin1String("<!DOCTYPE[^>]*>\\n")));
cantorMetadata.insert(QLatin1String("text_entry_content"), entryData);
}
else
entryData = doc->toPlainText();
metadata.insert(Cantor::JupyterUtils::cantorMetadataKey, cantorMetadata);
// Replace our $$ formulas to $
entryData.replace(QLatin1String("$$"), QLatin1String("$"));
}
else
{
entryType = QLatin1String("raw");
metadata.insert(QLatin1String("format"), m_convertTarget);
entryData = doc->toPlainText();
}
QJsonObject entry;
entry.insert(QLatin1String("cell_type"), entryType);
entry.insert(QLatin1String("metadata"), metadata);
Cantor::JupyterUtils::setSource(entry, entryData);
return entry;
}
QDomElement TextEntry::toXml(QDomDocument& doc, KZip* archive)
{
Q_UNUSED(archive);
QScopedPointer<QTextDocument> document(m_textItem->document()->clone());
//make sure that the latex code is shown instead of the rendered formulas
QTextCursor cursor = document->find(QString(QChar::ObjectReplacementCharacter));
while(!cursor.isNull())
{
QTextCharFormat format = cursor.charFormat();
if (format.hasProperty(Cantor::Renderer::CantorFormula))
showLatexCode(cursor);
cursor = document->find(QString(QChar::ObjectReplacementCharacter), cursor);
}
const QString& html = document->toHtml();
QDomElement el = doc.createElement(QLatin1String("Text"));
QDomDocument myDoc = QDomDocument();
myDoc.setContent(html);
el.appendChild(myDoc.documentElement().firstChildElement(QLatin1String("body")));
if (m_rawCell)
el.setAttribute(QLatin1String("convertTarget"), m_convertTarget);
return el;
}
QString TextEntry::toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq)
{
Q_UNUSED(commandSep);
if (commentStartingSeq.isEmpty())
return QString();
/*
// would this be plain enough?
QTextCursor cursor = m_textItem->textCursor();
cursor.movePosition(QTextCursor::Start);
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
QString text = m_textItem->resolveImages(cursor);
text.replace(QChar::ParagraphSeparator, '\n');
text.replace(QChar::LineSeparator, '\n');
*/
QString text = m_textItem->toPlainText();
if (!commentEndingSeq.isEmpty())
return commentStartingSeq + text + commentEndingSeq + QLatin1String("\n");
return commentStartingSeq + text.replace(QLatin1String("\n"), QLatin1String("\n") + commentStartingSeq) + QLatin1String("\n");
}
void TextEntry::interruptEvaluation()
{
}
bool TextEntry::evaluate(EvaluationOption evalOp)
{
int i = 0;
if (worksheet()->embeddedMathEnabled() && !m_rawCell)
{
// Render math in $$...$$ via Latex
QTextCursor cursor = findLatexCode();
while (!cursor.isNull())
{
QString latexCode = cursor.selectedText();
qDebug()<<"found latex: " << latexCode;
latexCode.remove(0, 2);
latexCode.remove(latexCode.length() - 2, 2);
latexCode.replace(QChar::ParagraphSeparator, QLatin1Char('\n'));
latexCode.replace(QChar::LineSeparator, QLatin1Char('\n'));
MathRenderer* renderer = worksheet()->mathRenderer();
renderer->renderExpression(++i, latexCode, Cantor::LatexRenderer::InlineEquation, this, SLOT(handleMathRender(QSharedPointer<MathRenderResult>)));
cursor = findLatexCode(cursor);
}
}
evaluateNext(evalOp);
return true;
}
void TextEntry::updateEntry()
{
qDebug() << "update Entry";
QTextCursor cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter));
while(!cursor.isNull())
{
QTextImageFormat format=cursor.charFormat().toImageFormat();
if (format.hasProperty(Cantor::Renderer::CantorFormula))
worksheet()->mathRenderer()->rerender(m_textItem->document(), format);
cursor = m_textItem->document()->find(QString(QChar::ObjectReplacementCharacter), cursor);
}
}
void TextEntry::resolveImagesAtCursor()
{
QTextCursor cursor = m_textItem->textCursor();
if (!cursor.hasSelection())
cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
cursor.insertText(m_textItem->resolveImages(cursor));
}
QTextCursor TextEntry::findLatexCode(const QTextCursor& cursor) const
{
QTextDocument *doc = m_textItem->document();
QTextCursor startCursor;
if (cursor.isNull())
startCursor = doc->find(QLatin1String("$$"));
else
startCursor = doc->find(QLatin1String("$$"), cursor);
if (startCursor.isNull())
return startCursor;
const QTextCursor endCursor = doc->find(QLatin1String("$$"), startCursor);
if (endCursor.isNull())
return endCursor;
startCursor.setPosition(startCursor.selectionStart());
startCursor.setPosition(endCursor.position(), QTextCursor::KeepAnchor);
return startCursor;
}
QString TextEntry::showLatexCode(QTextCursor& cursor)
{
QString latexCode = cursor.charFormat().property(Cantor::Renderer::Code).toString();
cursor.deletePreviousChar();
latexCode = QLatin1String("$$") + latexCode + QLatin1String("$$");
cursor.insertText(latexCode);
return latexCode;
}
int TextEntry::searchText(const QString& text, const QString& pattern,
QTextDocument::FindFlags qt_flags)
{
Qt::CaseSensitivity caseSensitivity;
if (qt_flags & QTextDocument::FindCaseSensitively)
caseSensitivity = Qt::CaseSensitive;
else
caseSensitivity = Qt::CaseInsensitive;
int position;
if (qt_flags & QTextDocument::FindBackward)
position = text.lastIndexOf(pattern, -1, caseSensitivity);
else
position = text.indexOf(pattern, 0, caseSensitivity);
return position;
}
WorksheetCursor TextEntry::search(const QString& pattern, unsigned flags,
QTextDocument::FindFlags qt_flags,
const WorksheetCursor& pos)
{
if (!(flags & WorksheetEntry::SearchText) ||
(pos.isValid() && pos.entry() != this))
return WorksheetCursor();
QTextCursor textCursor = m_textItem->search(pattern, qt_flags, pos);
int position = 0;
QTextCursor latexCursor;
QString latex;
if (flags & WorksheetEntry::SearchLaTeX) {
const QString repl = QString(QChar::ObjectReplacementCharacter);
latexCursor = m_textItem->search(repl, qt_flags, pos);
while (!latexCursor.isNull()) {
latex = m_textItem->resolveImages(latexCursor);
position = searchText(latex, pattern, qt_flags);
if (position >= 0) {
break;
}
WorksheetCursor c(this, m_textItem, latexCursor);
latexCursor = m_textItem->search(repl, qt_flags, c);
}
}
if (latexCursor.isNull()) {
if (textCursor.isNull())
return WorksheetCursor();
else
return WorksheetCursor(this, m_textItem, textCursor);
} else {
if (textCursor.isNull() || latexCursor < textCursor) {
int start = latexCursor.selectionStart();
latexCursor.insertText(latex);
QTextCursor c = m_textItem->textCursor();
c.setPosition(start + position);
c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor,
pattern.length());
return WorksheetCursor(this, m_textItem, c);
} else {
return WorksheetCursor(this, m_textItem, textCursor);
}
}
}
void TextEntry::layOutForWidth(qreal w, bool force)
{
if (size().width() == w && !force)
return;
m_textItem->setGeometry(0, 0, w);
setSize(QSizeF(m_textItem->width(), m_textItem->height() + VerticalMargin));
}
bool TextEntry::wantToEvaluate()
{
return !findLatexCode().isNull();
}
bool TextEntry::isConvertableToTextEntry(const QJsonObject& cell)
{
if (!Cantor::JupyterUtils::isMarkdownCell(cell))
return false;
QJsonObject cantorMetadata = Cantor::JupyterUtils::getCantorMetadata(cell);
const QJsonValue& textContentValue = cantorMetadata.value(QLatin1String("text_entry_content"));
if (!textContentValue.isString())
return false;
const QString& textContent = textContentValue.toString();
const QString& source = Cantor::JupyterUtils::getSource(cell);
return textContent == source;
}
void TextEntry::handleMathRender(QSharedPointer<MathRenderResult> result)
{
if (!result->successful)
{
qDebug() << "TextEntry: math render failed with message" << result->errorMessage;
return;
}
const QString& code = result->renderedMath.property(Cantor::Renderer::Code).toString();
const QString& delimiter = QLatin1String("$$");
QTextCursor cursor = m_textItem->document()->find(delimiter + code + delimiter);
if (!cursor.isNull())
{
m_textItem->document()->addResource(QTextDocument::ImageResource, result->uniqueUrl, QVariant(result->image));
result->renderedMath.setProperty(Cantor::Renderer::Delimiter, QLatin1String("$$"));
cursor.insertText(QString(QChar::ObjectReplacementCharacter), result->renderedMath);
}
}
void TextEntry::convertToRawCell()
{
m_rawCell = true;
m_targetMenu->actions().at(0)->setChecked(true);
KColorScheme scheme = KColorScheme(QPalette::Normal, KColorScheme::View);
m_textItem->setBackgroundColor(scheme.background(KColorScheme::AlternateBackground).color());
// Resolve all latex inserts
QTextCursor cursor(m_textItem->document());
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
cursor.insertText(m_textItem->resolveImages(cursor));
}
void TextEntry::convertToTextEntry()
{
m_rawCell = false;
m_convertTarget.clear();
KColorScheme scheme = KColorScheme(QPalette::Normal, KColorScheme::View);
m_textItem->setBackgroundColor(scheme.background(KColorScheme::NormalBackground).color());
}
void TextEntry::convertTargetChanged(QAction* action)
{
int index = standartRawCellTargetNames.indexOf(action->text());
if (index != -1)
{
m_convertTarget = standartRawCellTargetMimes[index];
}
else if (action == m_ownTarget)
{
bool ok;
const QString& target = QInputDialog::getText(worksheet()->worksheetView(), i18n("Cantor"), i18n("Target MIME type:"), QLineEdit::Normal, QString(), &ok);
if (ok && !target.isEmpty())
{
addNewTarget(target);
m_convertTarget = target;
}
}
else
{
m_convertTarget = action->text();
}
}
void TextEntry::addNewTarget(const QString& target)
{
QAction* action = new QAction(target, m_targetActionGroup);
action->setCheckable(true);
action->setChecked(true);
m_targetMenu->insertAction(m_targetMenu->actions().last(), action);
}
diff --git a/src/worksheet.cpp b/src/worksheet.cpp
index f6c73ce9..e4ad1b67 100644
--- a/src/worksheet.cpp
+++ b/src/worksheet.cpp
@@ -1,2269 +1,2269 @@
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2009 Alexander Rieder <alexanderrieder@gmail.com>
Copyright (C) 2012 Martin Kuettler <martin.kuettler@gmail.com>
*/
#include "worksheet.h"
#include <QtGlobal>
#include <QApplication>
#include <QBuffer>
#include <QDebug>
#include <QDrag>
#include <QGraphicsWidget>
#include <QPrinter>
#include <QTimer>
#include <QXmlQuery>
#include <QJsonArray>
#include <QJsonDocument>
#include <KMessageBox>
#include <KActionCollection>
#include <KFontAction>
#include <KFontSizeAction>
#include <KToggleAction>
#include <KLocalizedString>
#include "settings.h"
#include "commandentry.h"
#include "textentry.h"
#include "markdownentry.h"
#include "latexentry.h"
#include "imageentry.h"
#include "pagebreakentry.h"
#include "placeholderentry.h"
#include "lib/jupyterutils.h"
#include "lib/backend.h"
#include "lib/extension.h"
#include "lib/helpresult.h"
#include "lib/session.h"
#include "lib/defaulthighlighter.h"
#include <config-cantor.h>
const double Worksheet::LeftMargin = 4;
const double Worksheet::RightMargin = 4;
const double Worksheet::TopMargin = 12;
const double Worksheet::EntryCursorLength = 30;
const double Worksheet::EntryCursorWidth = 2;
Worksheet::Worksheet(Cantor::Backend* backend, QWidget* parent)
: QGraphicsScene(parent)
{
m_session = backend->createSession();
m_highlighter = nullptr;
m_firstEntry = nullptr;
m_lastEntry = nullptr;
m_lastFocusedTextItem = nullptr;
m_dragEntry = nullptr;
m_placeholderEntry = nullptr;
m_dragScrollTimer = nullptr;
m_choosenCursorEntry = nullptr;
m_isCursorEntryAfterLastEntry = false;
m_viewWidth = 0;
m_maxWidth = 0;
m_entryCursorItem = addLine(0,0,0,0);
const QColor& color = (palette().color(QPalette::Base).lightness() < 128) ? Qt::white : Qt::black;
QPen pen(color);
pen.setWidth(EntryCursorWidth);
m_entryCursorItem->setPen(pen);
m_entryCursorItem->hide();
m_cursorItemTimer = new QTimer(this);
connect(m_cursorItemTimer, &QTimer::timeout, this, &Worksheet::animateEntryCursor);
m_cursorItemTimer->start(500);
m_isPrinting = false;
m_readOnly = false;
m_isLoadingFromFile = false;
m_jupyterMetadata = nullptr;
enableHighlighting(Settings::self()->highlightDefault());
enableCompletion(Settings::self()->completionDefault());
enableExpressionNumbering(Settings::self()->expressionNumberingDefault());
enableAnimations(Settings::self()->animationDefault());
enableEmbeddedMath(Settings::self()->embeddedMathDefault());
}
Worksheet::~Worksheet()
{
// This is necessary, because a SearchBar might access firstEntry()
// while the scene is deleted. Maybe there is a better solution to
// this problem, but I can't seem to find it.
m_firstEntry = nullptr;
if (m_session && m_session->status() != Cantor::Session::Disable)
m_session->logout();
if (m_session)
{
disconnect(m_session, nullptr, nullptr, nullptr);
if (m_session->status() != Cantor::Session::Disable)
m_session->logout();
m_session->deleteLater();
m_session = nullptr;
}
if (m_jupyterMetadata)
delete m_jupyterMetadata;
}
void Worksheet::loginToSession()
{
m_session->login();
#ifdef WITH_EPS
if (Cantor::LatexRenderer::isLatexAvailable())
session()->setTypesettingEnabled(Settings::self()->typesetDefault());
else
session()->setTypesettingEnabled(false);
#else
session()->setTypesettingEnabled(false);
#endif
}
void Worksheet::print(QPrinter* printer)
{
m_epsRenderer.useHighResolution(true);
m_mathRenderer.useHighResolution(true);
m_isPrinting = true;
QRect pageRect = printer->pageRect();
qreal scale = 1; // todo: find good scale for page size
// todo: use epsRenderer()->scale() for printing ?
const qreal width = pageRect.width()/scale;
const qreal height = pageRect.height()/scale;
setViewSize(width, height, scale, true);
QPainter painter(printer);
painter.scale(scale, scale);
painter.setRenderHint(QPainter::Antialiasing);
WorksheetEntry* entry = firstEntry();
qreal y = TopMargin;
while (entry) {
qreal h = 0;
do {
if (entry->type() == PageBreakEntry::Type) {
entry = entry->next();
break;
}
h += entry->size().height();
entry = entry->next();
} while (entry && h + entry->size().height() <= height);
render(&painter, QRectF(0, 0, width, height),
QRectF(0, y, width, h));
y += h;
if (entry)
printer->newPage();
}
//render(&painter);
painter.end();
m_isPrinting = false;
m_epsRenderer.useHighResolution(false);
m_mathRenderer.useHighResolution(false);
m_epsRenderer.setScale(-1); // force update in next call to setViewSize,
worksheetView()->updateSceneSize(); // ... which happens in here
}
bool Worksheet::isPrinting()
{
return m_isPrinting;
}
void Worksheet::setViewSize(qreal w, qreal h, qreal s, bool forceUpdate)
{
Q_UNUSED(h);
m_viewWidth = w;
if (s != m_epsRenderer.scale() || forceUpdate) {
m_epsRenderer.setScale(s);
m_mathRenderer.setScale(s);
for (WorksheetEntry *entry = firstEntry(); entry; entry = entry->next())
entry->updateEntry();
}
updateLayout();
}
void Worksheet::updateLayout()
{
bool cursorRectVisible = false;
bool atEnd = worksheetView()->isAtEnd();
if (currentTextItem()) {
QRectF cursorRect = currentTextItem()->sceneCursorRect();
cursorRectVisible = worksheetView()->isVisible(cursorRect);
}
const qreal w = m_viewWidth - LeftMargin - RightMargin;
qreal y = TopMargin;
const qreal x = LeftMargin;
for (WorksheetEntry *entry = firstEntry(); entry; entry = entry->next())
y += entry->setGeometry(x, y, w);
setSceneRect(QRectF(0, 0, sceneRect().width(), y));
if (cursorRectVisible)
makeVisible(worksheetCursor());
else if (atEnd)
worksheetView()->scrollToEnd();
drawEntryCursor();
}
void Worksheet::updateEntrySize(WorksheetEntry* entry)
{
bool cursorRectVisible = false;
bool atEnd = worksheetView()->isAtEnd();
if (currentTextItem()) {
QRectF cursorRect = currentTextItem()->sceneCursorRect();
cursorRectVisible = worksheetView()->isVisible(cursorRect);
}
qreal y = entry->y() + entry->size().height();
for (entry = entry->next(); entry; entry = entry->next()) {
entry->setY(y);
y += entry->size().height();
}
setSceneRect(QRectF(0, 0, sceneRect().width(), y));
if (cursorRectVisible)
makeVisible(worksheetCursor());
else if (atEnd)
worksheetView()->scrollToEnd();
drawEntryCursor();
}
void Worksheet::setRequestedWidth(QGraphicsObject* object, qreal width)
{
qreal oldWidth = m_itemWidths[object];
m_itemWidths[object] = width;
if (width > m_maxWidth || oldWidth == m_maxWidth)
{
m_maxWidth = width;
qreal y = lastEntry() ? lastEntry()->size().height() + lastEntry()->y() : 0;
setSceneRect(QRectF(0, 0, m_maxWidth + LeftMargin + RightMargin, y));
}
}
void Worksheet::removeRequestedWidth(QGraphicsObject* object)
{
if (!m_itemWidths.contains(object))
return;
qreal width = m_itemWidths[object];
m_itemWidths.remove(object);
if (width == m_maxWidth)
{
m_maxWidth = 0;
for (qreal width : m_itemWidths.values())
if (width > m_maxWidth)
m_maxWidth = width;
qreal y = lastEntry() ? lastEntry()->size().height() + lastEntry()->y() : 0;
setSceneRect(QRectF(0, 0, m_maxWidth + LeftMargin + RightMargin, y));
}
}
bool Worksheet::isEmpty()
{
return !m_firstEntry;
}
bool Worksheet::isLoadingFromFile()
{
return m_isLoadingFromFile;
}
void Worksheet::makeVisible(WorksheetEntry* entry)
{
QRectF r = entry->boundingRect();
r = entry->mapRectToScene(r);
r.adjust(0, -10, 0, 10);
worksheetView()->makeVisible(r);
}
void Worksheet::makeVisible(const WorksheetCursor& cursor)
{
if (cursor.textCursor().isNull()) {
if (cursor.entry())
makeVisible(cursor.entry());
return;
}
QRectF r = cursor.textItem()->sceneCursorRect(cursor.textCursor());
QRectF er = cursor.entry()->boundingRect();
er = cursor.entry()->mapRectToScene(er);
er.adjust(0, -10, 0, 10);
r.adjust(0, qMax(qreal(-100.0), er.top() - r.top()),
0, qMin(qreal(100.0), er.bottom() - r.bottom()));
worksheetView()->makeVisible(r);
}
WorksheetView* Worksheet::worksheetView()
{
return qobject_cast<WorksheetView*>(views().first());
}
void Worksheet::setModified()
{
if (!m_isLoadingFromFile)
emit modified();
}
WorksheetCursor Worksheet::worksheetCursor()
{
WorksheetEntry* entry = currentEntry();
WorksheetTextItem* item = currentTextItem();
if (!entry || !item)
return WorksheetCursor();
return WorksheetCursor(entry, item, item->textCursor());
}
void Worksheet::setWorksheetCursor(const WorksheetCursor& cursor)
{
if (!cursor.isValid())
return;
if (m_lastFocusedTextItem)
m_lastFocusedTextItem->clearSelection();
m_lastFocusedTextItem = cursor.textItem();
cursor.textItem()->setTextCursor(cursor.textCursor());
}
WorksheetEntry* Worksheet::currentEntry()
{
QGraphicsItem* item = focusItem();
// Entry cursor activate
if (m_choosenCursorEntry || m_isCursorEntryAfterLastEntry)
return nullptr;
if (!item /*&& !hasFocus()*/)
item = m_lastFocusedTextItem;
/*else
m_focusItem = item;*/
while (item && (item->type() < QGraphicsItem::UserType ||
item->type() >= QGraphicsItem::UserType + 100))
item = item->parentItem();
if (item) {
WorksheetEntry* entry = qobject_cast<WorksheetEntry*>(item->toGraphicsObject());
if (entry && entry->aboutToBeRemoved()) {
if (entry->isAncestorOf(m_lastFocusedTextItem))
m_lastFocusedTextItem = nullptr;
return nullptr;
}
return entry;
}
return nullptr;
}
WorksheetEntry* Worksheet::firstEntry()
{
return m_firstEntry;
}
WorksheetEntry* Worksheet::lastEntry()
{
return m_lastEntry;
}
void Worksheet::setFirstEntry(WorksheetEntry* entry)
{
if (m_firstEntry)
disconnect(m_firstEntry, SIGNAL(aboutToBeDeleted()),
this, SLOT(invalidateFirstEntry()));
m_firstEntry = entry;
if (m_firstEntry)
connect(m_firstEntry, SIGNAL(aboutToBeDeleted()),
this, SLOT(invalidateFirstEntry()), Qt::DirectConnection);
}
void Worksheet::setLastEntry(WorksheetEntry* entry)
{
if (m_lastEntry)
disconnect(m_lastEntry, SIGNAL(aboutToBeDeleted()),
this, SLOT(invalidateLastEntry()));
m_lastEntry = entry;
if (m_lastEntry)
connect(m_lastEntry, SIGNAL(aboutToBeDeleted()),
this, SLOT(invalidateLastEntry()), Qt::DirectConnection);
}
void Worksheet::invalidateFirstEntry()
{
if (m_firstEntry)
setFirstEntry(m_firstEntry->next());
}
void Worksheet::invalidateLastEntry()
{
if (m_lastEntry)
setLastEntry(m_lastEntry->previous());
}
WorksheetEntry* Worksheet::entryAt(qreal x, qreal y)
{
QGraphicsItem* item = itemAt(x, y, QTransform());
while (item && (item->type() <= QGraphicsItem::UserType ||
item->type() >= QGraphicsItem::UserType + 100))
item = item->parentItem();
if (item)
return qobject_cast<WorksheetEntry*>(item->toGraphicsObject());
return nullptr;
}
WorksheetEntry* Worksheet::entryAt(QPointF p)
{
return entryAt(p.x(), p.y());
}
void Worksheet::focusEntry(WorksheetEntry *entry)
{
if (!entry)
return;
entry->focusEntry();
resetEntryCursor();
//bool rt = entry->acceptRichText();
//setActionsEnabled(rt);
//setAcceptRichText(rt);
//ensureCursorVisible();
}
void Worksheet::startDrag(WorksheetEntry* entry, QDrag* drag)
{
if (m_readOnly)
return;
resetEntryCursor();
m_dragEntry = entry;
WorksheetEntry* prev = entry->previous();
WorksheetEntry* next = entry->next();
m_placeholderEntry = new PlaceHolderEntry(this, entry->size());
m_placeholderEntry->setPrevious(prev);
m_placeholderEntry->setNext(next);
if (prev)
prev->setNext(m_placeholderEntry);
else
setFirstEntry(m_placeholderEntry);
if (next)
next->setPrevious(m_placeholderEntry);
else
setLastEntry(m_placeholderEntry);
m_dragEntry->hide();
Qt::DropAction action = drag->exec();
qDebug() << action;
if (action == Qt::MoveAction && m_placeholderEntry) {
qDebug() << "insert in new position";
prev = m_placeholderEntry->previous();
next = m_placeholderEntry->next();
}
m_dragEntry->setPrevious(prev);
m_dragEntry->setNext(next);
if (prev)
prev->setNext(m_dragEntry);
else
setFirstEntry(m_dragEntry);
if (next)
next->setPrevious(m_dragEntry);
else
setLastEntry(m_dragEntry);
m_dragEntry->show();
m_dragEntry->focusEntry();
const QPointF scenePos = worksheetView()->sceneCursorPos();
if (entryAt(scenePos) != m_dragEntry)
m_dragEntry->hideActionBar();
updateLayout();
if (m_placeholderEntry) {
m_placeholderEntry->setPrevious(nullptr);
m_placeholderEntry->setNext(nullptr);
m_placeholderEntry->hide();
m_placeholderEntry->deleteLater();
m_placeholderEntry = nullptr;
}
m_dragEntry = nullptr;
}
void Worksheet::evaluate()
{
qDebug()<<"evaluate worksheet";
if (!m_readOnly && m_session && m_session->status() == Cantor::Session::Disable)
loginToSession();
firstEntry()->evaluate(WorksheetEntry::EvaluateNext);
setModified();
}
void Worksheet::evaluateCurrentEntry()
{
if (!m_readOnly && m_session && m_session->status() == Cantor::Session::Disable)
loginToSession();
WorksheetEntry* entry = currentEntry();
if(!entry)
return;
entry->evaluateCurrentItem();
}
bool Worksheet::completionEnabled()
{
return m_completionEnabled;
}
void Worksheet::showCompletion()
{
WorksheetEntry* current = currentEntry();
if (current)
current->showCompletion();
}
WorksheetEntry* Worksheet::appendEntry(const int type, bool focus)
{
WorksheetEntry* entry = WorksheetEntry::create(type, this);
if (entry)
{
qDebug() << "Entry Appended";
entry->setPrevious(lastEntry());
if (lastEntry())
lastEntry()->setNext(entry);
if (!firstEntry())
setFirstEntry(entry);
setLastEntry(entry);
if (!m_isLoadingFromFile)
{
updateLayout();
if (focus)
{
makeVisible(entry);
focusEntry(entry);
}
setModified();
}
}
return entry;
}
WorksheetEntry* Worksheet::appendCommandEntry()
{
return appendEntry(CommandEntry::Type);
}
WorksheetEntry* Worksheet::appendTextEntry()
{
return appendEntry(TextEntry::Type);
}
WorksheetEntry* Worksheet::appendMarkdownEntry()
{
return appendEntry(MarkdownEntry::Type);
}
WorksheetEntry* Worksheet::appendPageBreakEntry()
{
return appendEntry(PageBreakEntry::Type);
}
WorksheetEntry* Worksheet::appendImageEntry()
{
return appendEntry(ImageEntry::Type);
}
WorksheetEntry* Worksheet::appendLatexEntry()
{
return appendEntry(LatexEntry::Type);
}
void Worksheet::appendCommandEntry(const QString& text)
{
WorksheetEntry* entry = lastEntry();
if(!entry->isEmpty())
{
entry = appendCommandEntry();
}
if (entry)
{
focusEntry(entry);
entry->setContent(text);
evaluateCurrentEntry();
}
}
WorksheetEntry* Worksheet::insertEntry(const int type, WorksheetEntry* current)
{
if (!current)
current = currentEntry();
if (!current)
return appendEntry(type);
WorksheetEntry *next = current->next();
WorksheetEntry *entry = nullptr;
if (!next || next->type() != type || !next->isEmpty())
{
entry = WorksheetEntry::create(type, this);
entry->setPrevious(current);
entry->setNext(next);
current->setNext(entry);
if (next)
next->setPrevious(entry);
else
setLastEntry(entry);
updateLayout();
setModified();
} else {
entry = next;
}
focusEntry(entry);
makeVisible(entry);
return entry;
}
WorksheetEntry* Worksheet::insertTextEntry(WorksheetEntry* current)
{
return insertEntry(TextEntry::Type, current);
}
WorksheetEntry* Worksheet::insertMarkdownEntry(WorksheetEntry* current)
{
return insertEntry(MarkdownEntry::Type, current);
}
WorksheetEntry* Worksheet::insertCommandEntry(WorksheetEntry* current)
{
return insertEntry(CommandEntry::Type, current);
}
WorksheetEntry* Worksheet::insertImageEntry(WorksheetEntry* current)
{
return insertEntry(ImageEntry::Type, current);
}
WorksheetEntry* Worksheet::insertPageBreakEntry(WorksheetEntry* current)
{
return insertEntry(PageBreakEntry::Type, current);
}
WorksheetEntry* Worksheet::insertLatexEntry(WorksheetEntry* current)
{
return insertEntry(LatexEntry::Type, current);
}
void Worksheet::insertCommandEntry(const QString& text)
{
WorksheetEntry* entry = insertCommandEntry();
if(entry&&!text.isNull())
{
entry->setContent(text);
evaluateCurrentEntry();
}
}
WorksheetEntry* Worksheet::insertEntryBefore(int type, WorksheetEntry* current)
{
if (!current)
current = currentEntry();
if (!current)
return nullptr;
WorksheetEntry *prev = current->previous();
WorksheetEntry *entry = nullptr;
if(!prev || prev->type() != type || !prev->isEmpty())
{
entry = WorksheetEntry::create(type, this);
entry->setNext(current);
entry->setPrevious(prev);
current->setPrevious(entry);
if (prev)
prev->setNext(entry);
else
setFirstEntry(entry);
updateLayout();
setModified();
}
else
entry = prev;
focusEntry(entry);
return entry;
}
WorksheetEntry* Worksheet::insertTextEntryBefore(WorksheetEntry* current)
{
return insertEntryBefore(TextEntry::Type, current);
}
WorksheetEntry* Worksheet::insertMarkdownEntryBefore(WorksheetEntry* current)
{
return insertEntryBefore(MarkdownEntry::Type, current);
}
WorksheetEntry* Worksheet::insertCommandEntryBefore(WorksheetEntry* current)
{
return insertEntryBefore(CommandEntry::Type, current);
}
WorksheetEntry* Worksheet::insertPageBreakEntryBefore(WorksheetEntry* current)
{
return insertEntryBefore(PageBreakEntry::Type, current);
}
WorksheetEntry* Worksheet::insertImageEntryBefore(WorksheetEntry* current)
{
return insertEntryBefore(ImageEntry::Type, current);
}
WorksheetEntry* Worksheet::insertLatexEntryBefore(WorksheetEntry* current)
{
return insertEntryBefore(LatexEntry::Type, current);
}
void Worksheet::interrupt()
{
if (m_session->status() == Cantor::Session::Running)
{
m_session->interrupt();
emit updatePrompt();
}
}
void Worksheet::interruptCurrentEntryEvaluation()
{
currentEntry()->interruptEvaluation();
}
void Worksheet::highlightItem(WorksheetTextItem* item)
{
if (!m_highlighter)
return;
QTextDocument *oldDocument = m_highlighter->document();
QList<QVector<QTextLayout::FormatRange> > formats;
if (oldDocument)
{
for (QTextBlock b = oldDocument->firstBlock();
b.isValid(); b = b.next())
{
formats.append(b.layout()->formats());
}
}
// Not every highlighter is a Cantor::DefaultHighligther (e.g. the
// highlighter for KAlgebra)
Cantor::DefaultHighlighter* hl = qobject_cast<Cantor::DefaultHighlighter*>(m_highlighter);
if (hl) {
hl->setTextItem(item);
} else {
m_highlighter->setDocument(item->document());
}
if (oldDocument)
{
QTextCursor cursor(oldDocument);
cursor.beginEditBlock();
for (QTextBlock b = oldDocument->firstBlock();
b.isValid(); b = b.next())
{
b.layout()->setFormats(formats.first());
formats.pop_front();
}
cursor.endEditBlock();
}
}
void Worksheet::rehighlight()
{
if(m_highlighter)
{
// highlight every entry
WorksheetEntry* entry;
for (entry = firstEntry(); entry; entry = entry->next()) {
WorksheetTextItem* item = entry->highlightItem();
if (!item)
continue;
highlightItem(item);
m_highlighter->rehighlight();
}
entry = currentEntry();
WorksheetTextItem* textitem = entry ? entry->highlightItem() : nullptr;
if (textitem && textitem->hasFocus())
highlightItem(textitem);
} else
{
// remove highlighting from entries
WorksheetEntry* entry;
for (entry = firstEntry(); entry; entry = entry->next()) {
WorksheetTextItem* item = entry->highlightItem();
if (!item)
continue;
QTextCursor cursor(item->document());
cursor.beginEditBlock();
for (QTextBlock b = item->document()->firstBlock();
b.isValid(); b = b.next())
{
b.layout()->clearFormats();
}
cursor.endEditBlock();
}
update();
}
}
void Worksheet::enableHighlighting(bool highlight)
{
if(highlight)
{
if(m_highlighter)
m_highlighter->deleteLater();
if (!m_readOnly)
m_highlighter=session()->syntaxHighlighter(this);
else
m_highlighter=nullptr;
if(!m_highlighter)
m_highlighter=new Cantor::DefaultHighlighter(this);
connect(m_highlighter, SIGNAL(rulesChanged()), this, SLOT(rehighlight()));
}else
{
if(m_highlighter)
m_highlighter->deleteLater();
m_highlighter=nullptr;
}
rehighlight();
}
void Worksheet::enableCompletion(bool enable)
{
m_completionEnabled=enable;
}
Cantor::Session* Worksheet::session()
{
return m_session;
}
bool Worksheet::isRunning()
{
return m_session && m_session->status()==Cantor::Session::Running;
}
bool Worksheet::isReadOnly()
{
return m_readOnly;
}
bool Worksheet::showExpressionIds()
{
return m_showExpressionIds;
}
bool Worksheet::animationsEnabled()
{
return m_animationsEnabled;
}
void Worksheet::enableAnimations(bool enable)
{
m_animationsEnabled = enable;
}
bool Worksheet::embeddedMathEnabled()
{
return m_embeddedMathEnabled && m_mathRenderer.mathRenderAvailable();
}
void Worksheet::enableEmbeddedMath(bool enable)
{
m_embeddedMathEnabled = enable;
}
void Worksheet::enableExpressionNumbering(bool enable)
{
m_showExpressionIds=enable;
emit updatePrompt();
}
QDomDocument Worksheet::toXML(KZip* archive)
{
QDomDocument doc( QLatin1String("CantorWorksheet") );
QDomElement root=doc.createElement( QLatin1String("Worksheet") );
root.setAttribute(QLatin1String("backend"), (m_session ? m_session->backend()->name(): m_backendName));
doc.appendChild(root);
for( WorksheetEntry* entry = firstEntry(); entry; entry = entry->next())
{
QDomElement el = entry->toXml(doc, archive);
root.appendChild( el );
}
return doc;
}
QJsonDocument Worksheet::toJupyterJson()
{
QJsonDocument doc;
QJsonObject root;
QJsonObject metadata(m_jupyterMetadata ? *m_jupyterMetadata : QJsonObject());
QJsonObject kernalInfo;
if (m_session && m_session->backend())
kernalInfo = Cantor::JupyterUtils::getKernelspec(m_session->backend());
else
kernalInfo.insert(QLatin1String("name"), m_backendName);
metadata.insert(QLatin1String("kernelspec"), kernalInfo);
root.insert(QLatin1String("metadata"), metadata);
// Not sure, but it looks like we support nbformat version 4.5
root.insert(QLatin1String("nbformat"), 4);
root.insert(QLatin1String("nbformat_minor"), 5);
QJsonArray cells;
for( WorksheetEntry* entry = firstEntry(); entry; entry = entry->next())
{
const QJsonValue entryJson = entry->toJupyterJson();
if (!entryJson.isNull())
cells.append(entryJson);
}
root.insert(QLatin1String("cells"), cells);
doc.setObject(root);
return doc;
}
void Worksheet::save( const QString& filename )
{
QFile file(filename);
if ( !file.open(QIODevice::WriteOnly) )
{
KMessageBox::error( worksheetView(),
i18n( "Cannot write file %1." , filename ),
i18n( "Error - Cantor" ));
return;
}
save(&file);
}
QByteArray Worksheet::saveToByteArray()
{
QBuffer buffer;
save(&buffer);
return buffer.buffer();
}
void Worksheet::save( QIODevice* device)
{
qDebug()<<"saving to filename";
switch (m_type)
{
case CantorWorksheet:
{
KZip zipFile( device );
if ( !zipFile.open(QIODevice::WriteOnly) )
{
KMessageBox::error( worksheetView(),
i18n( "Cannot write file." ),
i18n( "Error - Cantor" ));
return;
}
QByteArray content = toXML(&zipFile).toByteArray();
zipFile.writeFile( QLatin1String("content.xml"), content.data());
break;
}
case JupyterNotebook:
{
if (!device->isWritable())
{
KMessageBox::error( worksheetView(),
i18n( "Cannot write file." ),
i18n( "Error - Cantor" ));
return;
}
const QJsonDocument& doc = toJupyterJson();
device->write(doc.toJson(QJsonDocument::Indented));
break;
}
}
}
void Worksheet::savePlain(const QString& filename)
{
QFile file(filename);
if(!file.open(QIODevice::WriteOnly))
{
KMessageBox::error(worksheetView(), i18n("Error saving file %1", filename), i18n("Error - Cantor"));
return;
}
QString cmdSep=QLatin1String(";\n");
QString commentStartingSeq = QLatin1String("");
QString commentEndingSeq = QLatin1String("");
if (!m_readOnly)
{
Cantor::Backend * const backend=session()->backend();
if (backend->extensions().contains(QLatin1String("ScriptExtension")))
{
Cantor::ScriptExtension* e=dynamic_cast<Cantor::ScriptExtension*>(backend->extension(QLatin1String(("ScriptExtension"))));
if (e)
{
cmdSep=e->commandSeparator();
commentStartingSeq = e->commentStartingSequence();
commentEndingSeq = e->commentEndingSequence();
}
}
}
else
KMessageBox::information(worksheetView(), i18n("In read-only mode Cantor couldn't guarantee, that the export will be valid for %1", m_backendName), i18n("Cantor"));
QTextStream stream(&file);
for(WorksheetEntry * entry = firstEntry(); entry; entry = entry->next())
{
const QString& str=entry->toPlain(cmdSep, commentStartingSeq, commentEndingSeq);
if(!str.isEmpty())
stream << str + QLatin1Char('\n');
}
file.close();
}
void Worksheet::saveLatex(const QString& filename)
{
qDebug()<<"exporting to Latex: " <<filename;
QFile file(filename);
if(!file.open(QIODevice::WriteOnly))
{
KMessageBox::error(worksheetView(), i18n("Error saving file %1", filename), i18n("Error - Cantor"));
return;
}
QString xml = toXML().toString();
QTextStream stream(&file);
QXmlQuery query(QXmlQuery::XSLT20);
query.setFocus(xml);
QString stylesheet = QStandardPaths::locate(QStandardPaths::DataLocation, QLatin1String("xslt/latex.xsl"));
if (stylesheet.isEmpty())
{
KMessageBox::error(worksheetView(), i18n("Error loading latex.xsl stylesheet"), i18n("Error - Cantor"));
return;
}
query.setQuery(QUrl(stylesheet));
QString out;
if (query.evaluateTo(&out))
// Transform HTML escaped special characters to valid LaTeX characters (&, <, >)
stream << out.replace(QLatin1String("&amp;"), QLatin1String("&"))
.replace(QLatin1String("&gt;"), QLatin1String(">"))
.replace(QLatin1String("&lt;"), QLatin1String("<"));
file.close();
}
bool Worksheet::load(const QString& filename )
{
qDebug() << "loading worksheet" << filename;
QFile file(filename);
if (!file.open(QIODevice::ReadOnly)) {
KMessageBox::error(worksheetView(), i18n("Couldn't open the file %1.", filename), i18n("Open File"));
return false;
}
bool rc = load(&file);
if (rc && !m_readOnly)
m_session->setWorksheetPath(filename);
return rc;
}
void Worksheet::load(QByteArray* data)
{
QBuffer buf(data);
buf.open(QIODevice::ReadOnly);
load(&buf);
}
bool Worksheet::load(QIODevice* device)
{
if (!device->isReadable())
{
QApplication::restoreOverrideCursor();
KMessageBox::error(worksheetView(), i18n("Couldn't open the selected file for reading."), i18n("Open File"));
return false;
}
KZip archive(device);
if (archive.open(QIODevice::ReadOnly))
return loadCantorWorksheet(archive);
else
{
qDebug() <<"not a zip file";
// Go to begin of data, we need read all data in second time
device->seek(0);
QJsonParseError error;
const QJsonDocument& doc = QJsonDocument::fromJson(device->readAll(), &error);
if (error.error != QJsonParseError::NoError)
{
qDebug()<<"not a json file, parsing failed with error: " << error.errorString();
QApplication::restoreOverrideCursor();
KMessageBox::error(worksheetView(), i18n("The selected file is not a valid Cantor or Jupyter project file."), i18n("Open File"));
return false;
}
else
return loadJupyterNotebook(doc);
}
}
bool Worksheet::loadCantorWorksheet(const KZip& archive)
{
m_type = Type::CantorWorksheet;
const KArchiveEntry* contentEntry=archive.directory()->entry(QLatin1String("content.xml"));
if (!contentEntry->isFile())
{
qDebug()<<"content.xml file not found in the zip archive";
QApplication::restoreOverrideCursor();
KMessageBox::error(worksheetView(), i18n("The selected file is not a valid Cantor project file."), i18n("Open File"));
return false;
}
const KArchiveFile* content = static_cast<const KArchiveFile*>(contentEntry);
QByteArray data = content->data();
QDomDocument doc;
doc.setContent(data);
QDomElement root=doc.documentElement();
m_backendName=root.attribute(QLatin1String("backend"));
Cantor::Backend* b=Cantor::Backend::getBackend(m_backendName);
if (!b)
{
QApplication::restoreOverrideCursor();
KMessageBox::information(worksheetView(), i18n("%1 backend was not found. Editing and executing entries is not possible.", m_backendName), i18n("Open File"));
m_readOnly = true;
}
else
m_readOnly = false;
if(!m_readOnly && !b->isEnabled())
{
QApplication::restoreOverrideCursor();
KMessageBox::information(worksheetView(), i18n("There are some problems with the %1 backend,\n"\
"please check your configuration or install the needed packages.\n"
"You will only be able to view this worksheet.", m_backendName), i18n("Open File"));
m_readOnly = true;
}
if (m_readOnly)
{
// TODO: Handle this here?
for (QAction* action : m_richTextActionList)
action->setEnabled(false);
}
m_isLoadingFromFile = true;
//cleanup the worksheet and all it contains
delete m_session;
m_session=nullptr;
- //file can only be loaded in a worksheet that was not eidted/modified yet (s.a. CantorShell::load())
+ //file can only be loaded in a worksheet that was not edited/modified yet (s.a. CantorShell::load())
//in this case on the default "first entry" is available -> delete it.
if (m_firstEntry) {
delete m_firstEntry;
m_firstEntry = nullptr;
}
resetEntryCursor();
m_itemWidths.clear();
m_maxWidth = 0;
if (!m_readOnly)
m_session=b->createSession();
qDebug()<<"loading entries";
QDomElement expressionChild = root.firstChildElement();
WorksheetEntry* entry = nullptr;
while (!expressionChild.isNull()) {
QString tag = expressionChild.tagName();
// Don't add focus on load
if (tag == QLatin1String("Expression"))
{
entry = appendEntry(CommandEntry::Type, false);
entry->setContent(expressionChild, archive);
} else if (tag == QLatin1String("Text"))
{
entry = appendEntry(TextEntry::Type, false);
entry->setContent(expressionChild, archive);
} else if (tag == QLatin1String("Markdown"))
{
entry = appendEntry(MarkdownEntry::Type, false);
entry->setContent(expressionChild, archive);
} else if (tag == QLatin1String("Latex"))
{
entry = appendEntry(LatexEntry::Type, false);
entry->setContent(expressionChild, archive);
} else if (tag == QLatin1String("PageBreak"))
{
entry = appendEntry(PageBreakEntry::Type, false);
entry->setContent(expressionChild, archive);
}
else if (tag == QLatin1String("Image"))
{
entry = appendEntry(ImageEntry::Type, false);
entry->setContent(expressionChild, archive);
}
if (m_readOnly && entry)
{
entry->setAcceptHoverEvents(false);
entry = nullptr;
}
expressionChild = expressionChild.nextSiblingElement();
}
if (m_readOnly)
clearFocus();
m_isLoadingFromFile = false;
updateLayout();
//Set the Highlighting, depending on the current state
//If the session isn't logged in, use the default
enableHighlighting( m_highlighter!=nullptr || Settings::highlightDefault() );
emit loaded();
return true;
}
bool Worksheet::loadJupyterNotebook(const QJsonDocument& doc)
{
m_type = Type::JupyterNotebook;
int nbformatMajor, nbformatMinor;
if (!Cantor::JupyterUtils::isJupyterNotebook(doc))
{
- // Two possiblities: old jupyter notebook (version <= 4.0.0 and a another scheme) or just not a notebook at all
+ // Two possibilities: old jupyter notebook (version <= 4.0.0 and a another scheme) or just not a notebook at all
std::tie(nbformatMajor, nbformatMinor) = Cantor::JupyterUtils::getNbformatVersion(doc.object());
if (nbformatMajor == 0 && nbformatMinor == 0)
{
QApplication::restoreOverrideCursor();
showInvalidNotebookSchemeError();
}
else
{
KMessageBox::error(worksheetView(),
i18n("Jupyter notebooks with versions lower than 4.5 (detected version %1.%2) are not supported.", nbformatMajor, nbformatMinor ),
i18n("Open File"));
}
return false;
}
QJsonObject notebookObject = doc.object();
std::tie(nbformatMajor, nbformatMinor) = Cantor::JupyterUtils::getNbformatVersion(notebookObject);
if (QT_VERSION_CHECK(nbformatMajor, nbformatMinor, 0) > QT_VERSION_CHECK(4,5,0))
{
QApplication::restoreOverrideCursor();
KMessageBox::error(
worksheetView(),
i18n("Jupyter notebooks with versions higher than 4.5 (detected version %1.%2) are not supported.", nbformatMajor, nbformatMinor),
i18n("Open File")
);
return false;
}
const QJsonArray& cells = Cantor::JupyterUtils::getCells(notebookObject);
const QJsonObject& metadata = Cantor::JupyterUtils::getMetadata(notebookObject);
if (m_jupyterMetadata)
delete m_jupyterMetadata;
m_jupyterMetadata = new QJsonObject(metadata);
const QJsonObject& kernalspec = metadata.value(QLatin1String("kernelspec")).toObject();
m_backendName = Cantor::JupyterUtils::getKernelName(kernalspec);
if (kernalspec.isEmpty() || m_backendName.isEmpty())
{
QApplication::restoreOverrideCursor();
showInvalidNotebookSchemeError();
return false;
}
Cantor::Backend* backend = Cantor::Backend::getBackend(m_backendName);
if (!backend)
{
QApplication::restoreOverrideCursor();
KMessageBox::information(worksheetView(),
i18n("%1 backend was not found. Editing and executing entries is not possible.", m_backendName),
i18n("Open File"));
m_readOnly = true;
}
else
m_readOnly = false;
if(!m_readOnly && !backend->isEnabled())
{
QApplication::restoreOverrideCursor();
KMessageBox::information(worksheetView(), i18n("There are some problems with the %1 backend,\n"\
"please check your configuration or install the needed packages.\n"
"You will only be able to view this worksheet.", m_backendName), i18n("Open File"));
m_readOnly = true;
}
if (m_readOnly)
{
for (QAction* action : m_richTextActionList)
action->setEnabled(false);
}
m_isLoadingFromFile = true;
if (m_session)
delete m_session;
m_session = nullptr;
if (m_firstEntry) {
delete m_firstEntry;
m_firstEntry = nullptr;
}
resetEntryCursor();
m_itemWidths.clear();
m_maxWidth = 0;
if (!m_readOnly)
m_session=backend->createSession();
qDebug() << "loading jupyter entries";
WorksheetEntry* entry = nullptr;
for (QJsonArray::const_iterator iter = cells.begin(); iter != cells.end(); iter++) {
if (!Cantor::JupyterUtils::isJupyterCell(*iter))
{
QApplication::restoreOverrideCursor();
QString explanation;
if (iter->isObject())
explanation = i18n("an object with keys: %1", iter->toObject().keys().join(QLatin1String(", ")));
else
explanation = i18n("non object JSON value");
m_isLoadingFromFile = false;
showInvalidNotebookSchemeError(i18n("found incorrect data (%1) that is not Jupyter cell", explanation));
return false;
}
const QJsonObject& cell = iter->toObject();
QString cellType = Cantor::JupyterUtils::getCellType(cell);
if (cellType == QLatin1String("code"))
{
if (LatexEntry::isConvertableToLatexEntry(cell))
{
entry = appendEntry(LatexEntry::Type, false);
entry->setContentFromJupyter(cell);
entry->evaluate(WorksheetEntry::InternalEvaluation);
}
else
{
entry = appendEntry(CommandEntry::Type, false);
entry->setContentFromJupyter(cell);
}
}
else if (cellType == QLatin1String("markdown"))
{
if (TextEntry::isConvertableToTextEntry(cell))
{
entry = appendEntry(TextEntry::Type, false);
entry->setContentFromJupyter(cell);
}
else
{
entry = appendEntry(MarkdownEntry::Type, false);
entry->setContentFromJupyter(cell);
entry->evaluate(WorksheetEntry::InternalEvaluation);
}
}
else if (cellType == QLatin1String("raw"))
{
if (PageBreakEntry::isConvertableToPageBreakEntry(cell))
entry = appendEntry(PageBreakEntry::Type, false);
else
entry = appendEntry(TextEntry::Type, false);
entry->setContentFromJupyter(cell);
}
if (m_readOnly && entry)
{
entry->setAcceptHoverEvents(false);
entry = nullptr;
}
}
if (m_readOnly)
clearFocus();
m_isLoadingFromFile = false;
updateLayout();
enableHighlighting( m_highlighter!=nullptr || Settings::highlightDefault() );
emit loaded();
return true;
}
void Worksheet::showInvalidNotebookSchemeError(QString additionalInfo)
{
if (additionalInfo.isEmpty())
KMessageBox::error(worksheetView(), i18n("The file is not valid Jupyter notebook"), i18n("Open File"));
else
KMessageBox::error(worksheetView(), i18n("Invalid Jupyter notebook scheme: %1", additionalInfo), i18n("Open File"));
}
void Worksheet::gotResult(Cantor::Expression* expr)
{
if(expr==nullptr)
expr=qobject_cast<Cantor::Expression*>(sender());
if(expr==nullptr)
return;
//We're only interested in help results, others are handled by the WorksheetEntry
for (auto* result : expr->results())
{
if(result && result->type()==Cantor::HelpResult::Type)
{
QString help = result->toHtml();
//Do some basic LaTeX replacing
help.replace(QRegExp(QLatin1String("\\\\code\\{([^\\}]*)\\}")), QLatin1String("<b>\\1</b>"));
help.replace(QRegExp(QLatin1String("\\$([^\\$])\\$")), QLatin1String("<i>\\1</i>"));
emit showHelp(help);
//TODO: break after the first help result found, not clear yet how to handle multiple requests for help within one single command (e.g. ??ev;??int).
break;
}
}
}
void Worksheet::removeCurrentEntry()
{
qDebug()<<"removing current entry";
WorksheetEntry* entry=currentEntry();
if(!entry)
return;
// In case we just removed this
if (entry->isAncestorOf(m_lastFocusedTextItem))
m_lastFocusedTextItem = nullptr;
entry->startRemoving();
}
Cantor::Renderer* Worksheet::renderer()
{
return &m_epsRenderer;
}
MathRenderer* Worksheet::mathRenderer()
{
return &m_mathRenderer;
}
QMenu* Worksheet::createContextMenu()
{
QMenu *menu = new QMenu(worksheetView());
connect(menu, SIGNAL(aboutToHide()), menu, SLOT(deleteLater()));
return menu;
}
void Worksheet::populateMenu(QMenu *menu, QPointF pos)
{
WorksheetEntry* entry = entryAt(pos);
if (entry && !entry->isAncestorOf(m_lastFocusedTextItem)) {
WorksheetTextItem* item =
qgraphicsitem_cast<WorksheetTextItem*>(itemAt(pos, QTransform()));
if (item && item->isEditable())
m_lastFocusedTextItem = item;
}
if (!isRunning())
menu->addAction(QIcon::fromTheme(QLatin1String("system-run")), i18n("Evaluate Worksheet"),
this, SLOT(evaluate()), 0);
else
menu->addAction(QIcon::fromTheme(QLatin1String("process-stop")), i18n("Interrupt"), this,
SLOT(interrupt()), 0);
menu->addSeparator();
if (entry) {
QMenu* insert = new QMenu(menu);
QMenu* insertBefore = new QMenu(menu);
insert->addAction(QIcon::fromTheme(QLatin1String("run-build")), i18n("Command Entry"), entry, SLOT(insertCommandEntry()));
insert->addAction(QIcon::fromTheme(QLatin1String("draw-text")), i18n("Text Entry"), entry, SLOT(insertTextEntry()));
#ifdef Discount_FOUND
insert->addAction(QIcon::fromTheme(QLatin1String("text-x-markdown")), i18n("Markdown Entry"), entry, SLOT(insertMarkdownEntry()));
#endif
#ifdef WITH_EPS
insert->addAction(QIcon::fromTheme(QLatin1String("text-x-tex")), i18n("LaTeX Entry"), entry, SLOT(insertLatexEntry()));
#endif
insert->addAction(QIcon::fromTheme(QLatin1String("image-x-generic")), i18n("Image"), entry, SLOT(insertImageEntry()));
insert->addAction(QIcon::fromTheme(QLatin1String("go-next-view-page")), i18n("Page Break"), entry, SLOT(insertPageBreakEntry()));
insertBefore->addAction(QIcon::fromTheme(QLatin1String("run-build")), i18n("Command Entry"), entry, SLOT(insertCommandEntryBefore()));
insertBefore->addAction(QIcon::fromTheme(QLatin1String("draw-text")), i18n("Text Entry"), entry, SLOT(insertTextEntryBefore()));
#ifdef Discount_FOUND
insertBefore->addAction(QIcon::fromTheme(QLatin1String("text-x-markdown")), i18n("Markdown Entry"), entry, SLOT(insertMarkdownEntryBefore()));
#endif
#ifdef WITH_EPS
insertBefore->addAction(QIcon::fromTheme(QLatin1String("text-x-tex")), i18n("LaTeX Entry"), entry, SLOT(insertLatexEntryBefore()));
#endif
insertBefore->addAction(QIcon::fromTheme(QLatin1String("image-x-generic")), i18n("Image"), entry, SLOT(insertImageEntryBefore()));
insertBefore->addAction(QIcon::fromTheme(QLatin1String("go-next-view-page")), i18n("Page Break"), entry, SLOT(insertPageBreakEntryBefore()));
insert->setTitle(i18n("Insert Entry After"));
insert->setIcon(QIcon::fromTheme(QLatin1String("edit-table-insert-row-below")));
insertBefore->setTitle(i18n("Insert Entry Before"));
insertBefore->setIcon(QIcon::fromTheme(QLatin1String("edit-table-insert-row-above")));
menu->addMenu(insert);
menu->addMenu(insertBefore);
} else {
menu->addAction(QIcon::fromTheme(QLatin1String("run-build")), i18n("Insert Command Entry"), this, SLOT(appendCommandEntry()));
menu->addAction(QIcon::fromTheme(QLatin1String("draw-text")), i18n("Insert Text Entry"), this, SLOT(appendTextEntry()));
#ifdef Discount_FOUND
menu->addAction(QIcon::fromTheme(QLatin1String("text-x-markdown")), i18n("Insert Markdown Entry"), this, SLOT(appendMarkdownEntry()));
#endif
#ifdef WITH_EPS
menu->addAction(QIcon::fromTheme(QLatin1String("text-x-tex")), i18n("Insert LaTeX Entry"), this, SLOT(appendLatexEntry()));
#endif
menu->addAction(QIcon::fromTheme(QLatin1String("image-x-generic")), i18n("Insert Image"), this, SLOT(appendImageEntry()));
menu->addAction(QIcon::fromTheme(QLatin1String("go-next-view-page")), i18n("Insert Page Break"), this, SLOT(appendPageBreakEntry()));
}
}
void Worksheet::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
{
if (m_readOnly)
return;
// forward the event to the items
QGraphicsScene::contextMenuEvent(event);
if (!event->isAccepted()) {
event->accept();
QMenu *menu = createContextMenu();
populateMenu(menu, event->scenePos());
menu->popup(event->screenPos());
}
}
void Worksheet::mousePressEvent(QGraphicsSceneMouseEvent* event)
{
QGraphicsScene::mousePressEvent(event);
/*
if (event->button() == Qt::LeftButton && !focusItem() && lastEntry() &&
event->scenePos().y() > lastEntry()->y() + lastEntry()->size().height())
lastEntry()->focusEntry(WorksheetTextItem::BottomRight);
*/
if (!m_readOnly)
updateEntryCursor(event);
}
void Worksheet::keyPressEvent(QKeyEvent *keyEvent)
{
if (m_readOnly)
return;
// If we choose entry by entry cursor and press text button (not modifiers, for example, like Control)
if ((m_choosenCursorEntry || m_isCursorEntryAfterLastEntry) && !keyEvent->text().isEmpty())
addEntryFromEntryCursor();
QGraphicsScene::keyPressEvent(keyEvent);
}
void Worksheet::createActions(KActionCollection* collection)
{
// Mostly copied from KRichTextWidget::createActions(KActionCollection*)
// It would be great if this wasn't necessary.
// Text color
QAction * action;
/* This is "format-stroke-color" in KRichTextWidget */
action = new QAction(QIcon::fromTheme(QLatin1String("format-text-color")),
i18nc("@action", "Text &Color..."), collection);
action->setIconText(i18nc("@label text color", "Color"));
action->setPriority(QAction::LowPriority);
m_richTextActionList.append(action);
collection->addAction(QLatin1String("format_text_foreground_color"), action);
connect(action, SIGNAL(triggered()), this, SLOT(setTextForegroundColor()));
// Text color
action = new QAction(QIcon::fromTheme(QLatin1String("format-fill-color")),
i18nc("@action", "Text &Highlight..."), collection);
action->setPriority(QAction::LowPriority);
m_richTextActionList.append(action);
collection->addAction(QLatin1String("format_text_background_color"), action);
connect(action, SIGNAL(triggered()), this, SLOT(setTextBackgroundColor()));
// Font Family
m_fontAction = new KFontAction(i18nc("@action", "&Font"), collection);
m_richTextActionList.append(m_fontAction);
collection->addAction(QLatin1String("format_font_family"), m_fontAction);
connect(m_fontAction, SIGNAL(triggered(QString)), this,
SLOT(setFontFamily(QString)));
// Font Size
m_fontSizeAction = new KFontSizeAction(i18nc("@action", "Font &Size"),
collection);
m_richTextActionList.append(m_fontSizeAction);
collection->addAction(QLatin1String("format_font_size"), m_fontSizeAction);
connect(m_fontSizeAction, SIGNAL(fontSizeChanged(int)), this,
SLOT(setFontSize(int)));
// Bold
m_boldAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-text-bold")),
i18nc("@action boldify selected text", "&Bold"),
collection);
m_boldAction->setPriority(QAction::LowPriority);
QFont bold;
bold.setBold(true);
m_boldAction->setFont(bold);
m_richTextActionList.append(m_boldAction);
collection->addAction(QLatin1String("format_text_bold"), m_boldAction);
collection->setDefaultShortcut(m_boldAction, Qt::CTRL + Qt::Key_B);
connect(m_boldAction, SIGNAL(triggered(bool)), this, SLOT(setTextBold(bool)));
// Italic
m_italicAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-text-italic")),
i18nc("@action italicize selected text",
"&Italic"),
collection);
m_italicAction->setPriority(QAction::LowPriority);
QFont italic;
italic.setItalic(true);
m_italicAction->setFont(italic);
m_richTextActionList.append(m_italicAction);
collection->addAction(QLatin1String("format_text_italic"), m_italicAction);
collection->setDefaultShortcut(m_italicAction, Qt::CTRL + Qt::Key_I);
connect(m_italicAction, SIGNAL(triggered(bool)), this, SLOT(setTextItalic(bool)));
// Underline
m_underlineAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-text-underline")),
i18nc("@action underline selected text",
"&Underline"),
collection);
m_underlineAction->setPriority(QAction::LowPriority);
QFont underline;
underline.setUnderline(true);
m_underlineAction->setFont(underline);
m_richTextActionList.append(m_underlineAction);
collection->addAction(QLatin1String("format_text_underline"), m_underlineAction);
collection->setDefaultShortcut(m_underlineAction, Qt::CTRL + Qt::Key_U);
connect(m_underlineAction, SIGNAL(triggered(bool)), this, SLOT(setTextUnderline(bool)));
// Strike
m_strikeOutAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-text-strikethrough")),
i18nc("@action", "&Strike Out"),
collection);
m_strikeOutAction->setPriority(QAction::LowPriority);
m_richTextActionList.append(m_strikeOutAction);
collection->addAction(QLatin1String("format_text_strikeout"), m_strikeOutAction);
collection->setDefaultShortcut(m_strikeOutAction, Qt::CTRL + Qt::Key_L);
connect(m_strikeOutAction, SIGNAL(triggered(bool)), this, SLOT(setTextStrikeOut(bool)));
// Alignment
QActionGroup *alignmentGroup = new QActionGroup(this);
// Align left
m_alignLeftAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-justify-left")),
i18nc("@action", "Align &Left"),
collection);
m_alignLeftAction->setPriority(QAction::LowPriority);
m_alignLeftAction->setIconText(i18nc("@label left justify", "Left"));
m_richTextActionList.append(m_alignLeftAction);
collection->addAction(QLatin1String("format_align_left"), m_alignLeftAction);
connect(m_alignLeftAction, SIGNAL(triggered()), this,
SLOT(setAlignLeft()));
alignmentGroup->addAction(m_alignLeftAction);
// Align center
m_alignCenterAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-justify-center")),
i18nc("@action", "Align &Center"),
collection);
m_alignCenterAction->setPriority(QAction::LowPriority);
m_alignCenterAction->setIconText(i18nc("@label center justify", "Center"));
m_richTextActionList.append(m_alignCenterAction);
collection->addAction(QLatin1String("format_align_center"), m_alignCenterAction);
connect(m_alignCenterAction, SIGNAL(triggered()), this,
SLOT(setAlignCenter()));
alignmentGroup->addAction(m_alignCenterAction);
// Align right
m_alignRightAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-justify-right")),
i18nc("@action", "Align &Right"),
collection);
m_alignRightAction->setPriority(QAction::LowPriority);
m_alignRightAction->setIconText(i18nc("@label right justify", "Right"));
m_richTextActionList.append(m_alignRightAction);
collection->addAction(QLatin1String("format_align_right"), m_alignRightAction);
connect(m_alignRightAction, SIGNAL(triggered()), this,
SLOT(setAlignRight()));
alignmentGroup->addAction(m_alignRightAction);
// Align justify
m_alignJustifyAction = new KToggleAction(QIcon::fromTheme(QLatin1String("format-justify-fill")),
i18nc("@action", "&Justify"),
collection);
m_alignJustifyAction->setPriority(QAction::LowPriority);
m_alignJustifyAction->setIconText(i18nc("@label justify fill", "Justify"));
m_richTextActionList.append(m_alignJustifyAction);
collection->addAction(QLatin1String("format_align_justify"), m_alignJustifyAction);
connect(m_alignJustifyAction, SIGNAL(triggered()), this,
SLOT(setAlignJustify()));
alignmentGroup->addAction(m_alignJustifyAction);
/*
// List style
KSelectAction* selAction;
selAction = new KSelectAction(QIcon::fromTheme("format-list-unordered"),
i18nc("@title:menu", "List Style"),
collection);
QStringList listStyles;
listStyles << i18nc("@item:inmenu no list style", "None")
<< i18nc("@item:inmenu disc list style", "Disc")
<< i18nc("@item:inmenu circle list style", "Circle")
<< i18nc("@item:inmenu square list style", "Square")
<< i18nc("@item:inmenu numbered lists", "123")
<< i18nc("@item:inmenu lowercase abc lists", "abc")
<< i18nc("@item:inmenu uppercase abc lists", "ABC");
selAction->setItems(listStyles);
selAction->setCurrentItem(0);
action = selAction;
m_richTextActionList.append(action);
collection->addAction("format_list_style", action);
connect(action, SIGNAL(triggered(int)),
this, SLOT(_k_setListStyle(int)));
connect(action, SIGNAL(triggered()),
this, SLOT(_k_updateMiscActions()));
// Indent
action = new QAction(QIcon::fromTheme("format-indent-more"),
i18nc("@action", "Increase Indent"), collection);
action->setPriority(QAction::LowPriority);
m_richTextActionList.append(action);
collection->addAction("format_list_indent_more", action);
connect(action, SIGNAL(triggered()),
this, SLOT(indentListMore()));
connect(action, SIGNAL(triggered()),
this, SLOT(_k_updateMiscActions()));
// Dedent
action = new QAction(QIcon::fromTheme("format-indent-less"),
i18nc("@action", "Decrease Indent"), collection);
action->setPriority(QAction::LowPriority);
m_richTextActionList.append(action);
collection->addAction("format_list_indent_less", action);
connect(action, SIGNAL(triggered()), this, SLOT(indentListLess()));
connect(action, SIGNAL(triggered()), this, SLOT(_k_updateMiscActions()));
*/
}
WorksheetTextItem* Worksheet::lastFocusedTextItem()
{
return m_lastFocusedTextItem;
}
void Worksheet::updateFocusedTextItem(WorksheetTextItem* newItem)
{
// No need update and emit signals about editing actions in readonly
// So support only copy action and reset selection
if (m_readOnly)
{
if (m_lastFocusedTextItem && m_lastFocusedTextItem != newItem)
{
disconnect(this, SIGNAL(copy()), m_lastFocusedTextItem, SLOT(copy()));
m_lastFocusedTextItem->clearSelection();
}
if (newItem && m_lastFocusedTextItem != newItem)
{
connect(this, SIGNAL(copy()), newItem, SLOT(copy()));
emit copyAvailable(newItem->isCopyAvailable());
}
else if (!newItem)
{
emit copyAvailable(false);
}
m_lastFocusedTextItem = newItem;
return;
}
if (m_lastFocusedTextItem && m_lastFocusedTextItem != newItem) {
disconnect(m_lastFocusedTextItem, SIGNAL(undoAvailable(bool)),
this, SIGNAL(undoAvailable(bool)));
disconnect(m_lastFocusedTextItem, SIGNAL(redoAvailable(bool)),
this, SIGNAL(redoAvailable(bool)));
disconnect(this, SIGNAL(undo()), m_lastFocusedTextItem, SLOT(undo()));
disconnect(this, SIGNAL(redo()), m_lastFocusedTextItem, SLOT(redo()));
disconnect(m_lastFocusedTextItem, SIGNAL(cutAvailable(bool)),
this, SIGNAL(cutAvailable(bool)));
disconnect(m_lastFocusedTextItem, SIGNAL(copyAvailable(bool)),
this, SIGNAL(copyAvailable(bool)));
disconnect(m_lastFocusedTextItem, SIGNAL(pasteAvailable(bool)),
this, SIGNAL(pasteAvailable(bool)));
disconnect(this, SIGNAL(cut()), m_lastFocusedTextItem, SLOT(cut()));
disconnect(this, SIGNAL(copy()), m_lastFocusedTextItem, SLOT(copy()));
m_lastFocusedTextItem->clearSelection();
}
if (newItem && m_lastFocusedTextItem != newItem) {
setAcceptRichText(newItem->richTextEnabled());
emit undoAvailable(newItem->isUndoAvailable());
emit redoAvailable(newItem->isRedoAvailable());
connect(newItem, SIGNAL(undoAvailable(bool)),
this, SIGNAL(undoAvailable(bool)));
connect(newItem, SIGNAL(redoAvailable(bool)),
this, SIGNAL(redoAvailable(bool)));
connect(this, SIGNAL(undo()), newItem, SLOT(undo()));
connect(this, SIGNAL(redo()), newItem, SLOT(redo()));
emit cutAvailable(newItem->isCutAvailable());
emit copyAvailable(newItem->isCopyAvailable());
emit pasteAvailable(newItem->isPasteAvailable());
connect(newItem, SIGNAL(cutAvailable(bool)),
this, SIGNAL(cutAvailable(bool)));
connect(newItem, SIGNAL(copyAvailable(bool)),
this, SIGNAL(copyAvailable(bool)));
connect(newItem, SIGNAL(pasteAvailable(bool)),
this, SIGNAL(pasteAvailable(bool)));
connect(this, SIGNAL(cut()), newItem, SLOT(cut()));
connect(this, SIGNAL(copy()), newItem, SLOT(copy()));
} else if (!newItem) {
emit undoAvailable(false);
emit redoAvailable(false);
emit cutAvailable(false);
emit copyAvailable(false);
emit pasteAvailable(false);
}
m_lastFocusedTextItem = newItem;
}
/*!
* handles the paste action triggered in cantor_part.
* Pastes into the last focused text item.
* In case the "new entry"-cursor is currently shown,
* a new entry is created first which the content will be pasted into.
*/
void Worksheet::paste() {
if (m_choosenCursorEntry || m_isCursorEntryAfterLastEntry)
addEntryFromEntryCursor();
m_lastFocusedTextItem->paste();
}
void Worksheet::setRichTextInformation(const RichTextInfo& info)
{
m_boldAction->setChecked(info.bold);
m_italicAction->setChecked(info.italic);
m_underlineAction->setChecked(info.underline);
m_strikeOutAction->setChecked(info.strikeOut);
m_fontAction->setFont(info.font);
if (info.fontSize > 0)
m_fontSizeAction->setFontSize(info.fontSize);
if (info.align & Qt::AlignLeft)
m_alignLeftAction->setChecked(true);
else if (info.align & Qt::AlignCenter)
m_alignCenterAction->setChecked(true);
else if (info.align & Qt::AlignRight)
m_alignRightAction->setChecked(true);
else if (info.align & Qt::AlignJustify)
m_alignJustifyAction->setChecked(true);
}
void Worksheet::setAcceptRichText(bool b)
{
if (!m_readOnly)
for(QAction * action : m_richTextActionList)
action->setEnabled(b);
}
WorksheetTextItem* Worksheet::currentTextItem()
{
QGraphicsItem* item = focusItem();
if (!item)
item = m_lastFocusedTextItem;
while (item && item->type() != WorksheetTextItem::Type)
item = item->parentItem();
return qgraphicsitem_cast<WorksheetTextItem*>(item);
}
void Worksheet::setTextForegroundColor()
{
WorksheetTextItem* item = currentTextItem();
if (item)
item->setTextForegroundColor();
}
void Worksheet::setTextBackgroundColor()
{
WorksheetTextItem* item = currentTextItem();
if (item)
item->setTextBackgroundColor();
}
void Worksheet::setTextBold(bool b)
{
WorksheetTextItem* item = currentTextItem();
if (item)
item->setTextBold(b);
}
void Worksheet::setTextItalic(bool b)
{
WorksheetTextItem* item = currentTextItem();
if (item)
item->setTextItalic(b);
}
void Worksheet::setTextUnderline(bool b)
{
WorksheetTextItem* item = currentTextItem();
if (item)
item->setTextUnderline(b);
}
void Worksheet::setTextStrikeOut(bool b)
{
WorksheetTextItem* item = currentTextItem();
if (item)
item->setTextStrikeOut(b);
}
void Worksheet::setAlignLeft()
{
WorksheetTextItem* item = currentTextItem();
if (item)
item->setAlignment(Qt::AlignLeft);
}
void Worksheet::setAlignRight()
{
WorksheetTextItem* item = currentTextItem();
if (item)
item->setAlignment(Qt::AlignRight);
}
void Worksheet::setAlignCenter()
{
WorksheetTextItem* item = currentTextItem();
if (item)
item->setAlignment(Qt::AlignCenter);
}
void Worksheet::setAlignJustify()
{
WorksheetTextItem* item = currentTextItem();
if (item)
item->setAlignment(Qt::AlignJustify);
}
void Worksheet::setFontFamily(const QString& font)
{
WorksheetTextItem* item = currentTextItem();
if (item)
item->setFontFamily(font);
}
void Worksheet::setFontSize(int size)
{
WorksheetTextItem* item = currentTextItem();
if (item)
item->setFontSize(size);
}
bool Worksheet::isShortcut(const QKeySequence& sequence)
{
return m_shortcuts.contains(sequence);
}
void Worksheet::registerShortcut(QAction* action)
{
for (auto& shortcut : action->shortcuts())
m_shortcuts.insert(shortcut, action);
connect(action, SIGNAL(changed()), this, SLOT(updateShortcut()));
}
void Worksheet::updateShortcut()
{
QAction* action = qobject_cast<QAction*>(sender());
if (!action)
return;
// delete the old shortcuts of this action
QList<QKeySequence> shortcuts = m_shortcuts.keys(action);
for (auto& shortcut : shortcuts)
m_shortcuts.remove(shortcut);
// add the new shortcuts
for (auto& shortcut : action->shortcuts())
m_shortcuts.insert(shortcut, action);
}
void Worksheet::dragEnterEvent(QGraphicsSceneDragDropEvent* event)
{
qDebug() << "enter";
if (m_dragEntry)
event->accept();
else
QGraphicsScene::dragEnterEvent(event);
}
void Worksheet::dragLeaveEvent(QGraphicsSceneDragDropEvent* event)
{
if (!m_dragEntry) {
QGraphicsScene::dragLeaveEvent(event);
return;
}
qDebug() << "leave";
event->accept();
if (m_placeholderEntry) {
m_placeholderEntry->startRemoving();
m_placeholderEntry = nullptr;
}
}
void Worksheet::dragMoveEvent(QGraphicsSceneDragDropEvent* event)
{
if (!m_dragEntry) {
QGraphicsScene::dragMoveEvent(event);
return;
}
QPointF pos = event->scenePos();
WorksheetEntry* entry = entryAt(pos);
WorksheetEntry* prev = nullptr;
WorksheetEntry* next = nullptr;
if (entry) {
if (pos.y() < entry->y() + entry->size().height()/2) {
prev = entry->previous();
next = entry;
} else if (pos.y() >= entry->y() + entry->size().height()/2) {
prev = entry;
next = entry->next();
}
} else {
WorksheetEntry* last = lastEntry();
if (last && pos.y() > last->y() + last->size().height()) {
prev = last;
next = nullptr;
}
}
if (prev || next) {
PlaceHolderEntry* oldPlaceHolder = m_placeholderEntry;
if (prev && prev->type() == PlaceHolderEntry::Type &&
(!prev->aboutToBeRemoved() || prev->stopRemoving())) {
m_placeholderEntry = qgraphicsitem_cast<PlaceHolderEntry*>(prev);
m_placeholderEntry->changeSize(m_dragEntry->size());
} else if (next && next->type() == PlaceHolderEntry::Type &&
(!next->aboutToBeRemoved() || next->stopRemoving())) {
m_placeholderEntry = qgraphicsitem_cast<PlaceHolderEntry*>(next);
m_placeholderEntry->changeSize(m_dragEntry->size());
} else {
m_placeholderEntry = new PlaceHolderEntry(this, QSizeF(0,0));
m_placeholderEntry->setPrevious(prev);
m_placeholderEntry->setNext(next);
if (prev)
prev->setNext(m_placeholderEntry);
else
setFirstEntry(m_placeholderEntry);
if (next)
next->setPrevious(m_placeholderEntry);
else
setLastEntry(m_placeholderEntry);
m_placeholderEntry->changeSize(m_dragEntry->size());
}
if (oldPlaceHolder && oldPlaceHolder != m_placeholderEntry)
oldPlaceHolder->startRemoving();
updateLayout();
}
const QPoint viewPos = worksheetView()->mapFromScene(pos);
const int viewHeight = worksheetView()->viewport()->height();
if ((viewPos.y() < 10 || viewPos.y() > viewHeight - 10) &&
!m_dragScrollTimer) {
m_dragScrollTimer = new QTimer(this);
m_dragScrollTimer->setSingleShot(true);
m_dragScrollTimer->setInterval(100);
connect(m_dragScrollTimer, SIGNAL(timeout()), this,
SLOT(updateDragScrollTimer()));
m_dragScrollTimer->start();
}
event->accept();
}
void Worksheet::dropEvent(QGraphicsSceneDragDropEvent* event)
{
if (!m_dragEntry)
QGraphicsScene::dropEvent(event);
event->accept();
}
void Worksheet::updateDragScrollTimer()
{
if (!m_dragScrollTimer)
return;
const QPoint viewPos = worksheetView()->viewCursorPos();
const QWidget* viewport = worksheetView()->viewport();
const int viewHeight = viewport->height();
if (!m_dragEntry || !(viewport->rect().contains(viewPos)) ||
(viewPos.y() >= 10 && viewPos.y() <= viewHeight - 10)) {
delete m_dragScrollTimer;
m_dragScrollTimer = nullptr;
return;
}
if (viewPos.y() < 10)
worksheetView()->scrollBy(-10*(10 - viewPos.y()));
else
worksheetView()->scrollBy(10*(viewHeight - viewPos.y()));
m_dragScrollTimer->start();
}
void Worksheet::updateEntryCursor(QGraphicsSceneMouseEvent* event)
{
// determine the worksheet entry near which the entry cursor will be shown
resetEntryCursor();
if (event->button() == Qt::LeftButton && !focusItem())
{
const qreal y = event->scenePos().y();
for (WorksheetEntry* entry = firstEntry(); entry; entry = entry->next())
{
if (entry == firstEntry() && y < entry->y() )
{
m_choosenCursorEntry = firstEntry();
break;
}
else if (entry->y() < y && (entry->next() && y < entry->next()->y()))
{
m_choosenCursorEntry = entry->next();
break;
}
else if (entry->y() < y && entry == lastEntry())
{
m_isCursorEntryAfterLastEntry = true;
break;
}
}
}
if (m_choosenCursorEntry || m_isCursorEntryAfterLastEntry)
drawEntryCursor();
}
void Worksheet::addEntryFromEntryCursor()
{
qDebug() << "Add new entry from entry cursor";
if (m_isCursorEntryAfterLastEntry)
insertCommandEntry(lastEntry());
else
insertCommandEntryBefore(m_choosenCursorEntry);
resetEntryCursor();
}
void Worksheet::animateEntryCursor()
{
if ((m_choosenCursorEntry || m_isCursorEntryAfterLastEntry) && m_entryCursorItem)
m_entryCursorItem->setVisible(!m_entryCursorItem->isVisible());
}
void Worksheet::resetEntryCursor()
{
m_choosenCursorEntry = nullptr;
m_isCursorEntryAfterLastEntry = false;
m_entryCursorItem->hide();
}
void Worksheet::drawEntryCursor()
{
if (m_entryCursorItem && (m_choosenCursorEntry || (m_isCursorEntryAfterLastEntry && lastEntry())))
{
qreal x;
qreal y;
if (m_isCursorEntryAfterLastEntry)
{
x = lastEntry()->x();
y = lastEntry()->y() + lastEntry()->size().height() - (EntryCursorWidth - 1);
}
else
{
x = m_choosenCursorEntry->x();
y = m_choosenCursorEntry->y();
}
m_entryCursorItem->setLine(x,y,x+EntryCursorLength,y);
m_entryCursorItem->show();
}
}
void Worksheet::setType(Worksheet::Type type)
{
m_type = type;
}
Worksheet::Type Worksheet::type() const
{
return m_type;
}
diff --git a/src/worksheet.h b/src/worksheet.h
index 97994614..f0cf4df7 100644
--- a/src/worksheet.h
+++ b/src/worksheet.h
@@ -1,334 +1,334 @@
/*
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
---
Copyright (C) 2009 Alexander Rieder <alexanderrieder@gmail.com>
Copyright (C) 2012 Martin Kuettler <martin.kuettler@gmail.com>
*/
#ifndef WORKSHEET_H
#define WORKSHEET_H
#include <QGraphicsScene>
#include <QDomElement>
#include <QGraphicsLinearLayout>
#include <QSyntaxHighlighter>
#include <QGraphicsRectItem>
#include <KZip>
#include <QMenu>
#include "worksheetview.h"
#include "lib/renderer.h"
#include "mathrender.h"
#include "worksheetcursor.h"
namespace Cantor {
class Backend;
class Session;
class Expression;
}
class WorksheetEntry;
class PlaceHolderEntry;
class WorksheetTextItem;
class QAction;
class QDrag;
class QPrinter;
class KActionCollection;
class KToggleAction;
class KFontAction;
class KFontSizeAction;
class Worksheet : public QGraphicsScene
{
Q_OBJECT
public:
enum Type {
CantorWorksheet,
JupyterNotebook
};
Worksheet(Cantor::Backend* backend, QWidget* parent);
~Worksheet() override;
Cantor::Session* session();
void loginToSession();
bool isRunning();
bool isReadOnly();
bool showExpressionIds();
bool animationsEnabled();
bool embeddedMathEnabled();
bool isPrinting();
WorksheetView* worksheetView();
void makeVisible(WorksheetEntry*);
void makeVisible(const WorksheetCursor&);
void setModified();
void startDrag(WorksheetEntry* entry, QDrag* drag);
void createActions(KActionCollection*);
QMenu* createContextMenu();
void populateMenu(QMenu* menu, QPointF pos);
Cantor::Renderer* renderer();
MathRenderer* mathRenderer();
bool isEmpty();
bool isLoadingFromFile();
WorksheetEntry* currentEntry();
WorksheetEntry* firstEntry();
WorksheetEntry* lastEntry();
WorksheetTextItem* currentTextItem();
WorksheetTextItem* lastFocusedTextItem();
WorksheetCursor worksheetCursor();
void setWorksheetCursor(const WorksheetCursor&);
// For WorksheetEntry::startDrag
void resetEntryCursor();
/**
* How it works:
* There are two information streams
* 1. WorksheetView -> Worksheet -> subelemenets (ex. entries) about view width
* View width used by some sub elements for better visual appearance (for example, entries with text often are fitted to width of view).
* 2. Subelements -> Worksheet
- * Sub elements notify Worksheet about their needed widths and worksheet, used this information, set propper scene size.
+ * Sub elements notify Worksheet about their needed widths and worksheet, used this information, set proper scene size.
*/
/// First information stream
void setViewSize(qreal w, qreal h, qreal s, bool forceUpdate = false);
/// Second information stream
void setRequestedWidth(QGraphicsObject* object, qreal width);
void removeRequestedWidth(QGraphicsObject* object);
bool isShortcut(const QKeySequence&);
void setType(Worksheet::Type type);
Worksheet::Type type() const;
// richtext
struct RichTextInfo {
bool bold;
bool italic;
bool underline;
bool strikeOut;
QString font;
qreal fontSize;
Qt::Alignment align;
};
public Q_SLOTS:
WorksheetEntry* appendCommandEntry();
void appendCommandEntry(const QString& text);
WorksheetEntry* appendTextEntry();
WorksheetEntry* appendMarkdownEntry();
WorksheetEntry* appendImageEntry();
WorksheetEntry* appendPageBreakEntry();
WorksheetEntry* appendLatexEntry();
WorksheetEntry* insertCommandEntry(WorksheetEntry* current = nullptr);
void insertCommandEntry(const QString& text);
WorksheetEntry* insertTextEntry(WorksheetEntry* current = nullptr);
WorksheetEntry* insertMarkdownEntry(WorksheetEntry* current = nullptr);
WorksheetEntry* insertImageEntry(WorksheetEntry* current = nullptr);
WorksheetEntry* insertPageBreakEntry(WorksheetEntry* current = nullptr);
WorksheetEntry* insertLatexEntry(WorksheetEntry* current = nullptr);
WorksheetEntry* insertCommandEntryBefore(WorksheetEntry* current = nullptr);
WorksheetEntry* insertTextEntryBefore(WorksheetEntry* current = nullptr);
WorksheetEntry* insertMarkdownEntryBefore(WorksheetEntry* current = nullptr);
WorksheetEntry* insertImageEntryBefore(WorksheetEntry* current = nullptr);
WorksheetEntry* insertPageBreakEntryBefore(WorksheetEntry* current = nullptr);
WorksheetEntry* insertLatexEntryBefore(WorksheetEntry* current = nullptr);
void updateLayout();
void updateEntrySize(WorksheetEntry*);
void print(QPrinter*);
void paste();
void focusEntry(WorksheetEntry*);
void evaluate();
void evaluateCurrentEntry();
void interrupt();
void interruptCurrentEntryEvaluation();
bool completionEnabled();
//void showCompletion();
void highlightItem(WorksheetTextItem*);
void rehighlight();
void enableHighlighting(bool);
void enableCompletion(bool);
void enableExpressionNumbering(bool);
void enableAnimations(bool);
void enableEmbeddedMath(bool);
QDomDocument toXML(KZip* archive = nullptr);
void save(const QString& filename);
void save(QIODevice*);
QByteArray saveToByteArray();
void savePlain(const QString& filename);
void saveLatex(const QString& filename);
bool load(QIODevice*);
void load(QByteArray* data);
bool load(const QString& filename);
void gotResult(Cantor::Expression* expr = nullptr);
void removeCurrentEntry();
void setFirstEntry(WorksheetEntry*);
void setLastEntry(WorksheetEntry*);
void invalidateFirstEntry();
void invalidateLastEntry();
void updateFocusedTextItem(WorksheetTextItem*);
void updateDragScrollTimer();
void registerShortcut(QAction*);
void updateShortcut();
// richtext
void setRichTextInformation(const Worksheet::RichTextInfo&);
void setAcceptRichText(bool b);
void setTextForegroundColor();
void setTextBackgroundColor();
void setTextBold(bool b);
void setTextItalic(bool b);
void setTextUnderline(bool b);
void setTextStrikeOut(bool b);
void setAlignLeft();
void setAlignRight();
void setAlignCenter();
void setAlignJustify();
void setFontFamily(const QString&);
void setFontSize(int size);
Q_SIGNALS:
void modified();
void loaded();
void showHelp(const QString&);
void updatePrompt();
void undoAvailable(bool);
void redoAvailable(bool);
void undo();
void redo();
void cutAvailable(bool);
void copyAvailable(bool);
void pasteAvailable(bool);
void cut();
void copy();
protected:
void contextMenuEvent(QGraphicsSceneContextMenuEvent*) override;
void mousePressEvent(QGraphicsSceneMouseEvent*) override;
void dragEnterEvent(QGraphicsSceneDragDropEvent*) override;
void dragLeaveEvent(QGraphicsSceneDragDropEvent*) override;
void dragMoveEvent(QGraphicsSceneDragDropEvent*) override;
void dropEvent(QGraphicsSceneDragDropEvent*) override;
void keyPressEvent(QKeyEvent*) override;
QJsonDocument toJupyterJson();
private Q_SLOTS:
void showCompletion();
//void checkEntriesForSanity();
WorksheetEntry* appendEntry(int type, bool focus = true);
WorksheetEntry* insertEntry(int type, WorksheetEntry* current = nullptr);
WorksheetEntry* insertEntryBefore(int type, WorksheetEntry* current = nullptr);
void animateEntryCursor();
private:
WorksheetEntry* entryAt(qreal x, qreal y);
WorksheetEntry* entryAt(QPointF p);
WorksheetEntry* entryAt(int row);
void updateEntryCursor(QGraphicsSceneMouseEvent*);
void addEntryFromEntryCursor();
void drawEntryCursor();
int entryCount();
bool loadCantorWorksheet(const KZip& archive);
bool loadJupyterNotebook(const QJsonDocument& doc);
void showInvalidNotebookSchemeError(QString additionalInfo = QString());
private:
static const double LeftMargin;
static const double RightMargin;
static const double TopMargin;
static const double EntryCursorLength;
static const double EntryCursorWidth;
Cantor::Session *m_session;
QSyntaxHighlighter* m_highlighter;
Cantor::Renderer m_epsRenderer;
MathRenderer m_mathRenderer;
WorksheetEntry* m_firstEntry;
WorksheetEntry* m_lastEntry;
WorksheetEntry* m_dragEntry;
WorksheetEntry* m_choosenCursorEntry;
bool m_isCursorEntryAfterLastEntry;
QTimer* m_cursorItemTimer;
QGraphicsLineItem* m_entryCursorItem;
PlaceHolderEntry* m_placeholderEntry;
WorksheetTextItem* m_lastFocusedTextItem;
QTimer* m_dragScrollTimer;
qreal m_viewWidth;
QMap<QGraphicsObject*, qreal> m_itemWidths;
qreal m_maxWidth;
QMap<QKeySequence, QAction*> m_shortcuts;
QList<QAction *> m_richTextActionList;
KToggleAction* m_boldAction;
KToggleAction* m_italicAction;
KToggleAction* m_underlineAction;
KToggleAction* m_strikeOutAction;
KFontAction* m_fontAction;
KFontSizeAction* m_fontSizeAction;
KToggleAction* m_alignLeftAction;
KToggleAction* m_alignCenterAction;
KToggleAction* m_alignRightAction;
KToggleAction* m_alignJustifyAction;
bool m_completionEnabled;
bool m_embeddedMathEnabled;
bool m_showExpressionIds;
bool m_animationsEnabled;
bool m_isPrinting;
bool m_isLoadingFromFile;
bool m_readOnly;
Type m_type = CantorWorksheet;
QString m_backendName;
QJsonObject* m_jupyterMetadata;
};
#endif // WORKSHEET_H