/* 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 "blackgui/guiutility.h" #include "blackgui/models/allmodels.h" #include "blackgui/views/viewbase.h" #include "blackgui/views/viewbaseproxystyle.h" #include "blackgui/views/viewbaseitemdelegate.h" #include "blackgui/components/texteditdialog.h" #include "blackmisc/worker.h" #include "blackconfig/buildconfig.h" #include #include #include #include using namespace BlackMisc; using namespace BlackGui; using namespace BlackGui::Menus; using namespace BlackGui::Models; using namespace BlackGui::Filters; using namespace BlackGui::Settings; using namespace BlackGui::Components; namespace BlackGui { namespace Views { 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; } 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 typename CViewBase::ObjectType &CViewBase::at(const QModelIndex &index) const { Q_ASSERT(m_model); return m_model->at(index); } template const typename CViewBase::ContainerType &CViewBase::container() const { Q_ASSERT(m_model); return m_model->container(); } template const typename CViewBase::ContainerType &CViewBase::containerOrFilteredContainer(bool *filtered) const { Q_ASSERT(m_model); return m_model->containerOrFilteredContainer(filtered); } template typename CViewBase::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 typename CViewBase::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 typename CViewBase::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 typename CViewBase::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(); const ContainerType selected(selectedObjects()); const CVariant deletedObjsVariant = CVariant::from(selected); int delta = 0; if (!this->hasFilter() && currentRows == this->selectedRowCount()) { // shortcut if all are selected this->clear(); // clear all delta = currentRows; } else { ContainerType unselectedObjects(container()); unselectedObjects.removeIfInSubset(selected); this->updateContainerMaybeAsync(unselectedObjects); delta = currentRows - unselectedObjects.size(); } emit this->objectsDeleted(deletedObjsVariant); 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, bool allowDropJsonFile) { 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); m_model->allowFileDrop(allowDropJsonFile); } template bool CViewBase::isDropAllowed() const { Q_ASSERT(m_model); return m_model->isDropAllowed(); } template void CViewBase::dropEvent(QDropEvent *event) { if (m_model && m_model->isJsonFileDropAllowed() && CGuiUtility::isMimeRepresentingReadableJsonFile(event->mimeData())) { const QFileInfo fi = CGuiUtility::representedMimeFile(event->mimeData()); const CStatusMessage msgs = this->loadJsonFile(fi.absoluteFilePath()); Q_UNUSED(msgs); return; } CViewBaseNonTemplate::dropEvent(event); } template bool CViewBase::acceptDrop(const QMimeData *mimeData) const { Q_ASSERT(m_model); const bool a = m_model->acceptDrop(mimeData); return a; } template bool CViewBase::setSorting(const CPropertyIndex &propertyIndex, Qt::SortOrder order) { Q_ASSERT(m_model); return m_model->setSorting(propertyIndex, order); } template void CViewBase::sortByPropertyIndex(const CPropertyIndex &propertyIndex, Qt::SortOrder order) { m_model->sortByPropertyIndex(propertyIndex, order); } template QJsonObject CViewBase::toJson(bool selectedOnly) const { Q_ASSERT(m_model); return m_model->toJson(selectedOnly); } template QString CViewBase::toJsonString(QJsonDocument::JsonFormat format, bool selectedOnly) const { Q_ASSERT(m_model); return m_model->toJsonString(format, selectedOnly); } 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); m_model->setObjectName(name); } 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::setTabWidgetViewText(QTabWidget *tw, int index) { if (!tw) { return; } QString o = tw->tabText(index); const QString f = this->hasFilter() ? "F" : ""; o = CGuiUtility::replaceTabCountValue(o, this->rowCount()) + f; tw->setTabText(index, o); } 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::loadJsonFile(const QString &fileName) { CStatusMessage m; do { 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; } 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; } ContainerType container; if (jsonObject.contains("type") && jsonObject.contains("value")) { // read from variant format CVariant containerVariant; containerVariant.convertFromJson(jsonObject); if (!containerVariant.canConvert()) { m = CStatusMessage(this).warning("No valid swift JSON '%1'") << fileName; break; } container = containerVariant.value(); } else { // container format directly container.convertFromJson(jsonObject); } 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); this->rememberLastJsonDirectory(fileName); } 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 void CViewBase::displayContainerAsJsonPopup(bool selectedOnly) { const ContainerType container = selectedOnly ? this->selectedObjects() : this->container(); const QString json = container.toJsonString(); QTextEdit *te = this->textEditDialog()->textEdit(); te->setReadOnly(true); te->setText(json); this->textEditDialog()->show(); } template CStatusMessage CViewBase::ps_loadJson(const QString &directory) { const QString fileName = QFileDialog::getOpenFileName(nullptr, tr("Load data file"), directory.isEmpty() ? this->getFileDialogFileName(true) : directory, tr("swift (*.json *.txt)")); return this->loadJsonFile(fileName); } template CStatusMessage CViewBase::ps_saveJson(bool selectedOnly, const QString &directory) { const QString fileName = QFileDialog::getSaveFileName(nullptr, tr("Save data file"), directory.isEmpty() ? this->getFileDialogFileName(false) : directory, tr("swift (*.json *.txt)")); if (fileName.isEmpty()) { return CStatusMessage(this, CStatusMessage::SeverityDebug, "Save canceled", true); } const QString json(this->toJsonString(QJsonDocument::Indented, selectedOnly)); // save as CVariant JSON // save file const bool ok = CFileUtils::writeStringToFileInBackground(json, fileName); if (ok) { this->rememberLastJsonDirectory(fileName); } return ok ? CStatusMessage(this, CStatusMessage::SeverityInfo, "Writing " + fileName + " in progress", true) : CStatusMessage(this, CStatusMessage::SeverityError, "Writing " + fileName + " failed", true); } template void CViewBase::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::cut() { if (!QApplication::clipboard()) { return; } this->copy(); this->removeSelectedRows(); } template void CViewBase::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_acceptRowSelection) { return; } if (!index.isValid()) { return; } emit objectSelected(CVariant::fromValue(at(index))); } } // namespace } // namespace