From d9db579b7c78f3bb77fb8e7d8d1f9d040c6895ea Mon Sep 17 00:00:00 2001 From: Hannah von Reth Date: Tue, 20 Feb 2024 09:52:55 +0100 Subject: [PATCH] wip qml --- CMakeLists.txt | 2 +- src/gui/CMakeLists.txt | 18 +- src/gui/accountsettings.cpp | 246 +++++++------- src/gui/accountsettings.h | 22 +- src/gui/accountsettings.ui | 20 +- src/gui/folderstatusdelegate.cpp | 316 ------------------ src/gui/folderstatusdelegate.h | 57 ---- src/gui/folderstatusmodel.cpp | 212 ++++++------ src/gui/folderstatusmodel.h | 28 +- src/gui/main.cpp | 6 + src/gui/models/models.h | 3 + src/gui/owncloudgui.cpp | 2 + src/gui/qml/FolderDelegate.qml | 172 ++++++++++ src/gui/qml/FolderError.qml | 84 +++++ src/libsync/CMakeLists.txt | 1 + src/libsync/graphapi/space.cpp | 2 +- src/libsync/platform_mac.h | 1 + src/libsync/platform_win.cpp | 3 +- src/resources/CMakeLists.txt | 2 +- src/resources/client.qrc | 2 + .../font-awesome/dark/folder-solid.svg | 4 +- .../font-awesome/light/folder-solid.svg | 4 +- src/resources/remixicon/License.txt | 201 +++++++++++ src/resources/remixicon/dark/space.svg | 10 + src/resources/remixicon/light/space.svg | 10 + src/resources/resources.cpp | 28 +- src/resources/resources.h | 10 + 27 files changed, 814 insertions(+), 652 deletions(-) delete mode 100644 src/gui/folderstatusdelegate.cpp delete mode 100644 src/gui/folderstatusdelegate.h create mode 100644 src/gui/qml/FolderDelegate.qml create mode 100644 src/gui/qml/FolderError.qml create mode 100644 src/resources/remixicon/License.txt create mode 100644 src/resources/remixicon/dark/space.svg create mode 100644 src/resources/remixicon/light/space.svg diff --git a/CMakeLists.txt b/CMakeLists.txt index 460d15ef64e..e81421e27e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) find_package(QT 6.2 NAMES Qt6 COMPONENTS Core REQUIRED) -find_package(Qt6 COMPONENTS Core Concurrent Network Widgets Xml REQUIRED) +find_package(Qt6 COMPONENTS Core Concurrent Network Widgets Xml Quick QuickWidgets QuickControls2 REQUIRED) find_package(Qt6LinguistTools REQUIRED) get_target_property (QT_QMAKE_EXECUTABLE Qt::qmake IMPORTED_LOCATION) message(STATUS "Using Qt ${QT_VERSION} (${QT_QMAKE_EXECUTABLE})") diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index d6809f9c14d..b2aa2f2f81b 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1,4 +1,5 @@ include(ECMAddAppIcon) +include(ECMQmlModule) find_package(KDSingleApplication-qt6 1.0.0 REQUIRED) @@ -37,7 +38,6 @@ set(client_SRCS folder.cpp folderman.cpp folderstatusmodel.cpp - folderstatusdelegate.cpp folderwatcher.cpp generalsettings.cpp ignorelisteditor.cpp @@ -102,15 +102,27 @@ add_subdirectory(loginrequireddialog) add_library(owncloudCore STATIC ${final_src}) set_target_properties(owncloudCore PROPERTIES AUTOUIC ON AUTORCC ON) +# for the generated qml module +target_include_directories(owncloudCore PRIVATE models) target_link_libraries(owncloudCore PUBLIC - Qt::Widgets Qt::Network Qt::Xml + Qt::Widgets Qt::Network Qt::Xml Qt::Quick Qt::QuickWidgets Qt::QuickControls2 newwizard folderwizard spaces loginrequireddialog libsync Qt6Keychain::Qt6Keychain ) apply_common_target_settings(owncloudCore) +ecm_add_qml_module (owncloudCore + URI org.ownCloud.qmlcomponents + VERSION 1.0 + NAMESPACE OCC + # TODO: main.cpp: qml_register_types_org_ownCloud_qmlcomponents + QT_NO_PLUGIN + QML_FILES + qml/FolderDelegate.qml + qml/FolderError.qml +) add_subdirectory(spaces) @@ -159,7 +171,7 @@ set_target_properties(owncloud PROPERTIES AUTORCC ON ) apply_common_target_settings(owncloud) -target_link_libraries(owncloud owncloudCore owncloudResources KDAB::kdsingleapplication ) +target_link_libraries(owncloud PUBLIC owncloudCore owncloudResources KDAB::kdsingleapplication ) MESSAGE(STATUS "OWNCLOUD_SIDEBAR_ICONS: ${APPLICATION_ICON_NAME}: ${OWNCLOUD_SIDEBAR_ICONS}") diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 3a2f7069092..6d460303855 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -26,7 +26,6 @@ #include "configfile.h" #include "creds/httpcredentialsgui.h" #include "folderman.h" -#include "folderstatusdelegate.h" #include "folderstatusmodel.h" #include "folderwizard/folderwizard.h" #include "gui/accountmodalwidget.h" @@ -36,6 +35,7 @@ #include "loginrequireddialog.h" #include "oauthloginwidget.h" #include "quotainfo.h" +#include "resources/resources.h" #include "scheduling/syncscheduler.h" #include "settingsdialog.h" #include "theme.h" @@ -53,6 +53,9 @@ #include #include #include +#include + +#include namespace { @@ -63,11 +66,42 @@ namespace OCC { Q_LOGGING_CATEGORY(lcAccountSettings, "gui.account.settings", QtInfoMsg) +class ImageProvider : public QQuickImageProvider +{ + Q_OBJECT +public: + ImageProvider(AccountStatePtr accountStat) + : QQuickImageProvider(QQuickImageProvider::Pixmap, QQuickImageProvider::ForceAsynchronousImageLoading) + , _accountStat(accountStat) + { + } + + QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override + { + // TODO: the url hast random parts to enforce a reload + const auto ids = id.split(QLatin1Char('/')); + const auto *space = _accountStat->account()->spacesManager()->space(ids.last()); + QIcon icon; + if (space) { + icon = space->image(); + } else { + icon = Resources::getCoreIcon(QStringLiteral("space")); + } + const QSize actualSize = requestedSize.isValid() ? requestedSize : icon.availableSizes().first(); + if (size) { + *size = actualSize; + } + return icon.pixmap(actualSize); + } + + +private: + AccountStatePtr _accountStat; +}; AccountSettings::AccountSettings(const AccountStatePtr &accountState, QWidget *parent) : QWidget(parent) , ui(new Ui::AccountSettings) - , _delegate(new FolderStatusDelegate(this)) , _wasDisabledBefore(false) , _accountState(accountState) { @@ -76,30 +110,24 @@ AccountSettings::AccountSettings(const AccountStatePtr &accountState, QWidget *p _model = new FolderStatusModel(this); _model->setAccountState(_accountState); - auto weightedModel = new Models::WeightedQSortFilterProxyModel(this); + auto weightedModel = new QSortFilterProxyModel(this); weightedModel->setSourceModel(_model); - weightedModel->setWeightedColumn(static_cast(FolderStatusModel::Columns::Priority), Qt::DescendingOrder); + weightedModel->setSortRole(static_cast(FolderStatusModel::Columns::Priority)); weightedModel->setSortCaseSensitivity(Qt::CaseInsensitive); - weightedModel->sort(static_cast(FolderStatusModel::Columns::HeaderRole), Qt::DescendingOrder); + weightedModel->sort(0, Qt::DescendingOrder); _sortModel = weightedModel; - ui->_folderList->setModel(_sortModel); - - ui->_folderList->setItemDelegate(_delegate); + ui->quickWidget->rootContext()->setContextProperty(QStringLiteral("ctx"), this); + ui->quickWidget->engine()->addImageProvider(QStringLiteral("space"), new ImageProvider(_accountState)); + ui->quickWidget->engine()->addImageProvider(QStringLiteral("ownCloud"), new Resources::CoreImageProvider()); + ui->quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); + ui->quickWidget->setSource(QUrl::fromLocalFile(QStringLiteral(":/qt/qml/org/ownCloud/qmlcomponents/qml/FolderDelegate.qml"))); + for (const auto &e : ui->quickWidget->errors()) { + qDebug() << "aaaaaaaaaaa" << e; + } createAccountToolbox(); - connect(ui->_folderList, &QWidget::customContextMenuRequested, - this, &AccountSettings::slotCustomContextMenuRequested); - connect(ui->_folderList, &QAbstractItemView::clicked, this, &AccountSettings::slotFolderListClicked); - QAction *syncNowAction = new QAction(this); - connect(syncNowAction, &QAction::triggered, this, &AccountSettings::slotScheduleCurrentFolder); - addAction(syncNowAction); - - QAction *syncNowWithRemoteDiscovery = new QAction(this); - connect(syncNowWithRemoteDiscovery, &QAction::triggered, this, &AccountSettings::slotScheduleCurrentFolderForceFullDiscovery); - addAction(syncNowWithRemoteDiscovery); - connect(FolderMan::instance(), &FolderMan::folderListChanged, _model, &FolderStatusModel::resetFolders); connect(this, &AccountSettings::folderChanged, _model, &FolderStatusModel::resetFolders); @@ -155,12 +183,6 @@ void AccountSettings::createAccountToolbox() ui->_accountToolbox->setPopupMode(QToolButton::InstantPopup); } -Folder *AccountSettings::selectedFolder() const -{ - const QModelIndex selected = ui->_folderList->selectionModel()->currentIndex(); - return _model->folder(_sortModel->mapToSource(selected)); -} - void AccountSettings::slotToggleSignInState() { if (_accountState->isSignedOut()) { @@ -170,36 +192,32 @@ void AccountSettings::slotToggleSignInState() } } -void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) +void AccountSettings::slotCustomContextMenuRequested(const QString &id) { - auto *tv = ui->_folderList; - QModelIndex index = tv->indexAt(pos); - if (!index.isValid()) { - return; - } + auto *folder = FolderMan::instance()->folder(id.toUtf8()); - const auto isDeployed = index.siblingAtColumn(static_cast(FolderStatusModel::Columns::IsDeployed)).data().toBool(); + const auto isDeployed = folder->isDeployed(); const auto addRemoveFolderAction = [isDeployed, this](QMenu *menu) { Q_ASSERT(!isDeployed); return menu->addAction(tr("Remove folder sync connection"), this, &AccountSettings::slotRemoveCurrentFolder); }; // Only allow removal if the item isn't in "ready" state. - if (!index.siblingAtColumn(static_cast(FolderStatusModel::Columns::IsReady)).data().toBool() && !isDeployed) { - QMenu *menu = new QMenu(tv); + if (!folder->isReady() && !isDeployed) { + QMenu *menu = new QMenu(ui->quickWidget); menu->setAttribute(Qt::WA_DeleteOnClose); addRemoveFolderAction(menu); menu->popup(QCursor::pos()); return; } - QMenu *menu = new QMenu(tv); + QMenu *menu = new QMenu(ui->quickWidget); menu->setAttribute(Qt::WA_DeleteOnClose); // Add an action to open the folder in the system's file browser: QUrl folderUrl; - if (auto *folder = selectedFolder()) { + if (folder) { folderUrl = QUrl::fromLocalFile(folder->path()); } @@ -218,7 +236,7 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) // Add an action to open the folder on the server in a webbrowser: - if (auto folder = _model->folder(_sortModel->mapToSource(index))) { + if (folder) { if (folder->accountState()->account()->capabilities().privateLinkPropertyAvailable()) { QString path = folder->remotePathTrailingSlash(); menu->addAction(CommonStrings::showInWebBrowser(), [path, davUrl = folder->webDavUrl(), this] { @@ -233,12 +251,10 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) menu->addSeparator(); - tv->setCurrentIndex(index); - bool folderPaused = index.siblingAtColumn(static_cast(FolderStatusModel::Columns::FolderSyncPaused)).data().toBool(); - bool folderConnected = index.siblingAtColumn(static_cast(FolderStatusModel::Columns::FolderAccountConnected)).data().toBool(); + bool folderPaused = folder->syncPaused(); + bool folderConnected = folder->accountState()->isConnected(); // qpointer for the async context menu - QPointer folder = selectedFolder(); if (OC_ENSURE(folder && folder->isReady())) { if (!folderPaused) { QAction *ac = menu->addAction(tr("Force sync now")); @@ -246,11 +262,19 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) ac->setText(tr("Restart sync")); } ac->setEnabled(folderConnected); - connect(ac, &QAction::triggered, this, &AccountSettings::slotForceSyncCurrentFolder); + connect(ac, &QAction::triggered, this, [folder = QPointer(folder), this] { + if (folder) { + slotForceSyncCurrentFolder(folder); + } + }); } QAction *ac = menu->addAction(folderPaused ? tr("Resume sync") : tr("Pause sync")); - connect(ac, &QAction::triggered, this, &AccountSettings::slotEnableCurrentFolder); + connect(ac, &QAction::triggered, this, [folder = QPointer(folder), this] { + if (folder) { + slotEnableCurrentFolder(folder, true); + } + }); if (!isDeployed) { addRemoveFolderAction(menu); @@ -280,22 +304,6 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) } } -void AccountSettings::slotFolderListClicked(const QModelIndex &indx) -{ - // tries to find if we clicked on the '...' button. - auto *tv = ui->_folderList; - const auto pos = tv->mapFromGlobal(QCursor::pos()); - const auto rect = tv->visualRect(indx); - if (QStyle::visualRect(layoutDirection(), rect, _delegate->computeOptionsButtonRect(rect).toRect()).contains(pos)) { - slotCustomContextMenuRequested(pos); - return; - } - if (_delegate->errorsListRect(tv->visualRect(indx), indx).contains(pos)) { - emit showIssuesList(); - return; - } -} - void AccountSettings::showSelectiveSyncDialog(Folder *folder) { auto *selectiveSync = new SelectiveSyncWidget(_accountState->account(), this); @@ -352,6 +360,7 @@ void AccountSettings::slotFolderWizardAccepted() void AccountSettings::slotRemoveCurrentFolder() { +#if 0 auto folder = selectedFolder(); QModelIndex selected = ui->_folderList->selectionModel()->currentIndex(); if (selected.isValid() && folder) { @@ -382,10 +391,12 @@ void AccountSettings::slotRemoveCurrentFolder() }); messageBox->open(); } +#endif } void AccountSettings::slotEnableVfsCurrentFolder() { +#if 0 QPointer folder = selectedFolder(); QModelIndex selected = ui->_folderList->selectionModel()->currentIndex(); if (!selected.isValid() || !folder) { @@ -405,10 +416,12 @@ void AccountSettings::slotEnableVfsCurrentFolder() ui->_folderList->doItemsLayout(); } +#endif } void AccountSettings::slotDisableVfsCurrentFolder() { +#if 0 QPointer folder = selectedFolder(); QModelIndex selected = ui->_folderList->selectionModel()->currentIndex(); if (!selected.isValid() || !folder) @@ -440,6 +453,7 @@ void AccountSettings::slotDisableVfsCurrentFolder() ui->_folderList->doItemsLayout(); }); msgBox->open(); +#endif } void AccountSettings::showConnectionLabel(const QString &message, QStringList errors) @@ -458,80 +472,63 @@ void AccountSettings::showConnectionLabel(const QString &message, QStringList er ui->warningLabel->setVisible(!errors.isEmpty()); } -void AccountSettings::slotEnableCurrentFolder(bool terminate) +void AccountSettings::slotEnableCurrentFolder(Folder *folder, bool terminate) { - auto folder = selectedFolder(); - - if (folder) { - qCInfo(lcAccountSettings) << "Application: enable folder with alias " << folder->path(); - bool currentlyPaused = false; - - // this sets the folder status to disabled but does not interrupt it. - currentlyPaused = folder->syncPaused(); - if (!currentlyPaused && !terminate) { - // check if a sync is still running and if so, ask if we should terminate. - if (folder->isSyncRunning()) { // its still running - auto msgbox = new QMessageBox(QMessageBox::Question, tr("Sync Running"), - tr("The sync operation is running.
Do you want to stop it?"), - QMessageBox::Yes | QMessageBox::No, this); - msgbox->setAttribute(Qt::WA_DeleteOnClose); - msgbox->setDefaultButton(QMessageBox::Yes); - connect(msgbox, &QMessageBox::accepted, this, [this]{ - slotEnableCurrentFolder(true); - }); - msgbox->open(); - return; - } - } - - // message box can return at any time while the thread keeps running, - // so better check again after the user has responded. - if (folder->isSyncRunning() && terminate) { - folder->slotTerminateSync(tr("Sync paused by user")); + Q_ASSERT(folder); + qCInfo(lcAccountSettings) << "Application: enable folder with alias " << folder->path(); + bool currentlyPaused = false; + + // this sets the folder status to disabled but does not interrupt it. + currentlyPaused = folder->syncPaused(); + if (!currentlyPaused && !terminate) { + // check if a sync is still running and if so, ask if we should terminate. + if (folder->isSyncRunning()) { // its still running + auto msgbox = new QMessageBox(QMessageBox::Question, tr("Sync Running"), tr("The sync operation is running.
Do you want to stop it?"), + QMessageBox::Yes | QMessageBox::No, this); + msgbox->setAttribute(Qt::WA_DeleteOnClose); + msgbox->setDefaultButton(QMessageBox::Yes); + connect(msgbox, &QMessageBox::accepted, this, [folder = QPointer(folder), this] { + if (folder) { + slotEnableCurrentFolder(folder, true); + } + }); + msgbox->open(); + return; } - folder->slotNextSyncFullLocalDiscovery(); // ensure we don't forget about local errors - folder->setSyncPaused(!currentlyPaused); - - // keep state for the icon setting. - if (currentlyPaused) - _wasDisabledBefore = true; - - _model->slotUpdateFolderState(folder); } -} -void AccountSettings::slotScheduleCurrentFolder() -{ - if (auto folder = selectedFolder()) { - FolderMan::instance()->scheduler()->enqueueFolder(folder); + // message box can return at any time while the thread keeps running, + // so better check again after the user has responded. + if (folder->isSyncRunning() && terminate) { + folder->slotTerminateSync(tr("Sync paused by user")); } -} + folder->slotNextSyncFullLocalDiscovery(); // ensure we don't forget about local errors + folder->setSyncPaused(!currentlyPaused); -void AccountSettings::slotScheduleCurrentFolderForceFullDiscovery() -{ - if (auto folder = selectedFolder()) { - folder->slotWipeErrorBlacklist(); - folder->slotNextSyncFullLocalDiscovery(); - folder->journalDb()->forceRemoteDiscoveryNextSync(); - FolderMan::instance()->scheduler()->enqueueFolder(folder); - } + // keep state for the icon setting. + if (currentlyPaused) + _wasDisabledBefore = true; + + _model->slotUpdateFolderState(folder); } -void AccountSettings::slotForceSyncCurrentFolder() +void AccountSettings::slotForceSyncCurrentFolder(Folder *folder) { - if (auto selectedFolder = this->selectedFolder()) { - if (Utility::internetConnectionIsMetered() && ConfigFile().pauseSyncWhenMetered()) { - auto messageBox = new QMessageBox(QMessageBox::Question, tr("Internet connection is metered"), - tr("Synchronization is paused because the Internet connection is a metered connection" - "

Do you really want to force a Synchronization now?"), - QMessageBox::Yes | QMessageBox::No, ocApp()->gui()->settingsDialog()); - messageBox->setAttribute(Qt::WA_DeleteOnClose); - connect(messageBox, &QMessageBox::accepted, this, [this, selectedFolder] { doForceSyncCurrentFolder(selectedFolder); }); - ownCloudGui::raise(); - messageBox->open(); - } else { - doForceSyncCurrentFolder(selectedFolder); - } + if (Utility::internetConnectionIsMetered() && ConfigFile().pauseSyncWhenMetered()) { + auto messageBox = new QMessageBox(QMessageBox::Question, tr("Internet connection is metered"), + tr("Synchronization is paused because the Internet connection is a metered connection" + "

Do you really want to force a Synchronization now?"), + QMessageBox::Yes | QMessageBox::No, ocApp()->gui()->settingsDialog()); + messageBox->setAttribute(Qt::WA_DeleteOnClose); + connect(messageBox, &QMessageBox::accepted, this, [folder = QPointer(folder), this] { + if (folder) { + doForceSyncCurrentFolder(folder); + } + }); + ownCloudGui::raise(); + messageBox->open(); + } else { + doForceSyncCurrentFolder(folder); } } @@ -736,3 +733,4 @@ bool AccountSettings::event(QEvent *e) } } // namespace OCC +#include "accountsettings.moc" diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index 8c28a7bb396..0214281c59c 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -15,17 +15,19 @@ #ifndef ACCOUNTSETTINGS_H #define ACCOUNTSETTINGS_H -#include #include "folder.h" #include "loginrequireddialog.h" #include "owncloudgui.h" #include "progressdispatcher.h" + +#include +#include + class QModelIndex; class QNetworkReply; class QLabel; -class QSortFilterProxyModel; namespace OCC { class AccountModalWidget; @@ -49,6 +51,7 @@ class AccountSettings : public QWidget { Q_OBJECT Q_PROPERTY(AccountStatePtr accountState MEMBER _accountState) + Q_PROPERTY(QSortFilterProxyModel *model MEMBER _sortModel CONSTANT) public: enum class ModalWidgetSizePolicy { Minimum = QSizePolicy::Minimum, Expanding = QSizePolicy::Expanding }; @@ -62,6 +65,8 @@ class AccountSettings : public QWidget void addModalLegacyDialog(QWidget *widget, ModalWidgetSizePolicy sizePolicy); void addModalWidget(AccountModalWidget *widget); + auto model() { return _sortModel; } + signals: void folderChanged(); void showIssuesList(); @@ -71,18 +76,15 @@ public slots: protected slots: void slotAddFolder(); - void slotEnableCurrentFolder(bool terminate = false); - void slotScheduleCurrentFolder(); - void slotScheduleCurrentFolderForceFullDiscovery(); - void slotForceSyncCurrentFolder(); + void slotEnableCurrentFolder(Folder *folder, bool terminate = false); + void slotForceSyncCurrentFolder(Folder *folder); void slotRemoveCurrentFolder(); void slotEnableVfsCurrentFolder(); void slotDisableVfsCurrentFolder(); void slotFolderWizardAccepted(); void slotDeleteAccount(); void slotToggleSignInState(); - void slotCustomContextMenuRequested(const QPoint &); - void slotFolderListClicked(const QModelIndex &indx); + void slotCustomContextMenuRequested(const QString &id); private: void showSelectiveSyncDialog(Folder *folder); @@ -93,13 +95,9 @@ protected slots: void createAccountToolbox(); void doForceSyncCurrentFolder(Folder *selectedFolder); - /// Returns the alias of the selected folder, empty string if none - Folder *selectedFolder() const; - Ui::AccountSettings *ui; FolderStatusModel *_model; - FolderStatusDelegate *_delegate; QSortFilterProxyModel *_sortModel; bool _wasDisabledBefore; AccountStatePtr _accountState; diff --git a/src/gui/accountsettings.ui b/src/gui/accountsettings.ui index 0ec52091d8a..be42deea414 100644 --- a/src/gui/accountsettings.ui +++ b/src/gui/accountsettings.ui @@ -147,20 +147,7 @@ - - - - 0 - 5 - - - - Qt::CustomContextMenu - - - QAbstractItemView::NoEditTriggers - - + @@ -193,6 +180,11 @@ + + QQuickWidget + QWidget +

QtQuickWidgets/QQuickWidget
+ QProgressIndicator QWidget diff --git a/src/gui/folderstatusdelegate.cpp b/src/gui/folderstatusdelegate.cpp deleted file mode 100644 index 217ee0c55b0..00000000000 --- a/src/gui/folderstatusdelegate.cpp +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright (C) by Klaas Freitag - * Copyright (C) by Olivier Goffart - * - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - */ - -#include "folderstatusdelegate.h" -#include "folderstatusmodel.h" - -#include "folderman.h" -#include "accountstate.h" -#include "theme.h" -#include "account.h" -#include "guiutility.h" - -#include "resources/resources.h" - -#include -#include - -namespace { -const int barHeightC = 7; -} - -namespace OCC { - -FolderStatusDelegate::FolderStatusDelegate(QObject *parent) - : QStyledItemDelegate(parent) -{ -} - -// allocate each item size in listview. -QSize FolderStatusDelegate::sizeHint(const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - const_cast(this)->updateFont(option.font); - QFontMetricsF fm(_font); - - // calc height - qreal h = rootFolderHeightWithoutErrors() + _margin; - // this already includes the bottom margin - - // add some space for the message boxes. - for (auto column : {FolderStatusModel::Columns::FolderConflictMsg, FolderStatusModel::Columns::FolderErrorMsg}) { - auto msgs = index.siblingAtColumn(static_cast(column)).data().toStringList(); - if (!msgs.isEmpty()) { - h += _margin + 2 * _margin + msgs.count() * fm.height(); - } - } - - return QSize(0, h); -} - -qreal FolderStatusDelegate::rootFolderHeightWithoutErrors() const -{ - if (!_ready) { - return {}; - } - const QFontMetricsF fm(_font); - const QFontMetricsF aliasFm(_aliasFont); - qreal h = _aliasMargin; // margin to top - h += aliasFm.height(); // alias - h += _margin; // between alias and local path - h += fm.height(); // sync text - - // quota or progress bar - h += _margin; - h += fm.height(); // quota or progress bar - h += _margin; - h += fm.height(); // possible progress string - return h; -} - -void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, - const QModelIndex &index) const -{ - if (index.column() != 0) { - return; - } - - const_cast(this)->updateFont(option.font); - const auto textAlign = Qt::AlignLeft; - - const QFont errorFont = _font; - const QFont progressFont = [progressFont = _font]() mutable { - progressFont.setPointSize(progressFont.pointSize() - 2); - return progressFont; - }(); - - const QFontMetricsF subFm(_font); - const QFontMetricsF aliasFm(_aliasFont); - - painter->save(); - - const QString statusIconName = index.siblingAtColumn(static_cast(FolderStatusModel::Columns::FolderStatusIconRole)).data().toString(); - const QString aliasText = qvariant_cast(index.siblingAtColumn(static_cast(FolderStatusModel::Columns::HeaderRole)).data()); - const QStringList conflictTexts = qvariant_cast(index.siblingAtColumn(static_cast(FolderStatusModel::Columns::FolderConflictMsg)).data()); - const QStringList errorTexts = qvariant_cast(index.siblingAtColumn(static_cast(FolderStatusModel::Columns::FolderErrorMsg)).data()); - const QIcon spaceImage = index.siblingAtColumn(static_cast(FolderStatusModel::Columns::FolderImage)).data().value(); - - const int overallPercent = qvariant_cast(index.siblingAtColumn(static_cast(FolderStatusModel::Columns::SyncProgressOverallPercent)).data()); - const QString overallString = qvariant_cast(index.siblingAtColumn(static_cast(FolderStatusModel::Columns::SyncProgressOverallString)).data()); - const QString itemString = qvariant_cast(index.siblingAtColumn(static_cast(FolderStatusModel::Columns::SyncProgressItemString)).data()); - const int warningCount = qvariant_cast(index.siblingAtColumn(static_cast(FolderStatusModel::Columns::WarningCount)).data()); - const bool syncOngoing = qvariant_cast(index.siblingAtColumn(static_cast(FolderStatusModel::Columns::SyncRunning)).data()); - - const QString syncText = qvariant_cast(index.siblingAtColumn(static_cast(FolderStatusModel::Columns::FolderSyncText)).data()); - const bool showProgess = !overallString.isEmpty() || !itemString.isEmpty(); - - const auto iconState = - index.siblingAtColumn(static_cast(FolderStatusModel::Columns::FolderAccountConnected)).data().toBool() ? QIcon::Normal : QIcon::Disabled; - - const auto statusRect = QRectF{option.rect}.adjusted(0, 0, 0, rootFolderHeightWithoutErrors() - option.rect.height()); - const auto iconRect = - QRectF{statusRect.topLeft(), QSizeF{statusRect.height(), statusRect.height()}}.marginsRemoved({_aliasMargin, _aliasMargin, _aliasMargin, _aliasMargin}); - - // the rectangle next to the icon which will contain the strings - const auto infoRect = QRectF{iconRect.topRight(), QSizeF{statusRect.width() - iconRect.width(), iconRect.height()}}.marginsRemoved({_aliasMargin, 0, 0, 0}); - const auto aliasRect = QRectF{infoRect.topLeft(), QSizeF{infoRect.width(), aliasFm.height()}}; - - const auto optionsButtonRect = this->computeOptionsButtonRect(option.rect); - const auto marginOffset = QPointF{0, _margin}; - const auto localPathRect = QRectF{aliasRect.bottomLeft() + marginOffset, QSizeF{aliasRect.width(), subFm.height()}}; - const auto quotaTextRect = [&] { - QRectF rect{localPathRect.bottomLeft() + marginOffset, QSizeF{aliasRect.width(), subFm.height()}}; - rect.setRight(optionsButtonRect.left() - _margin); - return rect; - }(); - - - { - const auto iconVisualRect = QStyle::visualRect(option.direction, option.rect, iconRect.toRect()); - spaceImage.paint(painter, iconVisualRect, Qt::AlignCenter, iconState); - // paint the overlay in NormalState, on mac os disabled icons have an alpha channel, drawing semi transparent icons on top of each other... - Resources::getCoreIcon(QStringLiteral("states/%1").arg(statusIconName)).paint(painter, iconVisualRect, Qt::AlignCenter, QIcon::Normal); - } - - // only show the warning icon if the sync is running. Otherwise its - // encoded in the status icon. - if (warningCount > 0 && syncOngoing) { - Resources::getCoreIcon(QStringLiteral("warning")) - .paint(painter, QStyle::visualRect(option.direction, option.rect, QRectF{iconRect.bottomLeft() - QPointF(0, 17), QSizeF{16, 16}}.toRect()), - Qt::AlignCenter, iconState); - } - - auto palette = option.palette; - - QPalette::ColorGroup cg = option.state & QStyle::State_Enabled - ? QPalette::Normal - : QPalette::Disabled; - if (cg == QPalette::Normal && !(option.state & QStyle::State_Active)) - cg = QPalette::Inactive; - - if (option.state & QStyle::State_Selected) { -#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) - painter->setPen(palette.color(cg, QPalette::Accent)); -#else - painter->setPen(palette.color(cg, QPalette::HighlightedText)); -#endif - } else { - painter->setPen(palette.color(cg, QPalette::Text)); - } - painter->setFont(_aliasFont); - painter->drawText( - QStyle::visualRect(option.direction, option.rect, aliasRect.toRect()), textAlign, aliasFm.elidedText(aliasText, Qt::ElideRight, aliasRect.width())); - - painter->setFont(_font); - painter->drawText(QStyle::visualRect(option.direction, option.rect, localPathRect.toRect()), textAlign, - subFm.elidedText(syncText, Qt::ElideRight, localPathRect.width())); - - if (!showProgess) { - const auto totalQuota = index.siblingAtColumn(static_cast(FolderStatusModel::Columns::QuotaTotal)).data().value(); - // only draw a bar if we have a quota set - if (totalQuota > 0) { - const auto usedQuota = index.siblingAtColumn(static_cast(FolderStatusModel::Columns::QuotaUsed)).data().value(); - painter->setFont(_font); - painter->drawText(QStyle::visualRect(option.direction, option.rect, quotaTextRect.toRect()), Qt::AlignLeft | Qt::AlignVCenter, - subFm.elidedText( - tr("%1 of %2 in use").arg(Utility::octetsToString(usedQuota), Utility::octetsToString(totalQuota)), Qt::ElideRight, quotaTextRect.width())); - } - } else { - painter->save(); - - const auto pogressRect = quotaTextRect.marginsAdded({0, 0, 0, barHeightC + _margin + subFm.height()}); - // Overall Progress Bar. - const auto pBRect = QRectF{pogressRect.topLeft(), QSizeF{pogressRect.width() - 2 * _margin, barHeightC}}; - - QStyleOptionProgressBar pBarOpt; - - pBarOpt.state = option.state | QStyle::State_Horizontal; - pBarOpt.minimum = 0; - pBarOpt.maximum = 100; - pBarOpt.progress = overallPercent; - pBarOpt.rect = QStyle::visualRect(option.direction, option.rect, pBRect.toRect()); - QApplication::style()->drawControl(QStyle::CE_ProgressBar, &pBarOpt, painter, option.widget); - - // Overall Progress Text - const QRectF overallProgressRect = {pBRect.bottomLeft() + marginOffset, QSizeF{pogressRect.width(), subFm.height()}}; - painter->setFont(progressFont); - - painter->drawText(QStyle::visualRect(option.direction, option.rect, overallProgressRect.toRect()), Qt::AlignLeft | Qt::AlignVCenter, overallString); - - painter->restore(); - } - - // paint an error overlay if there is an error string or conflict string - auto drawTextBox = [&, pos = option.rect.top() + rootFolderHeightWithoutErrors() + _margin](const QStringList &texts, QColor color) mutable { - QRectF rect = quotaTextRect; - rect.setLeft(iconRect.left()); - rect.setTop(pos); - rect.setHeight(texts.count() * subFm.height() + 2 * _margin); - rect.setRight(option.rect.right() - _margin); - - painter->save(); - painter->setBrush(color); - painter->setPen(QColor(0xaa, 0xaa, 0xaa)); - painter->drawRoundedRect(QStyle::visualRect(option.direction, option.rect, rect.toRect()), 4, 4); - painter->setPen(Qt::white); - painter->setFont(errorFont); - QRect textRect(rect.left() + _margin, rect.top() + _margin, rect.width() - 2 * _margin, subFm.height()); - - for (const auto &eText : texts) { - painter->drawText(QStyle::visualRect(option.direction, option.rect, textRect), textAlign, subFm.elidedText(eText, Qt::ElideLeft, textRect.width())); - textRect.translate(0, textRect.height()); - } - painter->restore(); - - pos = rect.bottom() + _margin; - }; - - if (!conflictTexts.isEmpty()) { - drawTextBox(conflictTexts, QColor(0xba, 0xba, 0x4d)); - } - if (!errorTexts.isEmpty()) { - drawTextBox(errorTexts, QColor(0xbb, 0x4d, 0x4d)); - } - { - // was saved before we fetched the data from the model - painter->restore(); - QStyleOptionToolButton btnOpt; - btnOpt.state = option.state; - btnOpt.state &= ~(QStyle::State_Selected | QStyle::State_HasFocus); - btnOpt.state |= QStyle::State_Raised; - btnOpt.arrowType = Qt::NoArrow; - btnOpt.subControls = QStyle::SC_ToolButton; - btnOpt.rect = QStyle::visualRect(option.direction, option.rect, optionsButtonRect.toRect()); - btnOpt.icon = Resources::getCoreIcon(QStringLiteral("more")); - int e = QApplication::style()->pixelMetric(QStyle::PM_ButtonIconSize); - btnOpt.iconSize = QSize(e,e); - QApplication::style()->drawComplexControl(QStyle::CC_ToolButton, &btnOpt, painter); - } -} - -QRectF FolderStatusDelegate::computeOptionsButtonRect(QRectF within) const -{ - if (!_ready) { - return {}; - } - within.setHeight(FolderStatusDelegate::rootFolderHeightWithoutErrors()); - - QStyleOptionToolButton opt; - int e = QApplication::style()->pixelMetric(QStyle::PM_ButtonIconSize); - opt.rect.setSize(QSize(e,e)); - QSizeF size = QApplication::style()->sizeFromContents(QStyle::CT_ToolButton, &opt, opt.rect.size()); - - return {{within.right() - size.width() - QApplication::style()->pixelMetric(QStyle::PM_LayoutRightMargin), - within.top() + within.height() / 2 - size.height() / 2}, - size}; -} - -QRectF FolderStatusDelegate::errorsListRect(QRectF within, const QModelIndex &index) const -{ - if (!_ready) { - return {}; - } - const QFontMetrics fm(_font); - within.setTop(within.top() + FolderStatusDelegate::rootFolderHeightWithoutErrors() + _margin); - qreal h = 0; - for (auto column : { FolderStatusModel::Columns::FolderConflictMsg, FolderStatusModel::Columns::FolderErrorMsg }) { - const auto msgs = index.siblingAtColumn(static_cast(column)).data().toStringList(); - if (!msgs.isEmpty()) { - h += _margin + 2 * _margin + msgs.count() * fm.height() + _margin; - } - } - within.setHeight(h); - return within; -} - -void FolderStatusDelegate::updateFont(const QFont &font) -{ - if (!_ready || _font != font) { - _ready = true; - _aliasFont = [&]() { - auto aliasFont = font; - aliasFont.setBold(true); - aliasFont.setPointSizeF(font.pointSizeF() + 2); - return aliasFont; - }(); - - _margin = QFontMetricsF(_font).height() / 4.0; - _aliasMargin = QFontMetricsF(_aliasFont).height() / 2.0; - } -} - - -} // namespace OCC diff --git a/src/gui/folderstatusdelegate.h b/src/gui/folderstatusdelegate.h deleted file mode 100644 index db7027eb2af..00000000000 --- a/src/gui/folderstatusdelegate.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) by Klaas Freitag - * Copyright (C) by Olivier Goffart - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * for more details. - */ - -#pragma once -#include - -namespace OCC { - -/** - * @brief The FolderStatusDelegate class - * @ingroup gui - */ -class FolderStatusDelegate : public QStyledItemDelegate -{ - Q_OBJECT -public: - FolderStatusDelegate(QObject *parent); - - void paint(QPainter *, const QStyleOptionViewItem &, const QModelIndex &) const override; - QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override; - - - /** - * return the position of the option button within the item - */ - QRectF computeOptionsButtonRect(QRectF within) const; - QRectF errorsListRect(QRectF within, const QModelIndex &) const; - qreal rootFolderHeightWithoutErrors() const; - -private: - static QString addFolderText(bool useSapces); - - // a workaround for a design flaw of the class - // we need to know the actual font for most computations - // the font is only set in paint and sizeHint - void updateFont(const QFont &font); - - QFont _aliasFont; - QFont _font; - qreal _margin = 0; - qreal _aliasMargin = 0; - bool _ready = false; -}; - -} // namespace OCC diff --git a/src/gui/folderstatusmodel.cpp b/src/gui/folderstatusmodel.cpp index 05a2e054e2c..2fd0455ae52 100644 --- a/src/gui/folderstatusmodel.cpp +++ b/src/gui/folderstatusmodel.cpp @@ -17,7 +17,6 @@ #include "accountstate.h" #include "common/asserts.h" #include "folderman.h" -#include "folderstatusdelegate.h" #include "gui/quotainfo.h" #include "theme.h" @@ -29,6 +28,7 @@ #include #include #include +#include #include #include @@ -45,18 +45,29 @@ namespace { // minimum delay between progress updates constexpr auto progressUpdateTimeOutC = 1s; - int64_t getQuota(const AccountStatePtr &accountState, const QString &spaceId, FolderStatusModel::Columns type) + QString statusIconName(Folder *f) + { + auto status = f->syncResult(); + if (!f->accountState()->isConnected()) { + status.setStatus(SyncResult::Status::Offline); + } else if (f->syncPaused() || f->accountState()->state() == AccountState::PausedDueToMetered) { + status.setStatus(SyncResult::Status::Paused); + } + return Theme::instance()->syncStateIconName(status); + } + + QString getQuota(const AccountStatePtr &accountState, const QString &spaceId, int role) { if (auto spacesManager = accountState->account()->spacesManager()) { const auto *space = spacesManager->space(spaceId); if (space) { const auto quota = space->drive().getQuota(); if (quota.isValid()) { - switch (type) { + switch (static_cast(role)) { case FolderStatusModel::Columns::QuotaTotal: - return quota.getTotal(); + return Utility::octetsToString(quota.getTotal()); case FolderStatusModel::Columns::QuotaUsed: - return quota.getUsed(); + return Utility::octetsToString(quota.getUsed()); default: Q_UNREACHABLE(); } @@ -66,16 +77,20 @@ namespace { return {}; } - int64_t getQuotaOc10(const AccountStatePtr &accountState, FolderStatusModel::Columns type) + QString getQuotaOc10(const AccountStatePtr &accountState, int role) { - switch (type) { + int out = 0; + switch (static_cast(role)) { case FolderStatusModel::Columns::QuotaTotal: - return accountState->quotaInfo()->lastQuotaTotalBytes(); + out = accountState->quotaInfo()->lastQuotaTotalBytes(); + break; case FolderStatusModel::Columns::QuotaUsed: - return accountState->quotaInfo()->lastQuotaUsedBytes(); + out = accountState->quotaInfo()->lastQuotaUsedBytes(); + break; default: Q_UNREACHABLE(); } + return Utility::octetsToString(qMax(0, out)); } class SubFolderInfo @@ -85,11 +100,10 @@ namespace { { public: Progress() { } - bool isNull() const { return _progressString.isEmpty() && _warningCount == 0 && _overallSyncString.isEmpty(); } + bool isNull() const { return _progressString.isEmpty() && _overallSyncString.isEmpty(); } QString _progressString; QString _overallSyncString; - int _warningCount = 0; - int _overallPercent = 0; + float _overallPercent = 0; }; SubFolderInfo(Folder *folder) : _folder(folder) @@ -224,9 +238,16 @@ void FolderStatusModel::setAccountState(const AccountStatePtr &accountState) connect(FolderMan::instance(), &FolderMan::folderSyncStateChange, this, &FolderStatusModel::slotFolderSyncStateChange); if (accountState->supportsSpaces()) { - connect(accountState->account()->spacesManager(), &GraphApi::SpacesManager::updated, this, [this] { - beginResetModel(); - endResetModel(); + connect(accountState->account()->spacesManager(), &GraphApi::SpacesManager::updated, this, + [this] { Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0)); }); + connect(accountState->account()->spacesManager(), &GraphApi::SpacesManager::spaceChanged, this, [this](auto *space) { + for (int i = 0; i < rowCount(); ++i) { + auto *f = _folders[i]->_folder; + if (f->accountState()->supportsSpaces() && f->spaceId() == space->drive().getId()) { + Q_EMIT dataChanged(index(i, 0), index(i, 0)); + break; + } + } }); } } @@ -245,7 +266,7 @@ void FolderStatusModel::setAccountState(const AccountStatePtr &accountState) }); } - Q_EMIT endResetModel(); + endResetModel(); } QVariant FolderStatusModel::data(const QModelIndex &index, int role) const @@ -256,22 +277,11 @@ QVariant FolderStatusModel::data(const QModelIndex &index, int role) const if (role == Qt::EditRole) return QVariant(); - const Columns column = static_cast(index.column()); - switch (column) { - case Columns::IsUsingSpaces: - return _accountState->supportsSpaces(); - default: - break; - } - const auto &folderInfo = _folders.at(index.row()); auto f = folderInfo->_folder; if (!f) return QVariant(); - const auto &progress = folderInfo->_progress; - const bool accountConnected = _accountState->isConnected(); - const auto getSpace = [&]() -> GraphApi::Space * { if (_accountState->supportsSpaces()) { return _accountState->account()->spacesManager()->space(f->spaceId()); @@ -279,67 +289,57 @@ QVariant FolderStatusModel::data(const QModelIndex &index, int role) const return nullptr; }; - switch (role) { - case Qt::DisplayRole: - switch (column) { - case Columns::FolderPathRole: - return f->shortGuiLocalPath(); - case Columns::FolderSecondPathRole: - return f->remotePath(); - case Columns::FolderConflictMsg: - return (f->syncResult().hasUnresolvedConflicts()) - ? QStringList(tr("There are unresolved conflicts. Click for details.")) - : QStringList(); - case Columns::FolderErrorMsg: { - auto errors = f->syncResult().errorStrings(); - const auto legacyError = FolderMan::instance()->unsupportedConfiguration(f->path()); - if (!legacyError) { - // the error message might contain new lines, the delegate only expect multiple single line values - errors.append(legacyError.error().split(QLatin1Char('\n'))); - } - if (f->isReady() && f->virtualFilesEnabled() && f->vfs().mode() == Vfs::Mode::WithSuffix) { - errors.append({ - tr("The suffix VFS plugin is deprecated and will be removed in the 7.0 release."), - tr("Please use the context menu and select \"Disable virtual file support\" to ensure future access to your synced files."), - tr("You are going to lose access to your sync folder if you do not do so!"), - }); + switch (static_cast(role)) { + case Columns::Subtitle: { + if (auto *space = getSpace()) { + if (!space->drive().getDescription().isEmpty()) { + return space->drive().getDescription(); } - return errors; } + return tr("Local folder: %1").arg(f->shortGuiLocalPath()); + } + case Columns::FolderErrorMsg: { + auto errors = f->syncResult().errorStrings(); + const auto legacyError = FolderMan::instance()->unsupportedConfiguration(f->path()); + if (!legacyError) { + errors.append(legacyError.error()); + } + if (f->syncResult().hasUnresolvedConflicts()) { + errors.append(tr("There are unresolved conflicts. Click for details.")); + } + if (f->isReady() && f->virtualFilesEnabled() && f->vfs().mode() == Vfs::Mode::WithSuffix) { + errors.append({ + tr("The suffix VFS plugin is deprecated and will be removed in the 7.0 release.\n" + "Please use the context menu and select \"Disable virtual file support\" to ensure future access to your synced files.\n" + "You are going to lose access to your sync folder if you do not do so!"), + }); + } + return errors; + } case Columns::SyncRunning: return f->syncResult().status() == SyncResult::SyncRunning; - case Columns::HeaderRole: { + case Columns::DisplayName: { if (auto *space = getSpace()) { return space->displayName(); } return f->displayName(); } - case Columns::FolderImage: - if (auto *space = getSpace()) { - return space->image(); - } - return Resources::getCoreIcon(QStringLiteral("folder-sync")); - case Columns::FolderSyncPaused: - return f->syncPaused(); - case Columns::FolderAccountConnected: - return accountConnected; - case Columns::FolderStatusIconRole: { - auto status = f->syncResult(); - if (!accountConnected) { - status.setStatus(SyncResult::Status::Offline); - } else if (f->syncPaused() || f->accountState()->state() == AccountState::PausedDueToMetered) { - status.setStatus(SyncResult::Status::Paused); + case Columns::FolderImageUrl: + if (f->accountState()->supportsSpaces()) { + // TODO: the url hast random parts to enforce a reload + return QStringLiteral("image://space/%1/%2").arg(QString::number(QRandomGenerator::global()->generate()), f->spaceId()); } - return Theme::instance()->syncStateIconName(status); - } + return QStringLiteral("image://ownCloud/core/folder-sync"); + case Columns::FolderStatusUrl: + return QStringLiteral("image://ownCloud/core/states/%1").arg(statusIconName(f)); + case Columns::FolderId: + return f->id(); case Columns::SyncProgressItemString: - return progress._progressString; - case Columns::WarningCount: - return progress._warningCount; + return folderInfo->_progress._progressString; case Columns::SyncProgressOverallPercent: - return progress._overallPercent; + return folderInfo->_progress._overallPercent / 100.0; case Columns::SyncProgressOverallString: - return progress._overallSyncString; + return folderInfo->_progress._overallSyncString; case Columns::FolderSyncText: { if (auto *space = getSpace()) { if (!space->drive().getDescription().isEmpty()) { @@ -348,39 +348,31 @@ QVariant FolderStatusModel::data(const QModelIndex &index, int role) const } return tr("Local folder: %1").arg(f->shortGuiLocalPath()); } - case Columns::IsReady: - return f->isReady(); - case Columns::IsDeployed: - return f->isDeployed(); case Columns::Priority: - return f->priority(); + return QStringLiteral("%1%2").arg(f->priority(), 3, 10, QLatin1Char('0')).arg(f->displayName()); case Columns::QuotaTotal: [[fallthrough]]; case Columns::QuotaUsed: if (_accountState->supportsSpaces()) { - return QVariant::fromValue(getQuota(_accountState, f->spaceId(), column)); + return QVariant::fromValue(getQuota(_accountState, f->spaceId(), role)); } else { - return QVariant::fromValue(getQuotaOc10(_accountState, column)); + return QVariant::fromValue(getQuotaOc10(_accountState, role)); + } + case Columns::ToolTip: { + if (!folderInfo->_progress.isNull()) { + return folderInfo->_progress._progressString; + } + if (_accountState->isConnected()) { + return tr("The last syncronisation of %1 resulted in %2").arg(f->displayName(), Utility::enumToDisplayName(f->syncResult().status())); + } else { + return tr("The account %1 is currently not connected.").arg(f->accountState()->account()->displayName()); } - case Columns::IsUsingSpaces: // handled before - [[fallthrough]]; case Columns::ColumnCount: Q_UNREACHABLE(); break; + } break; } - break; - case Qt::ToolTipRole: { - if (!progress.isNull()) { - return progress._progressString; - } - if (accountConnected) { - return tr("%1\n%2").arg(Utility::enumToDisplayName(f->syncResult().status()), QDir::toNativeSeparators(folderInfo->_folder->path())); - } else { - return tr("Signed out\n%1").arg(QDir::toNativeSeparators(folderInfo->_folder->path())); - } - } - } - return QVariant(); + return {}; } Folder *FolderStatusModel::folder(const QModelIndex &index) const @@ -391,7 +383,7 @@ Folder *FolderStatusModel::folder(const QModelIndex &index) const int FolderStatusModel::columnCount(const QModelIndex &) const { - return static_cast(Columns::ColumnCount); + return 1; } int FolderStatusModel::rowCount(const QModelIndex &parent) const @@ -400,6 +392,25 @@ int FolderStatusModel::rowCount(const QModelIndex &parent) const return static_cast(_folders.size()); } +QHash FolderStatusModel::roleNames() const +{ + return { + {static_cast(Columns::DisplayName), "displayName"}, + {static_cast(Columns::Subtitle), "subTitle"}, + {static_cast(Columns::FolderImageUrl), "imageUrl"}, + {static_cast(Columns::FolderStatusUrl), "statusUrl"}, + {static_cast(Columns::SyncProgressOverallPercent), "progress"}, + {static_cast(Columns::SyncProgressOverallString), "overallText"}, + {static_cast(Columns::SyncProgressItemString), "itemText"}, + {static_cast(Columns::FolderSyncText), "descriptionText"}, + {static_cast(Columns::FolderId), "folderId"}, + {static_cast(Columns::FolderErrorMsg), "errorMsg"}, + {static_cast(Columns::QuotaTotal), "quotaTotal"}, + {static_cast(Columns::QuotaUsed), "quotaUsed"}, + {static_cast(Columns::ToolTip), "toolTip"}, + }; +} + void FolderStatusModel::slotUpdateFolderState(Folder *folder) { if (!folder) @@ -424,11 +435,6 @@ void FolderStatusModel::slotSetProgress(const ProgressInfo &progress, Folder *f) const auto &folder = _folders.at(folderIndex); auto *pi = &folder->_progress; - - if (progress.status() == ProgressInfo::Done && !progress._lastCompletedItem.isEmpty() - && Progress::isWarningKind(progress._lastCompletedItem._status)) { - pi->_warningCount++; - } // depending on the use of virtual files or small files this slot might be called very often. // throttle the model updates to prevent an needlessly high cpu usage used on ui updates. if (folder->_lastProgressUpdateStatus != progress.status() || (std::chrono::steady_clock::now() - folder->_lastProgressUpdated > progressUpdateTimeOutC)) { diff --git a/src/gui/folderstatusmodel.h b/src/gui/folderstatusmodel.h index 55352a11f1d..ecd930d967f 100644 --- a/src/gui/folderstatusmodel.h +++ b/src/gui/folderstatusmodel.h @@ -19,10 +19,11 @@ #include "progressdispatcher.h" #include -#include -#include #include +#include #include +#include +#include class QNetworkReply; @@ -43,34 +44,30 @@ namespace { class FolderStatusModel : public QAbstractTableModel { Q_OBJECT + QML_ELEMENT public: enum class Columns { - HeaderRole, // must be 0 as it is also used from the default delegate - FolderPathRole, // for a SubFolder it's the complete path - FolderSecondPathRole, - FolderConflictMsg, + ToolTip = Qt::ToolTipRole, + DisplayName = Qt::UserRole + 1, // must be 0 as it is also used from the default delegate + Subtitle, FolderErrorMsg, - FolderSyncPaused, - FolderStatusIconRole, - FolderAccountConnected, - FolderImage, SyncProgressOverallPercent, SyncProgressOverallString, SyncProgressItemString, - WarningCount, SyncRunning, FolderSyncText, - IsReady, // boolean - IsUsingSpaces, // boolean Priority, // uint32_t - IsDeployed, // bool QuotaUsed, QuotaTotal, + FolderId, + FolderImageUrl, + FolderStatusUrl, + ColumnCount }; Q_ENUMS(Columns); @@ -84,6 +81,8 @@ class FolderStatusModel : public QAbstractTableModel int columnCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QHash roleNames() const override; + public slots: void slotUpdateFolderState(Folder *); void resetFolders(); @@ -100,5 +99,4 @@ private slots: }; } // namespace OCC - #endif // FOLDERSTATUSMODEL_H diff --git a/src/gui/main.cpp b/src/gui/main.cpp index d7fc389bf04..32768b17c92 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #ifdef Q_OS_WIN @@ -387,9 +388,14 @@ QString setupTranslations(QApplication *app) return displayLanguage; } } // Anonymous namespace +namespace OCC { +// TODO: QT_NO_PLUGIN +extern void qml_register_types_org_ownCloud_qmlcomponents(); +} int main(int argc, char **argv) { + OCC::qml_register_types_org_ownCloud_qmlcomponents(); return RestartManager([](int argc, char **argv) { // when called with --cmd we run the cmd client in a sub process and forward everything if (argc > 1 && argv[1] == QByteArrayLiteral("--cmd")) { diff --git a/src/gui/models/models.h b/src/gui/models/models.h index 90d99db15e0..9842450cbb8 100644 --- a/src/gui/models/models.h +++ b/src/gui/models/models.h @@ -18,6 +18,8 @@ #include #include +#include + class QSortFilterProxyModel; class QMenu; @@ -25,6 +27,7 @@ namespace OCC { namespace Models { Q_NAMESPACE + QML_ELEMENT enum DataRoles { UnderlyingDataRole = Qt::UserRole + 100, diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index a948fb3f147..20f94603832 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -927,6 +927,8 @@ void ownCloudGui::runNewAccountWizard() } case ConnectionValidator::ClientUnsupported: break; + case ConnectionValidator::SslError: + break; default: Q_UNREACHABLE(); } diff --git a/src/gui/qml/FolderDelegate.qml b/src/gui/qml/FolderDelegate.qml new file mode 100644 index 00000000000..915ad0e21e0 --- /dev/null +++ b/src/gui/qml/FolderDelegate.qml @@ -0,0 +1,172 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import org.ownCloud.qmlcomponents 1.0 + +Pane { + readonly property real normalSize: 200 + + ScrollView { + id: scrollView + anchors.fill: parent + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AsNeeded + + ListView { + anchors.fill: parent + model: ctx.model + + spacing: 20 + + delegate: Pane { + id: folderDelegate + + required property string displayName + required property string descriptionText + required property url imageUrl + required property url statusUrl + required property double progress + required property string overallText + required property string itemText + required property string folderId + required property var errorMsg + required property string quotaTotal + required property string quotaUsed + required property string toolTip + + clip: true + width: ListView.view.width - scrollView.ScrollBar.vertical.width - 10 + + implicitHeight: normalSize + background: Rectangle { + color: scrollView.palette.base + } + + hoverEnabled: true + + ToolTip.text: folderDelegate.toolTip + ToolTip.visible: hovered + + ColumnLayout { + anchors.fill: parent + spacing: 10 + + RowLayout { + Layout.alignment: Qt.AlignTop + + Pane { + Layout.preferredHeight: normalSize - 20 + Layout.preferredWidth: normalSize - 20 + Layout.alignment: Qt.AlignTop + background: Rectangle { + color: scrollView.palette.alternateBase + } + Image { + id: image + anchors.fill: parent + fillMode: Image.PreserveAspectFit + source: imageUrl + } + } + ColumnLayout { + id: colLayout + spacing: 6 + Layout.margins: 10 + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + + RowLayout { + Layout.fillWidth: true + Image { + sourceSize.height: 16 + sourceSize.width: 16 + source: statusUrl + } + Label { + id: title + text: folderDelegate.displayName + font.bold: true + font.pointSize: 15 + elide: Text.ElideLeft + verticalAlignment: Text.AlignHCenter + } + } + Label { + id: subtitle + Layout.fillWidth: true + text: folderDelegate.descriptionText + elide: Text.ElideRight + verticalAlignment: Text.AlignHCenter + } + Label { + Layout.fillWidth: true + text: qsTr("%1 of %2 used").arg(folderDelegate.quotaUsed).arg(folderDelegate.quotaTotal) + elide: Text.ElideRight + verticalAlignment: Text.AlignHCenter + visible: folderDelegate.quotaUsed + } + + Item { + // ensure the progress bar always consumes its space + Layout.fillWidth: true + Layout.preferredHeight: 10 + ProgressBar { + anchors.fill: parent + value: folderDelegate.progress + visible: folderDelegate.overallText || folderDelegate.itemText + } + } + + Label { + id: progressText + Layout.fillWidth: true + text: folderDelegate.overallText + elide: Text.ElideLeft + } + Label { + id: progressText2 + Layout.fillWidth: true + text: folderDelegate.itemText + elide: Text.ElideMiddle + // only display the item text if we don't have errors + // visible: !folderDelegate.errorMsg.length + } + + FolderError { + Layout.fillWidth: true + Layout.fillHeight: true + + errorMessages: folderDelegate.errorMsg + onCollapsedChanged: { + if (!collapsed) { + folderDelegate.implicitHeight = normalSize + implicitHeight + Layout.margins; + } else { + folderDelegate.implicitHeight = normalSize; + } + } + } + } + Button { + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight + Layout.maximumHeight: 30 + display: AbstractButton.IconOnly + icon.source: "image://ownCloud/core/more" + onClicked: { + ctx.slotCustomContextMenuRequested(folderId); + } + } + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + + onClicked: { + ctx.slotCustomContextMenuRequested(folderId); + } + } + } + } + } +} diff --git a/src/gui/qml/FolderError.qml b/src/gui/qml/FolderError.qml new file mode 100644 index 00000000000..a676254e56c --- /dev/null +++ b/src/gui/qml/FolderError.qml @@ -0,0 +1,84 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +ColumnLayout { + property bool collapsed: true + required property var errorMessages + + component ErrorItem: RowLayout { + property alias text: label.text + property alias maximumLineCount: label.maximumLineCount + Layout.fillWidth: true + Image { + Layout.alignment: Qt.AlignTop + source: "image://ownCloud/core/warning" + Layout.maximumHeight: 16 + Layout.maximumWidth: 16 + } + Label { + id: label + Layout.fillWidth: true + elide: Label.ElideRight + wrapMode: Label.WordWrap + } + } + + Component { + id: expandedError + ColumnLayout { + Layout.fillWidth: true + Repeater { + model: errorMessages + delegate: ErrorItem { + required property string modelData + text: modelData + Layout.fillWidth: true + } + } + Label { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Show less") + onLinkActivated: { + collapsed = true; + } + } + } + } + + Component { + id: collapsedError + ColumnLayout { + Layout.fillWidth: true + ErrorItem { + Layout.fillWidth: true + text: errorMessages + maximumLineCount: 1 + } + Label { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Show more") + onLinkActivated: { + collapsed = false; + } + } + } + } + + function loadComponent() { + if (errorMessages.length) { + return collapsed ? collapsedError : expandedError; + } + return undefined; + } + + Loader { + id: loader + anchors.fill: parent + sourceComponent: loadComponent() + } + + onCollapsedChanged: { + loader.sourceComponent = loadComponent(); + } +} diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index c22dcd24afe..382b6cd1338 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -87,6 +87,7 @@ target_link_libraries(libsync Qt::Core Qt::Network Qt::Widgets + Qt::QuickControls2 PRIVATE Qt::Concurrent diff --git a/src/libsync/graphapi/space.cpp b/src/libsync/graphapi/space.cpp index 261a512b63d..e9368bf6ba9 100644 --- a/src/libsync/graphapi/space.cpp +++ b/src/libsync/graphapi/space.cpp @@ -95,7 +95,7 @@ QUrl Space::imageUrl() const QIcon Space::image() const { if (_image.isNull()) { - return Resources::getCoreIcon(QStringLiteral("folder-sync")); + return Resources::getCoreIcon(QStringLiteral("space")); } return _image; } diff --git a/src/libsync/platform_mac.h b/src/libsync/platform_mac.h index d431141f187..f84120bde4b 100644 --- a/src/libsync/platform_mac.h +++ b/src/libsync/platform_mac.h @@ -17,6 +17,7 @@ #include "platform.h" #include +#include #include namespace OCC { diff --git a/src/libsync/platform_win.cpp b/src/libsync/platform_win.cpp index 799596ebae7..ccf387ac3f3 100644 --- a/src/libsync/platform_win.cpp +++ b/src/libsync/platform_win.cpp @@ -21,7 +21,7 @@ #include #include #include -#include +#include #include @@ -67,6 +67,7 @@ void WinPlatform::setApplication(QCoreApplication *application) if (auto guiApp = qobject_cast(application)) { QApplication::setStyle(QStringLiteral("fusion")); + QQuickStyle::setStyle(QStringLiteral("Fusion")); } } diff --git a/src/resources/CMakeLists.txt b/src/resources/CMakeLists.txt index 4e40eea6a05..23b7da9bc3f 100644 --- a/src/resources/CMakeLists.txt +++ b/src/resources/CMakeLists.txt @@ -10,7 +10,7 @@ generate_theme(owncloudResources OWNCLOUD_SIDEBAR_ICONS) # make them available to the whole project set(OWNCLOUD_SIDEBAR_ICONS ${OWNCLOUD_SIDEBAR_ICONS} CACHE INTERNAL "Sidebar icons" FORCE) -target_link_libraries(owncloudResources PUBLIC Qt::Core Qt::Gui) +target_link_libraries(owncloudResources PUBLIC Qt::Core Qt::Gui Qt::Quick) apply_common_target_settings(owncloudResources) target_include_directories(owncloudResources PUBLIC $ $) target_compile_definitions(owncloudResources PRIVATE APPLICATION_SHORTNAME="${APPLICATION_SHORTNAME}") diff --git a/src/resources/client.qrc b/src/resources/client.qrc index 86d004ed394..d1ad8506f30 100644 --- a/src/resources/client.qrc +++ b/src/resources/client.qrc @@ -40,6 +40,8 @@ font-awesome/dark/undo-solid.svg wizard/style.qss oauth/oauth.html.in + remixicon/dark/space.svg + remixicon/light/space.svg font-awesome/dark/states/error.svg font-awesome/dark/states/information.svg font-awesome/dark/states/offline.svg diff --git a/src/resources/font-awesome/dark/folder-solid.svg b/src/resources/font-awesome/dark/folder-solid.svg index 7642e349fb5..6ec1b7b2c41 100644 --- a/src/resources/font-awesome/dark/folder-solid.svg +++ b/src/resources/font-awesome/dark/folder-solid.svg @@ -1 +1,3 @@ - + + + diff --git a/src/resources/font-awesome/light/folder-solid.svg b/src/resources/font-awesome/light/folder-solid.svg index 1b7d671395b..4ea422add8b 100644 --- a/src/resources/font-awesome/light/folder-solid.svg +++ b/src/resources/font-awesome/light/folder-solid.svg @@ -1 +1,3 @@ - + + + diff --git a/src/resources/remixicon/License.txt b/src/resources/remixicon/License.txt new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/src/resources/remixicon/License.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/resources/remixicon/dark/space.svg b/src/resources/remixicon/dark/space.svg new file mode 100644 index 00000000000..bbd3de5a548 --- /dev/null +++ b/src/resources/remixicon/dark/space.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/resources/remixicon/light/space.svg b/src/resources/remixicon/light/space.svg new file mode 100644 index 00000000000..1c852fd1155 --- /dev/null +++ b/src/resources/remixicon/light/space.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/resources/resources.cpp b/src/resources/resources.cpp index 555f818eed5..1dbf57db0bb 100644 --- a/src/resources/resources.cpp +++ b/src/resources/resources.cpp @@ -52,7 +52,6 @@ constexpr bool isVanilla() return std::string_view(APPLICATION_SHORTNAME) == "ownCloud"; } - bool hasTheme(IconType type, const QString &theme) { // <, bool @@ -79,7 +78,6 @@ bool Resources::hasMonoTheme() { // mono icons are only supported in vanilla and if a customer provides them // no fallback to vanilla - qDebug() << hasTheme(Resources::IconType::BrandedIcon, whiteTheme()); return hasTheme(Resources::IconType::BrandedIcon, whiteTheme()); } @@ -172,3 +170,29 @@ QIcon OCC::Resources::themeUniversalIcon(const QString &name, IconType iconType) { return loadIcon(QStringLiteral("universal"), name, iconType); } + +CoreImageProvider::CoreImageProvider() + : QQuickImageProvider(QQuickImageProvider::Pixmap) +{ +} +QPixmap CoreImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) +{ + const auto [type, path] = [&id] { + const QString type = id.mid(0, id.indexOf(QLatin1Char('/'))); + return std::make_tuple(type, id.mid(type.size())); + }(); + Q_ASSERT(!path.isEmpty()); + QIcon icon; + if (type == QLatin1String("theme")) { + icon = themeIcon(path); + } else if (type == QLatin1String("core")) { + icon = getCoreIcon(path); + } else { + Q_UNREACHABLE(); + } + const QSize actualSize = requestedSize.isValid() ? requestedSize : icon.availableSizes().first(); + if (size) { + *size = actualSize; + } + return icon.pixmap(actualSize); +} diff --git a/src/resources/resources.h b/src/resources/resources.h index 096a1a115ed..10e4f0e3b34 100644 --- a/src/resources/resources.h +++ b/src/resources/resources.h @@ -17,6 +17,7 @@ #include #include +#include namespace OCC::Resources { Q_NAMESPACE @@ -48,4 +49,13 @@ QIcon OWNCLOUDRESOURCES_EXPORT themeIcon(const QString &name, IconType iconType * Returns a universal (non color schema aware) icon. */ QIcon OWNCLOUDRESOURCES_EXPORT themeUniversalIcon(const QString &name, IconType iconType = IconType::BrandedIcon); + +class OWNCLOUDRESOURCES_EXPORT CoreImageProvider : public QQuickImageProvider +{ + Q_OBJECT +public: + CoreImageProvider(); + + QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; +}; }