/* Copyright (C) 2013 * swift project Community / Contributors * * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, * including this file, may be copied, modified, propagated, or distributed except according to the terms * contained in the LICENSE file. */ #include "viewbase.h" #include "blackmisc/fileutils.h" #include "blackmisc/project.h" #include "blackgui/models/allmodels.h" #include "blackgui/stylesheetutility.h" #include "blackgui/guiutility.h" #include "blackgui/shortcut.h" #include "blackcore/registermetadata.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace BlackMisc; using namespace BlackGui; using namespace BlackGui::Menus; using namespace BlackGui::Models; using namespace BlackGui::Filters; namespace BlackGui { namespace Views { CViewBaseNonTemplate::CViewBaseNonTemplate(QWidget *parent) : QTableView(parent) { this->setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &QWidget::customContextMenuRequested, this, &CViewBaseNonTemplate::ps_customMenuRequested); connect(this, &QTableView::clicked, this, &CViewBaseNonTemplate::ps_clicked); connect(this, &QTableView::doubleClicked, this, &CViewBaseNonTemplate::ps_doubleClicked); this->horizontalHeader()->setSortIndicatorShown(true); // scroll modes this->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); this->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); this->setWordWrap(true); // shortcuts QShortcut *filter = new QShortcut(CShortcut::keyDisplayFilter(), this, SLOT(ps_displayFilterDialog()), nullptr, Qt::WidgetShortcut); filter->setObjectName("Filter shortcut for " + this->objectName()); QShortcut *clearSelection = new QShortcut(CShortcut::keyClearSelection(), this, SLOT(clearSelection()), nullptr, Qt::WidgetShortcut); clearSelection->setObjectName("Clear selection shortcut for " + this->objectName()); QShortcut *saveJson = new QShortcut(CShortcut::keySaveViews(), this, SLOT(ps_saveJsonShortcut()), nullptr, Qt::WidgetShortcut); saveJson->setObjectName("Save JSON for " + this->objectName()); QShortcut *deleteRow = new QShortcut(CShortcut::keyDelete(), this, SLOT(ps_removeSelectedRows()), nullptr, Qt::WidgetShortcut); deleteRow->setObjectName("Delete selected rows for " + this->objectName()); } bool CViewBaseNonTemplate::setParentDockWidgetInfoArea(CDockWidgetInfoArea *parentDockableWidget) { bool c = CEnableForDockWidgetInfoArea::setParentDockWidgetInfoArea(parentDockableWidget); return c; } void CViewBaseNonTemplate::resizeToContents() { this->performModeBasedResizeToContent(); } void CViewBaseNonTemplate::setFilterWidgetImpl(QWidget *filterWidget) { // dialog or filter widget if (this->m_filterWidget) { disconnect(this->m_filterWidget); this->menuRemoveItems(MenuFilter); if (m_filterWidget->parent() == this) { m_filterWidget->deleteLater(); } m_filterWidget = nullptr; } if (filterWidget) { this->menuAddItems(MenuFilter); this->m_filterWidget = filterWidget; } } void CViewBaseNonTemplate::setFilterDialog(CFilterDialog *filterDialog) { this->setFilterWidgetImpl(filterDialog); if (filterDialog) { bool s = connect(filterDialog, &CFilterDialog::finished, this, &CViewBaseNonTemplate::ps_filterDialogFinished); Q_ASSERT_X(s, Q_FUNC_INFO, "filter dialog connect"); Q_UNUSED(s); } } void CViewBaseNonTemplate::setFilterWidget(CFilterWidget *filterWidget) { this->setFilterWidgetImpl(filterWidget); if (filterWidget) { bool s = connect(filterWidget, &CFilterWidget::changeFilter, this, &CViewBaseNonTemplate::ps_filterWidgetChangedFilter); Q_ASSERT_X(s, Q_FUNC_INFO, "filter connect"); s = connect(this, &CViewBaseNonTemplate::rowCountChanged, filterWidget, &CFilterWidget::onRowCountChanged); Q_ASSERT_X(s, Q_FUNC_INFO, "filter connect"); Q_UNUSED(s); } } void CViewBaseNonTemplate::enableLoadIndicator(bool enable) { m_enabledLoadIndicator = enable; } bool CViewBaseNonTemplate::isShowingLoadIndicator() const { return m_enabledLoadIndicator && m_showingLoadIndicator; } void CViewBaseNonTemplate::setSelectionModel(QItemSelectionModel *model) { if (this->selectionModel()) { disconnect(this->selectionModel()); } QTableView::setSelectionModel(model); if (this->selectionModel()) { connect(this->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &CViewBaseNonTemplate::ps_rowSelected); } } QWidget *CViewBaseNonTemplate::mainApplicationWindowWidget() const { return CGuiUtility::mainApplicationWindow(); } CStatusMessage CViewBaseNonTemplate::showFileLoadDialog() { return this->ps_loadJson(); } CStatusMessage CViewBaseNonTemplate::showFileSaveDialog() const { return this->ps_saveJson(); } IMenuDelegate *CViewBaseNonTemplate::setCustomMenu(IMenuDelegate *menu, bool nestPreviousMenu) { if (menu && nestPreviousMenu) { // new menu with nesting menu->setNestedDelegate(this->m_menu); m_menu = menu; } else if (!menu && nestPreviousMenu) { // nested new menu m_menu = m_menu->getNestedDelegate(); } else { // no nesting m_menu = menu; } return menu; } void CViewBaseNonTemplate::customMenu(QMenu &menu) const { // delegate? if (this->m_menu) { this->m_menu->customMenu(menu); } // separator if (!menu.isEmpty() && ((this->m_menus & MenuStandard) > 0)) { menu.addSeparator(); } // standard menus int items = menu.actions().size(); if (this->m_menus.testFlag(MenuRefresh)) { menu.addAction(BlackMisc::CIcons::refresh16(), "Update", this, SIGNAL(requestUpdate())); } if (this->m_menus.testFlag(MenuBackend)) { menu.addAction(BlackMisc::CIcons::refresh16(), "Reload from backend", this, SIGNAL(requestNewBackendData())); } if (this->m_menus.testFlag(MenuClear)) { menu.addAction(BlackMisc::CIcons::delete16(), "Clear", this, &CViewBaseNonTemplate::ps_clear); } if (this->m_menus.testFlag(MenuRemoveSelectedRows)) { if (this->hasSelection()) { menu.addAction(BlackMisc::CIcons::delete16(), "Remove selected rows", this, &CViewBaseNonTemplate::ps_removeSelectedRows, CShortcut::keyDelete()); } } if (this->m_menus.testFlag(MenuDisplayAutomatically)) { QAction *a = menu.addAction(CIcons::appMappings16(), "Automatically display (when loaded)", this, &CViewBaseNonTemplate::ps_toggleAutoDisplay); a->setCheckable(true); a->setChecked(this->displayAutomatically()); } if (menu.actions().size() > items) { menu.addSeparator(); } items = menu.actions().size(); if (this->m_menus.testFlag(MenuFilter)) { menu.addAction(CIcons::filter16(), "Filter", this, &CViewBaseNonTemplate::ps_displayFilterDialog, CShortcut::keyDisplayFilter()); menu.addAction(CIcons::filter16(), "Remove Filter", this, &CViewBaseNonTemplate::ps_removeFilter); } if (menu.actions().size() > items) { menu.addSeparator(); } // selection menus items = menu.actions().size(); SelectionMode sm = this->selectionMode(); if (sm == MultiSelection || sm == ExtendedSelection) { menu.addAction(QIcon(), "Select all", this, &CViewBaseNonTemplate::selectAll, Qt::CTRL + Qt::Key_A); } if ((this->m_originalSelectionMode == MultiSelection || this->m_originalSelectionMode == ExtendedSelection) && this->m_menus.testFlag(MenuToggleSelectionMode)) { if (sm != MultiSelection) { QAction *a = menu.addAction(QIcon(), "Switch to multi selection", this, &CViewBaseNonTemplate::ps_toggleSelectionMode); a->setData(MultiSelection); } if (sm != ExtendedSelection) { QAction *a = menu.addAction(QIcon(), "Switch to extended selection", this, &CViewBaseNonTemplate::ps_toggleSelectionMode); a->setData(ExtendedSelection); } if (sm != SingleSelection) { QAction *a = menu.addAction(QIcon(), "Switch to single selection", this, &CViewBaseNonTemplate::ps_toggleSelectionMode); a->setData(SingleSelection); } } if (sm != NoSelection) { menu.addAction(QIcon(), "Clear selection", this, &CViewBaseNonTemplate::clearSelection, CShortcut::keyClearSelection()); } if (menu.actions().size() > items) { menu.addSeparator(); } // load/save items = menu.actions().size(); if (m_menus.testFlag(MenuLoad)) { menu.addAction(CIcons::disk16(), "Load from file", this, &CViewBaseNonTemplate::ps_loadJson); } if (m_menus.testFlag(MenuSave) && !isEmpty()) { menu.addAction(CIcons::disk16(), "Save data in file", this, &CViewBaseNonTemplate::ps_saveJson, CShortcut::keySaveViews()); } if (menu.actions().size() > items) { menu.addSeparator(); } // resizing menu.addAction(BlackMisc::CIcons::resize16(), "Full resize", this, &CViewBaseNonTemplate::fullResizeToContents); // resize to content might decrease performance, // so I only allow changing to "content resizing" if size matches bool enabled = !this->reachedResizeThreshold(); bool autoResize = this->m_resizeMode == ResizingAuto; // when not auto let set how we want to resize rows if (m_rowResizeMode == Interactive) { QAction *a = menu.addAction(BlackMisc::CIcons::resizeVertical16(), " Resize rows to content (auto)", this, &CViewBaseNonTemplate::rowsResizeModeToContent); a->setEnabled(enabled && !autoResize); } else { QAction *a = menu.addAction(BlackMisc::CIcons::resizeVertical16(), "Resize rows interactive", this, &CViewBaseNonTemplate::rowsResizeModeToInteractive); a->setEnabled(!autoResize); } QAction *actionInteractiveResize = new QAction(&menu); actionInteractiveResize->setObjectName(this->objectName().append("ActionResizing")); actionInteractiveResize->setIconText("Resize (auto)"); actionInteractiveResize->setIcon(CIcons::viewMultiColumn()); actionInteractiveResize->setCheckable(true); actionInteractiveResize->setChecked(autoResize); actionInteractiveResize->setEnabled(enabled); menu.addAction(actionInteractiveResize); connect(actionInteractiveResize, &QAction::toggled, this, &CViewBaseNonTemplate::ps_toggleResizeMode); if (m_showingLoadIndicator) { // just in case, if this ever will be dangling menu.addAction(BlackMisc::CIcons::preloader16(), "Hide load indicator", this, &CViewBaseNonTemplate::hideLoadIndicator); } } void CViewBaseNonTemplate::paintEvent(QPaintEvent *event) { QTableView::paintEvent(event); // CStyleSheetUtility::useStyleSheetInDerivedWidget(this, QStyle::PE_Widget); } void CViewBaseNonTemplate::showEvent(QShowEvent *event) { if (this->isShowingLoadIndicator()) { // re-center this->centerLoadIndicator(); } QTableView::showEvent(event); } void CViewBaseNonTemplate::allowDragDropValueObjects(bool allowDrag, bool allowDrop) { // see model for implementing logic of drag this->setAcceptDrops(allowDrop); this->setDragEnabled(allowDrag); this->setDropIndicatorShown(allowDrop); CDropBase::allowDrop(allowDrop); } void CViewBaseNonTemplate::allowDrop(bool allow) { this->allowDragDropValueObjects(this->dragEnabled(), allow); } bool CViewBaseNonTemplate::isDropAllowed() const { return this->acceptDrops(); } int CViewBaseNonTemplate::getHorizontalHeaderFontHeight() const { QFontMetrics m(this->getHorizontalHeaderFont()); int h = m.height(); return h; } bool CViewBaseNonTemplate::hasSelection() const { return this->selectionModel()->hasSelection(); } QModelIndexList CViewBaseNonTemplate::selectedRows() const { return this->selectionModel()->selectedRows(); } int CViewBaseNonTemplate::selectedRowCount() const { if (!this->hasSelection()) { return 0;} return this->selectedRows().count(); } bool CViewBaseNonTemplate::hasSingleSelectedRow() const { return this->selectedRowCount() == 1; } bool CViewBaseNonTemplate::hasMultipleSelectedRows() const { return this->selectedRowCount() > 1; } void CViewBaseNonTemplate::init() { int fh = qRound(1.5 * this->getHorizontalHeaderFontHeight()); this->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); // faster mode this->horizontalHeader()->setStretchLastSection(true); this->verticalHeader()->setDefaultSectionSize(fh); // for height this->verticalHeader()->setMinimumSectionSize(fh); // for height switch (this->m_rowResizeMode) { case Interactive: this->rowsResizeModeToInteractive(); break; case Content: this->rowsResizeModeToContent(); break; default: Q_ASSERT_X(false, Q_FUNC_INFO, "wrong resize mode"); break; } } QString CViewBaseNonTemplate::getDefaultFilename(bool load) const { // some logic to find a useful default name QString dir = CProject::getDocumentationDirectory(); if (load) { return CFileUtils::appendFilePaths(dir, CFileUtils::jsonWildcardAppendix()); } // Save file path QString name(this->m_saveFileName); if (name.isEmpty()) { // create a name if (this->getDockWidgetInfoArea()) { name = this->getDockWidgetInfoArea()->windowTitle(); } else if (!windowTitle().isEmpty()) { name = this->windowType(); } else { name = this->metaObject()->className(); } } if (!name.endsWith(CFileUtils::jsonAppendix(), Qt::CaseInsensitive)) { name += CFileUtils::jsonAppendix(); } return CFileUtils::appendFilePaths(dir, name); } void CViewBaseNonTemplate::menuRemoveItems(Menu menusToRemove) { this->m_menus &= (~menusToRemove); } void CViewBaseNonTemplate::menuAddItems(Menu menusToAdd) { this->m_menus |= menusToAdd; if (menusToAdd.testFlag(MenuRemoveSelectedRows)) { this->m_enableDeleteSelectedRows = true; } } int CViewBaseNonTemplate::ps_updateContainer(const CVariant &variant, bool sort, bool resize) { return this->performUpdateContainer(variant, sort, resize); } void CViewBaseNonTemplate::ps_displayFilterDialog() { if (!this->m_menus.testFlag(MenuFilter)) { return; } if (!this->m_filterWidget) { return; } this->m_filterWidget->show(); } void CViewBaseNonTemplate::ps_saveJsonShortcut() { if (this->isEmpty()) { return; } if (!this->m_menus.testFlag(MenuSave)) { return; } this->ps_saveJson(); } void CViewBaseNonTemplate::rowsResizeModeToInteractive() { const int height = this->verticalHeader()->minimumSectionSize(); QHeaderView *verticalHeader = this->verticalHeader(); Q_ASSERT_X(verticalHeader, Q_FUNC_INFO, "Missing vertical header"); verticalHeader->setSectionResizeMode(QHeaderView::Interactive); verticalHeader->setDefaultSectionSize(height); this->m_rowResizeMode = Interactive; } void CViewBaseNonTemplate::rowsResizeModeToContent() { QHeaderView *verticalHeader = this->verticalHeader(); Q_ASSERT(verticalHeader); verticalHeader->setSectionResizeMode(QHeaderView::ResizeToContents); this->m_rowResizeMode = Content; } void CViewBaseNonTemplate::showLoadIndicator(int containerSizeDependent) { if (!m_enabledLoadIndicator) { return; } if (this->m_showingLoadIndicator) { return; } if (this->hasDockWidgetArea()) { if (!this->isVisibleWidget()) { return; } } if (containerSizeDependent >= 0) { // really with indicator? if (containerSizeDependent < ResizeSubsetThreshold) { return; } } this->m_showingLoadIndicator = true; emit loadIndicatorVisibilityChanged(this->m_showingLoadIndicator); // this->setStyleSheet(styleSheet()); if (!this->m_loadIndicator) { this->m_loadIndicator = new CLoadIndicator(64, 64, this); } this->centerLoadIndicator(); this->m_loadIndicator->startAnimation(); } void CViewBaseNonTemplate::centerLoadIndicator() { if (!m_loadIndicator) { return; } QPoint middle = this->viewport()->geometry().center(); int w = m_loadIndicator->width(); int h = m_loadIndicator->height(); int x = middle.x() - w / 2; int y = middle.y() - h / 2; this->m_loadIndicator->setGeometry(x, y, w, h); } void CViewBaseNonTemplate::hideLoadIndicator() { if (!this->m_showingLoadIndicator) { return; } this->m_showingLoadIndicator = false; emit loadIndicatorVisibilityChanged(this->m_showingLoadIndicator); if (!this->m_loadIndicator) { return; } this->m_loadIndicator->stopAnimation(); } bool CViewBaseNonTemplate::isResizeConditionMet(int containerSize) const { if (m_resizeMode == PresizeSubset) { return false; } if (m_resizeMode == ResizingOff) { return false; } if (m_resizeMode == ResizingOnce) { return m_resizeCount < 1; } if (m_resizeMode == ResizingAuto) { if (reachedResizeThreshold(containerSize)) { return false; } if (m_resizeAutoNthTime < 2) { return true; } return (m_resizeCount % m_resizeAutoNthTime) == 0; } return false; } void CViewBaseNonTemplate::fullResizeToContents() { this->setVisible(false); // magic trick from: // http://stackoverflow.com/q/3433664/356726 const QRect vporig = this->viewport()->geometry(); QRect vpnew = vporig; vpnew.setWidth(std::numeric_limits::max()); this->viewport()->setGeometry(vpnew); this->m_resizeCount++; this->resizeColumnsToContents(); // columns this->resizeRowsToContents(); // rows if (m_forceStretchLastColumnWhenResized) { // re-stretch this->horizontalHeader()->setStretchLastSection(true); } this->viewport()->setGeometry(vporig); this->setVisible(true); } void CViewBaseNonTemplate::ps_customMenuRequested(QPoint pos) { QMenu menu; this->customMenu(menu); if (menu.isEmpty()) { return; } QPoint globalPos = this->mapToGlobal(pos); menu.exec(globalPos); } void CViewBaseNonTemplate::ps_toggleResizeMode(bool checked) { if (checked) { this->m_resizeMode = ResizingAuto; } else { this->m_resizeMode = ResizingOff; } } void CViewBaseNonTemplate::ps_toggleAutoDisplay() { QAction *a = qobject_cast(QObject::sender()); if (!a) { return; } Q_ASSERT_X(a->isCheckable(), Q_FUNC_INFO, "object not checkable"); this->m_displayAutomatically = a->isChecked(); } void CViewBaseNonTemplate::ps_toggleSelectionMode() { if (this->m_originalSelectionMode == ExtendedSelection || this->m_originalSelectionMode == MultiSelection) { QAction *action = qobject_cast(sender()); if (action && action->data().isValid() && action->data().canConvert()) { SelectionMode sm = static_cast(action->data().toInt()); this->setSelectionMode(sm); } else { this->setSelectionMode(this->m_originalSelectionMode); } } } void CViewBaseNonTemplate::ps_removeSelectedRows() { if (!m_enableDeleteSelectedRows) { return; } this->removeSelectedRows(); } void CViewBaseNonTemplate::ps_updatedIndicator() { this->update(); } void CViewBaseNonTemplate::dragEnterEvent(QDragEnterEvent *event) { if (!event || !acceptDrop(event->mimeData())) { return; } setBackgroundRole(QPalette::Highlight); event->acceptProposedAction(); } void CViewBaseNonTemplate::dragMoveEvent(QDragMoveEvent *event) { if (!event || !acceptDrop(event->mimeData())) { return; } event->acceptProposedAction(); } void CViewBaseNonTemplate::dragLeaveEvent(QDragLeaveEvent *event) { if (!event) { return; } event->accept(); } void CViewBaseNonTemplate::dropEvent(QDropEvent *event) { Q_UNUSED(event); } template CViewBase::CViewBase(QWidget *parent, ModelClass *model) : CViewBaseNonTemplate(parent), m_model(model) { this->setSortingEnabled(true); if (model) { this->setModel(this->m_model); } } template int CViewBase::updateContainer(const ContainerType &container, bool sort, bool resize) { Q_ASSERT_X(this->m_model, Q_FUNC_INFO, "Missing model"); if (container.isEmpty()) { // shortcut this->clear(); return 0; } // we have data this->showLoadIndicator(container.size()); bool reallyResize = resize && isResizeConditionMet(container.size()); // do we really perform resizing bool presize = (m_resizeMode == PresizeSubset) && this->isEmpty() && // only when no data yet !reallyResize; // not when we resize later presize = presize || (this->isEmpty() && resize && !reallyResize); // we presize if we wanted to resize but actually do not because of condition const bool presizeThresholdReached = presize && container.size() > ResizeSubsetThreshold; // only when size making sense // when we will not resize, we might presize if (presizeThresholdReached) { const int presizeRandomElements = container.size() > 1000 ? container.size() / 100 : container.size() / 40; if (presizeRandomElements > 0) { this->m_model->update(container.sampleElements(presizeRandomElements), false); this->fullResizeToContents(); } } int c = this->m_model->update(container, sort); // resize after real update according to mode if (presizeThresholdReached) { // currently no furhter actions } else if (reallyResize) { this->resizeToContents(); } else if (presize && !presizeThresholdReached) { // small amount of data not covered before this->fullResizeToContents(); } this->hideLoadIndicator(); return c; } template BlackMisc::CWorker *CViewBase::updateContainerAsync(const ContainerType &container, bool sort, bool resize) { Q_UNUSED(sort); ModelClass *model = this->derivedModel(); auto sortColumn = model->getSortColumn(); auto sortOrder = model->getSortOrder(); this->showLoadIndicator(container.size()); BlackMisc::CWorker *worker = BlackMisc::CWorker::fromTask(this, "ViewSort", [model, container, sortColumn, sortOrder]() { return model->sortContainerByColumn(container, sortColumn, sortOrder); }); worker->thenWithResult(this, [this, resize](const ContainerType & sortedContainer) { this->ps_updateContainer(CVariant::from(sortedContainer), false, resize); }); worker->then(this, &CViewBase::asyncUpdateFinished); return worker; } template void CViewBase::updateContainerMaybeAsync(const ContainerType &container, bool sort, bool resize) { if (container.size() > ASyncRowsCountThreshold && sort) { // larger container with sorting updateContainerAsync(container, sort, resize); } else { updateContainer(container, sort, resize); } } template void CViewBase::insert(const ObjectType &value, bool resize) { Q_ASSERT(this->m_model); this->m_model->insert(value); if (resize) { this->performModeBasedResizeToContent(); } } template void CViewBase::insert(const ContainerType &container, bool resize) { Q_ASSERT(this->m_model); this->m_model->insert(container); if (resize) { this->performModeBasedResizeToContent(); } } template const ObjectType &CViewBase::at(const QModelIndex &index) const { Q_ASSERT(this->m_model); return this->m_model->at(index); } template const ContainerType &CViewBase::container() const { Q_ASSERT(this->m_model); return this->m_model->container(); } template const ContainerType &CViewBase::containerOrFilteredContainer() const { Q_ASSERT(this->m_model); return this->m_model->containerOrFilteredContainer(); } template ContainerType CViewBase::selectedObjects() const { if (!this->hasSelection()) { return ContainerType(); } ContainerType c; QModelIndexList indexes = this->selectedRows(); for (const QModelIndex &i : indexes) { c.push_back(this->at(i)); } return c; } template int CViewBase::updateSelected(const CPropertyIndexVariantMap &vm) { if (vm.isEmpty()) { return 0; } if (!hasSelection()) { return 0; } int c = 0; QModelIndexList indexes = this->selectedRows(); int lastUpdatedRow = -1; int firstUpdatedRow = -1; const CPropertyIndexList pis(vm.indexes()); for (const QModelIndex &i : indexes) { if (i.row() == lastUpdatedRow) { continue; } lastUpdatedRow = i.row(); if (firstUpdatedRow < 0 || lastUpdatedRow < firstUpdatedRow) { firstUpdatedRow = lastUpdatedRow; } ObjectType obj(this->at(i)); // update all properties in map for (const CPropertyIndex &pi : pis) { obj.setPropertyByIndex(vm.value(pi), pi); } // and update container if (this->derivedModel()->setInContainer(i, obj)) { c++; } } if (c > 0) { this->derivedModel()->sendDataChanged(firstUpdatedRow, lastUpdatedRow); } return c; } template int CViewBase::updateSelected(const CVariant &variant, const CPropertyIndex &index) { const CPropertyIndexVariantMap vm(index, variant); return this->updateSelected(vm); } template ObjectType CViewBase::selectedObject() const { ContainerType c = this->selectedObjects(); return c.frontOrDefault(); } template int CViewBase::removeSelectedRows() { if (!this->hasSelection()) { return 0; } if (this->isEmpty()) { return 0; } int currentRows = this->rowCount(); if (currentRows == selectedRowCount()) { this->clear(); return currentRows; } ContainerType selected(selectedObjects()); ContainerType newObjects(container()); for (const ObjectType &obj : selected) { newObjects.remove(obj); } int delta = currentRows - newObjects.size(); this->updateContainerMaybeAsync(newObjects); return delta; } template int CViewBase::rowCount() const { Q_ASSERT(this->m_model); return this->m_model->rowCount(); } template int CViewBase::columnCount() const { Q_ASSERT(this->m_model); return this->m_model->columnCount(QModelIndex()); } template bool CViewBase::isEmpty() const { Q_ASSERT(this->m_model); return this->m_model->rowCount() < 1; } template QJsonObject CViewBase::toJson() const { Q_ASSERT(this->m_model); return this->m_model->toJson(); } template QString CViewBase::toJsonString(QJsonDocument::JsonFormat format) const { Q_ASSERT(this->m_model); return this->m_model->toJsonString(format); } template void CViewBase::setObjectName(const QString &name) { // then name here is mainly set for debugging purposes so each model can be identified Q_ASSERT(m_model); QTableView::setObjectName(name); QString modelName = QString(name).append(':').append(this->m_model->getTranslationContext()); this->m_model->setObjectName(modelName); } template void CViewBase::takeFilterOwnership(std::unique_ptr > &filter) { this->derivedModel()->takeFilterOwnership(filter); } template void CViewBase::removeFilter() { this->derivedModel()->removeFilter(); } template bool CViewBase::hasFilter() const { return derivedModel()->hasFilter(); } template void CViewBase::setSortIndicator() { if (this->m_model->hasValidSortColumn()) { Q_ASSERT(this->horizontalHeader()); this->horizontalHeader()->setSortIndicator( this->m_model->getSortColumn(), this->m_model->getSortOrder()); } } template void CViewBase::standardInit(ModelClass *model) { Q_ASSERT_X(model || this->m_model, Q_FUNC_INFO, "Missing model"); if (model) { this->m_model = model; connect(this->m_model, &ModelClass::rowCountChanged, this, &CViewBase::rowCountChanged); connect(this->m_model, &ModelClass::objectChanged, this, &CViewBase::objectChanged); connect(this->m_model, &ModelClass::changed, this, &CViewBase::modelChanged); } this->setModel(this->m_model); // via QTableView CViewBaseNonTemplate::init(); this->setSortIndicator(); } template bool CViewBase::reachedResizeThreshold(int containerSize) const { if (containerSize < 0) { return this->rowCount() > m_skipResizeThreshold; } return containerSize > m_skipResizeThreshold; } template void CViewBase::performModeBasedResizeToContent() { // small set or large set? if (this->isResizeConditionMet()) { this->fullResizeToContents(); } else { this->m_resizeCount++; // skipped resize } } template int CViewBase::performUpdateContainer(const BlackMisc::CVariant &variant, bool sort, bool resize) { ContainerType c(variant.to()); return this->updateContainer(c, sort, resize); } template CStatusMessage CViewBase::ps_loadJson() { static const CLogCategoryList cats(CLogCategoryList(this).join({ CLogCategory::validation()})); const QString fileName = QFileDialog::getOpenFileName(nullptr, tr("Load data file"), getDefaultFilename(true), tr("swift (*.json *.txt)")); if (fileName.isEmpty()) { return CStatusMessage(cats, CStatusMessage::SeverityDebug, "Load canceled"); } QString json(CFileUtils::readFileToString(fileName)); if (json.isEmpty()) { return CStatusMessage(cats, CStatusMessage::SeverityWarning, "Reading " + fileName + " yields no data"); } ContainerType container; container.convertFromJson(json); this->updateContainerMaybeAsync(container); return CStatusMessage(cats, CStatusMessage::SeverityInfo, "Reading " + fileName + " completed"); } template CStatusMessage CViewBase::ps_saveJson() const { static const CLogCategoryList cats(CLogCategoryList(this).join({ CLogCategory::validation()})); const QString fileName = QFileDialog::getSaveFileName(nullptr, tr("Save data file"), getDefaultFilename(false), tr("swift (*.json *.txt)")); if (fileName.isEmpty()) { return CStatusMessage(cats, CStatusMessage::SeverityDebug, "Save canceled"); } const QString json(this->toJsonString()); bool ok = CFileUtils::writeStringToFileInBackground(json, fileName); if (ok) { return CStatusMessage(cats, CStatusMessage::SeverityInfo, "Writing " + fileName + " in progress"); } else { return CStatusMessage(cats, CStatusMessage::SeverityError, "Writing " + fileName + " failed"); } } template bool CViewBase::ps_filterDialogFinished(int status) { QDialog::DialogCode statusCode = static_cast(status); return ps_filterWidgetChangedFilter(statusCode == QDialog::Accepted); } template bool CViewBase::ps_filterWidgetChangedFilter(bool enabled) { if (enabled) { if (!this->m_filterWidget) { this->removeFilter(); } else { // takes the filter and triggers the filtering IModelFilterProvider *provider = dynamic_cast*>(this->m_filterWidget); Q_ASSERT_X(provider, Q_FUNC_INFO, "Filter widget does not provide interface"); if (!provider) { return false; } std::unique_ptr> f(provider->createModelFilter()); this->takeFilterOwnership(f); } } else { // no filter this->removeFilter(); } return true; // handled } template void CViewBase::ps_removeFilter() { this->derivedModel()->removeFilter(); } template void CViewBase::ps_clicked(const QModelIndex &index) { if (!m_acceptClickSelection) { return; } if (!index.isValid()) { return; } emit objectClicked(CVariant::fromValue(at(index))); } template void CViewBase::ps_doubleClicked(const QModelIndex &index) { if (!m_acceptDoubleClickSelection) { return; } if (!index.isValid()) { return; } emit objectDoubleClicked(CVariant::fromValue(at(index))); } template void CViewBase::ps_rowSelected(const QModelIndex &index) { if (!m_acceptRowSelected) { return; } if (!index.isValid()) { return; } emit objectSelected(CVariant::fromValue(at(index))); } // see here for the reason of thess forward instantiations // http://www.parashift.com/c++-faq/separate-template-class-defn-from-decl.html template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; } // namespace } // namespace