diff --git a/CMakeLists.txt b/CMakeLists.txt index 460d15ef64e..021b5c63f24 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})") @@ -37,6 +37,9 @@ include(ECMSetupVersion) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMEnableSanitizers) +# while we use qt_add_qml_module instead of ecm_add_qml_module +# include ECMQmlModule to set up the policies +include(ECMQmlModule) if(UNIT_TESTING) message(DEPRECATION "Setting UNIT_TESTING is deprecated please use BUILD_TESTING") diff --git a/changelog/unreleased/11491 b/changelog/unreleased/11491 new file mode 100644 index 00000000000..645e9498b8b --- /dev/null +++ b/changelog/unreleased/11491 @@ -0,0 +1,6 @@ +Enhancement: New Folder status + +We rewrote the Folder status page. + +https://github.com/owncloud/client/pull/11491 +https://github.com/owncloud/client/issues/11527 diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index d6809f9c14d..e5a7eb78230 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -37,7 +37,6 @@ set(client_SRCS folder.cpp folderman.cpp folderstatusmodel.cpp - folderstatusdelegate.cpp folderwatcher.cpp generalsettings.cpp ignorelisteditor.cpp @@ -102,16 +101,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) +qt_add_qml_module(owncloudCore + URI org.ownCloud.qmlcomponents + VERSION 1.0 + NAMESPACE OCC + QML_FILES + qml/FolderDelegate.qml + qml/FolderError.qml +) +target_link_libraries(owncloudCore PUBLIC owncloudCoreplugin) add_subdirectory(spaces) @@ -159,7 +169,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..6fbd70fa679 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -26,34 +26,23 @@ #include "configfile.h" #include "creds/httpcredentialsgui.h" #include "folderman.h" -#include "folderstatusdelegate.h" #include "folderstatusmodel.h" #include "folderwizard/folderwizard.h" #include "gui/accountmodalwidget.h" #include "gui/models/models.h" #include "gui/selectivesyncwidget.h" #include "guiutility.h" -#include "loginrequireddialog.h" #include "oauthloginwidget.h" #include "quotainfo.h" +#include "resources/resources.h" #include "scheduling/syncscheduler.h" #include "settingsdialog.h" #include "theme.h" #include -#include -#include -#include -#include -#include -#include #include -#include -#include #include -#include -#include - +#include namespace { constexpr auto modalWidgetStretchedMarginC = 50; @@ -63,11 +52,9 @@ namespace OCC { Q_LOGGING_CATEGORY(lcAccountSettings, "gui.account.settings", QtInfoMsg) - AccountSettings::AccountSettings(const AccountStatePtr &accountState, QWidget *parent) : QWidget(parent) , ui(new Ui::AccountSettings) - , _delegate(new FolderStatusDelegate(this)) , _wasDisabledBefore(false) , _accountState(accountState) { @@ -76,30 +63,28 @@ 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::Roles::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 SpaceImageProvider(_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"))); + if (!ui->quickWidget->errors().isEmpty()) { + qCCritical(lcAccountSettings) << "A qml error occured" << ui->quickWidget->errors(); + auto box = new QMessageBox(QMessageBox::Critical, QStringLiteral("QML Error"), QDebug::toString(ui->quickWidget->errors())); + box->setAttribute(Qt::WA_DeleteOnClose); + box->exec(); + qApp->quit(); + } 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 +140,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,39 +149,27 @@ void AccountSettings::slotToggleSignInState() } } -void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) +void AccountSettings::slotCustomContextMenuRequested(Folder *folder) { - auto *tv = ui->_folderList; - QModelIndex index = tv->indexAt(pos); - if (!index.isValid()) { - return; - } - - const auto isDeployed = index.siblingAtColumn(static_cast(FolderStatusModel::Columns::IsDeployed)).data().toBool(); - const auto addRemoveFolderAction = [isDeployed, this](QMenu *menu) { + // qpointer for async calls + const auto isDeployed = folder->isDeployed(); + const auto addRemoveFolderAction = [isDeployed, folder, this](QMenu *menu) { Q_ASSERT(!isDeployed); - return menu->addAction(tr("Remove folder sync connection"), this, &AccountSettings::slotRemoveCurrentFolder); + return menu->addAction(tr("Remove folder sync connection"), this, [folder, this] { slotRemoveCurrentFolder(folder); }); }; + + auto *menu = new QMenu(ui->quickWidget); + menu->setAttribute(Qt::WA_DeleteOnClose); + connect(folder, &OCC::Folder::destroyed, menu, &QMenu::close); // 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); - menu->setAttribute(Qt::WA_DeleteOnClose); + if (!folder->isReady() && !isDeployed) { addRemoveFolderAction(menu); menu->popup(QCursor::pos()); return; } - - QMenu *menu = new QMenu(tv); - menu->setAttribute(Qt::WA_DeleteOnClose); - // Add an action to open the folder in the system's file browser: - - QUrl folderUrl; - if (auto *folder = selectedFolder()) { - folderUrl = QUrl::fromLocalFile(folder->path()); - } - + const QUrl folderUrl = QUrl::fromLocalFile(folder->path()); if (!folderUrl.isEmpty()) { QAction *ac = menu->addAction(CommonStrings::showInFileBrowser(), [folderUrl]() { qCInfo(lcAccountSettings) << "Opening local folder" << folderUrl; @@ -217,40 +184,34 @@ 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->accountState()->account()->capabilities().privateLinkPropertyAvailable()) { - QString path = folder->remotePathTrailingSlash(); - menu->addAction(CommonStrings::showInWebBrowser(), [path, davUrl = folder->webDavUrl(), this] { - fetchPrivateLinkUrl(_accountState->account(), davUrl, path, this, [](const QUrl &url) { - Utility::openBrowser(url, nullptr); - }); - }); - } + if (folder->accountState()->account()->capabilities().privateLinkPropertyAvailable()) { + QString path = folder->remotePathTrailingSlash(); + menu->addAction(CommonStrings::showInWebBrowser(), [path, davUrl = folder->webDavUrl(), this] { + fetchPrivateLinkUrl(_accountState->account(), davUrl, path, this, [](const QUrl &url) { Utility::openBrowser(url, nullptr); }); + }); } + // Root-folder specific actions: 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 (OC_ENSURE(folder->isReady())) { if (!folderPaused) { QAction *ac = menu->addAction(tr("Force sync now")); if (folder->isSyncRunning()) { ac->setText(tr("Restart sync")); } ac->setEnabled(folderConnected); - connect(ac, &QAction::triggered, this, &AccountSettings::slotForceSyncCurrentFolder); + connect(ac, &QAction::triggered, this, [folder, this] { 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, this] { slotEnableCurrentFolder(folder, true); }); if (!isDeployed) { addRemoveFolderAction(menu); @@ -258,41 +219,25 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) if (Theme::instance()->showVirtualFilesOption()) { if (folder->virtualFilesEnabled()) { if (!Theme::instance()->forceVirtualFilesOption()) { - menu->addAction(tr("Disable virtual file support"), this, &AccountSettings::slotDisableVfsCurrentFolder); - } - } else { - const auto mode = VfsPluginManager::instance().bestAvailableVfsMode(); - if (FolderMan::instance()->checkVfsAvailability(folder->path(), mode)) { - if (mode == Vfs::WindowsCfApi) { - ac = menu->addAction(tr("Enable virtual file support")); - connect(ac, &QAction::triggered, this, &AccountSettings::slotEnableVfsCurrentFolder); + menu->addAction(tr("Disable virtual file support"), this, [folder, this] { slotDisableVfsCurrentFolder(folder); }); + } else { + const auto mode = VfsPluginManager::instance().bestAvailableVfsMode(); + if (FolderMan::instance()->checkVfsAvailability(folder->path(), mode)) { + if (mode == Vfs::WindowsCfApi) { + ac = menu->addAction(tr("Enable virtual file support")); + connect(ac, &QAction::triggered, this, [folder, this] { slotEnableVfsCurrentFolder(folder); }); + } } } } } + if (!folder->virtualFilesEnabled()) { + menu->addAction(tr("Choose what to sync"), this, [folder, this] { showSelectiveSyncDialog(folder); }); + } + menu->popup(QCursor::pos()); + } else { + menu->deleteLater(); } - if (!folder->virtualFilesEnabled()) { - menu->addAction(tr("Choose what to sync"), this, [folder, this] { showSelectiveSyncDialog(folder); }); - } - menu->popup(QCursor::pos()); - } else { - menu->deleteLater(); - } -} - -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; } } @@ -350,47 +295,31 @@ void AccountSettings::slotFolderWizardAccepted() FolderMan::instance()->scheduleAllFolders(); } -void AccountSettings::slotRemoveCurrentFolder() +void AccountSettings::slotRemoveCurrentFolder(Folder *folder) { - auto folder = selectedFolder(); - QModelIndex selected = ui->_folderList->selectionModel()->currentIndex(); - if (selected.isValid() && folder) { - int row = selected.row(); - - qCInfo(lcAccountSettings) << "Remove Folder " << folder->path(); - QString shortGuiLocalPath = folder->shortGuiLocalPath(); - - auto messageBox = new QMessageBox(QMessageBox::Question, - tr("Confirm Folder Sync Connection Removal"), - tr("

Do you really want to stop syncing the folder %1?

" - "

Note: This will not delete any files.

") - .arg(shortGuiLocalPath), - QMessageBox::NoButton, - ocApp()->gui()->settingsDialog()); - messageBox->setAttribute(Qt::WA_DeleteOnClose); - QPushButton *yesButton = - messageBox->addButton(tr("Remove Folder Sync Connection"), QMessageBox::YesRole); - messageBox->addButton(tr("Cancel"), QMessageBox::NoRole); - connect(messageBox, &QMessageBox::finished, this, [messageBox, yesButton, folder, row, this]{ - if (messageBox->clickedButton() == yesButton) { - FolderMan::instance()->removeFolder(folder); - _sortModel->removeRow(row); - - // single folder fix to show add-button and hide remove-button - emit folderChanged(); - } - }); - messageBox->open(); - } + qCInfo(lcAccountSettings) << "Remove Folder " << folder->path(); + QString shortGuiLocalPath = folder->shortGuiLocalPath(); + + auto messageBox = new QMessageBox(QMessageBox::Question, tr("Confirm Folder Sync Connection Removal"), + tr("

Do you really want to stop syncing the folder %1?

" + "

Note: This will not delete any files.

") + .arg(shortGuiLocalPath), + QMessageBox::NoButton, ocApp()->gui()->settingsDialog()); + messageBox->setAttribute(Qt::WA_DeleteOnClose); + QPushButton *yesButton = messageBox->addButton(tr("Remove Folder Sync Connection"), QMessageBox::YesRole); + messageBox->addButton(tr("Cancel"), QMessageBox::NoRole); + connect(messageBox, &QMessageBox::finished, this, [messageBox, yesButton, folder, this] { + if (messageBox->clickedButton() == yesButton) { + FolderMan::instance()->removeFolder(folder); + // single folder fix to show add-button and hide remove-button + emit folderChanged(); + } + }); + messageBox->open(); } -void AccountSettings::slotEnableVfsCurrentFolder() +void AccountSettings::slotEnableVfsCurrentFolder(Folder *folder) { - QPointer folder = selectedFolder(); - QModelIndex selected = ui->_folderList->selectionModel()->currentIndex(); - if (!selected.isValid() || !folder) { - return; - } if (OC_ENSURE(VfsPluginManager::instance().bestAvailableVfsMode() == Vfs::WindowsCfApi)) { if (!folder) { return; @@ -402,18 +331,11 @@ void AccountSettings::slotEnableVfsCurrentFolder() // don't schedule the folder, it might not be ready yet. // it will schedule its self once set up - - ui->_folderList->doItemsLayout(); } } -void AccountSettings::slotDisableVfsCurrentFolder() +void AccountSettings::slotDisableVfsCurrentFolder(Folder *folder) { - QPointer folder = selectedFolder(); - QModelIndex selected = ui->_folderList->selectionModel()->currentIndex(); - if (!selected.isValid() || !folder) - return; - auto msgBox = new QMessageBox( QMessageBox::Question, tr("Disable virtual file support?"), @@ -436,8 +358,6 @@ void AccountSettings::slotDisableVfsCurrentFolder() // Also wipes virtual files, schedules remote discovery folder->setVirtualFilesEnabled(false); - - ui->_folderList->doItemsLayout(); }); msgBox->open(); } @@ -458,80 +378,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); } } diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index 8c28a7bb396..ebfe2720a39 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 slotRemoveCurrentFolder(); - void slotEnableVfsCurrentFolder(); - void slotDisableVfsCurrentFolder(); + void slotEnableCurrentFolder(Folder *folder, bool terminate = false); + void slotForceSyncCurrentFolder(Folder *folder); + void slotRemoveCurrentFolder(Folder *folder); + void slotEnableVfsCurrentFolder(Folder *folder); + void slotDisableVfsCurrentFolder(Folder *folder); void slotFolderWizardAccepted(); void slotDeleteAccount(); void slotToggleSignInState(); - void slotCustomContextMenuRequested(const QPoint &); - void slotFolderListClicked(const QModelIndex &indx); + void slotCustomContextMenuRequested(Folder *folder); 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/folder.h b/src/gui/folder.h index 86653e25ef9..0581c6977d3 100644 --- a/src/gui/folder.h +++ b/src/gui/folder.h @@ -26,8 +26,8 @@ #include #include -#include #include +#include #include #include @@ -146,6 +146,7 @@ class FolderDefinition class Folder : public QObject { Q_OBJECT + QML_ELEMENT public: enum class ChangeReason { 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..10be38a4f88 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,37 +45,15 @@ 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) { - 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) { - case FolderStatusModel::Columns::QuotaTotal: - return quota.getTotal(); - case FolderStatusModel::Columns::QuotaUsed: - return quota.getUsed(); - default: - Q_UNREACHABLE(); - } - } - } - } - return {}; - } - - int64_t getQuotaOc10(const AccountStatePtr &accountState, FolderStatusModel::Columns type) - { - switch (type) { - case FolderStatusModel::Columns::QuotaTotal: - return accountState->quotaInfo()->lastQuotaTotalBytes(); - case FolderStatusModel::Columns::QuotaUsed: - return accountState->quotaInfo()->lastQuotaUsedBytes(); - default: - Q_UNREACHABLE(); + 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); } class SubFolderInfo @@ -85,11 +63,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) @@ -209,9 +186,7 @@ FolderStatusModel::FolderStatusModel(QObject *parent) { } -FolderStatusModel::~FolderStatusModel() -{ -} +FolderStatusModel::~FolderStatusModel() { } void FolderStatusModel::setAccountState(const AccountStatePtr &accountState) { @@ -224,9 +199,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 +227,7 @@ void FolderStatusModel::setAccountState(const AccountStatePtr &accountState) }); } - Q_EMIT endResetModel(); + endResetModel(); } QVariant FolderStatusModel::data(const QModelIndex &index, int role) const @@ -256,22 +238,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,108 +250,98 @@ 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'))); + switch (static_cast(role)) { + case Roles::Subtitle: { + if (auto *space = getSpace()) { + if (!space->drive().getDescription().isEmpty()) { + return space->drive().getDescription(); } - 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!"), - }); - } - return errors; } - case Columns::SyncRunning: - return f->syncResult().status() == SyncResult::SyncRunning; - case Columns::HeaderRole: { - if (auto *space = getSpace()) { - return space->displayName(); - } - return f->displayName(); + return tr("Local folder: %1").arg(f->shortGuiLocalPath()); + } + case Roles::FolderErrorMsg: { + auto errors = f->syncResult().errorStrings(); + const auto legacyError = FolderMan::instance()->unsupportedConfiguration(f->path()); + if (!legacyError) { + errors.append(legacyError.error()); } - 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); - } - return Theme::instance()->syncStateIconName(status); + if (f->syncResult().hasUnresolvedConflicts()) { + errors.append(tr("There are unresolved conflicts. Click for details.")); } - case Columns::SyncProgressItemString: - return progress._progressString; - case Columns::WarningCount: - return progress._warningCount; - case Columns::SyncProgressOverallPercent: - return progress._overallPercent; - case Columns::SyncProgressOverallString: - return progress._overallSyncString; - case Columns::FolderSyncText: { - if (auto *space = getSpace()) { - if (!space->drive().getDescription().isEmpty()) { - return space->drive().getDescription(); - } + 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 Roles::DisplayName: { + if (auto *space = getSpace()) { + return space->displayName(); + } + return f->displayName(); + } + case Roles::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 QStringLiteral("image://ownCloud/core/folder-sync"); + case Roles::FolderStatusUrl: + return QStringLiteral("image://ownCloud/core/states/%1").arg(statusIconName(f)); + case Roles::SyncProgressItemString: + return folderInfo->_progress._progressString; + case Roles::SyncProgressOverallPercent: + return folderInfo->_progress._overallPercent / 100.0; + case Roles::SyncProgressOverallString: + return folderInfo->_progress._overallSyncString; + case Roles::FolderSyncText: { + if (auto *space = getSpace()) { + if (!space->drive().getDescription().isEmpty()) { + return space->drive().getDescription(); } - 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(); - case Columns::QuotaTotal: - [[fallthrough]]; - case Columns::QuotaUsed: - if (_accountState->supportsSpaces()) { - return QVariant::fromValue(getQuota(_accountState, f->spaceId(), column)); - } else { - return QVariant::fromValue(getQuotaOc10(_accountState, column)); + return tr("Local folder: %1").arg(f->shortGuiLocalPath()); + } + case Roles::Priority: + // TODO: + return QStringLiteral("%1%2").arg(f->priority(), 3, 10, QLatin1Char('0')).arg(f->displayName()); + case Roles::Quota: { + qint64 used{}; + qint64 total{}; + if (_accountState->supportsSpaces()) { + if (auto spacesManager = f->accountState()->account()->spacesManager()) { + const auto *space = spacesManager->space(f->spaceId()); + if (space) { + const auto quota = space->drive().getQuota(); + if (quota.isValid()) { + used = quota.getUsed(); + total = quota.getTotal(); + } + } } - case Columns::IsUsingSpaces: // handled before - [[fallthrough]]; - case Columns::ColumnCount: - Q_UNREACHABLE(); - break; + } else { + used = f->accountState()->quotaInfo()->lastQuotaUsedBytes(); + total = f->accountState()->quotaInfo()->lastQuotaTotalBytes(); } - break; - case Qt::ToolTipRole: { - if (!progress.isNull()) { - return progress._progressString; + if (total <= 0) { + return {}; } - if (accountConnected) { - return tr("%1\n%2").arg(Utility::enumToDisplayName(f->syncResult().status()), QDir::toNativeSeparators(folderInfo->_folder->path())); + return tr("%1 of %2 used").arg(Utility::octetsToString(used), Utility::octetsToString(total)); + } + case Roles::ToolTip: { + if (_accountState->isConnected()) { + return tr("The status of %1 is %2").arg(f->displayName(), Utility::enumToDisplayName(f->syncResult().status())); } else { - return tr("Signed out\n%1").arg(QDir::toNativeSeparators(folderInfo->_folder->path())); + return tr("The account %1 is currently not connected.").arg(f->accountState()->account()->displayName()); } } + case Roles::Folder: + return QVariant::fromValue(f); } - return QVariant(); + return {}; } Folder *FolderStatusModel::folder(const QModelIndex &index) const @@ -391,7 +352,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 +361,24 @@ int FolderStatusModel::rowCount(const QModelIndex &parent) const return static_cast(_folders.size()); } +QHash FolderStatusModel::roleNames() const +{ + return { + {static_cast(Roles::DisplayName), "displayName"}, + {static_cast(Roles::Subtitle), "subTitle"}, + {static_cast(Roles::FolderImageUrl), "imageUrl"}, + {static_cast(Roles::FolderStatusUrl), "statusUrl"}, + {static_cast(Roles::SyncProgressOverallPercent), "progress"}, + {static_cast(Roles::SyncProgressOverallString), "overallText"}, + {static_cast(Roles::SyncProgressItemString), "itemText"}, + {static_cast(Roles::FolderSyncText), "descriptionText"}, + {static_cast(Roles::FolderErrorMsg), "errorMsg"}, + {static_cast(Roles::Quota), "quota"}, + {static_cast(Roles::ToolTip), "toolTip"}, + {static_cast(Roles::Folder), "folder"}, + }; +} + void FolderStatusModel::slotUpdateFolderState(Folder *folder) { if (!folder) @@ -424,11 +403,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)) { @@ -504,4 +478,29 @@ void FolderStatusModel::resetFolders() setAccountState(_accountState); } + +SpaceImageProvider::SpaceImageProvider(AccountStatePtr accountStat) + : QQuickImageProvider(QQuickImageProvider::Pixmap, QQuickImageProvider::ForceAsynchronousImageLoading) + , _accountStat(accountStat) +{ +} + +QPixmap SpaceImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) +{ + // 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); +} + } // namespace OCC diff --git a/src/gui/folderstatusmodel.h b/src/gui/folderstatusmodel.h index 55352a11f1d..8fb970601ab 100644 --- a/src/gui/folderstatusmodel.h +++ b/src/gui/folderstatusmodel.h @@ -19,10 +19,10 @@ #include "progressdispatcher.h" #include -#include -#include #include -#include +#include +#include +#include class QNetworkReply; @@ -43,37 +43,24 @@ 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, + enum class Roles { + 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, - - ColumnCount + Priority, + Quota, + FolderImageUrl, + FolderStatusUrl, + Folder }; - Q_ENUMS(Columns); + Q_ENUMS(Roles); FolderStatusModel(QObject *parent = nullptr); ~FolderStatusModel() override; @@ -84,6 +71,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(); @@ -99,6 +88,18 @@ private slots: std::vector> _folders; }; -} // namespace OCC +class SpaceImageProvider : public QQuickImageProvider +{ + Q_OBJECT +public: + SpaceImageProvider(AccountStatePtr accountStat); + QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; + + +private: + AccountStatePtr _accountStat; +}; + +} // namespace OCC #endif // FOLDERSTATUSMODEL_H diff --git a/src/gui/qml/FolderDelegate.qml b/src/gui/qml/FolderDelegate.qml new file mode 100644 index 00000000000..2e573b2f882 --- /dev/null +++ b/src/gui/qml/FolderDelegate.qml @@ -0,0 +1,185 @@ +/* + * Copyright (C) by Hannah von Reth + * + * 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. + */ + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import org.ownCloud.qmlcomponents 1.0 + +Pane { + // TODO: not cool + readonly property real normalSize: 170 + + ScrollView { + id: scrollView + anchors.fill: parent + clip: true + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + + 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 var errorMsg + required property string quota + required property string toolTip + + required property Folder folder + + 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 + ToolTip.delay: 1000 + ToolTip.timeout: 5000 + + ColumnLayout { + id: colLayout + anchors.fill: parent + spacing: 10 + + RowLayout { + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + + Pane { + Layout.preferredHeight: normalSize - 20 + Layout.preferredWidth: normalSize - 20 + Layout.alignment: Qt.AlignTop + background: Rectangle { + color: scrollView.palette.alternateBase + } + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectFit + source: imageUrl + } + } + ColumnLayout { + spacing: 6 + Layout.alignment: Qt.AlignTop | Qt.AlignLeft + Layout.fillWidth: true + + RowLayout { + Layout.fillWidth: true + Image { + Layout.preferredHeight: 16 + Layout.preferredWidth: 16 + source: statusUrl + } + Label { + Layout.fillWidth: true + text: folderDelegate.displayName + font.bold: true + font.pointSize: 15 + elide: Text.ElideRight + } + } + Label { + Layout.fillWidth: true + text: folderDelegate.descriptionText + elide: Text.ElideRight + } + // we will either display quota or overallText + Label { + Layout.fillWidth: true + text: folderDelegate.quota + elide: Text.ElideRight + visible: folderDelegate.quota && !folderDelegate.overallText + } + + Item { + // ensure the progress bar always consumes its space + Layout.preferredHeight: 10 + Layout.fillWidth: true + ProgressBar { + anchors.fill: parent + value: folderDelegate.progress + visible: folderDelegate.overallText || folderDelegate.itemText + } + } + + Label { + + Layout.fillWidth: true + text: folderDelegate.overallText + elide: Text.ElideMiddle + } + + Label { + 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 + errorMessages: folderDelegate.errorMsg + onCollapsedChanged: { + if (!collapsed) { + // TODO: not cool + folderDelegate.implicitHeight = normalSize + implicitHeight + 10; + } 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(folder); + } + } + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + + onClicked: { + ctx.slotCustomContextMenuRequested(folder); + } + } + } + } + } +} diff --git a/src/gui/qml/FolderError.qml b/src/gui/qml/FolderError.qml new file mode 100644 index 00000000000..1d6efad8e98 --- /dev/null +++ b/src/gui/qml/FolderError.qml @@ -0,0 +1,98 @@ +/* + * Copyright (C) by Hannah von Reth + * + * 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. + */ + +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 + Image { + Layout.alignment: Qt.AlignTop + source: "image://ownCloud/core/warning" + Layout.maximumHeight: 16 + Layout.maximumWidth: 16 + } + Label { + id: label + Layout.fillWidth: true + elide: Label.ElideLeft + 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 + Layout.fillHeight: true + Layout.fillWidth: true + 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_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; +}; }