/*************************************************************************** source::worx xtree Copyright © 2024-2025 c.holzheuer christoph.holzheuer@gmail.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. ***************************************************************************/ #include #include #include #include #include #include #include #include #include #include // create global dummy item as // fallback return value (klappt nicht) //Q_GLOBAL_STATIC(XQItem,s_dummyItem) //! hilfsfunkion, zeigt den string-content() für alle elemente der liste void showItemList( const XQItemList& list) { for(const auto& entry : list ) qDebug() << " --- itemList: " << entry->text(); qDebug(); } void showSelectionList( const QModelIndexList& list) { for(const auto& entry : list ) qDebug() << " --- SelectionList: " << entry.data().toString(); qDebug(); } //! Konstruktor mit parent. XQViewModel::XQViewModel( QObject* parent ) : QStandardItemModel{ parent }, _itemFactory{ XQItemFactory::instance() } { invisibleRootItem()->setData( "[rootItem]", Qt::DisplayRole ); setItemPrototype( new XQItem ); // auf änderungen kann in den unterklassen reagiert werden connect(this, &QStandardItemModel::itemChanged, this, [this](QStandardItem *item) { XQItem* xqItem = static_cast(item); emit xqItemChanged( *xqItem ); }); //qRegisterMetaType("XQItem"); } //! gibt einen static-cast auf 'invisibleRootItem()' zurück const XQItem& XQViewModel::xqRootItem() { // das ist ein hack, denn 'invisibleRootItem()' ist und bleibt ein // QStandardItem. Trick: keine eigenen members in XQItem, alles // dynamisch über den ItemData Mechanismus wie in QStandardItem return *static_cast(invisibleRootItem()); } //! Gibt den daten root node des models zurück. XQNodePtr XQViewModel::contentRootNode() { return _contentRoot; } //! hifsfunktion, die das item zu einen index zurückgibt XQItem& XQViewModel::xqItemFromIndex(const QModelIndex& index) const { if( index.isValid() ) { QStandardItem* xqItem = QStandardItemModel::itemFromIndex(index); if( xqItem ) return *static_cast(xqItem); } return XQItem::fallBackDummyItem(); } //! hilfsfunktiom, die das erste xqitem einer zeile zurückgibt. XQItem& XQViewModel::xqFirstItem(int row) const { return *static_cast( QStandardItemModel::item(row) ); } void XQViewModel::expandNewItem(const QModelIndex& index) { if( _treeTable ) { // ... ausklappen... _treeTable->expand( index ); // ... und markieren _treeTable->setCurrentIndex( index ); } } //! initialisiert dieses model über den namen. Es wird hier //! nur die strukur erzeugt, keine inhalte. void XQViewModel::initModel(const QString& modelName) { /* model section header data section ... */ setObjectName( modelName ); // model rootnode finden -> XQNodePtr modelSheet = _itemFactory.findModelSheet( modelName ); // throws // #1: über alle sections for( auto& sectionNode : modelSheet->children() ) { // #2: (optionalen?) header erzeugen const XQNodePtr header = sectionNode->find_child_by_tag_name( c_Header ); if( header ) { XQItemList list = _itemFactory.makeRow( header, nullptr ); addSection(list, sectionNode ); } } } //! Hilfsfunktion: fügt die item-liste unserem model hinzu und erzeugt eine 'section'. //! die section kann erst gültig sein, wenn die items im model gelandet sind, //! deswegen ist das hier zusammengefasst. //! Wrzeugt dann eine section aus einer frisch erzeugten itemlist. Der erste modelindex //! der liste und der root knoten der model-beschreibung werden gespeichert. void XQViewModel::addSection(const XQItemList& list, const XQNodePtr& sheetNode ) { // 1. die liste darf nicht leer sein Q_ASSERT(!list.isEmpty()); // 2. sheetNode muss da sein Q_ASSERT(sheetNode); // 3. 'ContenType' muss vorhanden sein if( !sheetNode->has_attribute( c_ContentType) ) throw XQException( "section list: Section node needs attribute 'ContentType'!"); // 5. das erzeugt dann auch valide indices appendRow(list); // 6. jetzt können wir auch die section erzeugen const XQModelSection& section = _sections.createSection( list[0]->index(), sheetNode ); // ... und es der welt mitteilen. emit sectionCreated( section ); } //! SLOT, toggled die section mit dem 'sectionKey' (hier: contentType) void XQViewModel::onToggleSection(const QString& sectionKey ) { toggleSection( _sections.sectionByKey(sectionKey) ); } //! toggled die gegebene model section. void XQViewModel::toggleSection( const XQModelSection& section ) { if( section.isValid() && _treeTable ) { XQSectionRange pos = _sections.sectionRange(section); _treeTable->toggleRowsHidden(pos.firstRow, pos.lastRow ); emit sectionToggled(section); } } /* //! SLOT als weiterleitung vom SIGNAL itemchanged void XQViewModel::onItemChanged(XQItem* item ) { qDebug() << " --- BASE item changed: " << item->text(); } */ //! SLOT, der aufgerufen wird, wenn eine edit-action getriggert wurde. void XQViewModel::onActionTriggered(QAction* action) { qDebug() << " --- onActionTriggered: count:" << action->text() <<": " << XQNode::s_Count; // all selected indices QModelIndexList selectionList = treeTable()->selectionModel()->selectedRows(); // extract command type XQCommand::CmdType cmdType = action->data().value(); switch( cmdType ) { // just handle undo ... case XQCommand::cmdUndo : return _undoStack->undo(); // ... or do/redo case XQCommand::cmdRedo : return _undoStack->redo(); // for copy & cut, we create a clone of the dataNodes in the clipboard case XQCommand::cmdCopy : case XQCommand::cmdCut : // don't 'copy' empty selections if( !selectionList.isEmpty() ) _clipBoard.saveNodes( selectionList ); // for copy, we are done, since copy cannot be undone if( cmdType == XQCommand::cmdCopy ) return; default: break; } // we create a command XQCommand* command = new XQCommand( cmdType, this ); QModelIndex currentIndex = treeTable()->currentIndex(); command->setOriginIndex(currentIndex); // store the row positions of the selected indices showSelectionList(selectionList); command->saveNodes( selectionList ); // execute command _undoStack->push( command ); } //! führt die 'redo' action des gegebenen commnds aus. void XQViewModel::onCommandRedo( const XQCommand& command ) { static MemCallMap redoCalls { { XQCommand::cmdToggleSection, &XQViewModel::cmdToggleSection }, { XQCommand::cmdCut, &XQViewModel::cmdCut }, { XQCommand::cmdPaste, &XQViewModel::cmdPaste }, { XQCommand::cmdNew, &XQViewModel::cmdNew }, { XQCommand::cmdDelete, &XQViewModel::cmdDelete } }; try { MemCall memCall = redoCalls[command.commandType()]; if( memCall ) (this->*memCall)( command ); else qDebug() << " --- onCommandRedo: default: not handled: " << command.toString(); } catch( XQException& exception ) { qDebug() << exception.what(); QMessageBox::critical( nullptr, "Failure", QString("Failure: %1").arg(exception.what()) ); } } //! führt die 'undo' action des gegebenen commnds aus. void XQViewModel::onCommandUndo( const XQCommand& command ) { qDebug() << " --- onCommandUndo: count: " << XQNode::s_Count; static MemCallMap undoCalls { { XQCommand::cmdToggleSection, &XQViewModel::cmdToggleSection }, { XQCommand::cmdCut, &XQViewModel::cmdCutUndo }, { XQCommand::cmdPaste, &XQViewModel::cmdPasteUndo }, { XQCommand::cmdNew, &XQViewModel::cmdNewUndo }, { XQCommand::cmdDelete, &XQViewModel::cmdDeleteUndo }, }; try { MemCall memCall = undoCalls[command.commandType()]; if( memCall ) (this->*memCall)( command ); else qDebug() << " --- onCommandUndo: default: not handled: " << command.toString(); } catch( XQException& exception ) { qDebug() << exception.what(); QMessageBox::critical( nullptr, "Failure", QString("Failure: %1").arg(exception.what()) ); } } // undo-/redo-able stuff //! markierte knoten entfernen, 'command' enthält die liste void XQViewModel::cmdCut( const XQCommand& command ) { int itmPos = command.first().itemPos; const XQModelSection& section = _sections.sectionByRow( itmPos ); qDebug() << " --- HEADSHOT I: " << itmPos << "->" << section.contentType(); // wir gehen rückwärts über alle gemerkten knoten ... for (auto it = command.rbegin(); it != command.rend(); ++it) { // ... holen das erste item, das auch den content node enthält //const XQNodeBackup& entry = *it; // jetzt löschen, dabei wird die parent-verbindung entfernt const XQNodeBackup& entry = *it; XQItem& firstItem = xqFirstItem( (*it).itemPos ); //qDebug() << " --- Cut: " << firstItem.text() << " " << firstItem.row() << " id#" << entry.contentNode->_id; qDebug() << " ---- command CUT: itemPos: " << entry.itemPos << " nodePos: "<< entry.nodePos << " is " << entry.contentNode->friendly_name(); entry.contentNode->unlink_self(); removeRow(entry.itemPos ); } } //! entfernte knoten wieder einfügen , 'command' enthält die liste void XQViewModel::cmdCutUndo( const XQCommand& command ) { // die anfangsposition int itmPos = command.first().itemPos; // die 'zuständige' section rausfinden const XQModelSection& section = _sections.sectionByRow( itmPos ); qDebug() << " --- HEADSHOT II: " << itmPos << "->" << section.contentType(); // über alle einträge ... for (auto& entry : command ) { const XQNodePtr& savedNode = entry.contentNode; // __fix! should not be _contentRoot! savedNode->add_me_at( entry.nodePos, _contentRoot ); XQItemList list = _itemFactory.makeRow( section.sheetRootNode(), savedNode ); insertRow( entry.itemPos, list ); XQItem& firstItem = *((XQItem*)list[0]); qDebug() << " ---- command cut UNDO2: itemPos: " << entry.itemPos << " nodePos: "<< entry.nodePos << " is " << entry.contentNode->friendly_name(); qDebug() << " --- Cut Undo: " << firstItem.text() << " " << firstItem.row() << " id#" << entry.contentNode->_id << " count: " << entry.contentNode.use_count(); } } //! clipboard inhalte einfügen void XQViewModel::cmdPaste( const XQCommand& command ) { // selection holen ... QItemSelectionModel* selectionModel = treeTable()->selectionModel(); // ... und löschen selectionModel->clearSelection(); // aktuelles item finden const XQItem& item = xqItemFromIndex( command.originIndex() ); // die neue item position ist unter dem akutellen item int insRow = item.row()+1; int nodePos = item.contentNode()->own_pos()+1; // die zugehörige section finden const XQModelSection& section = _sections.sectionByRow( insRow-1 ); // wir pasten das clipboard for (auto& entry : _clipBoard ) { // noch einen clone vom clone im clipboard erzeugen ... XQNodePtr newNode = entry.contentNode->clone(section.contentRootNode() ); // ... diesen einfügen ... newNode->add_me_at( nodePos ); // ... und damit eine frische item-row erzeugen XQItemList list = _itemFactory.makeRow( section.sheetRootNode(), newNode ); insertRow( insRow, list ); // die neue item-row selektieren const QModelIndex& selIdx = list[0]->index(); _treeTable->selectionModel()->select(selIdx, QItemSelectionModel::Select | QItemSelectionModel::Rows); // zur nächsten zeile insRow++; nodePos++; } // unsere änderungen merken fürs 'undo' /// fix_xx const_cast(command).saveNodes( selectionModel->selectedRows() ); } //! einfügen aus dem clipboard wieder rückgängig machen void XQViewModel::cmdPasteUndo( const XQCommand& command ) { command.dumpList("Paste UNDO"); // wir gehen rückwärts über alle markieren knoten ... for (auto it = command.rbegin(); it != command.rend(); ++it) { // ... holen das erste item, das auch den content node enthält const XQNodeBackup& entry = *it; XQItem& firstItem = xqFirstItem( (*it).itemPos ); qDebug() << " --- Cut: " << firstItem.text() << " " << firstItem.row(); // jetzt löschen entry.contentNode->unlink_self(); removeRow(entry.itemPos ); } } // don't clone into clipboard, remove items //! entfernen der selection ohne copy in clipboard. void XQViewModel::cmdDelete( const XQCommand& command ) { // wir gehen rückwärts über alle markieren knoten ... for (auto it = command.rbegin(); it != command.rend(); ++it) { // ... holen das erste item, das auch den content node enthält const XQNodeBackup& entry = *it; XQItem& firstItem = xqFirstItem( (*it).itemPos ); qDebug() << " --- delete: " << firstItem.text() << " " << firstItem.row(); // jetzt löschen entry.contentNode->unlink_self(); removeRow(entry.itemPos ); } } //! macht 'delete' wieder rückgängig. void XQViewModel::cmdDeleteUndo( const XQCommand& command ) { for (const auto& entry : command) { qDebug() << " --- delete UNDo: " << entry.contentNode->friendly_name(); } cmdCutUndo(command); } //! legt eine neue, leere zeile an. void XQViewModel::cmdNew( const XQCommand& command ) { const QModelIndex& origin = command.originIndex(); XQItem& target = xqItemFromIndex( origin ); // current data node XQNodePtr node = target.contentNode(); // we create a new data node XQNodePtr newNode = XQNode::make_node( node->tag_name(), node->tag_value() ); // store node in node->parent() newNode->add_me_at( node->own_pos(), node->parent() ); //... const XQModelSection& section = _sections.sectionByRow( origin.row() ); // neue, leere zeile erzeugen ... XQItemList list =_itemFactory.makeRow( section.sheetRootNode(), newNode ); // ... zur treeview hinzufügen ... insertRow( origin.row(), list ); // ... editierbar machen ... QModelIndex newIndex = list[0]->index(); treeTable()->setCurrentIndex( newIndex ); treeTable()->edit( newIndex ); // ,,, und fürs undo speichern const_cast(command).saveNodes( {newIndex} ); } //! entfernt die neu angelegte zeile. void XQViewModel::cmdNewUndo( const XQCommand& command ) { cmdDelete( command ); } //! schaltet eine section sichtbar oder unsichtbar. void XQViewModel::cmdToggleSection( const XQCommand& command ) { const QModelIndex& index = command.originIndex(); Q_ASSERT(index.isValid()); const XQModelSection& section = _sections.sectionByRow(index.row()); toggleSection( section ); } //! gibt die treetable zurück XQTreeTable* XQViewModel::treeTable() { return _treeTable; } //! setzt die treetable als member. void XQViewModel::setTreeTable(XQTreeTable* mainView ) { // store view for direct access: the maintree _treeTable = mainView; // set myself as model to the mainview _treeTable->setModel(this); XQItemDelegate* delegate = new XQItemDelegate( *this ); _treeTable->setItemDelegate( delegate ); _contextMenu = new XQContextMenu( mainView ); connect( _treeTable, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onShowContextMenu(QPoint))); //connect( _treeTable, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(onDoubleClicked(QModelIndex)) ); connect(_contextMenu, SIGNAL(triggered(QAction*)), this, SLOT(onActionTriggered(QAction*))); // __fixme, die view soll über das modelsheet konfiguriert werden! setupViewProperties(); } //! setzt die eigenschaften der TreeTable. void XQViewModel::setupViewProperties() { _treeTable->setContextMenuPolicy(Qt::CustomContextMenu); _treeTable->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed); _treeTable->setSelectionBehavior(QAbstractItemView::SelectRows); _treeTable->setSelectionMode(QAbstractItemView::ExtendedSelection); //_treeTable->setSelectionMode(QAbstractItemView::ContiguousSelection); _treeTable->setSelectionModel( new XQSelectionModel(this) ); } //! gibt den undo-stack zurück. QUndoStack* XQViewModel::undoStack() { return _undoStack; } //! setzt den undo-stack. void XQViewModel::setUndoStack( QUndoStack* undoStack ) { _undoStack = undoStack; } //! SLOT, der die erstellung & anzeige es context-menues triggert. void XQViewModel::onShowContextMenu(const QPoint& point) { initContextMenu(); _contextMenu->popup(_treeTable->mapToGlobal(point)); } //! gibt die namen der neuen data-roles zurück. //! __fix: die alten roles fehlen hier! QHash XQViewModel::roleNames() const { QHash roles = QStandardItemModel::roleNames(); roles[XQItem::ContentRole] = "content"; roles[XQItem::ItemTypeRole] = "itemType"; roles[XQItem::RenderStyleRole] = "renderStyle"; roles[XQItem::EditorTypeRole] = "editorType"; roles[XQItem::UnitTypeRole] = "unitType"; roles[XQItem::FixedChoicesRole] = "fixedChoices"; roles[XQItem::ContentNodeRole] = "contentNode"; roles[XQItem::SheetNodeRole] = "sheetNode"; roles[XQItem::TypeKeyRole] = "typeKey"; return roles; }