/*************************************************************************** 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: " << ((XQItem*)entry)->content(); qDebug(); } //! Konstruktur mit parent. XQViewModel::XQViewModel( QObject* parent ) : QStandardItemModel{ parent }, _itemFactory{ XQItemFactory::instance() } { invisibleRootItem()->setData( "[rootItem]", Qt::DisplayRole ); setItemPrototype( new 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()); } //! 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) ); } //! 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 ... */ // model rootnode finden -> XQNodePtr modelSheet = _itemFactory.findModelSheet( modelName ); // throws // #1: über alle sections for( auto& section : modelSheet->children() ) { // #2: (optionalen?) header erzeugen const XQNodePtr header = section->find_child_by_tag_name( "Header"); if( header ) { XQItemList list = _itemFactory.makeContentRow( header, nullptr ); addSection(list, section ); } } } //! hilfsfunktion: fügt die 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. //! erzeugt dann eine section aus einer frisch erzeugten itemlist. der erste modelindex //! der liste und der unterknoten 'Data' 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'!"); // 4. Data child muss auch da sein XQNodePtr dataNode = sheetNode->find_child_by_tag_name( c_Data ); if( !dataNode ) throw XQException( "section list: 'Data' child is missing!"); // 5. das erzeugt dann auch valide indices appendRow(list); XQModelSection section(list[0]->index(), dataNode ); _sections.addAtKey(sheetNode->attribute( c_ContentType), section); emit sectionCreated( §ion ); } //! SLOT, der aufgerufen wird, wenn eine edit-action getriggert wurde. void XQViewModel::onActionTriggered(QAction* action) { qDebug() << " --- onActionTriggered: count:" << 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 ); // store the row positions of the selected indices command->saveNodes( selectionList ); command->setOriginIndex( treeTable()->currentIndex() ); // execute command _undoStack->push( command ); } /* switch (command.commandType()) { case XQCommand::cmdToggleSection: return cmdToggleSection( command.originIndex() ); case XQCommand::cmdCut: return cmdCut( command ); case XQCommand::cmdPaste: return cmdPaste( command ); case XQCommand::cmdNew: return cmdNew( command ); case XQCommand::cmdDelete: return cmdDelete( command ); case XQCommand::cmdMove: break; default: qDebug() << " --- onCommandRedo: default: not handled: " << command.toString(); } */ //! führt die 'redo' action des gegebenen commnds aus. void XQViewModel::onCommandRedo( 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()) ); } } /* try { switch (command.commandType()) { case XQCommand::cmdToggleSection: return cmdToggleSection( command.originIndex() ); break; // undo Cut -> perform undoCut case XQCommand::cmdCut: return cmdCutUndo( command ); // undo Paste -> perform Cut case XQCommand::cmdPaste: return cmdPasteUndo( command ); // undo Move -> perform move back case XQCommand::cmdMove: // not yet implemented break; // undo New -> perform Delete case XQCommand::cmdNew: cmdNewUndo( command ); break; // undo Delete -> perform New case XQCommand::cmdDelete: qDebug() << " --- onCommandUndo: delete: " << command.toString(); return cmdDeleteUndo( command ); default: qDebug() << " --- onCommandUndo: default: not handled: " << command.toString(); } */ //! führt die 'undo' action des gegebenen commnds aus. void XQViewModel::onCommandUndo( 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( XQCommand& command ) { // 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; entry.contentNode->unlink_self(); removeRow(entry.itemPos ); } } //! entfernte knoten wieder einfügen , 'command' enthält die liste void XQViewModel::cmdCutUndo( XQCommand& command ) { // die anfangsposition int itmPos = command.first().itemPos; // die 'zuständige' section rausfinden const XQModelSection& section = _sections.sectionFromRow( itmPos ); // ü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.makeContentRow( section.sheetRootNode, savedNode ); XQItem& firstItem = *((XQItem*)list[0]); qDebug() << " --- Cut Undo: " << firstItem.text() << " " << firstItem.row() << " id#" << entry.contentNode->_id << " count: " << entry.contentNode.use_count(); insertRow( entry.itemPos, list ); } } //! clipboard inhalte einfügen void XQViewModel::cmdPaste( 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.sectionFromRow( insRow-1 ); // wir pasten das clipboard for (auto& entry : _clipBoard ) { // noch ein clone vom clone erzeugen ... XQNodePtr newNode = entry.contentNode->clone(section.contentRootNode ); newNode->clone(section.contentRootNode )->add_me_at( nodePos ); // ... und damit eine frische item-row erzeugen XQItemList list = _itemFactory.makeContentRow( 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' command.saveNodes( selectionModel->selectedRows() ); } //! einfügen aus dem clipboard wieder rückgängig machen void XQViewModel::cmdPasteUndo( 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( 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() << " --- Cut: " << firstItem.text() << " " << firstItem.row(); // jetzt löschen entry.contentNode->unlink_self(); removeRow(entry.itemPos ); } } //! macht 'delete' wirder rückgängig. void XQViewModel::cmdDeleteUndo( XQCommand& command ) { } //! legt eine neue, leere zeile an. void XQViewModel::cmdNew( XQCommand& command ) { // __fix /* const QModelIndex& origin = command.originIndex(); if( !origin.isValid() ) throw XQException("cmdNewRow failed: index not valid "); XQItem* target = xqItemFromIndex( origin ); // current data node XQNodePtr node = target->contentNode(); // we create a new data node //XQNodePtr newNode = new XQNodePtr( node->tag_name(), node->parent() ); XQNodePtr newNode = XQNode::make_node( node->tag_name(), node->tag_value(), node->parent() ); // store node in node->parent() //node->add_before_me( newNode ); // store node also in 'command' to enable undo const XQModelSection& section = _sections.sectionFromIndex( origin ); // create new item row XQItemList list = _itemFactory.createGenericRow( newNode, section.sheetRootNode ); // add it to the treeview ... insertRow( origin.row(), list ); // ... and make it ... treeTable()->setCurrentIndex( list[0]->index() ); // ... editable treeTable()->edit( list[0]->index() ); */ } //! entfernt die neu angelegte zeile. void XQViewModel::cmdNewUndo( XQCommand& command ) { } //! schaltet eine section sichtbar oder unsichtbar. void XQViewModel::cmdToggleSection( XQCommand& command ) { const QModelIndex& index = command.originIndex(); Q_ASSERT(index.isValid()); int fstRow = _sections.firstRow( index ); int lstRow = _sections.lastRow( index ); bool hidden =_treeTable->isRowHidden( fstRow, _treeTable->rootIndex() ); for (int row = fstRow; row < lstRow; ++row ) _treeTable->setRowHidden( row, _treeTable->rootIndex(), !hidden ); } //! git 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; // connect 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; 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; }