Files
xtree/src/model/xqviewmodel.cpp
Christoph Holzheuer 95b7b026ff --- mess
2025-09-08 15:42:15 +02:00

617 lines
17 KiB
C++

/***************************************************************************
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 <QMessageBox>
#include <QUndoStack>
#include <xqexception.h>
#include <xqviewmodel.h>
#include <xqselectionmodel.h>
#include <xqtreetable.h>
#include <xqcommand.h>
#include <xqitemdelegate.h>
#include <xqitemfactory.h>
#include <znode_factory.h>
// 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<XQItem*>(item);
emit xqItemChanged( *xqItem );
});
//qRegisterMetaType<XQItem>("XQItem");
}
//! gibt einen static-cast<QXItem*> 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<XQItem*>(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*>(xqItem);
}
return XQItem::fallBackDummyItem();
}
//! hilfsfunktiom, die das erste xqitem einer zeile zurückgibt.
XQItem& XQViewModel::xqFirstItem(int row) const
{
return *static_cast<XQItem*>( 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 -> <DocumentTreeModel>
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<XQCommand::CmdType>();
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<XQCommand&>(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<XQCommand&>(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<int, QByteArray> XQViewModel::roleNames() const
{
QHash<int, QByteArray> 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;
}