File indexing completed on 2024-04-28 11:20:56

0001 /*
0002     SPDX-License-Identifier: GPL-2.0-or-later
0003     SPDX-FileCopyrightText: 2020 Nikita Sirgienko <warquark@gmail.com>
0004 */
0005 
0006 #include "hierarchyentry.h"
0007 #include "settings.h"
0008 #include "worksheetview.h"
0009 #include "lib/jupyterutils.h"
0010 
0011 #include <QJsonObject>
0012 #include <QRegularExpression>
0013 #include <QDrag>
0014 #include <QBitmap>
0015 #include <QMimeData>
0016 #include <QPainter>
0017 #include <QDebug>
0018 #include <QActionGroup>
0019 
0020 #include <KLocalizedString>
0021 
0022 static QStringList hierarchyLevelNames = {i18n("Chapter"), i18n("Subchapter"), i18n("Section"), i18n("Subsection"), i18n("Paragraph"), i18n("Subparagraph")};
0023 
0024 HierarchyEntry::HierarchyEntry(Worksheet* worksheet) : WorksheetEntry(worksheet)
0025     , m_hierarchyLevelItem(new WorksheetTextItem(this, Qt::NoTextInteraction))
0026     , m_textItem(new WorksheetTextItem(this, Qt::TextEditorInteraction))
0027     , m_depth(HierarchyLevel::Chapter)
0028     , m_hierarchyNumber(1)
0029     , m_hidedSubentries(nullptr)
0030 {
0031     // Font and sizes should be regulated from future Settings "Styles" option
0032     m_textItem->enableRichText(false);
0033 
0034     connect(m_textItem, &WorksheetTextItem::moveToPrevious, this, &HierarchyEntry::moveToPreviousEntry);
0035     connect(m_textItem, &WorksheetTextItem::moveToNext, this, &HierarchyEntry::moveToNextEntry);
0036     // Modern syntax of signal/stots don't work on this connection (arguments don't match)
0037     connect(m_textItem, SIGNAL(execute()), this, SLOT(evaluate()));
0038 
0039     connect(this, &HierarchyEntry::hierarhyEntryNameChange, worksheet, &Worksheet::hierarhyEntryNameChange);
0040 
0041     connect(&m_controlElement, &WorksheetControlItem::doubleClick, this, &HierarchyEntry::handleControlElementDoubleClick);
0042 
0043     m_setLevelActionGroup = new QActionGroup(this);
0044     m_setLevelActionGroup->setExclusive(true);
0045     connect(m_setLevelActionGroup, &QActionGroup::triggered, this, &HierarchyEntry::setLevelTriggered);
0046 
0047     Q_ASSERT(hierarchyLevelNames.size() == (int)HierarchyLevel::EndValue-1);
0048     m_setLevelMenu = new QMenu(i18n("Set Hierarchy Level"));
0049     for (int i = 1; i < (int)HierarchyLevel::EndValue; i++)
0050     {
0051         QAction* action = new QAction(hierarchyLevelNames[i-1], m_setLevelActionGroup);
0052         action->setCheckable(true);
0053         m_setLevelMenu->addAction(action);
0054     }
0055 
0056     updateFonts(true);
0057 }
0058 
0059 HierarchyEntry::~HierarchyEntry()
0060 {
0061     m_setLevelMenu->deleteLater();
0062 }
0063 
0064 void HierarchyEntry::populateMenu(QMenu* menu, QPointF pos)
0065 {
0066     menu->addMenu(m_setLevelMenu);
0067     //menu->addSeparator();
0068     WorksheetEntry::populateMenu(menu, pos);
0069 }
0070 
0071 bool HierarchyEntry::isEmpty()
0072 {
0073     return m_textItem->document()->isEmpty();
0074 }
0075 
0076 int HierarchyEntry::type() const
0077 {
0078     return Type;
0079 }
0080 
0081 bool HierarchyEntry::acceptRichText()
0082 {
0083     return false;
0084 }
0085 
0086 bool HierarchyEntry::focusEntry(int pos, qreal xCoord)
0087 {
0088     if (aboutToBeRemoved())
0089         return false;
0090     m_textItem->setFocusAt(pos, xCoord);
0091     return true;
0092 }
0093 
0094 
0095 void HierarchyEntry::setContent(const QString& content)
0096 {
0097     m_textItem->setPlainText(content);
0098 }
0099 
0100 void HierarchyEntry::setContent(const QDomElement& content, const KZip& file)
0101 {
0102     Q_UNUSED(file);
0103     if(content.firstChildElement(QLatin1String("body")).isNull())
0104         return;
0105 
0106     m_textItem->setPlainText(content.firstChildElement(QLatin1String("body")).text());
0107 
0108     const QDomElement& subentriesMainElem = content.firstChildElement(QLatin1String("HidedSubentries"));
0109     if (!subentriesMainElem.isNull())
0110     {
0111         m_controlElement.isCollapsable = true;
0112         m_controlElement.isCollapsed = true;
0113 
0114         const QDomNodeList& entries = subentriesMainElem.childNodes();
0115 
0116         WorksheetEntry* tail = nullptr;
0117         for (int i = 0; i < entries.size(); i++)
0118         {
0119             const QDomElement& entryElem = entries.at(i).toElement();
0120             int type = Worksheet::typeForTagName(entryElem.tagName());
0121             Q_ASSERT(type != 0);
0122 
0123             WorksheetEntry* entry = WorksheetEntry::create(type, worksheet());
0124             entry->setContent(entryElem, file);
0125             entry->hide();
0126 
0127             // set m_hidedSubentries to head element
0128             if (!m_hidedSubentries)
0129                 m_hidedSubentries = entry;
0130 
0131             if (tail)
0132             {
0133                 entry->setPrevious(tail);
0134                 tail->setNext(entry);
0135                 tail = entry;
0136             }
0137             else
0138             {
0139                 entry->setPrevious(nullptr);
0140                 tail = entry;
0141             }
0142         }
0143     }
0144 
0145     m_depth = (HierarchyLevel)content.attribute(QLatin1String("level")).toInt();
0146     m_hierarchyNumber = content.attribute(QLatin1String("level-number")).toInt();
0147 
0148     updateFonts(true);
0149 }
0150 
0151 void HierarchyEntry::setContentFromJupyter(const QJsonObject& cell)
0152 {
0153     if (Cantor::JupyterUtils::isMarkdownCell(cell))
0154     {
0155         QJsonObject cantorMetadata = Cantor::JupyterUtils::getCantorMetadata(cell);
0156         m_textItem->setPlainText(cantorMetadata.value(QLatin1String("hierarchy_entry_content")).toString());
0157 
0158         m_depth = (HierarchyLevel)cantorMetadata.value(QLatin1String("level")).toInt();
0159         m_hierarchyNumber= cantorMetadata.value(QLatin1String("level-number")).toInt();
0160 
0161         updateFonts(true);
0162     }
0163 }
0164 
0165 QJsonValue HierarchyEntry::toJupyterJson()
0166 {
0167     QTextDocument* doc = m_textItem->document();
0168 
0169     QJsonObject metadata(jupyterMetadata());
0170 
0171     QString entryData;
0172     QString entryType;
0173 
0174     entryType = QLatin1String("markdown");
0175 
0176     // Add raw text of entry to metadata, for situation when
0177     // Cantor opens .ipynb converted from our .cws format
0178     QJsonObject cantorMetadata;
0179 
0180     if (Settings::storeTextEntryFormatting())
0181     {
0182         entryData = doc->toPlainText();
0183 
0184         cantorMetadata.insert(QLatin1String("hierarchy_entry_content"), entryData);
0185     }
0186     else
0187         entryData = doc->toPlainText();
0188 
0189     cantorMetadata.insert(QLatin1String("level"), (int)m_depth);
0190     cantorMetadata.insert(QLatin1String("level-number"), m_hierarchyNumber);
0191 
0192     // Don't store subentriesMainElem, because actually too complex
0193     // Maybe this is a place for future work
0194 
0195     metadata.insert(Cantor::JupyterUtils::cantorMetadataKey, cantorMetadata);
0196 
0197     QJsonObject entry;
0198     entry.insert(QLatin1String("cell_type"), entryType);
0199     entry.insert(QLatin1String("metadata"), metadata);
0200     Cantor::JupyterUtils::setSource(entry, entryData);
0201 
0202     return entry;
0203 }
0204 
0205 bool HierarchyEntry::isConvertableToHierarchyEntry(const QJsonObject& cell)
0206 {
0207     if (!Cantor::JupyterUtils::isMarkdownCell(cell))
0208         return false;
0209 
0210     QJsonObject cantorMetadata = Cantor::JupyterUtils::getCantorMetadata(cell);
0211     const QJsonValue& textContentValue = cantorMetadata.value(QLatin1String("hierarchy_entry_content"));
0212 
0213     if (!textContentValue.isString())
0214         return false;
0215 
0216     const QString& textContent = textContentValue.toString();
0217     const QString& source = Cantor::JupyterUtils::getSource(cell);
0218 
0219     return textContent == source;
0220 }
0221 
0222 QDomElement HierarchyEntry::toXml(QDomDocument& doc, KZip* archive)
0223 {
0224     Q_UNUSED(archive);
0225     QDomElement el = doc.createElement(QLatin1String("Hierarchy"));
0226 
0227     QDomElement textBodyEl = doc.createElement(QLatin1String("body"));
0228     const QString& text = m_textItem->document()->toPlainText();
0229     textBodyEl.appendChild(doc.createTextNode(text));
0230     el.appendChild(textBodyEl);
0231 
0232     if(m_hidedSubentries)
0233     {
0234         QDomElement entriesElem = doc.createElement(QLatin1String("HidedSubentries"));
0235         for (WorksheetEntry* entry = m_hidedSubentries; entry; entry = entry->next())
0236             entriesElem.appendChild(entry->toXml(doc, archive));
0237         el.appendChild(entriesElem);
0238     }
0239 
0240     el.setAttribute(QLatin1String("level"), (int)m_depth);
0241     el.setAttribute(QLatin1String("level-number"), m_hierarchyNumber);
0242 
0243     return el;
0244 }
0245 
0246 QString HierarchyEntry::toPlain(const QString& commandSep, const QString& commentStartingSeq, const QString& commentEndingSeq)
0247 {
0248     Q_UNUSED(commandSep);
0249 
0250     if (commentStartingSeq.isEmpty())
0251         return QString();
0252 
0253     QString text = m_hierarchyLevelItem->toPlainText() + QLatin1String(" ") + m_textItem->toPlainText();
0254     if (!commentEndingSeq.isEmpty())
0255         return commentStartingSeq + text + commentEndingSeq + QLatin1String("\n");
0256     return commentStartingSeq + text.replace(QLatin1String("\n"), QLatin1String("\n") + commentStartingSeq) + QLatin1String("\n");
0257 
0258 }
0259 
0260 bool HierarchyEntry::evaluate(EvaluationOption evalOp)
0261 {
0262     emit hierarhyEntryNameChange(text(), hierarchyText(), ((int)m_depth)-1);
0263     evaluateNext(evalOp);
0264 
0265     return true;
0266 }
0267 
0268 void HierarchyEntry::updateEntry()
0269 {
0270 }
0271 
0272 int HierarchyEntry::searchText(const QString& text, const QString& pattern,
0273                           QTextDocument::FindFlags qt_flags)
0274 {
0275     Qt::CaseSensitivity caseSensitivity;
0276     if (qt_flags & QTextDocument::FindCaseSensitively)
0277         caseSensitivity = Qt::CaseSensitive;
0278     else
0279         caseSensitivity = Qt::CaseInsensitive;
0280 
0281     int position;
0282     if (qt_flags & QTextDocument::FindBackward)
0283         position = text.lastIndexOf(pattern, -1, caseSensitivity);
0284     else
0285         position = text.indexOf(pattern, 0, caseSensitivity);
0286 
0287     return position;
0288 }
0289 
0290 WorksheetCursor HierarchyEntry::search(const QString& pattern, unsigned flags,
0291                                   QTextDocument::FindFlags qt_flags,
0292                                   const WorksheetCursor& pos)
0293 {
0294     if (!(flags & WorksheetEntry::SearchText) ||
0295         (pos.isValid() && pos.entry() != this))
0296         return WorksheetCursor();
0297 
0298     QTextCursor textCursor = m_textItem->search(pattern, qt_flags, pos);
0299 
0300     if (textCursor.isNull())
0301         return WorksheetCursor();
0302     else
0303         return WorksheetCursor(this, m_textItem, textCursor);
0304 }
0305 
0306 
0307 void HierarchyEntry::layOutForWidth(qreal entry_zone_x, qreal w, bool force)
0308 {
0309     if (size().width() == w && m_textItem->pos().x() == entry_zone_x && !force)
0310         return;
0311 
0312     const qreal margin = worksheet()->isPrinting() ? 0 : RightMargin;
0313 
0314     m_hierarchyLevelItem->setPos(entry_zone_x - m_hierarchyLevelItem->width() - HorizontalSpacing, 0);
0315 
0316     m_textItem->setGeometry(entry_zone_x, 0, w - margin - entry_zone_x);
0317     setSize(QSizeF(m_textItem->width() + margin + entry_zone_x, std::max(m_textItem->height(), m_hierarchyLevelItem->height()) + VerticalMargin));
0318 }
0319 
0320 bool HierarchyEntry::wantToEvaluate()
0321 {
0322     return false;
0323 }
0324 
0325 QString HierarchyEntry::text() const
0326 {
0327     return m_textItem->toPlainText();
0328 }
0329 
0330 QString HierarchyEntry::hierarchyText() const
0331 {
0332     return m_hierarchyLevelItem->toPlainText();
0333 }
0334 
0335 HierarchyEntry::HierarchyLevel HierarchyEntry::level() const
0336 {
0337     return m_depth;
0338 }
0339 
0340 void HierarchyEntry::setLevel(HierarchyEntry::HierarchyLevel depth)
0341 {
0342     m_depth = depth;
0343 }
0344 
0345 int HierarchyEntry::hierarchyNumber() const
0346 {
0347     return m_hierarchyNumber;
0348 }
0349 
0350 void HierarchyEntry::updateControlElementForHierarchy(qreal responsibilityZoneYEnd, int maxHierarchyDepth, bool haveSubElements)
0351 {
0352     if (!m_hidedSubentries)
0353         m_controlElement.isCollapsable = haveSubElements;
0354 
0355     qreal controlZoneStart = m_textItem->x() + m_textItem->width() + HorizontalSpacing;
0356     qreal controlElemenXPos = controlZoneStart + static_cast<double>(maxHierarchyDepth - (int)m_depth + 1) * (ControlElementWidth + ControlElementBorder);
0357 
0358     m_controlElement.setRect(
0359         controlElemenXPos, 0,
0360         ControlElementWidth, responsibilityZoneYEnd-y()
0361     );
0362     m_controlElement.update();
0363     update();
0364 }
0365 
0366 
0367 void HierarchyEntry::updateHierarchyLevel(std::vector<int>& currectNumbers)
0368 {
0369     HierarchyLevel nextLevel = (HierarchyLevel)(currectNumbers.size()+1);
0370     if (m_depth >= nextLevel)
0371     {
0372         for (int i = (int)nextLevel; i <= (int)m_depth; i++)
0373             currectNumbers.push_back(1);
0374         m_hierarchyNumber = 1;
0375     }
0376     else
0377     {
0378         int idx = (int)m_depth - 1;
0379         size_t size = currectNumbers.size();
0380         for (size_t i = (size_t)idx+1; i < size; i++)
0381             currectNumbers.pop_back();
0382         currectNumbers[idx] += 1;
0383         m_hierarchyNumber = currectNumbers[idx];
0384     }
0385 
0386     QString s;
0387     Q_ASSERT(currectNumbers.size() != 0);
0388     s += QString::number(currectNumbers.front());
0389     for (size_t i = 1U; i < currectNumbers.size(); i++)
0390         s += QLatin1String(".") + QString::number(currectNumbers[i]);
0391 
0392     qreal previousWidth = m_hierarchyLevelItem->width();
0393     m_hierarchyLevelItem->setPlainText(s);
0394     m_hierarchyLevelItem->setPos(m_hierarchyLevelItem->x() - (m_hierarchyLevelItem->width() - previousWidth), 0);
0395     updateFonts();
0396 }
0397 
0398 qreal HierarchyEntry::hierarchyItemWidth()
0399 {
0400     return m_hierarchyLevelItem->width() + HorizontalSpacing;
0401 }
0402 
0403 void HierarchyEntry::setLevelTriggered(QAction* action)
0404 {
0405     int idx = m_setLevelActionGroup->actions().indexOf(action);
0406     m_depth = (HierarchyLevel)(idx + 1);
0407 
0408     worksheet()->updateHierarchyLayout();
0409     worksheet()->updateLayout();
0410 }
0411 
0412 void HierarchyEntry::recalculateControlGeometry()
0413 {
0414     // do nothing, update the control elements will be done in ;;updateControlElementForHierarchy
0415 }
0416 
0417 void HierarchyEntry::startDrag(QPointF grabPos)
0418 {
0419     // We need reset entry cursor manually, because otherwise the entry cursor will be visible on dragable item
0420     worksheet()->resetEntryCursor();
0421 
0422     QDrag* drag = new QDrag(worksheetView());
0423     const qreal scale = worksheet()->renderer()->scale();
0424 
0425     QRectF hierarchyBound(boundingRect().x(), boundingRect().y(), boundingRect().width(), m_controlElement.boundingRect().height());
0426     QSizeF hierarchyZoneSize(size().width(), m_controlElement.boundingRect().height());
0427 
0428     QPixmap pixmap((hierarchyZoneSize*scale).toSize());
0429     pixmap.fill(QColor(255, 255, 255, 0));
0430 
0431     QPainter painter(&pixmap);
0432     const QRectF sceneRect = mapRectToScene(hierarchyBound);
0433     worksheet()->render(&painter, pixmap.rect(), sceneRect);
0434     painter.end();
0435 
0436     QBitmap mask = pixmap.createMaskFromColor(QColor(255, 255, 255), Qt::MaskInColor);
0437     pixmap.setMask(mask);
0438 
0439     drag->setPixmap(pixmap);
0440     if (grabPos.isNull()) {
0441         const QPointF scenePos = worksheetView()->sceneCursorPos();
0442         drag->setHotSpot((mapFromScene(scenePos) * scale).toPoint());
0443     } else {
0444         drag->setHotSpot((grabPos * scale).toPoint());
0445     }
0446     drag->setMimeData(new QMimeData());
0447 
0448     worksheet()->startDragWithHierarchy(this, drag, hierarchyZoneSize);
0449 }
0450 
0451 void HierarchyEntry::updateFonts(bool force)
0452 {
0453     QFont font;
0454     switch(m_depth)
0455     {
0456         case HierarchyLevel::Chapter:
0457             font = Settings::chapterFontFamily();
0458             font.setPointSize(Settings::chapterFontSize());
0459             font.setItalic(Settings::chapterFontItalic());
0460             font.setBold(Settings::chapterFontBold());
0461             break;
0462 
0463         case HierarchyLevel::Subchapter:
0464             font = Settings::subchapterFontFamily();
0465             font.setPointSize(Settings::subchapterFontSize());
0466             font.setItalic(Settings::subchapterFontItalic());
0467             font.setBold(Settings::subchapterFontBold());
0468             break;
0469 
0470         case HierarchyLevel::Section:
0471             font = Settings::sectionFontFamily();
0472             font.setPointSize(Settings::sectionFontSize());
0473             font.setItalic(Settings::sectionFontItalic());
0474             font.setBold(Settings::sectionFontBold());
0475             break;
0476 
0477         case HierarchyLevel::Subsection:
0478             font = Settings::subsectionFontFamily();
0479             font.setPointSize(Settings::subsectionFontSize());
0480             font.setItalic(Settings::subsectionFontItalic());
0481             font.setBold(Settings::subsectionFontBold());
0482             break;
0483 
0484         case HierarchyLevel::Paragraph:
0485             font = Settings::paragraphFontFamily();
0486             font.setPointSize(Settings::paragraphFontSize());
0487             font.setItalic(Settings::paragraphFontItalic());
0488             font.setBold(Settings::paragraphFontBold());
0489             break;
0490 
0491         case HierarchyLevel::Subparagraph:
0492             font = Settings::subparagraphFontFamily();
0493             font.setPointSize(Settings::subparagraphFontSize());
0494             font.setItalic(Settings::subparagraphFontItalic());
0495             font.setBold(Settings::subparagraphFontBold());
0496             break;
0497 
0498         default:
0499             Q_ASSERT(false);
0500             break;
0501     }
0502 
0503     const QFont& currectFont = m_textItem->font();
0504     bool isSameFont =
0505            currectFont.family() == font.family()
0506         && currectFont.pointSize() == font.pointSize()
0507         && currectFont.bold() == font.bold()
0508         && currectFont.italic() == font.italic();
0509 
0510     if (force || !isSameFont)
0511     {
0512         m_hierarchyLevelItem->setFont(font);
0513         m_hierarchyLevelItem->testSize();
0514 
0515         m_textItem->setFont(font);
0516         // And update current text of item
0517         QTextCursor cursor = m_textItem->textCursor();
0518         cursor.select(QTextCursor::Document);
0519         QTextCharFormat format = cursor.charFormat();
0520         format.setFont(font);
0521         cursor.setCharFormat(format);
0522         m_textItem->testSize();
0523 
0524         // Recalculate size (because it can changed due font changes) and update worksheet layout
0525         recalculateSize();
0526         worksheet()->updateEntrySize(this);
0527     }
0528 }
0529 
0530 
0531 void HierarchyEntry::handleControlElementDoubleClick()
0532 {
0533     qDebug() << "HierarchyEntry::handleControlElementDoubleClick";
0534     if (m_controlElement.isCollapsed)
0535     {
0536         worksheet()->insertSubentriesForHierarchy(this, m_hidedSubentries);
0537         m_controlElement.isCollapsed = false;
0538     }
0539     else
0540     {
0541         m_hidedSubentries = worksheet()->cutSubentriesForHierarchy(this);
0542         m_controlElement.isCollapsed = true;
0543     }
0544 
0545     m_controlElement.update();
0546 
0547     worksheet()->updateLayout();
0548     worksheet()->updateHierarchyLayout();
0549 }
0550 
0551 void HierarchyEntry::updateAfterSettingsChanges()
0552 {
0553     updateFonts();
0554 }