No OneTemporary

File Metadata

Created
Wed, May 15, 8:28 PM
diff --git a/src/backends/lua/luahelper.cpp b/src/backends/lua/luahelper.cpp
index 54ae4045..13195fc1 100644
--- a/src/backends/lua/luahelper.cpp
+++ b/src/backends/lua/luahelper.cpp
@@ -1,132 +1,133 @@
/*
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) 2014 Lucas Hermann Negri <lucashnegri@gmail.com>
*/
#include "luahelper.h"
#include "luakeywords.h"
#include <lua.hpp>
+#include <QRegularExpression>
#include <QString>
#include <QStringList>
QString luahelper_tostring(lua_State* L, int idx)
{
lua_getglobal(L, "tostring");
lua_pushvalue(L, idx - 1); // tostring is on the top now!
lua_call(L, 1, 1);
QString str = QString::fromUtf8(lua_tostring(L, -1));
lua_pop(L, 1);
return str;
}
QString luahelper_dostring(lua_State* L, const QString& str)
{
const QByteArray arr = str.toUtf8();
bool err = ( luaL_loadbuffer(L, arr.data(), arr.size(), nullptr) || lua_pcall (L, 0, LUA_MULTRET, 0) );
QString ret;
if(err)
{
ret = QString::fromUtf8(lua_tostring(L, -1));
lua_pop(L, 1);
}
return ret;
}
QString luahelper_getprinted(lua_State* L)
{
luaL_loadstring(L, "return table.concat(__cantor, '\\n')");
QString printed;
if(!lua_pcall(L, 0, 1, 0) )
printed = QString::fromUtf8(lua_tostring(L, -1));
lua_pop(L, 1);
luaL_loadstring(L, "__cantor = {}"); // survives a __cantor = nil!
if( lua_pcall(L, 0, 0, 0) ) lua_pop(L, 1);
return printed;
}
static void luahelper_getkeys(lua_State* L, QStringList& list, const QString& prefix = QLatin1String(""))
{
if(lua_type(L, -1) == LUA_TTABLE)
{
// ok, its a table, iterate the keys
lua_pushnil(L);
while (lua_next(L, -2) != 0)
{
if(lua_type(L, -2) == LUA_TSTRING)
{
QString key = QString::fromUtf8(lua_tostring(L, -2));
list << prefix + key;
}
lua_pop(L, 1);
}
}
}
QStringList luahelper_completion(lua_State* L, const QString& name)
{
int top = lua_gettop(L);
QStringList list;
- QStringList sections = name.split(QRegExp(QLatin1String("\\.|:")));
+ QStringList sections = name.split(QRegularExpression(QStringLiteral("\\.|:")));
QString table, prefix;
if(sections.size() == 1) // global table
{
list = LuaKeywords::instance()->keywords();
table = QLatin1String("_G");
}
else
{
if(sections.size() == 2)
{
table = sections.first(); // table.key
prefix = name.left(sections.first().length() + 1); // table.
}
}
if(!table.isEmpty())
{
// get keys from the table
QByteArray arr = table.toUtf8();
lua_getglobal(L, arr.data());
luahelper_getkeys(L, list, prefix);
// get keys from the metatable.__index
if( lua_getmetatable(L, -1) )
{
lua_getfield (L, -1, "__index");
luahelper_getkeys(L, list, prefix);
lua_pop(L, 2);
// pop metatable and metatable.__index
}
lua_pop(L, 1); // pop table
}
lua_settop(L, top);
return list;
}
diff --git a/src/cantor.cpp b/src/cantor.cpp
index d99e6482..d57c7666 100644
--- a/src/cantor.cpp
+++ b/src/cantor.cpp
@@ -1,720 +1,721 @@
/*
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 <QRegularExpression>
#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(QString());
m_tabWidget->setCurrentIndex(m_parts.size()-1);
}
if (!m_part->openUrl( url ))
closeTab(m_tabWidget->currentIndex());
if (m_recentProjectsAction)
m_recentProjectsAction->addUrl(url);
}
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'
}
/*!
* called when one of the "new backend" action or the "New" action are called
* adds a new worksheet with the backend assossiated with the called action
* or opens the "Choose Backend" dialog, respectively.
*/
void CantorShell::fileNew()
{
QAction* a = static_cast<QAction*>(sender());
const QString& backendName = a->data().toString();
if (!backendName.isEmpty())
{
addWorksheet(backendName);
return;
}
//"New" action was called -> open the "Choose Backend" dialog.
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
// 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()
{
bool hasBackend = false;
for (auto* b : Cantor::Backend::availableBackends())
{
if(b->isEnabled())
{
hasBackend = true;
break;
}
}
if(hasBackend) //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 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)
{
if (!backendName.isEmpty())
{
Cantor::Backend* backend = Cantor::Backend::getBackend(backendName);
if (!backend)
{
KMessageBox::error(this, i18n("Backend %1 is not installed", backendName), i18n("Cantor"));
return;
}
else
{
if (!backend->isEnabled())
{
KMessageBox::error(this, i18n("%1 backend installed, but inactive. Please check installation and Cantor settings", backendName), i18n("Cantor"));
return;
}
}
}
// 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(), 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
{
// 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$")));
+ name.remove(QRegularExpression(QStringLiteral("-.*\\.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/result.cpp b/src/lib/result.cpp
index 563ea00a..f1316105 100644
--- a/src/lib/result.cpp
+++ b/src/lib/result.cpp
@@ -1,93 +1,93 @@
/*
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 "result.h"
using namespace Cantor;
-#include <QRegExp>
#include <QUrl>
#include <QJsonObject>
+#include <QRegularExpression>
class Cantor::ResultPrivate
{
public:
~ResultPrivate()
{
if (jupyterMetadata)
delete jupyterMetadata;
}
QJsonObject* jupyterMetadata{nullptr};
int executionIndex{-1};
};
Result::Result() : d(new ResultPrivate)
{
}
Result::~Result()
{
delete d;
}
QUrl Result::url()
{
return QUrl();
}
QString Result::toLatex()
{
QString html=toHtml();
//replace linebreaks
- html.replace(QRegExp(QLatin1String("<br/>[\n]")), QStringLiteral("\n"));
+ html.replace(QRegularExpression(QStringLiteral("<br/>[\n]")), QStringLiteral("\n"));
//remove all the unknown tags
- html.remove( QRegExp( QLatin1String("<[a-zA-Z\\/][^>]*>") ) );
+ html.remove(QRegularExpression(QStringLiteral("<[a-zA-Z\\/][^>]*>") ));
return QStringLiteral("\\begin{verbatim} %1 \\end{verbatim}").arg(html);
}
void Result::saveAdditionalData(KZip* archive)
{
Q_UNUSED(archive)
//Do nothing
}
QJsonObject Cantor::Result::jupyterMetadata() const
{
return d->jupyterMetadata ? *d->jupyterMetadata : QJsonObject();
}
void Cantor::Result::setJupyterMetadata(QJsonObject metadata)
{
if (!d->jupyterMetadata)
d->jupyterMetadata = new QJsonObject();
*d->jupyterMetadata = metadata;
}
int Cantor::Result::executionIndex() const
{
return d->executionIndex;
}
void Cantor::Result::setExecutionIndex(int index)
{
d->executionIndex = index;
}
diff --git a/src/lib/test/backendtest.cpp b/src/lib/test/backendtest.cpp
index 02d446f2..19e13094 100644
--- a/src/lib/test/backendtest.cpp
+++ b/src/lib/test/backendtest.cpp
@@ -1,121 +1,121 @@
/*
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 "backendtest.h"
#include "backend.h"
#include "session.h"
#include "../../config-cantor.h"
#include <KLocalizedString>
#include <QSignalSpy>
#include <QDebug>
void BackendTest::createSession()
{
// Add our custom plugins path, where we install our plugins, if it isn't default path
const QString& path = QString::fromLocal8Bit(PATH_TO_CANTOR_PLUGINS);
qDebug() << "Adding additional application library path" << path;
if (!QCoreApplication::libraryPaths().contains(path))
QCoreApplication::addLibraryPath(path);
Cantor::Backend* b=Cantor::Backend::getBackend( backendName() );
if(!b || !b->requirementsFullfilled() )
{
m_session = nullptr;
return;
}
m_session=b->createSession();
QSignalSpy spy(m_session, SIGNAL(loginDone()) );
m_session->login();
if(spy.isEmpty())
waitForSignal(m_session, SIGNAL(loginDone()) );
QVERIFY(!spy.isEmpty());
}
Cantor::Expression* BackendTest::evalExp(const QString& exp )
{
Cantor::Expression* e=m_session->evaluateExpression(exp);
if(e->status()==Cantor::Expression::Queued)
{
waitForSignal( e, SIGNAL(statusChanged(Cantor::Expression::Status)) );
}
if (e->status()==Cantor::Expression::Computing)
{
waitForSignal( e, SIGNAL(statusChanged(Cantor::Expression::Status)) );
}
return e;
}
QString BackendTest::cleanOutput(const QString& out)
{
QString cleaned=out;
cleaned.replace( QLatin1String("&nbsp;"),QLatin1String(" ") );
cleaned.remove( QLatin1String("<br/>") );
cleaned.replace( QChar::ParagraphSeparator, QLatin1Char('\n') );
- cleaned.replace( QRegExp( QLatin1String("\\n\\n") ), QLatin1String("\n") );
- cleaned.replace( QRegExp( QLatin1String("\\n\\s*") ), QLatin1String("\n") );
+ cleaned.replace( QRegularExpression( QStringLiteral("\\n{2}") ), QStringLiteral("\n") );
+ cleaned.replace( QRegularExpression( QStringLiteral("\\n\\s*") ), QStringLiteral("\n") );
return cleaned.trimmed();
}
void BackendTest::initTestCase()
{
QCoreApplication::setApplicationName(QLatin1String("cantor"));
createSession();
if (!m_session)
{
QString reason = i18n("This test requires a functioning %1 backend", backendName() );
QSKIP( reason.toStdString().c_str(), SkipAll );
}
}
void BackendTest::cleanupTestCase()
{
if (m_session)
{
m_session->logout();
}
}
Cantor::Session* BackendTest::session()
{
return m_session;
}
void BackendTest::waitForSignal(QObject* sender, const char* signal)
{
QTimer timeout( this );
timeout.setSingleShot( true );
QEventLoop loop;
connect( sender, signal, &loop, SLOT(quit()) );
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start( 25000 );
loop.exec();
}
diff --git a/src/loadedexpression.cpp b/src/loadedexpression.cpp
index 4a19de87..49988dbd 100644
--- a/src/loadedexpression.cpp
+++ b/src/loadedexpression.cpp
@@ -1,333 +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>
*/
#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 <QRegularExpression>
#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")));
+ traceback.remove(QRegularExpression(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 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 bc04af74..8f150f05 100644
--- a/src/markdownentry.cpp
+++ b/src/markdownentry.cpp
@@ -1,742 +1,744 @@
/*
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 <QKeyEvent>
+#include <QRegularExpression>
#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 if (worksheet()->embeddedMathEnabled())
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;
const qreal margin = worksheet()->isPrinting() ? 0 : RightMargin;
m_textItem->setGeometry(0, 0, w - margin);
setSize(QSizeF(m_textItem->width() + margin, 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;
}
}
}
else if (event->type() == QEvent::KeyPress)
{
auto* key_event = static_cast<QKeyEvent*>(event);
if (key_event->matches(QKeySequence::Cancel))
{
setRenderedHtml(html);
for (auto iter = foundMath.begin(); iter != foundMath.end(); iter++)
iter->second = false;
rendered = true;
markUpMath();
if (worksheet()->embeddedMathEnabled())
renderMath();
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));
// 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);
+ QTextCursor prevSymCursor = m_textItem->document()->find(QRegularExpression(QStringLiteral("[^\\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);
+ QTextCursor nextSymCursor = m_textItem->document()->find(QRegularExpression(QStringLiteral("[^\\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(" "));
+ searchText.replace(QRegularExpression(QStringLiteral("\\s+")), QStringLiteral(" "));
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();
}
QString MarkdownEntry::plainText() const
{
return m_textItem->toPlainText();
}
diff --git a/src/textentry.cpp b/src/textentry.cpp
index f4a0681e..fadb12e5 100644
--- a/src/textentry.cpp
+++ b/src/textentry.cpp
@@ -1,598 +1,599 @@
/*
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 <QRegularExpression>
#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 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 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")));
+ entryData.remove(QRegularExpression(QStringLiteral("<!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;
const qreal margin = worksheet()->isPrinting() ? 0 : RightMargin;
m_textItem->setGeometry(0, 0, w - margin);
setSize(QSizeF(0 + m_textItem->width() + margin, 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);
}
QString TextEntry::text() const
{
return m_textItem->toPlainText();
}
diff --git a/src/worksheet.cpp b/src/worksheet.cpp
index 631b3dca..39165252 100644
--- a/src/worksheet.cpp
+++ b/src/worksheet.cpp
@@ -1,2511 +1,2512 @@
/*
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 <QRegularExpression>
#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, bool useDeafultWorksheetParameters)
: QGraphicsScene(parent)
{
m_session = nullptr;
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_useDefaultWorksheetParameters = useDeafultWorksheetParameters;
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_jupyterMetadata = nullptr;
if (backend)
initSession(backend);
}
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;
//disconnect from everything, no need to react on session status changes
//in the logout() when deleting the worksheet
disconnect(m_session, nullptr, nullptr, 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();
}
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()
{
// Entry cursor activate
if (m_choosenCursorEntry || m_isCursorEntryAfterLastEntry)
return nullptr;
QGraphicsItem* item = focusItem();
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"));
//There is "Python" only now, replace "Python 3" by "Python"
if (m_backendName == QLatin1String("Python 3"))
m_backendName = QLatin1String("Python");
//"Python 2" in older projects not supported anymore, switch to Python (=Python3)
if (m_backendName == QLatin1String("Python 2"))
{
QApplication::restoreOverrideCursor();
KMessageBox::information(worksheetView(),
i18n("This worksheet was created using Python2 which is not supported anymore. Python3 will be used."),
i18n("Python2 not supported anymore"));
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
m_backendName = QLatin1String("Python");
}
auto* 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 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)
initSession(b);
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;
}
void Worksheet::initSession(Cantor::Backend* backend)
{
m_session = backend->createSession();
if (m_useDefaultWorksheetParameters)
{
enableHighlighting(Settings::self()->highlightDefault());
enableCompletion(Settings::self()->completionDefault());
enableExpressionNumbering(Settings::self()->expressionNumberingDefault());
enableAnimations(Settings::self()->animationDefault());
enableEmbeddedMath(Settings::self()->embeddedMathDefault());
}
}
bool Worksheet::loadJupyterNotebook(const QJsonDocument& doc)
{
m_type = Type::JupyterNotebook;
int nbformatMajor, nbformatMinor;
if (!Cantor::JupyterUtils::isJupyterNotebook(doc))
{
// 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);
//There is "Python" only now, replace "python3" by "Python"
if (m_backendName == QLatin1String("python3"))
m_backendName = QLatin1String("Python");
//"python 2" in older projects not supported anymore, switch to Python (=Python3)
if (m_backendName == QLatin1String("python2"))
{
QApplication::restoreOverrideCursor();
KMessageBox::information(worksheetView(),
i18n("This notebook was created using Python2 which is not supported anymore. Python3 will be used."),
i18n("Python2 not supported anymore"));
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
m_backendName = QLatin1String("Python");
}
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)
initSession(backend);
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>"));
+ help.replace(QRegularExpression(QStringLiteral("\\\\code\\{([^\\}]*)\\}")), QStringLiteral("<b>\\1</b>"));
+ help.replace(QRegularExpression(QStringLiteral("\\$([^\\$])\\$")), QStringLiteral("<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)
{
// Two menu: for particular entry and for selection (multiple entry)
if (m_selectedEntries.isEmpty())
{
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* convertTo = new QMenu(menu);
QMenu* insert = new QMenu(menu);
QMenu* insertBefore = new QMenu(menu);
convertTo->addAction(QIcon::fromTheme(QLatin1String("run-build")), i18n("Command Entry"), entry, &WorksheetEntry::convertToCommandEntry);
convertTo->addAction(QIcon::fromTheme(QLatin1String("draw-text")), i18n("Text Entry"), entry, &WorksheetEntry::convertToTextEntry);
#ifdef Discount_FOUND
convertTo->addAction(QIcon::fromTheme(QLatin1String("text-x-markdown")), i18n("Markdown Entry"), entry, &WorksheetEntry::convertToMarkdownEntry);
#endif
#ifdef WITH_EPS
convertTo->addAction(QIcon::fromTheme(QLatin1String("text-x-tex")), i18n("LaTeX Entry"), entry, &WorksheetEntry::convertToLatexEntry);
#endif
convertTo->addAction(QIcon::fromTheme(QLatin1String("image-x-generic")), i18n("Image"), entry, &WorksheetEntry::convertToImageEntry);
convertTo->addAction(QIcon::fromTheme(QLatin1String("go-next-view-page")), i18n("Page Break"), entry, &WorksheetEntry::converToPageBreakEntry);
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()));
convertTo->setTitle(i18n("Convert Entry To"));
convertTo->setIcon(QIcon::fromTheme(QLatin1String("gtk-convert")));
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(convertTo);
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()));
}
}
else
{
menu->clear();
menu->addAction(QIcon::fromTheme(QLatin1String("go-up")), i18n("Move Entries Up"), this, SLOT(selectionMoveUp()), 0);
menu->addAction(QIcon::fromTheme(QLatin1String("go-down")), i18n("Move Entries Down"), this, SLOT(selectionMoveDown()), 0);
menu->addAction(QIcon::fromTheme(QLatin1String("media-playback-start")), i18n("Evaluate Entries"), this, SLOT(selectionEvaluate()), 0);
menu->addAction(QIcon::fromTheme(QLatin1String("edit-delete")), i18n("Remove Entries"), this, SLOT(selectionRemove()), 0);
}
}
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)
{
/*
if (event->button() == Qt::LeftButton && !focusItem() && lastEntry() &&
event->scenePos().y() > lastEntry()->y() + lastEntry()->size().height())
lastEntry()->focusEntry(WorksheetTextItem::BottomRight);
*/
if (!m_readOnly && event->buttons() & Qt::LeftButton)
{
WorksheetEntry* selectedEntry = entryAt(event->scenePos());
if (event->modifiers() & Qt::ControlModifier)
{
clearFocus();
resetEntryCursor();
if (selectedEntry)
{
selectedEntry->setCellSelected(!selectedEntry->isCellSelected());
selectedEntry->update();
WorksheetEntry* lastSelectedEntry = m_circularFocusBuffer.size() > 0 ? m_circularFocusBuffer.last() : nullptr;
if (lastSelectedEntry)
{
lastSelectedEntry->setCellSelected(!lastSelectedEntry->isCellSelected());
lastSelectedEntry->update();
m_circularFocusBuffer.clear();
}
for (WorksheetEntry* entry : {selectedEntry, lastSelectedEntry})
if (entry)
{
if (entry->isCellSelected())
m_selectedEntries.append(entry);
else if (!entry->isCellSelected())
m_selectedEntries.removeOne(entry);
}
}
}
else
{
for (WorksheetEntry* entry : m_selectedEntries)
{
if(isValidEntry(entry))
{
entry->setCellSelected(false);
entry->update();
}
}
m_selectedEntries.clear();
if (selectedEntry)
notifyEntryFocus(selectedEntry);
updateEntryCursor(event);
}
}
QGraphicsScene::mousePressEvent(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;
}
void Worksheet::changeEntryType(WorksheetEntry* target, int newType)
{
if (target && target->type() != newType)
{
bool animation_state = m_animationsEnabled;
m_animationsEnabled = false;
QString content;
switch(target->type())
{
case CommandEntry::Type:
content = static_cast<CommandEntry*>(target)->command();
break;
case MarkdownEntry::Type:
content = static_cast<MarkdownEntry*>(target)->plainText();
break;
case TextEntry::Type:
content = static_cast<TextEntry*>(target)->text();
break;
case LatexEntry::Type:
content = static_cast<LatexEntry*>(target)->plain();
}
WorksheetEntry* newEntry = WorksheetEntry::create(newType, this);
newEntry->setContent(content);
if (newEntry)
{
WorksheetEntry* tmp = target;
newEntry->setPrevious(tmp->previous());
newEntry->setNext(tmp->next());
tmp->setPrevious(nullptr);
tmp->setNext(nullptr);
tmp->clearFocus();
tmp->forceRemove();
if (newEntry->previous())
newEntry->previous()->setNext(newEntry);
else
setFirstEntry(newEntry);
if (newEntry->next())
newEntry->next()->setPrevious(newEntry);
else
setLastEntry(newEntry);
updateLayout();
makeVisible(newEntry);
focusEntry(newEntry);
setModified();
newEntry->focusEntry();
}
m_animationsEnabled = animation_state;
}
}
bool Worksheet::isValidEntry(WorksheetEntry* entry)
{
for (WorksheetEntry* iter = firstEntry(); iter; iter = iter->next())
if (entry == iter)
return true;
return false;
}
void Worksheet::selectionRemove()
{
for (WorksheetEntry* entry : m_selectedEntries)
if (isValidEntry(entry))
entry->startRemoving();
m_selectedEntries.clear();
}
void Worksheet::selectionEvaluate()
{
// run entries in worksheet order: from top to down
for (WorksheetEntry* entry = firstEntry(); entry; entry = entry->next())
if (m_selectedEntries.indexOf(entry) != -1)
entry->evaluate();
}
void Worksheet::selectionMoveUp()
{
// movement up should have an order from top to down.
for(WorksheetEntry* entry = firstEntry(); entry; entry = entry->next())
if(m_selectedEntries.indexOf(entry) != -1)
if (entry->previous() && m_selectedEntries.indexOf(entry->previous()) == -1)
entry->moveToPrevious(false);
updateLayout();
}
void Worksheet::selectionMoveDown()
{
// movement up should have an order from down to top.
for(WorksheetEntry* entry = lastEntry(); entry; entry = entry->previous())
if(m_selectedEntries.indexOf(entry) != -1)
if (entry->next() && m_selectedEntries.indexOf(entry->next()) == -1)
entry->moveToNext(false);
updateLayout();
}
void Worksheet::notifyEntryFocus(WorksheetEntry* entry)
{
if (entry)
{
m_circularFocusBuffer.enqueue(entry);
if (m_circularFocusBuffer.size() > 2)
m_circularFocusBuffer.dequeue();
}
else
m_circularFocusBuffer.clear();
}