/* * Copyright (C) by Roeland Jago Douma * * 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 "shareusergroupwidget.h" #include "ui_shareusergroupwidget.h" #include "ui_shareuserline.h" #include "account.h" #include "folderman.h" #include "folder.h" #include "accountmanager.h" #include "theme.h" #include "configfile.h" #include "capabilities.h" #include "guiutility.h" #include "thumbnailjob.h" #include "sharee.h" #include "sharemanager.h" #include "guiutility.h" #include "QProgressIndicator.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace OCC { ShareUserGroupWidget::ShareUserGroupWidget(AccountPtr account, const QString &sharePath, const QString &localPath, SharePermissions maxSharingPermissions, const QString &privateLinkUrl, QWidget *parent) : QWidget(parent) , _ui(new Ui::ShareUserGroupWidget) , _account(account) , _sharePath(sharePath) , _localPath(localPath) , _maxSharingPermissions(maxSharingPermissions) , _privateLinkUrl(privateLinkUrl) , _disableCompleterActivated(false) { setAttribute(Qt::WA_DeleteOnClose); setObjectName("SharingDialogUG"); // required as group for saveGeometry call _ui->setupUi(this); //Is this a file or folder? _isFile = QFileInfo(localPath).isFile(); _completer = new QCompleter(this); _completerModel = new ShareeModel(_account, _isFile ? QLatin1String("file") : QLatin1String("folder"), _completer); connect(_completerModel, &ShareeModel::shareesReady, this, &ShareUserGroupWidget::slotShareesReady); connect(_completerModel, &ShareeModel::displayErrorMessage, this, &ShareUserGroupWidget::displayError); _completer->setModel(_completerModel); _completer->setCaseSensitivity(Qt::CaseInsensitive); _completer->setCompletionMode(QCompleter::UnfilteredPopupCompletion); _ui->shareeLineEdit->setCompleter(_completer); _manager = new ShareManager(_account, this); connect(_manager, &ShareManager::sharesFetched, this, &ShareUserGroupWidget::slotSharesFetched); connect(_manager, &ShareManager::shareCreated, this, &ShareUserGroupWidget::getShares); connect(_manager, &ShareManager::serverError, this, &ShareUserGroupWidget::displayError); connect(_ui->shareeLineEdit, &QLineEdit::returnPressed, this, &ShareUserGroupWidget::slotLineEditReturn); connect(_ui->privateLinkText, &QLabel::linkActivated, this, &ShareUserGroupWidget::slotPrivateLinkShare); // By making the next two QueuedConnections we can override // the strings the completer sets on the line edit. connect(_completer, SIGNAL(activated(QModelIndex)), SLOT(slotCompleterActivated(QModelIndex)), Qt::QueuedConnection); connect(_completer, SIGNAL(highlighted(QModelIndex)), SLOT(slotCompleterHighlighted(QModelIndex)), Qt::QueuedConnection); // Queued connection so this signal is recieved after textChanged connect(_ui->shareeLineEdit, &QLineEdit::textEdited, this, &ShareUserGroupWidget::slotLineEditTextEdited, Qt::QueuedConnection); connect(&_completionTimer, &QTimer::timeout, this, &ShareUserGroupWidget::searchForSharees); _completionTimer.setSingleShot(true); _completionTimer.setInterval(600); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding); _ui->errorLabel->hide(); // Setup the sharee search progress indicator _ui->shareeHorizontalLayout->addWidget(&_pi_sharee); } ShareUserGroupWidget::~ShareUserGroupWidget() { delete _ui; } void ShareUserGroupWidget::on_shareeLineEdit_textChanged(const QString &) { _completionTimer.stop(); } void ShareUserGroupWidget::slotLineEditTextEdited(const QString &text) { _disableCompleterActivated = false; // First textChanged is called first and we stopped the timer when the text is changed, programatically or not // Then we restart the timer here if the user touched a key if (!text.isEmpty()) { _completionTimer.start(); } } void ShareUserGroupWidget::slotLineEditReturn() { _disableCompleterActivated = false; // did the user type in one of the options? const auto text = _ui->shareeLineEdit->text(); for (int i = 0; i < _completerModel->rowCount(); ++i) { const auto sharee = _completerModel->getSharee(i); if (sharee->format() == text || sharee->displayName() == text || sharee->shareWith() == text) { slotCompleterActivated(_completerModel->index(i)); // make sure we do not send the same item twice (because return is called when we press // return to activate an item inthe completer) _disableCompleterActivated = true; return; } } // nothing found? try to refresh completion _completionTimer.start(); } void ShareUserGroupWidget::searchForSharees() { _completionTimer.stop(); _pi_sharee.startAnimation(); ShareeModel::ShareeSet blacklist; // Add the current user to _sharees since we can't share with ourself QSharedPointer currentUser(new Sharee(_account->credentials()->user(), "", Sharee::Type::User)); blacklist << currentUser; foreach (auto sw, _ui->scrollArea->findChildren()) { blacklist << sw->share()->getShareWith(); } _ui->errorLabel->hide(); _completerModel->fetch(_ui->shareeLineEdit->text(), blacklist); } void ShareUserGroupWidget::getShares() { _manager->fetchShares(_sharePath); } void ShareUserGroupWidget::slotSharesFetched(const QList> &shares) { QScrollArea *scrollArea = _ui->scrollArea; auto newViewPort = new QWidget(scrollArea); auto layout = new QVBoxLayout(newViewPort); layout->setMargin(0); layout->setSpacing(0); QSize minimumSize = newViewPort->sizeHint(); int x = 0; foreach (const auto &share, shares) { // We don't handle link shares if (share->getShareType() == Share::TypeLink) { continue; } ShareUserLine *s = new ShareUserLine(share, _maxSharingPermissions, _isFile, _ui->scrollArea); connect(s, &ShareUserLine::resizeRequested, this, &ShareUserGroupWidget::slotAdjustScrollWidgetSize); connect(s, &ShareUserLine::visualDeletionDone, this, &ShareUserGroupWidget::getShares); s->setBackgroundRole(layout->count() % 2 == 0 ? QPalette::Base : QPalette::AlternateBase); layout->addWidget(s); x++; if (x <= 3) { minimumSize = newViewPort->sizeHint(); } else { minimumSize.rwidth() = qMax(newViewPort->sizeHint().width(), minimumSize.width()); } } if (layout->isEmpty()) { layout->addWidget(new QLabel(tr("The item is not shared with any users or groups"))); } else { layout->addStretch(1); } minimumSize.rwidth() += layout->spacing(); minimumSize.rheight() += layout->spacing(); scrollArea->setMinimumSize(minimumSize); scrollArea->setWidget(newViewPort); _disableCompleterActivated = false; _ui->shareeLineEdit->setEnabled(true); } void ShareUserGroupWidget::slotAdjustScrollWidgetSize() { QScrollArea *scrollArea = _ui->scrollArea; if (scrollArea->findChildren().count() <= 3) { auto minimumSize = scrollArea->widget()->sizeHint(); auto spacing = scrollArea->widget()->layout()->spacing(); minimumSize.rwidth() += spacing; minimumSize.rheight() += spacing; scrollArea->setMinimumSize(minimumSize); } } void ShareUserGroupWidget::slotPrivateLinkShare() { auto menu = new QMenu(this); menu->setAttribute(Qt::WA_DeleteOnClose); menu->addAction(tr("Open link in browser"), this, SLOT(slotPrivateLinkOpenBrowser())); menu->addAction(tr("Copy link to clipboard"), this, SLOT(slotPrivateLinkCopy())); menu->addAction(tr("Send link by email"), this, SLOT(slotPrivateLinkEmail())); menu->exec(QCursor::pos()); } void ShareUserGroupWidget::slotShareesReady() { _pi_sharee.stopAnimation(); if (_completerModel->rowCount() == 0) { displayError(0, tr("No results for '%1'").arg(_completerModel->currentSearch())); return; } _completer->complete(); } void ShareUserGroupWidget::slotCompleterActivated(const QModelIndex &index) { if (_disableCompleterActivated) return; // The index is an index from the QCompletion model which is itelf a proxy // model proxying the _completerModel auto sharee = qvariant_cast>(index.data(Qt::UserRole)); if (sharee.isNull()) { return; } /* * Add spinner to the bottom of the widget list */ auto viewPort = _ui->scrollArea->widget(); auto layout = qobject_cast(viewPort->layout()); auto indicator = new QProgressIndicator(viewPort); indicator->startAnimation(); if (layout->count() == 1) { // No shares yet! Remove the label, add some stretch. delete layout->itemAt(0)->widget(); layout->addStretch(1); } layout->insertWidget(layout->count() - 1, indicator); /* * Don't send the reshare permissions for federated shares for servers <9.1 * https://github.com/owncloud/core/issues/22122#issuecomment-185637344 * https://github.com/owncloud/client/issues/4996 */ const SharePermissions defaultPermissions = static_cast(_account->capabilities().defaultPermissions()); if (sharee->type() == Sharee::Federated && _account->serverVersionInt() < Account::makeServerVersion(9, 1, 0)) { SharePermissions permissions = SharePermissionRead | SharePermissionUpdate; if (!_isFile) { permissions |= SharePermissionCreate | SharePermissionDelete; } permissions &= defaultPermissions; _manager->createShare(_sharePath, Share::ShareType(sharee->type()), sharee->shareWith(), permissions); } else { _manager->createShare(_sharePath, Share::ShareType(sharee->type()), sharee->shareWith(), _maxSharingPermissions & defaultPermissions); } _ui->shareeLineEdit->setEnabled(false); _ui->shareeLineEdit->setText(QString()); } void ShareUserGroupWidget::slotCompleterHighlighted(const QModelIndex &index) { // By default the completer would set the text to EditRole, // override that here. _ui->shareeLineEdit->setText(index.data(Qt::DisplayRole).toString()); } void ShareUserGroupWidget::displayError(int code, const QString &message) { _pi_sharee.stopAnimation(); // Also remove the spinner in the widget list, if any foreach (auto pi, _ui->scrollArea->findChildren()) { delete pi; } qCWarning(lcSharing) << "Sharing error from server" << code << message; _ui->errorLabel->setText(message); _ui->errorLabel->show(); _ui->shareeLineEdit->setEnabled(true); } void ShareUserGroupWidget::slotPrivateLinkOpenBrowser() { Utility::openBrowser(_privateLinkUrl, this); } void ShareUserGroupWidget::slotPrivateLinkCopy() { QApplication::clipboard()->setText(_privateLinkUrl); } void ShareUserGroupWidget::slotPrivateLinkEmail() { Utility::openEmailComposer( tr("I shared something with you"), _privateLinkUrl, this); } ShareUserLine::ShareUserLine(QSharedPointer share, SharePermissions maxSharingPermissions, bool isFile, QWidget *parent) : QWidget(parent) , _ui(new Ui::ShareUserLine) , _share(share) , _isFile(isFile) { _ui->setupUi(this); _ui->sharedWith->setText(share->getShareWith()->format()); // Create detailed permissions menu QMenu *menu = new QMenu(this); _permissionCreate = new QAction(tr("create"), this); _permissionCreate->setCheckable(true); _permissionCreate->setEnabled(maxSharingPermissions & SharePermissionCreate); _permissionUpdate = new QAction(tr("change"), this); _permissionUpdate->setCheckable(true); _permissionUpdate->setEnabled(maxSharingPermissions & SharePermissionUpdate); _permissionDelete = new QAction(tr("delete"), this); _permissionDelete->setCheckable(true); _permissionDelete->setEnabled(maxSharingPermissions & SharePermissionDelete); menu->addAction(_permissionUpdate); /* * Files can't have create or delete permissions */ if (!_isFile) { menu->addAction(_permissionCreate); menu->addAction(_permissionDelete); } _ui->permissionToolButton->setMenu(menu); _ui->permissionToolButton->setPopupMode(QToolButton::InstantPopup); _ui->permissionToolButton->setIcon(Utility::getCoreIcon(QStringLiteral("more"))); // If there's only a single entry in the detailed permission menu, hide it if (menu->actions().size() == 1) { _ui->permissionToolButton->hide(); } // Set the permissions checkboxes displayPermissions(); _ui->permissionShare->setEnabled(maxSharingPermissions & SharePermissionShare); _ui->permissionsEdit->setEnabled(maxSharingPermissions & (SharePermissionCreate | SharePermissionUpdate | SharePermissionDelete)); connect(_permissionUpdate, &QAction::triggered, this, &ShareUserLine::slotPermissionsChanged); connect(_permissionCreate, &QAction::triggered, this, &ShareUserLine::slotPermissionsChanged); connect(_permissionDelete, &QAction::triggered, this, &ShareUserLine::slotPermissionsChanged); connect(_ui->permissionShare, &QAbstractButton::clicked, this, &ShareUserLine::slotPermissionsChanged); connect(_ui->permissionsEdit, &QAbstractButton::clicked, this, &ShareUserLine::slotEditPermissionsChanged); /* * We don't show permssion share for federated shares with server <9.1 * https://github.com/owncloud/core/issues/22122#issuecomment-185637344 * https://github.com/owncloud/client/issues/4996 */ if (share->getShareType() == Share::TypeRemote && share->account()->serverVersionInt() < Account::makeServerVersion(9, 1, 0)) { _ui->permissionShare->setVisible(false); _ui->permissionToolButton->setVisible(false); } connect(share.data(), &Share::permissionsSet, this, &ShareUserLine::slotPermissionsSet); connect(share.data(), &Share::shareDeleted, this, &ShareUserLine::slotShareDeleted); _ui->deleteShareButton->setIcon(QIcon::fromTheme(QStringLiteral("user-trash"), Utility::getCoreIcon(QStringLiteral("delete")))); if (!share->account()->capabilities().shareResharing()) { _ui->permissionShare->hide(); } loadAvatar(); } void ShareUserLine::loadAvatar() { const int avatarSize = 36; // Set size of the placeholder _ui->avatar->setMinimumHeight(avatarSize); _ui->avatar->setMinimumWidth(avatarSize); _ui->avatar->setMaximumHeight(avatarSize); _ui->avatar->setMaximumWidth(avatarSize); _ui->avatar->setAlignment(Qt::AlignCenter); /* Create the fallback avatar. * * This will be shown until the avatar image data arrives. */ const QByteArray hash = QCryptographicHash::hash(_ui->sharedWith->text().toUtf8(), QCryptographicHash::Md5); double hue = static_cast(hash[0]) / 255.; // See core/js/placeholder.js for details on colors and styling const QColor bg = QColor::fromHslF(hue, 0.7, 0.68); const QString style = QString(R"(* { color: #fff; background-color: %1; border-radius: %2px; text-align: center; line-height: %2px; font-size: %2px; })").arg(bg.name(), QString::number(avatarSize / 2)); _ui->avatar->setStyleSheet(style); // The avatar label is the first character of the user name. const QString text = _share->getShareWith()->displayName(); _ui->avatar->setText(text.at(0).toUpper()); /* Start the network job to fetch the avatar data. * * Currently only regular users can have avatars. */ if (_share->getShareWith()->type() == Sharee::User) { AvatarJob *job = new AvatarJob(_share->account(), _share->getShareWith()->shareWith(), avatarSize, this); connect(job, &AvatarJob::avatarPixmap, this, &ShareUserLine::slotAvatarLoaded); job->start(); } } void ShareUserLine::slotAvatarLoaded(const QPixmap &avatar) { if (avatar.isNull()) return; const QPixmap _avatar = AvatarJob::makeCircularAvatar(avatar); _ui->avatar->setPixmap(_avatar); // Remove the stylesheet for the fallback avatar _ui->avatar->setStyleSheet(""); } void ShareUserLine::on_deleteShareButton_clicked() { setEnabled(false); _share->deleteShare(); } ShareUserLine::~ShareUserLine() { delete _ui; } void ShareUserLine::slotEditPermissionsChanged() { setEnabled(false); // Can never manually be set to "partial". // This works because the state cycle for clicking is // unchecked -> partial -> checked -> unchecked. if (_ui->permissionsEdit->checkState() == Qt::PartiallyChecked) { _ui->permissionsEdit->setCheckState(Qt::Checked); } Share::Permissions permissions = SharePermissionRead; if (_ui->permissionShare->checkState() == Qt::Checked) { permissions |= SharePermissionShare; } if (_ui->permissionsEdit->checkState() == Qt::Checked) { if (_permissionUpdate->isEnabled()) permissions |= SharePermissionUpdate; /* * Files can't have create or delete permisisons */ if (!_isFile) { if (_permissionCreate->isEnabled()) permissions |= SharePermissionCreate; if (_permissionDelete->isEnabled()) permissions |= SharePermissionDelete; } } _share->setPermissions(permissions); } void ShareUserLine::slotPermissionsChanged() { setEnabled(false); Share::Permissions permissions = SharePermissionRead; if (_permissionUpdate->isChecked()) { permissions |= SharePermissionUpdate; } if (_permissionCreate->isChecked()) { permissions |= SharePermissionCreate; } if (_permissionDelete->isChecked()) { permissions |= SharePermissionDelete; } if (_ui->permissionShare->checkState() == Qt::Checked) { permissions |= SharePermissionShare; } _share->setPermissions(permissions); } void ShareUserLine::slotDeleteAnimationFinished() { emit resizeRequested(); emit visualDeletionDone(); deleteLater(); // There is a painting bug where a small line of this widget isn't // properly cleared. This explicit repaint() call makes sure any trace of // the share widget is removed once it's destroyed. #4189 connect(this, SIGNAL(destroyed(QObject *)), parentWidget(), SLOT(repaint())); } void ShareUserLine::slotShareDeleted() { QPropertyAnimation *animation = new QPropertyAnimation(this, "maximumHeight", this); animation->setDuration(500); animation->setStartValue(height()); animation->setEndValue(0); connect(animation, &QAbstractAnimation::finished, this, &ShareUserLine::slotDeleteAnimationFinished); connect(animation, &QVariantAnimation::valueChanged, this, &ShareUserLine::resizeRequested); animation->start(); } void ShareUserLine::slotPermissionsSet() { displayPermissions(); setEnabled(true); } QSharedPointer ShareUserLine::share() const { return _share; } void ShareUserLine::displayPermissions() { const SharePermissions perm = _share->getPermissions(); _permissionUpdate->setChecked(perm & SharePermissionUpdate); _permissionCreate->setChecked(!_isFile && perm & SharePermissionCreate); _permissionDelete->setChecked(!_isFile && perm & SharePermissionDelete); if (perm & SharePermissionUpdate && (_isFile || (perm & SharePermissionCreate && perm & SharePermissionDelete))) { _ui->permissionsEdit->setCheckState(Qt::Checked); } else if (perm & (SharePermissionUpdate | SharePermissionCreate | SharePermissionDelete)) { _ui->permissionsEdit->setCheckState(Qt::PartiallyChecked); } else { _ui->permissionsEdit->setCheckState(Qt::Unchecked); } _ui->permissionShare->setChecked(perm & SharePermissionShare); } }