/* 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 "blackconfig/buildconfig.h" #include "blackgui/dockwidgetinfoarea.h" #include "blackgui/filters/filterdialog.h" #include "blackgui/filters/filterwidget.h" #include "blackgui/guiapplication.h" #include "blackgui/guiutility.h" #include "blackgui/loadindicator.h" #include "blackgui/models/allmodels.h" #include "blackgui/models/allmodelcontainers.h" #include "blackgui/menus/menuaction.h" #include "blackgui/menus/menudelegate.h" #include "blackgui/menus/fontmenus.h" #include "blackgui/shortcut.h" #include "blackgui/views/viewbase.h" #include "blackgui/views/viewbaseproxystyle.h" #include "blackgui/views/viewbaseitemdelegate.h" #include "blackmisc/fileutils.h" #include "blackmisc/icons.h" #include "blackmisc/logmessage.h" #include "blackmisc/namevariantpair.h" #include "blackmisc/namevariantpairlist.h" #include "blackmisc/propertyindexvariantmap.h" #include "blackmisc/worker.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace BlackConfig; using namespace BlackMisc; using namespace BlackGui; using namespace BlackGui::Menus; using namespace BlackGui::Models; using namespace BlackGui::Filters; using namespace BlackGui::Settings; namespace BlackGui { namespace Views { CViewBaseNonTemplate::CViewBaseNonTemplate(QWidget *parent) : QTableView(parent) { this->setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &QWidget::customContextMenuRequested, this, &CViewBaseNonTemplate::customMenuRequested); connect(this, &QTableView::clicked, this, &CViewBaseNonTemplate::ps_clicked); connect(this, &QTableView::doubleClicked, this, &CViewBaseNonTemplate::ps_doubleClicked); this->horizontalHeader()->setSortIndicatorShown(true); // setting resize mode rowsResizeModeToContent() causes extremly slow views // default, see: m_rowResizeMode // scroll modes this->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); this->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); this->setWordWrap(true); this->setTextElideMode(Qt::ElideMiddle); // shortcuts QShortcut *filter = new QShortcut(CShortcut::keyDisplayFilter(), this); connect(filter, &QShortcut::activated, this, &CViewBaseNonTemplate::displayFilterDialog); filter->setObjectName("Filter shortcut for " + this->objectName()); filter->setContext(Qt::WidgetShortcut); QShortcut *clearSelection = new QShortcut(CShortcut::keyClearSelection(), this); connect(clearSelection, &QShortcut::activated, this, &CViewBaseNonTemplate::clearSelection); clearSelection->setObjectName("Clear selection shortcut for " + this->objectName()); clearSelection->setContext(Qt::WidgetShortcut); QShortcut *saveJson = new QShortcut(CShortcut::keySaveViews(), this); connect(saveJson, &QShortcut::activated, this, &CViewBaseNonTemplate::saveJsonAction); saveJson->setObjectName("Save JSON for " + this->objectName()); saveJson->setContext(Qt::WidgetShortcut); QShortcut *deleteRow = new QShortcut(CShortcut::keyDelete(), this); connect(deleteRow, &QShortcut::activated, this, &CViewBaseNonTemplate::removeSelectedRowsChecked); deleteRow->setObjectName("Remove selected rows for " + this->objectName()); deleteRow->setContext(Qt::WidgetShortcut); QShortcut *copy = new QShortcut(CShortcut::keyCopy(), this); connect(copy, &QShortcut::activated, this, &CViewBaseNonTemplate::ps_copy); copy->setObjectName("Copy selection shortcut for " + this->objectName()); copy->setContext(Qt::WidgetShortcut); } CViewBaseNonTemplate::~CViewBaseNonTemplate() { // dtor } bool CViewBaseNonTemplate::setParentDockWidgetInfoArea(CDockWidgetInfoArea *parentDockableWidget) { const bool c = CEnableForDockWidgetInfoArea::setParentDockWidgetInfoArea(parentDockableWidget); return c; } void CViewBaseNonTemplate::resizeToContents() { this->performModeBasedResizeToContent(); } void CViewBaseNonTemplate::setFilterWidgetImpl(QWidget *filterWidget) { if (filterWidget == m_filterWidget) { return; } // dialog or filter widget if (m_filterWidget) { disconnect(m_filterWidget); this->menuRemoveItems(MenuFilter); if (m_filterWidget->parent() == this) { m_filterWidget->deleteLater(); } m_filterWidget = nullptr; } if (filterWidget) { this->menuAddItems(MenuFilter); m_filterWidget = filterWidget; } } void CViewBaseNonTemplate::setFilterDialog(CFilterDialog *filterDialog) { if (filterDialog == m_filterWidget) { return; } this->setFilterWidgetImpl(filterDialog); if (filterDialog) { const 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) { if (filterWidget == m_filterWidget) { return; } 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::modelDataChanged, 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_loadIndicator && 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::mainApplicationWidget(); } CStatusMessage CViewBaseNonTemplate::showFileLoadDialog() { return this->ps_loadJson(); } CStatusMessage CViewBaseNonTemplate::showFileSaveDialog() { return this->ps_saveJson(); } IMenuDelegate *CViewBaseNonTemplate::setCustomMenu(IMenuDelegate *menu, bool nestPreviousMenu) { if (menu && nestPreviousMenu) { // new menu with nesting menu->setNestedDelegate(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; } CMenuActions CViewBaseNonTemplate::initMenuActions(CViewBaseNonTemplate::MenuFlag menu) { if (m_menuFlagActions.contains(menu)) { return m_menuFlagActions.value(menu); } CMenuActions ma; switch (menu) { case MenuRefresh: { static const QMetaMethod requestSignal = QMetaMethod::fromSignal(&CViewBaseNonTemplate::requestUpdate); if (!this->isSignalConnected(requestSignal)) break; ma.addAction(CIcons::refresh16(), "Update", CMenuAction::pathViewUpdates(), { this, &CViewBaseNonTemplate::ps_triggerReload }); break; } case MenuBackend: { static const QMetaMethod requestSignal = QMetaMethod::fromSignal(&CViewBaseNonTemplate::requestNewBackendData); if (!this->isSignalConnected(requestSignal)) break; ma.addAction(CIcons::refresh16(), "Reload from backend", CMenuAction::pathViewUpdates(), { this, &CViewBaseNonTemplate::ps_triggerReloadFromBackend }); break; } case MenuDisplayAutomatically: { QAction *a = ma.addAction(CIcons::appMappings16(), "Automatically display (when loaded)", CMenuAction::pathViewUpdates(), { this, &CViewBaseNonTemplate::toggleAutoDisplay }); a->setCheckable(true); a->setChecked(this->displayAutomatically()); break; } case MenuRemoveSelectedRows: { ma.addAction(CIcons::delete16(), "Remove selected rows", CMenuAction::pathViewAddRemove(), { this, &CViewBaseNonTemplate::removeSelectedRowsChecked }, CShortcut::keyDelete()); break; } case MenuClear: { ma.addAction(CIcons::delete16(), "Clear", CMenuAction::pathViewAddRemove(), { this, &CViewBaseNonTemplate::clear }); break; } case MenuFilter: { if (m_filterWidget) { const bool dialog = qobject_cast(m_filterWidget); if (dialog) ma.addAction(CIcons::filter16(), "Show filter", CMenuAction::pathViewFilter(), { this, &CViewBaseNonTemplate::displayFilterDialog }, CShortcut::keyDisplayFilter()); ma.addAction(CIcons::filter16(), "Remove Filter", CMenuAction::pathViewFilter(), { this, &CViewBaseNonTemplate::ps_removeFilter }); } break; } case MenuMaterializeFilter: { ma.addAction(CIcons::tableRelationship16(), "Materialize filtered data", CMenuAction::pathViewFilter(), { this, &CViewBaseNonTemplate::materializeFilter }); break; } case MenuLoad: { ma.addAction(CIcons::disk16(), "Load from file", CMenuAction::pathViewLoadSave(), { this, &CViewBaseNonTemplate::ps_loadJsonAction }); break; } case MenuSave: { ma.addAction(CIcons::disk16(), "Save data in file", CMenuAction::pathViewLoadSave(), { this, &CViewBaseNonTemplate::saveJsonAction }, CShortcut::keySaveViews()); break; } case MenuCut: { if (!QApplication::clipboard()) break; ma.addAction(CIcons::cut16(), "Cut", CMenuAction::pathViewCutPaste(), { this, &CViewBaseNonTemplate::ps_cut }, QKeySequence(QKeySequence::Paste)); break; } case MenuPaste: { if (!QApplication::clipboard()) break; ma.addAction(CIcons::paste16(), "Paste", CMenuAction::pathViewCutPaste(), { this, &CViewBaseNonTemplate::ps_paste }, QKeySequence(QKeySequence::Paste)); break; } case MenuCopy: { if (!QApplication::clipboard()) break; ma.addAction(CIcons::copy16(), "Copy", CMenuAction::pathViewCutPaste(), { this, &CViewBaseNonTemplate::ps_copy }, QKeySequence(QKeySequence::Copy)); break; } default: break; } m_menuFlagActions.insert(menu, ma); return ma; } void CViewBaseNonTemplate::settingsChanged() { if (!this->allowsMultipleSelectedRows()) { return; } const CGeneralGuiSettings settings = m_guiSettings.getThreadLocal(); m_originalSelectionMode = settings.getPreferredSelection(); if (this->isCurrentlyAllowingMultipleRowSelections()) { this->setSelectionMode(settings.getPreferredSelection()); } } void CViewBaseNonTemplate::rememberLastJsonDirectory(const QString &selectedFileOrDir) { if (selectedFileOrDir.isEmpty()) { return; } const QFileInfo fi(selectedFileOrDir); if ((fi.isDir() && fi.exists()) || (fi.isFile() && fi.exists())) { // existing dir m_lastJsonDirectory = fi.absolutePath(); } } QString CViewBaseNonTemplate::getRememberedLastJsonDirectory() const { if (!m_lastJsonDirectory.isEmpty()) { return m_lastJsonDirectory; } QString dirCandidate = m_dirSettings.get(); const QFileInfo fi(dirCandidate); if ((fi.isDir() && fi.exists()) || (fi.isFile() && fi.exists())) { return fi.absolutePath(); } return QStringLiteral(""); } void CViewBaseNonTemplate::customMenu(CMenuActions &menuActions) { // delegate? if (m_menu) { m_menu->customMenu(menuActions); } // standard view menus if (m_menus.testFlag(MenuRefresh)) { menuActions.addActions(this->initMenuActions(MenuRefresh)); } if (m_menus.testFlag(MenuBackend)) { menuActions.addActions(this->initMenuActions(MenuBackend)); } if (m_showingLoadIndicator) { // just in case, if this ever will be dangling menuActions.addAction(CIcons::preloader16(), "Hide load indicator", CMenuAction::pathViewUpdates(), nullptr, { this, &CViewBaseNonTemplate::hideLoacIndicatorForced }); } if (m_menus.testFlag(MenuClear)) { menuActions.addActions(this->initMenuActions(MenuClear)); } if (m_menus.testFlag(MenuDisplayAutomatically)) { // here I expect only one action QAction *a = menuActions.addActions(this->initMenuActions(MenuDisplayAutomatically)).first(); a->setChecked(this->displayAutomatically()); } if (m_menus.testFlag(MenuRemoveSelectedRows)) { if (this->hasSelection()) { menuActions.addActions(this->initMenuActions(MenuRemoveSelectedRows)); } } if (m_menus.testFlag(MenuCopy)) { menuActions.addActions(this->initMenuActions(MenuCopy)); } if (m_menus.testFlag(MenuCut)) { menuActions.addActions(this->initMenuActions(MenuCut)); } if (m_menus.testFlag(MenuPaste)) { menuActions.addActions(this->initMenuActions(MenuPaste)); } if (m_menus.testFlag(MenuFont) && m_fontMenu) { menuActions.addActions(m_fontMenu->getActions(), CMenuAction::pathFont()); } if (m_menus.testFlag(MenuFilter) && m_filterWidget) { menuActions.addActions(this->initMenuActions(MenuFilter)); if (m_menus.testFlag(MenuMaterializeFilter)) { menuActions.addActions(this->initMenuActions(MenuMaterializeFilter)); } } // selection menus, not in menu action list because it depends on current selection const SelectionMode sm = this->selectionMode(); if (sm == MultiSelection || sm == ExtendedSelection) { menuActions.addAction("Select all", CMenuAction::pathViewSelection(), nullptr, { this, &CViewBaseNonTemplate::selectAll }, Qt::CTRL + Qt::Key_A); } if (sm != NoSelection) { menuActions.addAction("Clear selection", CMenuAction::pathViewSelection(), nullptr, { this, &CViewBaseNonTemplate::clearSelection }, CShortcut::keyClearSelection()); } if ((m_originalSelectionMode == MultiSelection || m_originalSelectionMode == ExtendedSelection) && m_menus.testFlag(MenuToggleSelectionMode)) { if (sm != MultiSelection) { menuActions.addAction("Switch to multi selection", CMenuAction::pathViewSelection(), nullptr, { this, &CViewBaseNonTemplate::setMultiSelection }); } if (sm != ExtendedSelection) { menuActions.addAction("Switch to extended selection", CMenuAction::pathViewSelection(), nullptr, { this, &CViewBaseNonTemplate::setExtendedSelection }); } if (sm != SingleSelection) { menuActions.addAction("Switch to single selection", CMenuAction::pathViewSelection(), nullptr, { this, &CViewBaseNonTemplate::setSingleSelection }); } } // load/save if (m_menus.testFlag(MenuLoad)) { menuActions.addActions(this->initMenuActions(MenuLoad)); } if (m_menus.testFlag(MenuSave) && !isEmpty()) { menuActions.addActions(this->initMenuActions(MenuSave)); } // resizing menuActions.addAction(CIcons::resize16(), "Resize", CMenuAction::pathViewResize(), nullptr, { this, &CViewBaseNonTemplate::presizeOrFullResizeToContents }); // resize to content might decrease performance, // so I only allow changing to "content resizing" if size matches const bool enabled = !this->reachedResizeThreshold(); const bool autoResize = (m_resizeMode == ResizingAuto); // when not set to auto, then lets set how we want to resize rows if (m_rowResizeMode == Interactive) { QAction *a = menuActions.addAction(CIcons::resizeVertical16(), " Resize rows to content (auto), can be slow", CMenuAction::pathViewResize(), nullptr, { this, &CViewBaseNonTemplate::rowsResizeModeToContent }); a->setEnabled(enabled && !autoResize); } else { QAction *a = menuActions.addAction(CIcons::resizeVertical16(), "Resize rows interactively", CMenuAction::pathViewResize(), nullptr, { this, &CViewBaseNonTemplate::rowsResizeModeToInteractive }); a->setEnabled(!autoResize); } QAction *actionInteractiveResize = menuActions.addAction(CIcons::viewMultiColumn(), "Resize (auto)", CMenuAction::pathViewResize(), nullptr); actionInteractiveResize->setObjectName(this->objectName().append("ActionResizing")); actionInteractiveResize->setCheckable(true); actionInteractiveResize->setChecked(autoResize); actionInteractiveResize->setEnabled(enabled); connect(actionInteractiveResize, &QAction::toggled, this, &CViewBaseNonTemplate::toggleResizeMode); } void CViewBaseNonTemplate::resizeEvent(QResizeEvent *event) { if (this->isShowingLoadIndicator()) { // re-center this->centerLoadIndicator(); } QTableView::resizeEvent(event); } int CViewBaseNonTemplate::getHorizontalHeaderFontHeight() const { const QFontMetrics m(this->getHorizontalHeaderFont()); const int h = m.height(); return h; } bool CViewBaseNonTemplate::hasSelection() const { return this->selectionModel()->hasSelection(); } QModelIndexList CViewBaseNonTemplate::selectedRows() const { return this->selectionModel()->selectedRows(); } QModelIndexList CViewBaseNonTemplate::unselectedRows() const { QModelIndexList selected = this->selectedRows(); QModelIndexList unselected; const int rows = this->rowCount(); for (int r = 0; r < rows; r++) { const QModelIndex mi = this->model()->index(r, 0); if (selected.contains(mi)) { continue; } unselected.push_back(mi); } return unselected; } void CViewBaseNonTemplate::selectRows(const QSet &rows) { if (!this->selectionModel()) { return; } // multiple times faster than multiple than this->selectRow() this->clearSelection(); QItemSelection selectedItems; const int columns = this->model()->columnCount() - 1; for (int r : rows) { selectedItems.select(this->model()->index(r, 0), this->model()->index(r, columns)); } this->selectionModel()->select(selectedItems, QItemSelectionModel::Select); } int CViewBaseNonTemplate::selectedRowCount() const { if (!this->hasSelection()) { return 0;} return this->selectedRows().count(); } int CViewBaseNonTemplate::unselectedRowCount() const { return this->rowCount() - this->selectedRowCount(); } bool CViewBaseNonTemplate::hasSingleSelectedRow() const { return this->selectedRowCount() == 1; } bool CViewBaseNonTemplate::hasMultipleSelectedRows() const { return this->selectedRowCount() > 1; } bool CViewBaseNonTemplate::allowsMultipleSelectedRows() const { return m_originalSelectionMode == ExtendedSelection || m_originalSelectionMode == MultiSelection; } bool CViewBaseNonTemplate::isCurrentlyAllowingMultipleRowSelections() const { QAbstractItemView::SelectionMode m = this->selectionMode(); return m == QAbstractItemView::MultiSelection || m == QAbstractItemView::ExtendedSelection; } void CViewBaseNonTemplate::init() { this->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); // faster mode this->horizontalHeader()->setStretchLastSection(true); const int fh = qRound(1.5 * this->getHorizontalHeaderFontHeight()); this->verticalHeader()->setDefaultSectionSize(fh); // for height this->verticalHeader()->setMinimumSectionSize(fh); // for height switch (m_rowResizeMode) { case Interactive: this->rowsResizeModeToInteractive(); break; case Content: this->rowsResizeModeToContent(); break; default: Q_ASSERT_X(false, Q_FUNC_INFO, "wrong resize mode"); break; } // call this deferred, otherwise the values are overridden with any values // from the UI builder const QPointer guard(this); QTimer::singleShot(500, this, [ = ]() { if (guard.isNull()) { return; } CViewBaseNonTemplate::settingsChanged(); }); } QString CViewBaseNonTemplate::getFileDialogFileName(bool load) const { // some logic to find a useful default name if (load) { return CFileUtils::appendFilePaths(this->getRememberedLastJsonDirectory(), CFileUtils::jsonWildcardAppendix()); } // Save file path const QString dir = m_dirSettings.get(); QString name(m_saveFileName); if (name.isEmpty()) { // create a name if (this->getDockWidgetInfoArea()) { name = this->getDockWidgetInfoArea()->windowTitle(); } 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) { m_menus &= (~menusToRemove); } void CViewBaseNonTemplate::menuAddItems(Menu menusToAdd) { m_menus |= menusToAdd; if (menusToAdd.testFlag(MenuRemoveSelectedRows)) { m_enableDeleteSelectedRows = true; } } int CViewBaseNonTemplate::ps_updateContainer(const CVariant &variant, bool sort, bool resize) { return this->performUpdateContainer(variant, sort, resize); } void CViewBaseNonTemplate::displayFilterDialog() { if (!m_menus.testFlag(MenuFilter)) { return; } if (!m_filterWidget) { return; } m_filterWidget->show(); } void CViewBaseNonTemplate::ps_loadJsonAction() { if (!m_menus.testFlag(MenuLoad)) { return; } const CStatusMessage m = this->ps_loadJson(); if (!m.isEmpty()) { CLogMessage::preformatted(m); } } void CViewBaseNonTemplate::saveJsonAction() { if (this->isEmpty()) { return; } if (!m_menus.testFlag(MenuSave)) { return; } const CStatusMessage m = this->ps_saveJson(); if (!m.isEmpty()) { CLogMessage::preformatted(m); } } void CViewBaseNonTemplate::ps_triggerReload() { this->showLoadIndicatorWithTimeout(m_loadIndicatorTimeoutMsDefault); emit this->requestUpdate(); } void CViewBaseNonTemplate::ps_triggerReloadFromBackend() { this->showLoadIndicatorWithTimeout(m_loadIndicatorTimeoutMsDefault); emit this->requestNewBackendData(); } void CViewBaseNonTemplate::onModelChanged() { this->updateSortIndicator(); } 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); m_rowResizeMode = Interactive; } void CViewBaseNonTemplate::rowsResizeModeToContent() { QHeaderView *verticalHeader = this->verticalHeader(); Q_ASSERT(verticalHeader); verticalHeader->setSectionResizeMode(QHeaderView::ResizeToContents); m_rowResizeMode = Content; } void CViewBaseNonTemplate::rowsResizeModeBasedOnThreshold(int elements) { if (elements > ResizeRowsToContentThreshold) { this->rowsResizeModeToInteractive(); } else { this->rowsResizeModeToContent(); } } int CViewBaseNonTemplate::showLoadIndicator(int containerSizeDependent, int timeoutMs, bool processEvents) { if (!m_enabledLoadIndicator) { return -1; } if (m_showingLoadIndicator) { return -1; } if (this->hasDockWidgetArea()) { if (!this->isVisibleWidget()) { return -1; } } if (containerSizeDependent >= 0) { // really with indicator? if (containerSizeDependent < ResizeSubsetThreshold) { return -1; } } m_showingLoadIndicator = true; emit loadIndicatorVisibilityChanged(m_showingLoadIndicator); if (!m_loadIndicator) { m_loadIndicator = new CLoadIndicator(64, 64, this); } this->centerLoadIndicator(); return m_loadIndicator->startAnimation(timeoutMs > 0 ? timeoutMs : m_loadIndicatorTimeoutMsDefault, processEvents); } int CViewBaseNonTemplate::showLoadIndicatorWithTimeout(int timeoutMs, bool processEvents) { return this->showLoadIndicator(-1, timeoutMs, processEvents); } void CViewBaseNonTemplate::centerLoadIndicator() { if (!m_loadIndicator) { return; } const QPoint middle = this->viewport()->geometry().center(); m_loadIndicator->centerLoadIndicator(middle); } void CViewBaseNonTemplate::hideLoadIndicator(int loadingId) { if (!m_showingLoadIndicator) { return; } m_showingLoadIndicator = false; emit this->loadIndicatorVisibilityChanged(m_showingLoadIndicator); if (!m_loadIndicator) { return; } m_loadIndicator->stopAnimation(loadingId); } bool CViewBaseNonTemplate::isResizeConditionMet(int containerSize) const { if (m_resizeMode == ResizingAlways) { return true; } 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() { // resize to maximum magic trick from: // http://stackoverflow.com/q/3433664/356726 this->setVisible(false); const QRect vpOriginal = this->viewport()->geometry(); if (m_forceColumnsToMaxSize) { // vpNew.setWidth(std::numeric_limits::max()); // largest finite value const QRect screenGeometry = QApplication::desktop()->screenGeometry(); QRect vpNew = vpOriginal; vpNew.setWidth(screenGeometry.width()); this->viewport()->setGeometry(vpNew); } this->resizeColumnsToContents(); // columns // useless if mode is Interactive if (m_rowResizeMode == Content) { this->resizeRowsToContents(); // rows } m_resizeCount++; // re-stretch if (m_forceStretchLastColumnWhenResized) { this->horizontalHeader()->setStretchLastSection(true); } if (m_forceColumnsToMaxSize) { this->viewport()->setGeometry(vpOriginal); } // if I store the original visibility and then // set it back here, the whole view disappears this->setVisible(true); } void CViewBaseNonTemplate::customMenuRequested(QPoint pos) { QMenu menu; CMenuActions menuActions; this->customMenu(menuActions); if (menuActions.isEmpty()) { return; } menuActions.toQMenu(menu, true); // Nested dock widget menu const CDockWidgetInfoArea *dockWidget = this->getDockWidgetInfoArea(); if (dockWidget) { if (!menu.isEmpty()) { menu.addSeparator(); } const QString mm = QString("Dock widget '%1'").arg(dockWidget->windowTitleOrBackup()); QMenu *dockWidgetSubMenu = menu.addMenu(CIcons::text16(), mm); dockWidget->addToContextMenu(dockWidgetSubMenu); } const QPoint globalPos = this->mapToGlobal(pos); menu.exec(globalPos); } void CViewBaseNonTemplate::toggleResizeMode(bool checked) { m_resizeMode = checked ? ResizingAuto : ResizingOff; } void CViewBaseNonTemplate::toggleAutoDisplay() { const QAction *a = qobject_cast(QObject::sender()); if (!a) { return; } Q_ASSERT_X(a->isCheckable(), Q_FUNC_INFO, "object not checkable"); m_displayAutomatically = a->isChecked(); } void CViewBaseNonTemplate::setSingleSelection() { this->setSelectionMode(SingleSelection); } void CViewBaseNonTemplate::setExtendedSelection() { if (this->allowsMultipleSelectedRows()) { this->setSelectionMode(ExtendedSelection); } } void CViewBaseNonTemplate::setMultiSelection() { if (this->allowsMultipleSelectedRows()) { this->setSelectionMode(MultiSelection); } } void CViewBaseNonTemplate::removeSelectedRowsChecked() { if (!m_enableDeleteSelectedRows) { return; } this->removeSelectedRows(); } void CViewBaseNonTemplate::updatedIndicator() { this->update(); } void CViewBaseNonTemplate::dragEnterEvent(QDragEnterEvent *event) { if (!event || !this->acceptDrop(event->mimeData())) { return; } this->setBackgroundRole(QPalette::Highlight); event->acceptProposedAction(); } void CViewBaseNonTemplate::dragMoveEvent(QDragMoveEvent *event) { if (!event || !this->acceptDrop(event->mimeData())) { return; } event->acceptProposedAction(); } void CViewBaseNonTemplate::dragLeaveEvent(QDragLeaveEvent *event) { if (!event) { return; } event->accept(); } void CViewBaseNonTemplate::dropEvent(QDropEvent *event) { if (!event) { return; } QTableView::dropEvent(event); } template CViewBase::CViewBase(QWidget *parent, ModelClass *model) : CViewBaseNonTemplate(parent), m_model(model) { this->setSortingEnabled(true); if (model) { this->standardInit(model); } } template int CViewBase::updateContainer(const ContainerType &container, bool sort, bool resize) { Q_ASSERT_X(m_model, Q_FUNC_INFO, "Missing model"); if (container.isEmpty()) { // shortcut this->clear(); return 0; } // we have data this->showLoadIndicator(container.size()); const 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 = this->getPresizeRandomElementsSize(container.size()); if (presizeRandomElements > 0) { m_model->update(container.sampleElements(presizeRandomElements), false); this->fullResizeToContents(); } } const int c = m_model->update(container, sort); // resize after real update according to mode if (presizeThresholdReached) { // currently no furhter actions } else if (reallyResize) { this->resizeToContents(); // mode based resize } else if (presize && !presizeThresholdReached) { // small amount of data not covered before this->fullResizeToContents(); } this->updateSortIndicator(); // make sure sort indicator represents sort order this->hideLoadIndicator(); return c; } int CViewBaseNonTemplate::getPresizeRandomElementsSize(int containerSize) const { containerSize = containerSize >= 0 ? containerSize : this->rowCount(); const int presizeRandomElements = containerSize > 1000 ? containerSize / 100 : containerSize / 40; return presizeRandomElements; } template CWorker *CViewBase::updateContainerAsync(const ContainerType &container, bool sort, bool resize) { // avoid unnecessary effort when empty if (container.isEmpty()) { this->clear(); return nullptr; } Q_UNUSED(sort); ModelClass *model = this->derivedModel(); auto sortColumn = model->getSortColumn(); auto sortOrder = model->getSortOrder(); this->showLoadIndicator(container.size()); CWorker *worker = CWorker::fromTask(this, "ViewSort", [model, container, sortColumn, sortOrder]() { return model->sortContainerByColumn(container, sortColumn, sortOrder); }); worker->thenWithResult(this, [this, resize](const ContainerType & sortedContainer) { this->updateContainer(sortedContainer, false, resize); }); worker->then(this, &CViewBase::asyncUpdateFinished); return worker; } template void CViewBase::updateContainerMaybeAsync(const ContainerType &container, bool sort, bool resize) { if (container.isEmpty()) { this->clear(); } else if (container.size() > ASyncRowsCountThreshold && sort) { // larger container with sorting this->updateContainerAsync(container, sort, resize); } else { this->updateContainer(container, sort, resize); } } template void CViewBase::insert(const ObjectType &value, bool resize) { Q_ASSERT(m_model); if (this->rowCount() < 1) { // this allows presizing this->updateContainerMaybeAsync(ContainerType({value}), true, resize); } else { m_model->insert(value); if (resize) { this->performModeBasedResizeToContent(); } } } template void CViewBase::insert(const ContainerType &container, bool resize) { Q_ASSERT(m_model); if (this->rowCount() < 1) { // this allows presizing this->updateContainerMaybeAsync(container, true, resize); } else { m_model->insert(container); if (resize) { this->performModeBasedResizeToContent(); } } } template void CViewBase::push_back(const ObjectType &value, bool resize) { Q_ASSERT(m_model); if (this->rowCount() < 1) { // this allows presizing this->updateContainerMaybeAsync(ContainerType({value}), true, resize); } else { m_model->push_back(value); if (resize) { this->performModeBasedResizeToContent(); } } } template void CViewBase::push_back(const ContainerType &container, bool resize) { Q_ASSERT(m_model); if (this->rowCount() < 1) { // this allows presizing this->updateContainerMaybeAsync(container, true, resize); } else { m_model->push_back(container); if (resize) { this->performModeBasedResizeToContent(); } } } template const ObjectType &CViewBase::at(const QModelIndex &index) const { Q_ASSERT(m_model); return m_model->at(index); } template const ContainerType &CViewBase::container() const { Q_ASSERT(m_model); return m_model->container(); } template const ContainerType &CViewBase::containerOrFilteredContainer(bool *filtered) const { Q_ASSERT(m_model); return m_model->containerOrFilteredContainer(filtered); } template ContainerType CViewBase::selectedObjects() const { if (!this->hasSelection()) { return ContainerType(); } ContainerType c; const QModelIndexList indexes = this->selectedRows(); for (const QModelIndex &i : indexes) { c.push_back(this->at(i)); } return c; } template ContainerType CViewBase::unselectedObjects() const { if (!this->hasSelection()) { return this->containerOrFilteredContainer(); } ContainerType c; const QModelIndexList indexes = this->unselectedRows(); for (const QModelIndex &i : indexes) { c.push_back(this->at(i)); } return c; } template ObjectType CViewBase::firstSelectedOrDefaultObject() const { if (this->hasSelection()) { return this->selectedObjects().front(); } if (this->rowCount() < 2) { return this->containerOrFilteredContainer().frontOrDefault(); } // too many, not selected return ObjectType(); } template int CViewBase::updateSelected(const CPropertyIndexVariantMap &vm) { if (vm.isEmpty()) { return 0; } if (!hasSelection()) { return 0; } int c = 0; int lastUpdatedRow = -1; int firstUpdatedRow = -1; const CPropertyIndexList propertyIndexes(vm.indexes()); const QModelIndexList indexes = this->selectedRows(); 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 : propertyIndexes) { obj.setPropertyByIndex(pi, vm.value(pi)); } // and update container if (this->derivedModel()->setInContainer(i, obj)) { c++; } } if (c > 0) { this->derivedModel()->emitDataChanged(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 { const ContainerType c = this->selectedObjects(); return c.frontOrDefault(); } template int CViewBase::removeSelectedRows() { if (!this->hasSelection()) { return 0; } if (this->isEmpty()) { return 0; } const int currentRows = this->rowCount(); if (!this->hasFilter() && currentRows == this->selectedRowCount()) { // shortcut if all are selected this->clear(); // clear all return currentRows; } const ContainerType selected(selectedObjects()); ContainerType unselectedObjects(container()); for (const ObjectType &obj : selected) { unselectedObjects.remove(obj); } const int delta = currentRows - unselectedObjects.size(); this->updateContainerMaybeAsync(unselectedObjects); return delta; } template void CViewBase::presizeOrFullResizeToContents() { const int rc = this->rowCount(); if (rc > ResizeSubsetThreshold) { const int presizeRandomElements = this->getPresizeRandomElementsSize(rc); if (presizeRandomElements > 0) { const ContainerType containerBackup(this->container()); m_model->update(containerBackup.sampleElements(presizeRandomElements), false); this->fullResizeToContents(); m_model->update(containerBackup, false); } } else { this->fullResizeToContents(); } } template void CViewBase::clearHighlighting() { Q_ASSERT(m_model); return m_model->clearHighlighting(); } template void CViewBase::materializeFilter() { Q_ASSERT(m_model); if (!m_model->hasFilter()) { return; } if (this->isEmpty()) { return; } ContainerType filtered(m_model->containerFiltered()); this->removeFilter(); this->updateContainerMaybeAsync(filtered); } template void CViewBase::clear() { Q_ASSERT(m_model); m_model->clear(); this->hideLoadIndicator(); } template int CViewBase::rowCount() const { Q_ASSERT(m_model); return m_model->rowCount(); } template int CViewBase::columnCount() const { Q_ASSERT(m_model); return m_model->columnCount(QModelIndex()); } template bool CViewBase::isEmpty() const { Q_ASSERT(m_model); return m_model->rowCount() < 1; } template bool CViewBase::isOrderable() const { Q_ASSERT(m_model); return m_model->isOrderable(); } template void CViewBase::allowDragDrop(bool allowDrag, bool allowDrop) { Q_ASSERT(m_model); // see model for implementing logic of drag this->viewport()->setAcceptDrops(allowDrop); this->setDragEnabled(allowDrag); this->setDropIndicatorShown(allowDrag || allowDrop); m_model->allowDrop(allowDrop); } template bool CViewBase::isDropAllowed() const { Q_ASSERT(m_model); return m_model->isDropAllowed(); } template bool CViewBase::acceptDrop(const QMimeData *mimeData) const { Q_ASSERT(m_model); const bool a = m_model->acceptDrop(mimeData); return a; } template void CViewBase::setSorting(const CPropertyIndex &propertyIndex, Qt::SortOrder order) { Q_ASSERT(m_model); m_model->setSorting(propertyIndex, order); } template void CViewBase::sortByPropertyIndex(const CPropertyIndex &propertyIndex, Qt::SortOrder order) { m_model->sortByPropertyIndex(propertyIndex, order); } template QJsonObject CViewBase::toJson() const { Q_ASSERT(m_model); return m_model->toJson(); } template QString CViewBase::toJsonString(QJsonDocument::JsonFormat format) const { Q_ASSERT(m_model); return 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(m_model->getTranslationContext()); 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::addContainerTypesAsDropTypes(bool objectType, bool containerType) { if (objectType) { m_model->addAcceptedMetaTypeId(qMetaTypeId()); } if (containerType) { m_model->addAcceptedMetaTypeId(qMetaTypeId()); } } template void CViewBase::initAsOrderable() { Q_ASSERT_X(isOrderable(), Q_FUNC_INFO, "Model not orderable"); this->allowDragDrop(true, true); this->setDragDropMode(InternalMove); this->setDropActions(Qt::MoveAction); this->addContainerTypesAsDropTypes(true, true); } template void CViewBase::setSortIndicator() { if (m_model->hasValidSortColumn()) { Q_ASSERT(this->horizontalHeader()); this->horizontalHeader()->setSortIndicator( m_model->getSortColumn(), m_model->getSortOrder()); } } template void CViewBase::standardInit(ModelClass *model) { Q_ASSERT_X(model || m_model, Q_FUNC_INFO, "Missing model"); if (model) { if (model == m_model) { return; } if (m_model) { m_model->disconnect(); } m_model = model; m_model->setSelectionModel(this); bool c = connect(m_model, &ModelClass::modelDataChanged, this, &CViewBase::modelDataChanged); Q_ASSERT_X(c, Q_FUNC_INFO, "Connect failed"); c = connect(m_model, &ModelClass::modelDataChangedDigest, this, &CViewBase::modelDataChangedDigest); Q_ASSERT_X(c, Q_FUNC_INFO, "Connect failed"); c = connect(m_model, &ModelClass::objectChanged, this, &CViewBase::objectChanged); Q_ASSERT_X(c, Q_FUNC_INFO, "Connect failed"); c = connect(m_model, &ModelClass::changed, this, &CViewBase::modelChanged); Q_ASSERT_X(c, Q_FUNC_INFO, "Connect failed"); c = connect(m_model, &ModelClass::changed, this, &CViewBase::onModelChanged); Q_ASSERT_X(c, Q_FUNC_INFO, "Connect failed"); Q_UNUSED(c); } this->setModel(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? This only performs real resizing, no presizing // remark, see also presizeOrFullResizeToContents if (this->isResizeConditionMet()) { this->fullResizeToContents(); } else { 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 void CViewBase::updateSortIndicator() { if (this->derivedModel()->hasValidSortColumn()) { const int index = this->derivedModel()->getSortColumn(); Qt::SortOrder order = this->derivedModel()->getSortOrder(); this->horizontalHeader()->setSortIndicator(index, order); } } template void CViewBase::mouseOverCallback(const QModelIndex &index, bool mouseOver) { // void Q_UNUSED(index); Q_UNUSED(mouseOver); } template void CViewBase::drawDropIndicator(bool indicator) { m_dropIndicator = indicator; } template void CViewBase::selectObjects(const ContainerType &selectedObjects) { Q_UNUSED(selectedObjects); } template CStatusMessage CViewBase::modifyLoadedJsonData(ContainerType &data) const { Q_UNUSED(data); static const CStatusMessage e(this, CStatusMessage::SeverityInfo, "no modification", true); return e; } template CStatusMessage CViewBase::validateLoadedJsonData(const ContainerType &data) const { Q_UNUSED(data); static const CStatusMessage e(this, CStatusMessage::SeverityInfo, "validation passed", true); return e; } template void CViewBase::jsonLoadedAndModelUpdated(const ContainerType &data) { Q_UNUSED(data); } template void CViewBase::customMenu(CMenuActions &menuActions) { CViewBaseNonTemplate::customMenu(menuActions); // Clear highlighting if (this->derivedModel()->hasHighlightedRows()) { menuActions.addAction(CIcons::refresh16(), "Clear highlighting", CMenuAction::pathViewClearHighlighting(), nullptr, { this, &CViewBaseNonTemplate::clearHighlighting }); } } template CStatusMessage CViewBase::ps_loadJson() { CStatusMessage m; do { const QString fileName = QFileDialog::getOpenFileName(nullptr, tr("Load data file"), this->getFileDialogFileName(true), tr("swift (*.json *.txt)")); if (fileName.isEmpty()) { m = CStatusMessage(this).error("Load canceled, no file name"); break; } const QString json(CFileUtils::readFileToString(fileName)); if (json.isEmpty()) { m = CStatusMessage(this).warning("Reading '%1' yields no data") << fileName; break; } if (!Json::looksLikeSwiftJson(json)) { m = CStatusMessage(this).warning("No swift JSON '%1'") << fileName; break; } this->rememberLastJsonDirectory(fileName); try { const bool allowCacheFormat = this->allowCacheFileFormatJson(); const QJsonObject jsonObject = Json::jsonObjectFromString(json, allowCacheFormat); if (jsonObject.isEmpty()) { m = CStatusMessage(this).warning("No valid swift JSON '%1'") << fileName; break; } CVariant containerVariant; containerVariant.convertFromJson(jsonObject); if (!containerVariant.canConvert()) { m = CStatusMessage(this).warning("No valid swift JSON '%1'") << fileName; break; } ContainerType container = containerVariant.value(); const int countBefore = container.size(); m = this->modifyLoadedJsonData(container); if (m.isFailure()) { break; } // modification error if (countBefore > 0 && container.isEmpty()) { break; } m = this->validateLoadedJsonData(container); if (m.isFailure()) { break; } // validaton error this->updateContainerMaybeAsync(container); m = CStatusMessage(this, CStatusMessage::SeverityInfo, "Reading " + fileName + " completed", true); this->jsonLoadedAndModelUpdated(container); } catch (const CJsonException &ex) { m = ex.toStatusMessage(this, QString("Reading JSON from '%1'").arg(fileName)); break; } } while (false); emit this->jsonLoadCompleted(m); return m; } template CStatusMessage CViewBase::ps_saveJson() { const QString fileName = QFileDialog::getSaveFileName(nullptr, tr("Save data file"), getFileDialogFileName(false), tr("swift (*.json *.txt)")); if (fileName.isEmpty()) { return CStatusMessage(this, CStatusMessage::SeverityDebug, "Save canceled", true); } const QString json(this->toJsonString()); // save as CVariant JSON // keep directory for settings const QFileInfo file(fileName); const QDir fileDir(file.absoluteDir()); if (fileDir.exists()) { m_dirSettings.setAndSave(fileDir.absolutePath()); } // save file const bool ok = CFileUtils::writeStringToFileInBackground(json, fileName); return ok ? CStatusMessage(this, CStatusMessage::SeverityInfo, "Writing " + fileName + " in progress", true) : CStatusMessage(this, CStatusMessage::SeverityError, "Writing " + fileName + " failed", true); } template void CViewBase::ps_copy() { QClipboard *clipboard = QApplication::clipboard(); if (!clipboard) { return; } if (!this->hasSelection()) { return; } const ContainerType selection = this->selectedObjects(); if (selection.isEmpty()) { return; } const CVariant copyJson = CVariant::from(selection); const QString json = copyJson.toJsonString(); clipboard->setText(json); } template void CViewBase::ps_cut() { if (!QApplication::clipboard()) { return; } this->ps_copy(); this->removeSelectedRows(); } template void CViewBase::ps_paste() { const QClipboard *clipboard = QApplication::clipboard(); if (!clipboard) { return; } const QString json = clipboard->text(); if (json.isEmpty()) { return; } if (!Json::looksLikeSwiftJson(json)) { return; } // no JSON try { ContainerType objects; objects.convertFromJson(json); if (!objects.isEmpty()) { this->insert(objects); } } catch (const CJsonException &ex) { Q_UNUSED(ex); } } 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 (!m_filterWidget) { this->removeFilter(); } else { // takes the filter and triggers the filtering IModelFilterProvider *provider = dynamic_cast*>(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()); if (f->isValid()) { this->takeFilterOwnership(f); } else { this->removeFilter(); } } } 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 // https://isocpp.org/wiki/faq/templates#separate-template-fn-defn-from-decl 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; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; template class CViewBase; } // namespace } // namespace