commandentry.cpp 50.5 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
    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.

    ---
18
    Copyright (C) 2009 Alexander Rieder <alexanderrieder@gmail.com>
19
    Copyright (C) 2012 Martin Kuettler <martin.kuettler@gmail.com>
20
    Copyright (C) 2018-2019 Alexander Semke <alexander.semke@web.de>
21
22
23
 */

#include "commandentry.h"
24
#include "resultitem.h"
25
#include "loadedexpression.h"
26
#include "lib/jupyterutils.h"
27
28
#include "lib/result.h"
#include "lib/helpresult.h"
29
30
#include "lib/epsresult.h"
#include "lib/latexresult.h"
31
32
33
34
#include "lib/completionobject.h"
#include "lib/syntaxhelpobject.h"
#include "lib/session.h"

35
#include <QGuiApplication>
36
#include <QDebug>
37
#include <QDesktopWidget>
38
#include <QFontDialog>
Yuri Chornoivan's avatar
Yuri Chornoivan committed
39
#include <QScreen>
40
#include <QTimer>
41
#include <QToolTip>
42
#include <QPropertyAnimation>
43
#include <QJsonArray>
44
#include <QJsonObject>
45

46
#include <KLocalizedString>
47
48
#include <KColorScheme>

49
50
51
const QString CommandEntry::Prompt     = QLatin1String(">>> ");
const QString CommandEntry::MidPrompt  = QLatin1String(">>  ");
const QString CommandEntry::HidePrompt = QLatin1String(">   ");
Martin Küttler's avatar
Martin Küttler committed
52
53
const double CommandEntry::HorizontalSpacing = 4;
const double CommandEntry::VerticalSpacing = 4;
54

55
56
57
58
59
60
61
62
63
64
65
66
67
static const int colorsCount = 26;
static QColor colors[colorsCount] = {QColor(255,255,255), QColor(0,0,0),
							QColor(192,0,0), QColor(255,0,0), QColor(255,192,192), //red
							QColor(0,192,0), QColor(0,255,0), QColor(192,255,192), //green
							QColor(0,0,192), QColor(0,0,255), QColor(192,192,255), //blue
							QColor(192,192,0), QColor(255,255,0), QColor(255,255,192), //yellow
							QColor(0,192,192), QColor(0,255,255), QColor(192,255,255), //cyan
							QColor(192,0,192), QColor(255,0,255), QColor(255,192,255), //magenta
							QColor(192,88,0), QColor(255,128,0), QColor(255,168,88), //orange
							QColor(128,128,128), QColor(160,160,160), QColor(195,195,195) //grey
							};


68
69
70
CommandEntry::CommandEntry(Worksheet* worksheet) : WorksheetEntry(worksheet),
    m_promptItem(new WorksheetTextItem(this, Qt::NoTextInteraction)),
    m_commandItem(new WorksheetTextItem(this, Qt::TextEditorInteraction)),
71
    m_resultsCollapsed(false),
72
73
74
    m_errorItem(nullptr),
    m_expression(nullptr),
    m_completionObject(nullptr),
75
    m_syntaxHelpObject(nullptr),
76
    m_evaluationOption(DoNothing),
77
    m_menusInitialized(false),
78
79
    m_textColorCustom(false),
    m_backgroundColorCustom(false),
80
    m_backgroundColorActionGroup(nullptr),
81
82
    m_backgroundColorMenu(nullptr),
    m_textColorActionGroup(nullptr),
83
    m_textColorMenu(nullptr),
84
85
    m_fontMenu(nullptr),
    m_isExecutionEnabled(true)
86
{
87
    m_promptItem->setPlainText(Prompt);
88
    m_promptItem->setItemDragable(true);
Martin Küttler's avatar
Martin Küttler committed
89
    m_commandItem->enableCompletion(true);
90

91
92
93
    KColorScheme scheme = KColorScheme(QPalette::Normal, KColorScheme::View);
    m_commandItem->setBackgroundColor(scheme.background(KColorScheme::AlternateBackground).color());

94
    m_promptItemAnimation = new QPropertyAnimation(m_promptItem, "opacity", this);
95
96
97
98
99
    m_promptItemAnimation->setDuration(600);
    m_promptItemAnimation->setStartValue(1);
    m_promptItemAnimation->setKeyValueAt(0.5, 0);
    m_promptItemAnimation->setEndValue(1);
    connect(m_promptItemAnimation, &QPropertyAnimation::finished, this, &CommandEntry::animatePromptItem);
100

101
102
103
    m_promptItem->setDoubleClickBehaviour(WorksheetTextItem::DoubleClickEventBehaviour::Simple);
    connect(m_promptItem, &WorksheetTextItem::doubleClick, this, &CommandEntry::changeResultCollapsingAction);

Nikita Sirgienko's avatar
Nikita Sirgienko committed
104
105
    connect(&m_controlElement, &WorksheetControlItem::doubleClick, this, &CommandEntry::changeResultCollapsingAction);

106
107
108
    connect(m_commandItem, &WorksheetTextItem::tabPressed, this, &CommandEntry::showCompletion);
    connect(m_commandItem, &WorksheetTextItem::backtabPressed, this, &CommandEntry::selectPreviousCompletion);
    connect(m_commandItem, &WorksheetTextItem::applyCompletion, this, &CommandEntry::applySelectedCompletion);
109
    connect(m_commandItem, &WorksheetTextItem::execute, this, [=]() { evaluate();} );
110
111
    connect(m_commandItem, &WorksheetTextItem::moveToPrevious, this, &CommandEntry::moveToPreviousItem);
    connect(m_commandItem, &WorksheetTextItem::moveToNext, this, &CommandEntry::moveToNextItem);
112
    connect(m_commandItem, &WorksheetTextItem::receivedFocus, worksheet, &Worksheet::highlightItem);
113
    connect(m_promptItem, &WorksheetTextItem::drag, this, &CommandEntry::startDrag);
114
    connect(worksheet, &Worksheet::updatePrompt, this, [=]() { updatePrompt();} );
115
116

    m_defaultDefaultTextColor = m_commandItem->defaultTextColor();
117
118
119
120
}

CommandEntry::~CommandEntry()
{
121
    if (m_completionBox)
Martin Küttler's avatar
Martin Küttler committed
122
        m_completionBox->deleteLater();
123
124
125
126
127
128
129

    if (m_menusInitialized)
    {
        m_backgroundColorMenu->deleteLater();
        m_textColorMenu->deleteLater();
        m_fontMenu->deleteLater();
    }
130
131
}

132
int CommandEntry::type() const
133
{
134
    return Type;
135
136
}

137
138
139
140
141
142
143
144
145
146
147
148
149
void CommandEntry::initMenus() {
    //background color
	const QString colorNames[colorsCount] = {i18n("White"), i18n("Black"),
							i18n("Dark Red"), i18n("Red"), i18n("Light Red"),
							i18n("Dark Green"), i18n("Green"), i18n("Light Green"),
							i18n("Dark Blue"), i18n("Blue"), i18n("Light Blue"),
							i18n("Dark Yellow"), i18n("Yellow"), i18n("Light Yellow"),
							i18n("Dark Cyan"), i18n("Cyan"), i18n("Light Cyan"),
							i18n("Dark Magenta"), i18n("Magenta"), i18n("Light Magenta"),
							i18n("Dark Orange"), i18n("Orange"), i18n("Light Orange"),
							i18n("Dark Grey"), i18n("Grey"), i18n("Light Grey")
							};

150
    //background color
151
    m_backgroundColorActionGroup = new QActionGroup(this);
152
153
    m_backgroundColorActionGroup->setExclusive(true);
    connect(m_backgroundColorActionGroup, &QActionGroup::triggered, this, &CommandEntry::backgroundColorChanged);
154
155

    m_backgroundColorMenu = new QMenu(i18n("Background Color"));
156
157
    m_backgroundColorMenu->setIcon(QIcon::fromTheme(QLatin1String("format-fill-color")));

158
159
    QPixmap pix(16,16);
    QPainter p(&pix);
160
161
162
163
164
165
166
167
168
169
170
171
172

    // Create default action
    KColorScheme scheme = KColorScheme(QPalette::Normal, KColorScheme::View);
    p.fillRect(pix.rect(), scheme.background(KColorScheme::AlternateBackground).color());
    QAction* action = new QAction(QIcon(pix), i18n("Default"), m_backgroundColorActionGroup);
    action->setCheckable(true);
    m_backgroundColorMenu->addAction(action);
    if (!m_backgroundColorCustom)
        action->setChecked(true);

    for (int i=0; i<colorsCount; ++i) {
        p.fillRect(pix.rect(), colors[i]);
        action = new QAction(QIcon(pix), colorNames[i], m_backgroundColorActionGroup);
173
174
        action->setCheckable(true);
        m_backgroundColorMenu->addAction(action);
175

176
177
        const QColor& backgroundColor = (m_isExecutionEnabled ? m_commandItem->backgroundColor() : m_activeExecutionBackgroundColor);
        if (m_backgroundColorCustom && backgroundColor == colors[i])
178
179
            action->setChecked(true);
    }
180

181
    //text color
182
    m_textColorActionGroup = new QActionGroup(this);
183
184
    m_textColorActionGroup->setExclusive(true);
    connect(m_textColorActionGroup, &QActionGroup::triggered, this, &CommandEntry::textColorChanged);
185

186
    m_textColorMenu = new QMenu(i18n("Text Color"));
187
    m_textColorMenu->setIcon(QIcon::fromTheme(QLatin1String("format-text-color")));
188

189
190
191
192
193
194
195
196
    // Create default action
    p.fillRect(pix.rect(), m_defaultDefaultTextColor);
    action = new QAction(QIcon(pix), i18n("Default"), m_textColorActionGroup);
    action->setCheckable(true);
    m_textColorMenu->addAction(action);
    if (!m_textColorCustom)
        action->setChecked(true);

197
    for (int i=0; i<colorsCount; ++i) {
198
        QAction* action;
199
200
        p.fillRect(pix.rect(), colors[i]);
        action = new QAction(QIcon(pix), colorNames[i], m_textColorActionGroup);
201
202
        action->setCheckable(true);
        m_textColorMenu->addAction(action);
203

204
205
        const QColor& textColor = (m_isExecutionEnabled ? m_commandItem->defaultTextColor() : m_activeExecutionTextColor);
        if (m_textColorCustom && textColor == colors[i])
206
207
            action->setChecked(true);
    }
208

209
	//font
210
	QFont font = m_commandItem->font();
211
212
213
	m_fontMenu = new QMenu(i18n("Font"));
    m_fontMenu->setIcon(QIcon::fromTheme(QLatin1String("preferences-desktop-font")));

214
    action = new QAction(QIcon::fromTheme(QLatin1String("format-text-bold")), i18n("Bold"));
215
216
217
    action->setCheckable(true);
    connect(action, &QAction::triggered, this, &CommandEntry::fontBoldTriggered);
    m_fontMenu->addAction(action);
218
219
    if (font.bold())
        action->setChecked(true);
220
221
222
223
224

    action = new QAction(QIcon::fromTheme(QLatin1String("format-text-italic")), i18n("Italic"));
    action->setCheckable(true);
    connect(action, &QAction::triggered, this, &CommandEntry::fontItalicTriggered);
    m_fontMenu->addAction(action);
225
226
    if (font.italic())
        action->setChecked(true);
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
    m_fontMenu->addSeparator();

    action = new QAction(QIcon::fromTheme(QLatin1String("format-font-size-less")), i18n("Increase Size"));
    connect(action, &QAction::triggered, this, &CommandEntry::fontIncreaseTriggered);
    m_fontMenu->addAction(action);

    action = new QAction(QIcon::fromTheme(QLatin1String("format-font-size-more")), i18n("Decrease Size"));
    connect(action, &QAction::triggered, this, &CommandEntry::fontDecreaseTriggered);
    m_fontMenu->addAction(action);
    m_fontMenu->addSeparator();

    action = new QAction(QIcon::fromTheme(QLatin1String("preferences-desktop-font")), i18n("Select"));
    connect(action, &QAction::triggered, this, &CommandEntry::fontSelectTriggered);
    m_fontMenu->addAction(action);

242
    action = new QAction(QIcon::fromTheme(QLatin1String("preferences-desktop-font")), i18n("Reset to Default"));
243
244
245
    connect(action, &QAction::triggered, this, &CommandEntry::resetFontTriggered);
    m_fontMenu->addAction(action);

246
247
248
249
    m_menusInitialized = true;
}

void CommandEntry::backgroundColorChanged(QAction* action) {
250
251
252
    int index = m_backgroundColorActionGroup->actions().indexOf(action);
    if (index == -1 || index>=colorsCount)
        index = 0;
253

254
    QColor color;
255
256
257
    if (index == 0)
    {
        KColorScheme scheme = KColorScheme(QPalette::Normal, KColorScheme::View);
258
        color = scheme.background(KColorScheme::AlternateBackground).color();
259
260
    }
    else
261
262
263
264
265
266
        color = colors[index-1];

    if (m_isExecutionEnabled)
        m_commandItem->setBackgroundColor(color);
    else
        m_activeExecutionBackgroundColor = color;
267
268
}

269
void CommandEntry::textColorChanged(QAction* action) {
270
271
272
    int index = m_textColorActionGroup->actions().indexOf(action);
    if (index == -1 || index>=colorsCount)
        index = 0;
273

274
    QColor color;
275
276
    if (index == 0)
    {
277
        color = m_defaultDefaultTextColor;
278
279
    }
    else
280
281
282
283
284
285
        color = colors[index-1];

    if (m_isExecutionEnabled)
        m_commandItem->setDefaultTextColor(color);
    else
        m_activeExecutionTextColor = color;
286
287
}

288
289
290
291
292
293
294
295
void CommandEntry::fontBoldTriggered()
{
    QAction* action = static_cast<QAction*>(QObject::sender());
    QFont font = m_commandItem->font();
    font.setBold(action->isChecked());
    m_commandItem->setFont(font);
}

296
297
298
299
300
void CommandEntry::resetFontTriggered()
{
    m_commandItem->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
}

301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
void CommandEntry::fontItalicTriggered()
{
    QAction* action = static_cast<QAction*>(QObject::sender());
    QFont font = m_commandItem->font();
    font.setItalic(action->isChecked());
    m_commandItem->setFont(font);
}

void CommandEntry::fontIncreaseTriggered()
{
    QFont font = m_commandItem->font();
    const int currentSize = font.pointSize();
    QFontDatabase fdb;
    QList<int> sizes = fdb.pointSizes(font.family(), font.styleName());

    for (int i = 0; i < sizes.size(); ++i)
    {
        const int size = sizes.at(i);
        if (currentSize == size)
        {
            if (i + 1 < sizes.size())
            {
                font.setPointSize(sizes.at(i+1));
                m_commandItem->setFont(font);
            }

            break;
        }
    }
}

void CommandEntry::fontDecreaseTriggered()
{
    QFont font = m_commandItem->font();
    const int currentSize = font.pointSize();
    QFontDatabase fdb;
    QList<int> sizes = fdb.pointSizes(font.family(), font.styleName());

    for (int i = 0; i < sizes.size(); ++i)
    {
        const int size = sizes.at(i);
        if (currentSize == size)
        {
            if (i - 1 >= 0)
            {
                font.setPointSize(sizes.at(i-1));
                m_commandItem->setFont(font);
            }

            break;
        }
    }
}

void CommandEntry::fontSelectTriggered()
{
    bool ok;
    QFont font = QFontDialog::getFont(&ok, m_commandItem->font(), nullptr);

    if (ok)
        m_commandItem->setFont(font);
}

Alexander Semke's avatar
Alexander Semke committed
364
void CommandEntry::populateMenu(QMenu* menu, QPointF pos)
Martin Küttler's avatar
Martin Küttler committed
365
{
366
367
368
    if (!m_menusInitialized)
        initMenus();

369
370
371
372
373
374
375
    if (!m_resultItems.isEmpty()) {
        if (m_resultsCollapsed)
            menu->addAction(i18n("Show Results"), this, &CommandEntry::expandResults);
        else
            menu->addAction(i18n("Hide Results"), this, &CommandEntry::collapseResults);
    }

376
377
378
379
380
    if (m_isExecutionEnabled)
        menu->addAction(i18n("Exclude from Execution"), this, &CommandEntry::excludeFromExecution);
    else
        menu->addAction(i18n("Add to Execution"), this, &CommandEntry::addToExecution);

381
    menu->addMenu(m_backgroundColorMenu);
382
    menu->addMenu(m_textColorMenu);
383
    menu->addMenu(m_fontMenu);
384
    menu->addSeparator();
385
    WorksheetEntry::populateMenu(menu, pos);
Martin Küttler's avatar
Martin Küttler committed
386
387
}

388
389
390
391
392
void CommandEntry::moveToNextItem(int pos, qreal x)
{
    WorksheetTextItem* item = qobject_cast<WorksheetTextItem*>(sender());

    if (!item)
Martin Küttler's avatar
Martin Küttler committed
393
        return;
394

Martin Küttler's avatar
Martin Küttler committed
395
    if (item == m_commandItem) {
Martin Küttler's avatar
Martin Küttler committed
396
397
398
399
400
        if (m_informationItems.isEmpty() ||
            !currentInformationItem()->isEditable())
            moveToNextEntry(pos, x);
        else
            currentInformationItem()->setFocusAt(pos, x);
401
    } else if (item == currentInformationItem()) {
Martin Küttler's avatar
Martin Küttler committed
402
        moveToNextEntry(pos, x);
403
404
405
406
407
408
409
410
    }
}

void CommandEntry::moveToPreviousItem(int pos, qreal x)
{
    WorksheetTextItem* item = qobject_cast<WorksheetTextItem*>(sender());

    if (!item)
Martin Küttler's avatar
Martin Küttler committed
411
        return;
412

413
    if (item == m_commandItem || item == nullptr) {
Martin Küttler's avatar
Martin Küttler committed
414
        moveToPreviousEntry(pos, x);
415
    } else if (item == currentInformationItem()) {
Martin Küttler's avatar
Martin Küttler committed
416
        m_commandItem->setFocusAt(pos, x);
417
418
419
    }
}

420
421
QString CommandEntry::command()
{
422
    QString cmd = m_commandItem->toPlainText();
423
424
    cmd.replace(QChar::ParagraphSeparator, QLatin1Char('\n')); //Replace the U+2029 paragraph break by a Normal Newline
    cmd.replace(QChar::LineSeparator, QLatin1Char('\n')); //Replace the line break by a Normal Newline
425
426
427
428
429
    return cmd;
}

void CommandEntry::setExpression(Cantor::Expression* expr)
{
430
    /*
431
    if ( m_expression ) {
Martin Küttler's avatar
Martin Küttler committed
432
        if (m_expression->status() == Cantor::Expression::Computing) {
433
            qDebug() << "OLD EXPRESSION STILL ACTIVE";
Martin Küttler's avatar
Martin Küttler committed
434
435
            m_expression->interrupt();
        }
436
        m_expression->deleteLater();
Martin Küttler's avatar
Martin Küttler committed
437
        }*/
438

439
    // Delete any previous error
440
    if(m_errorItem)
441
    {
442
        m_errorItem->deleteLater();
443
        m_errorItem = nullptr;
444
    }
445

446
    for (auto* item : m_informationItems)
447
    {
Martin Küttler's avatar
Martin Küttler committed
448
        item->deleteLater();
449
    }
450
    m_informationItems.clear();
451

452
    // Delete any previous result
453
    clearResultItems();
454

455
    m_expression = expr;
456
    m_resultsCollapsed = false;
457

458
459
460
461
462
463
464
465
    connect(expr, &Cantor::Expression::gotResult, this, &CommandEntry::updateEntry);
    connect(expr, &Cantor::Expression::resultsCleared, this, &CommandEntry::clearResultItems);
    connect(expr, &Cantor::Expression::resultRemoved, this, &CommandEntry::removeResultItem);
    connect(expr, &Cantor::Expression::resultReplaced, this, &CommandEntry::replaceResultItem);
    connect(expr, &Cantor::Expression::idChanged, this,  [=]() { updatePrompt();} );
    connect(expr, &Cantor::Expression::statusChanged, this, &CommandEntry::expressionChangedStatus);
    connect(expr, &Cantor::Expression::needsAdditionalInformation, this, &CommandEntry::showAdditionalInformationPrompt);
    connect(expr, &Cantor::Expression::statusChanged, this,  [=]() { updatePrompt();} );
466
467
468
469

    updatePrompt();

    if(expr->result())
470
    {
471
        worksheet()->gotResult(expr);
472
        updateEntry();
473
    }
474
475

    expressionChangedStatus(expr->status());
476
477
478
479
480
481
482
483
484
485
486
487
488
489
}

Cantor::Expression* CommandEntry::expression()
{
    return m_expression;
}

bool CommandEntry::acceptRichText()
{
    return false;
}

void CommandEntry::setContent(const QString& content)
{
490
    m_commandItem->setPlainText(content);
491
492
493
494
}

void CommandEntry::setContent(const QDomElement& content, const KZip& file)
{
495
    m_commandItem->setPlainText(content.firstChildElement(QLatin1String("Command")).text());
496

497
    LoadedExpression* expr = new LoadedExpression( worksheet()->session() );
498
499
    expr->loadFromXml(content, file);

500
501
502
503
504
505
506
507
508
    //background color
    QDomElement backgroundElem = content.firstChildElement(QLatin1String("Background"));
    if (!backgroundElem.isNull())
    {
        QColor color;
        color.setRed(backgroundElem.attribute(QLatin1String("red")).toInt());
        color.setGreen(backgroundElem.attribute(QLatin1String("green")).toInt());
        color.setBlue(backgroundElem.attribute(QLatin1String("blue")).toInt());
        m_commandItem->setBackgroundColor(color);
509
        m_backgroundColorCustom = true;
510
511
512
513
514
515
516
517
    }

    //text properties
    QDomElement textElem = content.firstChildElement(QLatin1String("Text"));
    if (!textElem.isNull())
    {
        //text color
        QDomElement colorElem = textElem.firstChildElement(QLatin1String("Color"));
Nikita Sirgienko's avatar
Nikita Sirgienko committed
518
        if (!colorElem.isNull() && !colorElem.hasAttribute(QLatin1String("default")))
519
520
521
522
523
524
525
        {
            m_defaultDefaultTextColor = m_commandItem->defaultTextColor();
            QColor color;
            color.setRed(colorElem.attribute(QLatin1String("red")).toInt());
            color.setGreen(colorElem.attribute(QLatin1String("green")).toInt());
            color.setBlue(colorElem.attribute(QLatin1String("blue")).toInt());
            m_commandItem->setDefaultTextColor(color);
Nikita Sirgienko's avatar
Nikita Sirgienko committed
526
            m_textColorCustom = true;
527
        }
528
529
530

        //font properties
        QDomElement fontElem = textElem.firstChildElement(QLatin1String("Font"));
Nikita Sirgienko's avatar
Nikita Sirgienko committed
531
        if (!fontElem.isNull() && !fontElem.hasAttribute(QLatin1String("default")))
532
533
534
535
536
537
538
539
        {
            QFont font;
            font.setFamily(fontElem.attribute(QLatin1String("family")));
            font.setPointSize(fontElem.attribute(QLatin1String("pointSize")).toInt());
            font.setWeight(fontElem.attribute(QLatin1String("weight")).toInt());
            font.setItalic(fontElem.attribute(QLatin1String("italic")).toInt());
            m_commandItem->setFont(font);
        }
540
541
    }

542
543
544
545
    m_isExecutionEnabled = !(bool)(content.attribute(QLatin1String("ExecutionDisabled"), QLatin1String("0")).toInt());
    if (m_isExecutionEnabled == false)
        excludeFromExecution();

546
547
548
    setExpression(expr);
}

549
550
void CommandEntry::setContentFromJupyter(const QJsonObject& cell)
{
551
    m_commandItem->setPlainText(Cantor::JupyterUtils::getSource(cell));
552

553
554
555
    LoadedExpression* expr=new LoadedExpression( worksheet()->session() );
    expr->loadFromJupyter(cell);
    setExpression(expr);
556
557
558
559
560
561
562
563

    // https://nbformat.readthedocs.io/en/latest/format_description.html#cell-metadata
    // 'collapsed': +
    // 'scrolled', 'deletable', 'name', 'tags' don't supported Cantor, so ignore them
    // 'source_hidden' don't supported
    // 'format' for raw entry, so ignore
    // I haven't found 'outputs_hidden' inside Jupyter notebooks, and difference from 'collapsed'
    // not clear, so also ignore
564
    const QJsonObject& metadata = Cantor::JupyterUtils::getMetadata(cell);
565
    const QJsonValue& collapsed = metadata.value(QLatin1String("collapsed"));
566
    if (collapsed.isBool() && collapsed.toBool() == true && !m_resultItems.isEmpty())
567
568
569
570
571
572
573
    {
        // Disable animation for hiding results, we don't need animation on document load stage
        bool animationValue = worksheet()->animationsEnabled();
        worksheet()->enableAnimations(false);
        collapseResults();
        worksheet()->enableAnimations(animationValue);
    }
574
575

    setJupyterMetadata(metadata);
576
577
}

578
579
580
581
582
583
584
585
586
587
588
QJsonValue CommandEntry::toJupyterJson()
{
    QJsonObject entry;

    entry.insert(QLatin1String("cell_type"), QLatin1String("code"));

    QJsonValue executionCountValue;
    if (expression() && expression()->id() != -1)
        executionCountValue = QJsonValue(expression()->id());
    entry.insert(QLatin1String("execution_count"), executionCountValue);

589
    QJsonObject metadata(jupyterMetadata());
590
591
592
    if (m_resultsCollapsed)
        metadata.insert(QLatin1String("collapsed"), true);

593
594
    entry.insert(QLatin1String("metadata"), metadata);

595
    Cantor::JupyterUtils::setSource(entry, command());
596
597
598

    QJsonArray outputs;
    if (expression())
599
600
601
602
603
    {
        Cantor::Expression::Status status = expression()->status();
        if (status == Cantor::Expression::Error || status == Cantor::Expression::Interrupted)
        {
            QJsonObject errorOutput;
604
            errorOutput.insert(Cantor::JupyterUtils::outputTypeKey, QLatin1String("error"));
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
            errorOutput.insert(QLatin1String("ename"), QLatin1String("Unknown"));
            errorOutput.insert(QLatin1String("evalue"), QLatin1String("Unknown"));

            QJsonArray traceback;
            if (status == Cantor::Expression::Error)
            {
                const QStringList& error = expression()->errorMessage().split(QLatin1Char('\n'));
                for (const QString& line: error)
                    traceback.append(line);
            }
            else
            {
                traceback.append(i18n("Interrupted"));
            }
            errorOutput.insert(QLatin1String("traceback"), traceback);

            outputs.append(errorOutput);
        }

624
        for (auto* result : expression()->results())
625
626
627
        {
            const QJsonValue& resultJson = result->toJupyterJson();

Nikita Sirgienko's avatar
Nikita Sirgienko committed
628
            if (!resultJson.isNull())
629
                outputs.append(resultJson);
630
631
        }
    }
632
633
634
635
    entry.insert(QLatin1String("outputs"), outputs);

    return entry;
}
636

637
638
639
640
void CommandEntry::showCompletion()
{
    const QString line = currentLine();

641
    if(!worksheet()->completionEnabled() || line.trimmed().isEmpty())
642
    {
643
644
        if (m_commandItem->hasFocus())
            m_commandItem->insertTab();
645
646
        return;
    } else if (isShowingCompletionPopup()) {
Martin Küttler's avatar
Martin Küttler committed
647
        QString comp = m_completionObject->completion();
648
649
        qDebug() << "command" << m_completionObject->command();
        qDebug() << "completion" << comp;
Martin Küttler's avatar
Martin Küttler committed
650
651
652
653
654
655
656
657
658
659
660
        if (comp != m_completionObject->command()
            || !m_completionObject->hasMultipleMatches()) {
            if (m_completionObject->hasMultipleMatches()) {
                completeCommandTo(comp, PreliminaryCompletion);
            } else {
                completeCommandTo(comp, FinalCompletion);
                m_completionBox->hide();
            }
        } else {
            m_completionBox->down();
        }
Martin Küttler's avatar
Martin Küttler committed
661
    } else {
Martin Küttler's avatar
Martin Küttler committed
662
        int p = m_commandItem->textCursor().positionInBlock();
Martin Küttler's avatar
Martin Küttler committed
663
        Cantor::CompletionObject* tco=worksheet()->session()->completionFor(line, p);
664
665
666
667
668
        if(tco)
            setCompletion(tco);
    }
}

Martin Küttler's avatar
Martin Küttler committed
669
670
671
void CommandEntry::selectPreviousCompletion()
{
    if (isShowingCompletionPopup())
Martin Küttler's avatar
Martin Küttler committed
672
        m_completionBox->up();
Martin Küttler's avatar
Martin Küttler committed
673
674
}

675
QString CommandEntry::toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq)
676
677
678
679
680
681
682
683
684
685
686
{
    Q_UNUSED(commentStartingSeq);
    Q_UNUSED(commentEndingSeq);

    if (command().isEmpty())
        return QString();
    return command() + commandSep;
}

QDomElement CommandEntry::toXml(QDomDocument& doc, KZip* archive)
{
687
688
689
690
691
    QDomElement exprElem = doc.createElement( QLatin1String("Expression") );
    QDomElement cmdElem = doc.createElement( QLatin1String("Command") );
    cmdElem.appendChild(doc.createTextNode( command() ));
    exprElem.appendChild(cmdElem);

692
693
694
    if (!m_isExecutionEnabled)
        exprElem.setAttribute(QLatin1String("ExecutionDisabled"), true);

695
    // save results of the expression if they exist
696
    if (expression())
697
698
699
700
701
702
703
704
    {
        const QString& errorMessage = expression()->errorMessage();
        if (!errorMessage.isEmpty())
        {
            QDomElement errorElem = doc.createElement( QLatin1String("Error") );
            errorElem.appendChild(doc.createTextNode(errorMessage));
            exprElem.appendChild(errorElem);
        }
705
        for (auto* result : expression()->results())
706
        {
707
            const QDomElement& resultElem = result->toXml(doc);
708
709
710
            exprElem.appendChild(resultElem);

            if (archive)
711
                result->saveAdditionalData(archive);
712
        }
713
    }
714

715
716
717
718
    bool isBackgroundColorNotDefault = false;
    // If user can change value from menu (menus have been inited) - check via menu
    // If use don't have menu, check if loaded color was custom color
    if (m_backgroundColorActionGroup)
719
        isBackgroundColorNotDefault = m_backgroundColorActionGroup->actions().indexOf(m_backgroundColorActionGroup->checkedAction()) != 0;
720
721
722
    else
        isBackgroundColorNotDefault = m_backgroundColorCustom;
    if (isBackgroundColorNotDefault)
723
    {
724
        QColor backgroundColor = (m_isExecutionEnabled ? m_commandItem->backgroundColor() : m_activeExecutionBackgroundColor);
725
726
727
728
729
        QDomElement colorElem = doc.createElement( QLatin1String("Background") );
        colorElem.setAttribute(QLatin1String("red"), QString::number(backgroundColor.red()));
        colorElem.setAttribute(QLatin1String("green"), QString::number(backgroundColor.green()));
        colorElem.setAttribute(QLatin1String("blue"), QString::number(backgroundColor.blue()));
        exprElem.appendChild(colorElem);
730
    }
731
732
733

    //save the text properties if they differ from default values
    const QFont& font = m_commandItem->font();
734
    const QColor& textColor = (m_isExecutionEnabled ? m_commandItem->defaultTextColor() : m_activeExecutionTextColor);
735
    bool isFontNotDefault = font != QFontDatabase::systemFont(QFontDatabase::FixedFont);
736
737
738

    bool isTextColorNotDefault = false;
    if (m_textColorActionGroup)
739
        isTextColorNotDefault = m_textColorActionGroup->actions().indexOf(m_textColorActionGroup->checkedAction()) != 0;
740
741
742
    else
        isTextColorNotDefault = m_textColorCustom;

Nikita Sirgienko's avatar
Nikita Sirgienko committed
743
744
    // Setting both values is necessary for previous Cantor versions compability
    // Value, added only for compability reason, marks with attribute
745
    if (isFontNotDefault || isTextColorNotDefault)
746
747
748
749
750
    {
        QDomElement textElem = doc.createElement(QLatin1String("Text"));

        //font properties
        QDomElement fontElem = doc.createElement(QLatin1String("Font"));
751
        if (!isFontNotDefault)
Nikita Sirgienko's avatar
Nikita Sirgienko committed
752
            fontElem.setAttribute(QLatin1String("default"), true);
753
754
755
756
757
758
759
760
        fontElem.setAttribute(QLatin1String("family"), font.family());
        fontElem.setAttribute(QLatin1String("pointSize"), QString::number(font.pointSize()));
        fontElem.setAttribute(QLatin1String("weight"), QString::number(font.weight()));
        fontElem.setAttribute(QLatin1String("italic"), QString::number(font.italic()));
        textElem.appendChild(fontElem);

        //text color
        QDomElement colorElem = doc.createElement( QLatin1String("Color") );
761
        if (!isTextColorNotDefault)
Nikita Sirgienko's avatar
Nikita Sirgienko committed
762
            colorElem.setAttribute(QLatin1String("default"), true);
763
764
765
766
767
768
769
770
771
        colorElem.setAttribute(QLatin1String("red"), QString::number(textColor.red()));
        colorElem.setAttribute(QLatin1String("green"), QString::number(textColor.green()));
        colorElem.setAttribute(QLatin1String("blue"), QString::number(textColor.blue()));
        textElem.appendChild(colorElem);

        exprElem.appendChild(textElem);
    }

    return exprElem;
772
773
}

774
QString CommandEntry::currentLine()
775
{
776
    if (!m_commandItem->hasFocus())
Martin Küttler's avatar
Martin Küttler committed
777
        return QString();
778

779
    QTextBlock block = m_commandItem->textCursor().block();
780
781
782
    return block.text();
}

783
bool CommandEntry::evaluateCurrentItem()
784
{
785
786
787
788
    // we can't use m_commandItem->hasFocus() here, because
    // that doesn't work when the scene doesn't have the focus,
    // e.g. when an assistant is used.
    if (m_commandItem == worksheet()->focusItem()) {
Martin Küttler's avatar
Martin Küttler committed
789
        return evaluate();
790
    } else if (informationItemHasFocus()) {
Martin Küttler's avatar
Martin Küttler committed
791
792
        addInformation();
        return true;
793
    }
794

795
    return false;
796
797
}

798
bool CommandEntry::evaluate(EvaluationOption evalOp)
799
{
800
801
802
803
    if (m_isExecutionEnabled)
    {
        if (worksheet()->session()->status() == Cantor::Session::Disable)
            worksheet()->loginToSession();
804

805
806
807
808
809
        removeContextHelp();
        QToolTip::hideText();

        QString cmd = command();
        m_evaluationOption = evalOp;
810

811
812
813
814
815
816
817
        if(cmd.isEmpty()) {
            removeResults();
            for (auto* item : m_informationItems) {
                item->deleteLater();
            }
            m_informationItems.clear();
            recalculateSize();
818

819
820
            evaluateNext(m_evaluationOption);
            return false;
Martin Küttler's avatar
Martin Küttler committed
821
822
        }

823
824
825
826
827
828
829
830
831
        Cantor::Expression* expr = worksheet()->session()->evaluateExpression(cmd);
        connect(expr, &Cantor::Expression::gotResult, this, [=]() { worksheet()->gotResult(expr); });

        setExpression(expr);

        return true;
    }
    else
    {
Martin Küttler's avatar
Martin Küttler committed
832
        evaluateNext(m_evaluationOption);
833
        return true;
834
    }
835
836
837
838
}

void CommandEntry::interruptEvaluation()
{
839
    Cantor::Expression *expr = expression();
840
841
842
843
    if(expr)
        expr->interrupt();
}

844
void CommandEntry::updateEntry()
845
{
846
    qDebug() << "update Entry";
847
848
    Cantor::Expression* expr = expression();
    if (expr == nullptr || expr->results().isEmpty())
Martin Küttler's avatar
Martin Küttler committed
849
        return;
850

851
    if (expr->results().last()->type() == Cantor::HelpResult::Type)
Martin Küttler's avatar
Martin Küttler committed
852
        return; // Help is handled elsewhere
853

854
    //CommandEntry::updateEntry() is only called if the worksheet view is resized
855
    //or when we got a new result(s).
856
    //In the second case the number of results and the number of result graphic objects is different
857
    //and we add a new graphic objects for the new result(s) (new result(s) are located in the end).
858
859
    // NOTE: LatexResult could request update (change from rendered to code, for example)
    // So, just update results, if we haven't new results or something similar
860
    if (m_resultItems.size() < expr->results().size())
861
    {
862
863
864
        if (m_resultsCollapsed)
            expandResults();

865
866
        for (int i = m_resultItems.size(); i < expr->results().size(); i++)
            m_resultItems << ResultItem::create(this, expr->results()[i]);
867
    }
868
869
870
871
872
    else
    {
        for (ResultItem* item: m_resultItems)
            item->update();
    }
Nikita Sirgienko's avatar
Nikita Sirgienko committed
873
874
875

    m_controlElement.isCollapsable = m_resultItems.size() > 0;

876
    animateSizeChange();
877
878
879
880
881
882
}

void CommandEntry::expressionChangedStatus(Cantor::Expression::Status status)
{
    switch (status)
    {
883
884
    case Cantor::Expression::Computing:
    {
885
886
887
        //change the background of the promt item and start animating it (fade in/out).
        //don't start the animation immediately in order to avoid unwanted flickering for "short" commands,
        //start the animation after 1 second passed.
888
889
890
891
892
893
894
895
        if (worksheet()->animationsEnabled())
        {
            const int id = m_expression->id();
            QTimer::singleShot(1000, this, [this, id] () {
                if(m_expression->status() == Cantor::Expression::Computing && m_expression->id() == id)
                    m_promptItemAnimation->start();
            });
        }
896
897
        break;
    }
898
899
    case Cantor::Expression::Error:
    case Cantor::Expression::Interrupted:
900
        m_promptItemAnimation->stop();
901
        m_promptItem->setOpacity(1.);
902
903
904
905
906
907
908
909
910

        m_commandItem->setFocusAt(WorksheetTextItem::BottomRight, 0);

        if(!m_errorItem)
        {
            m_errorItem = new WorksheetTextItem(this, Qt::TextSelectableByMouse);
        }

        if (status == Cantor::Expression::Error)
Nikita Sirgienko's avatar
Nikita Sirgienko committed
911
912
913
914
915
916
917
918
        {
            QString error = m_expression->errorMessage().toHtmlEscaped();
            while (error.endsWith(QLatin1Char('\n')))
                error.chop(1);
            error.replace(QLatin1String("\n"), QLatin1String("<br>"));
            error.replace(QLatin1String(" "), QLatin1String("&nbsp;"));
            m_errorItem->setHtml(error);
        }
919
920
921
922
        else
            m_errorItem->setHtml(i18n("Interrupted"));

        recalculateSize();
923
924
925
926
        // Mostly we handle setting of modification in WorksheetEntry inside ::evaluateNext.
        // But command entry wouldn't triger ::evaluateNext for Error and Interrupted states
        // So, we set it here
        worksheet()->setModified();
Martin Küttler's avatar
Martin Küttler committed
927
        break;
928
    case Cantor::Expression::Done:
929
        m_promptItemAnimation->stop();
930
        m_promptItem->setOpacity(1.);
Martin Küttler's avatar
Martin Küttler committed
931
932
        evaluateNext(m_evaluationOption);
        m_evaluationOption = DoNothing;
933
        break;
934
    default:
935
        break;
936
937
938
    }
}

939
void CommandEntry::animatePromptItem() {
940
941
    if(m_expression->status() == Cantor::Expression::Computing)
        m_promptItemAnimation->start();
942
943
}

944
945
bool CommandEntry::isEmpty()
{
946
    if (m_commandItem->toPlainText().trimmed().isEmpty()) {
947
        if (!m_resultItems.isEmpty())
Martin Küttler's avatar
Martin Küttler committed
948
949
            return false;
        return true;
950
    }
951
    return false;
952
953
}

954
955
bool CommandEntry::focusEntry(int pos, qreal xCoord)
{
956
    if (aboutToBeRemoved())
Martin Küttler's avatar
Martin Küttler committed
957
        return false;
958
959
    WorksheetTextItem* item;
    if (pos == WorksheetTextItem::TopLeft || pos == WorksheetTextItem::TopCoord)
Martin Küttler's avatar
Martin Küttler committed
960
        item = m_commandItem;
961
    else if (m_informationItems.size() && currentInformationItem()->isEditable())
Martin Küttler's avatar
Martin Küttler committed
962
        item = currentInformationItem();
963
    else
Martin Küttler's avatar
Martin Küttler committed
964
        item = m_commandItem;
965

966
    item->setFocusAt(pos, xCoord);
967
968
969
    return true;
}

970
971
void CommandEntry::setCompletion(Cantor::CompletionObject* tc)
{
972
    if (m_completionObject)
973
        delete m_completionObject;
974

975
    m_completionObject = tc;
Filipe Saraiva's avatar
Filipe Saraiva committed
976
977
    connect(m_completionObject, &Cantor::CompletionObject::done, this, &CommandEntry::showCompletions);
    connect(m_completionObject, &Cantor::CompletionObject::lineDone, this, &CommandEntry::completeLineTo);
978
979
980
981
}

void CommandEntry::showCompletions()
{
982
    disconnect(m_completionObject, &Cantor::CompletionObject::done, this, &CommandEntry::showCompletions);
983
    QString completion = m_completionObject->completion();
984
985
    qDebug()<<"completion: "<<completion;
    qDebug()<<"showing "<<m_completionObject->allMatches();
986
987
988

    if(m_completionObject->hasMultipleMatches())
    {
Martin Küttler's avatar
Martin Küttler committed
989
        completeCommandTo(completion);
990

991
        QToolTip::showText(QPoint(), QString(), worksheetView());
992
993
994
995
        if (!m_completionBox)
               m_completionBox = new KCompletionBox(worksheetView());

        m_completionBox->clear();
Martin Küttler's avatar
Martin Küttler committed
996
997
998
999
1000
1001
        m_completionBox->setItems(m_completionObject->allMatches());
        QList<QListWidgetItem*> items = m_completionBox->findItems(m_completionObject->command(), Qt::MatchFixedString|Qt::MatchCaseSensitive);
        if (!items.empty())
            m_completionBox->setCurrentItem(items.first());
        m_completionBox->setTabHandling(false);
        m_completionBox->setActivateOnSelect(true);
1002

1003
        connect(m_completionBox.data(), &KCompletionBox::activated, this, &CommandEntry::applySelectedCompletion);
1004
        connect(m_commandItem->document(), &QTextDocument::contentsChanged, this, &CommandEntry::completedLineChanged);
1005
        connect(m_completionObject, &Cantor::CompletionObject::done, this, &CommandEntry::updateCompletions);
Martin Küttler's avatar
Martin Küttler committed
1006
1007
1008

        m_commandItem->activateCompletion(true);
        m_completionBox->popup();
1009
        m_completionBox->move(getPopupPosition());
1010
    } else
1011
    {
Martin Küttler's avatar
Martin Küttler committed
1012
        completeCommandTo(completion, FinalCompletion);
1013
1014
1015
1016