diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 4ea867d1a9..9d14122fe9 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -8820,5 +8820,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_folder_archive_failed" = "Failed to create archive."; "lng_folder_archive_status" = "ZIP archive"; +"lng_folder_archive_unpack" = "Unpack archive"; +"lng_folder_archive_pack" = "Pack into ZIP archive"; +"lng_folder_archive_pack_selected" = "Pack selected into ZIP archive"; +"lng_folder_archive_select" = "Select"; +"lng_folder_archive_deselect" = "Deselect"; // Keys finished diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 10bc780e74..96ad152baf 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -545,6 +545,21 @@ rpl::producer SendFilesBox::Block::itemRenameRequest() const { } } +rpl::producer SendFilesBox::Block::itemSelectRequest() const { + using namespace rpl::mappers; + + const auto preview = _preview.get(); + const auto from = _from; + if (_isAlbum) { + const auto album = static_cast(preview); + return album->thumbSelected() | rpl::map(_1 + from); + } else if (_isSingleMedia) { + return rpl::never(); + } + const auto single = static_cast(preview); + return single->selectRequests() | rpl::map_to(from); +} + rpl::producer<> SendFilesBox::Block::orderUpdated() const { if (_isAlbum) { const auto album = static_cast(_preview.get()); @@ -582,6 +597,28 @@ void SendFilesBox::Block::toggleSpoilers(bool enabled) { } } +void SendFilesBox::Block::setSelectionMode(bool enabled) { + if (_isAlbum) { + const auto album = static_cast(_preview.get()); + album->setSelectionMode(enabled); + } else if (!_isSingleMedia) { + const auto single = static_cast( + _preview.get()); + single->setSelectionMode(enabled); + } +} + +void SendFilesBox::Block::setSelected(int index, bool selected) { + if (_isAlbum) { + const auto album = static_cast(_preview.get()); + album->setSelected(index - _from, selected); + } else if (!_isSingleMedia) { + const auto single = static_cast( + _preview.get()); + single->setSelected(selected); + } +} + void SendFilesBox::Block::applyChanges() { if (!_isAlbum) { if (_isSingleMedia) { @@ -959,6 +996,171 @@ void SendFilesBox::refreshAllAfterChanges(int fromItem, Fn perform) { _inner->resizeToWidth(st::boxWideWidth); refreshControls(); captionResized(); + updateSelectionMode(); +} + +void SendFilesBox::unpackArchive(int index) { + if (_preparing || index < 0 || index >= _list.files.size()) { + return; + } + const auto archive = _list.files[index].archive; + if (!archive) { + return; + } + const auto paths = archive->folder.isEmpty() + ? archive->paths + : Storage::FolderFilesForSending(archive->folder); + auto list = Storage::PrepareMediaList( + paths, + st::sendMediaPreviewSize, + _show->session().premium()); + if (list.error != Ui::PreparedList::Error::None + || list.files.empty()) { + showToast(tr::lng_send_media_invalid_files(tr::now)); + return; + } else if (!checkWith(list, _sendWay.current())) { + return; + } else if (_limits & SendFilesAllow::OnlyOne) { + auto removing = std::move(_list.files[index]); + std::swap(_list.files[index], _list.files.back()); + _list.files.pop_back(); + const auto ok = _list.canBeSentInSlowmodeWith(list); + _list.files.push_back(std::move(removing)); + std::swap(_list.files[index], _list.files.back()); + if (!ok) { + showToast(tr::lng_slowmode_no_many(tr::now)); + return; + } + } + for (auto i = 0; i != archive->names.size(); ++i) { + if (archive->names[i].isEmpty()) { + continue; + } + for (auto &file : list.files) { + if (file.path == archive->paths[i]) { + file.displayName = archive->names[i]; + } + } + } + refreshAllAfterChanges(index, [&] { + _list.files.erase(_list.files.begin() + index); + _list.files.insert( + _list.files.begin() + index, + std::make_move_iterator(list.files.begin()), + std::make_move_iterator(list.files.end())); + }); + _list.filesToProcess.insert( + _list.filesToProcess.end(), + std::make_move_iterator(list.filesToProcess.begin()), + std::make_move_iterator(list.filesToProcess.end())); + enqueueNextPrepare(); +} + +std::vector SendFilesBox::archivableIndices(bool selectedOnly) const { + auto result = std::vector(); + for (auto i = 0, count = int(_list.files.size()); i != count; ++i) { + const auto &file = _list.files[i]; + if (!file.archive + && !file.path.isEmpty() + && (!selectedOnly || file.selected)) { + result.push_back(i); + } + } + return result; +} + +bool SendFilesBox::canArchiveAll() const { + const auto count = int(_list.files.size()); + return !_preparing + && (count > 0) + && (archivableIndices(false).size() == count); +} + +bool SendFilesBox::hasArchives() const { + return !_preparing && ranges::any_of(_list.files, [](const auto &file) { + return file.archive != nullptr; + }); +} + +void SendFilesBox::unpackArchives() { + for (auto i = int(_list.files.size()); i != 0;) { + --i; + if (_list.files[i].archive) { + unpackArchive(i); + } + } +} + +bool SendFilesBox::hasSelection() const { + return ranges::any_of(_list.files, &Ui::PreparedFile::selected); +} + +void SendFilesBox::toggleSelection(int index) { + applyBlockChanges(); + if (index < 0 || index >= _list.files.size()) { + return; + } + auto &file = _list.files[index]; + file.selected = !file.selected; + for (auto &block : _blocks) { + if (index >= block.fromIndex() && index < block.tillIndex()) { + block.setSelected(index, file.selected); + } + } + updateSelectionMode(); +} + +bool SendFilesBox::clearSelection() { + if (!hasSelection()) { + return false; + } + for (auto i = 0, count = int(_list.files.size()); i != count; ++i) { + auto &file = _list.files[i]; + if (!file.selected) { + continue; + } + file.selected = false; + for (auto &block : _blocks) { + if (i >= block.fromIndex() && i < block.tillIndex()) { + block.setSelected(i, false); + } + } + } + updateSelectionMode(); + return true; +} + +void SendFilesBox::updateSelectionMode() { + const auto enabled = hasSelection(); + for (auto &block : _blocks) { + block.setSelectionMode(enabled); + } +} + +void SendFilesBox::archiveFiles(const std::vector &indices) { + if (_preparing || indices.empty()) { + return; + } + auto paths = QStringList(); + auto names = QStringList(); + for (const auto index : indices) { + const auto &file = _list.files[index]; + paths.push_back(file.path); + names.push_back(file.displayName); + } + auto list = Ui::PreparedList(); + list.files.push_back(Storage::PrepareFilesArchive(paths, names)); + if (!checkWith(list, _sendWay.current())) { + return; + } + refreshAllAfterChanges(indices.front(), [&] { + for (const auto index : ranges::views::reverse(indices)) { + _list.files.erase(_list.files.begin() + index); + } + _list.files.insert( + _list.files.begin() + indices.front(), + std::move(list.files.front())); + }); } bool SendFilesBox::setDisplayNameInSingleFilePreview( @@ -1244,7 +1446,7 @@ QImage SendFilesBox::preparePriceTagBg(QSize size) const { void SendFilesBox::addMenuButton() { const auto details = _sendMenuDetails(); - if (!hasSendMenu(details)) { + if (!hasSendMenu(details) && !canArchiveAll() && !hasArchives()) { return; } @@ -1254,6 +1456,18 @@ void SendFilesBox::addMenuButton() { _menu = base::make_unique_q(top, tabbed.menu); _menu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); const auto position = QCursor::pos(); + if (canArchiveAll()) { + _menu->addAction( + tr::lng_folder_archive_pack(tr::now), + [=] { archiveFiles(archivableIndices(false)); }, + &st::menuIconArchive); + } + if (hasArchives()) { + _menu->addAction( + tr::lng_folder_archive_unpack(tr::now), + [=] { unpackArchives(); }, + &st::menuIconUnarchive); + } const auto result = SendMenu::FillSendMenu( _menu.get(), _show, @@ -1261,11 +1475,11 @@ void SendFilesBox::addMenuButton() { _sendMenuCallback, &_st.tabbed.icons, position); - if (result != SendMenu::FillMenuResult::Prepared) { + if (result == SendMenu::FillMenuResult::Failed || _menu->empty()) { _menu = nullptr; return true; } - _menu->popupPrepared(); + _menu->popup(position); return true; }); } @@ -1396,6 +1610,7 @@ void SendFilesBox::pushBlock(int from, int till) { gifPaused, _sendWay.current()); auto &block = _blocks.back(); + block.setSelectionMode(hasSelection()); const auto widget = _inner->add( block.takeWidget(), QMargins(0, _inner->count() ? st::sendMediaRowSkip : 0, 0, 0)); @@ -1598,7 +1813,7 @@ void SendFilesBox::pushBlock(int from, int till) { if (!canEditFileData) { return; } - const auto allowExtensionEdit = file.path.isEmpty(); + const auto allowExtensionEdit = file.path.isEmpty() && !file.archive; _show->show(Box( RenameFileBox, file.displayName, @@ -1808,6 +2023,23 @@ void SendFilesBox::pushBlock(int from, int till) { &st::menuIconCancel); } } + if (!compressed && !_preparing && !file.archive) { + const auto selected = archivableIndices(true); + if (!selected.empty()) { + state->menu->addAction( + tr::lng_folder_archive_pack_selected(tr::now), + [=] { archiveFiles(selected); }, + &st::menuIconArchive); + } + if (!file.path.isEmpty()) { + state->menu->addAction( + (file.selected + ? tr::lng_folder_archive_deselect + : tr::lng_folder_archive_select)(tr::now), + [=] { toggleSelection(fileIndex); }, + &st::menuIconSelect); + } + } if (state->menu->empty()) { state->menu = nullptr; return false; @@ -1862,6 +2094,11 @@ void SendFilesBox::pushBlock(int from, int till) { renameFile(index); }, widget->lifetime()); + block.itemSelectRequest( + ) | rpl::on_next([=](int index) { + toggleSelection(index); + }, widget->lifetime()); + block.orderUpdated() | rpl::on_next([=]{ if (_priceTag) { _priceTagBg = QImage(); @@ -2069,6 +2306,9 @@ void SendFilesBox::setupCaption() { }, _caption->lifetime()); _caption->cancelled( ) | rpl::on_next([=] { + if (hasSelection()) { + return; + } requestToTakeTextWithTags(); closeBox(); }, _caption->lifetime()); @@ -2436,6 +2676,8 @@ void SendFilesBox::keyPressEvent(QKeyEvent *e) { || modifiers.testFlag(Qt::MetaModifier); const auto shift = modifiers.testFlag(Qt::ShiftModifier); send({}, ctrl && shift); + } else if (e->key() == Qt::Key_Escape && clearSelection()) { + e->accept(); } else { BoxContent::keyPressEvent(e); } diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 4aad174c08..fe724d4b74 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -184,10 +184,13 @@ private: [[nodiscard]] rpl::producer itemReplaceRequest() const; [[nodiscard]] rpl::producer itemModifyRequest() const; [[nodiscard]] rpl::producer itemRenameRequest() const; + [[nodiscard]] rpl::producer itemSelectRequest() const; [[nodiscard]] rpl::producer<> orderUpdated() const; void setSendWay(Ui::SendFilesWay way); void toggleSpoilers(bool enabled); + void setSelectionMode(bool enabled); + void setSelected(int index, bool selected); void applyChanges(); [[nodiscard]] QImage generatePriceTagBackground() const; @@ -267,6 +270,16 @@ private: void openDialogToAddFileToAlbum(); void refreshAllAfterChanges(int fromItem, Fn perform = nullptr); + void unpackArchive(int index); + [[nodiscard]] std::vector archivableIndices(bool selectedOnly) const; + [[nodiscard]] bool hasSelection() const; + void toggleSelection(int index); + void updateSelectionMode(); + [[nodiscard]] bool clearSelection(); + void archiveFiles(const std::vector &indices); + [[nodiscard]] bool canArchiveAll() const; + [[nodiscard]] bool hasArchives() const; + void unpackArchives(); [[nodiscard]] bool setDisplayNameInSingleFilePreview( int fileIndex, const QString &displayName); diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index b301638a17..72a0674875 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -227,6 +227,7 @@ ComposeFiles { buttonFile: IconButton; buttonFileEdit: icon; buttonFileDelete: icon; + selectCheck: RoundCheckbox; iconBg: color; iconPlay: icon; iconImage: icon; @@ -705,6 +706,9 @@ sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) { } sendBoxAlbumGroupEditButtonIconFile: editMediaButtonIconFile; sendBoxAlbumGroupDeleteButtonIconFile: icon {{ "send_media/send_media_cross", menuIconFg }}; +sendBoxFileSelectCheck: RoundCheckbox(defaultPeerListCheck) { + bgActive: boxTextFgGood; +} sendBoxAlbumButtonMediaMore: icon {{ "send_media/send_media_more", roundedFg }}; sendBoxAlbumGroupButtonMediaMore: icon {{ "send_media/send_media_more", roundedFg, point(4px, 1px) }}; @@ -1578,6 +1582,7 @@ defaultComposeFiles: ComposeFiles { buttonFile: sendBoxAlbumGroupButtonFile; buttonFileEdit: sendBoxAlbumGroupEditButtonIconFile; buttonFileDelete: sendBoxAlbumGroupDeleteButtonIconFile; + selectCheck: sendBoxFileSelectCheck; iconBg: msgFileInBg; iconPlay: icon {{ "history_file_play", historyFileInIconFg }}; iconImage: icon {{ "history_file_image", historyFileInIconFg }}; diff --git a/Telegram/SourceFiles/history/history_drag_area.cpp b/Telegram/SourceFiles/history/history_drag_area.cpp index 73adba4287..f2849dec2d 100644 --- a/Telegram/SourceFiles/history/history_drag_area.cpp +++ b/Telegram/SourceFiles/history/history_drag_area.cpp @@ -57,7 +57,9 @@ constexpr auto kDragAreaEvents = { } } if (!modifiers) { - return state; + return (state == DragState::FilesArchive) + ? DragState::Files + : state; } return (state == DragState::PhotoFiles) ? DragState::PhotoFilesArchive @@ -68,7 +70,9 @@ constexpr auto kDragAreaEvents = { [[nodiscard]] bool IsPromotedToArchive(Storage::MimeDataState state) { using DragState = Storage::MimeDataState; - return (state == DragState::PhotoFilesArchive) + return (state == DragState::FilesArchive) + || (state == DragState::FilesArchiveOnly) + || (state == DragState::PhotoFilesArchive) || (state == DragState::MediaFilesArchive); } diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index cdddd73d51..3af04a02c2 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -7970,14 +7970,6 @@ bool HistoryWidget::confirmSendingFiles( } return true; } - if (overrideSendImagesAsPhotos == true - && (Storage::ComputeMimeDataState(data) - == Storage::MimeDataState::FilesArchive)) { - auto list = Ui::PreparedList(); - list.files.push_back(Storage::PrepareFilesArchive(urls)); - confirmSendingFiles(std::move(list), QString()); - return true; - } auto list = Storage::PrepareMediaList( urls, st::sendMediaPreviewSize, diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 154b64b1f2..e81307d82f 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -2022,14 +2022,6 @@ bool ChatWidget::confirmSendingFiles( } return true; } - if (overrideSendImagesAsPhotos == true - && (Storage::ComputeMimeDataState(data) - == Storage::MimeDataState::FilesArchive)) { - auto list = Ui::PreparedList(); - list.files.push_back(Storage::PrepareFilesArchive(urls)); - confirmSendingFiles(std::move(list), QString()); - return true; - } auto list = Storage::PrepareMediaList( urls, st::sendMediaPreviewSize, diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 4e335dfc32..60e88fd905 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -573,14 +573,6 @@ bool ScheduledWidget::confirmSendingFiles( } return true; } - if (overrideSendImagesAsPhotos == true - && (Storage::ComputeMimeDataState(data) - == Storage::MimeDataState::FilesArchive)) { - auto list = Ui::PreparedList(); - list.files.push_back(Storage::PrepareFilesArchive(urls)); - confirmSendingFiles(std::move(list), QString()); - return true; - } auto list = Storage::PrepareMediaList( urls, st::sendMediaPreviewSize, diff --git a/Telegram/SourceFiles/media/view/media_view.style b/Telegram/SourceFiles/media/view/media_view.style index 8e69f8d125..95862758e9 100644 --- a/Telegram/SourceFiles/media/view/media_view.style +++ b/Telegram/SourceFiles/media/view/media_view.style @@ -900,6 +900,14 @@ storiesComposeControls: ComposeControls(defaultComposeControls) { } buttonFileEdit: icon {{ "send_media/send_media_replace", storiesComposeGrayIcon }}; buttonFileDelete: icon {{ "send_media/send_media_delete", storiesComposeGrayIcon }}; + selectCheck: RoundCheckbox(sendBoxFileSelectCheck) { + border: storiesComposeBg; + check: icon {{ + "default_checkbox_check", + storiesComposeWhiteText, + point(3px, 6px) + }}; + } iconBg: storiesComposeBlue; iconPlay: icon {{ "history_file_play", storiesComposeWhiteText }}; iconImage: icon {{ "history_file_image", storiesComposeWhiteText }}; diff --git a/Telegram/SourceFiles/storage/storage_folder_archive.cpp b/Telegram/SourceFiles/storage/storage_folder_archive.cpp index fbaa912ffe..00dd8138ec 100644 --- a/Telegram/SourceFiles/storage/storage_folder_archive.cpp +++ b/Telegram/SourceFiles/storage/storage_folder_archive.cpp @@ -273,10 +273,12 @@ void FillEntryDate(const QDateTime &modified, zip_fileinfo *result) { } [[nodiscard]] std::optional GatherFiles( - const QStringList &paths) { + const QStringList &paths, + const QStringList &names) { auto result = ArchiveEntries(); auto counts = base::flat_map(); - for (const auto &path : paths) { + for (auto i = 0; i != paths.size(); ++i) { + const auto &path = paths[i]; const auto info = QFileInfo(path); if (info.isDir()) { continue; @@ -286,10 +288,13 @@ void FillEntryDate(const QDateTime &modified, zip_fileinfo *result) { continue; } const auto size = info.size(); + const auto name = (i < names.size() && !names[i].isEmpty()) + ? names[i] + : info.fileName(); result.total += size; result.list.push_back({ info.absoluteFilePath(), - DedupedName(counts, info.fileName()), + DedupedName(counts, name), info.lastModified(), size, }); @@ -469,17 +474,24 @@ Ui::PreparedFile PrepareFilesArchive(const QList &urls) { paths.push_back(Platform::File::UrlToLocal(url)); } } + return PrepareFilesArchive(paths); +} + +Ui::PreparedFile PrepareFilesArchive( + const QStringList &paths, + const QStringList &names) { const auto parent = CommonParentName(paths); const auto name = parent.isEmpty() ? u"Archive"_q : parent; return ArchiveFile(name + u".zip"_q, { .paths = paths, + .names = names, }); } std::optional GatherArchiveEntries( const Ui::PreparedFileArchive &job) { return job.folder.isEmpty() - ? GatherFiles(job.paths) + ? GatherFiles(job.paths, job.names) : WalkFolder(job.folder, job.root); } diff --git a/Telegram/SourceFiles/storage/storage_folder_archive.h b/Telegram/SourceFiles/storage/storage_folder_archive.h index 817a5ef447..24557888d4 100644 --- a/Telegram/SourceFiles/storage/storage_folder_archive.h +++ b/Telegram/SourceFiles/storage/storage_folder_archive.h @@ -45,6 +45,9 @@ struct ArchiveWriteResult { [[nodiscard]] Ui::PreparedFile PrepareFolderArchive(const QString &folder); [[nodiscard]] Ui::PreparedFile PrepareFilesArchive(const QList &urls); +[[nodiscard]] Ui::PreparedFile PrepareFilesArchive( + const QStringList &paths, + const QStringList &names = {}); [[nodiscard]] std::optional GatherArchiveEntries( const Ui::PreparedFileArchive &job); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp index 1fe46e5871..0217aae175 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.cpp @@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "ui/chat/attach/attach_abstract_single_file_preview.h" -#include "base/timer_rpl.h" #include "ui/image/image_prepare.h" #include "ui/painter.h" #include "ui/text/text_options.h" @@ -30,7 +29,8 @@ AbstractSingleFilePreview::AbstractSingleFilePreview( , _type(type) , _captionContext(captionContext) , _editMedia(this, _st.files.buttonFile) -, _deleteMedia(this, _st.files.buttonFile) { +, _deleteMedia(this, _st.files.buttonFile) +, _selectCheck(_st.files.selectCheck, [=] { update(); }) { const auto repaint = _captionContext.repaint; _captionContext.repaint = [=] { if (repaint) { @@ -63,15 +63,17 @@ AbstractSingleFilePreview::AbstractSingleFilePreview( AbstractSingleFilePreview::~AbstractSingleFilePreview() = default; rpl::producer<> AbstractSingleFilePreview::editRequests() const { - return _editMedia->clicks() | rpl::map([] { - return base::timer_once(st::historyAttach.ripple.hideDuration); - }) | rpl::flatten_latest(); + return _editMedia->clicks() | rpl::to_empty; } rpl::producer<> AbstractSingleFilePreview::renameRequests() const { return _renameRequests.events(); } +rpl::producer<> AbstractSingleFilePreview::selectRequests() const { + return _selectRequests.events(); +} + rpl::producer<> AbstractSingleFilePreview::deleteRequests() const { return _deleteMedia->clicks() | rpl::to_empty; } @@ -91,6 +93,24 @@ void AbstractSingleFilePreview::setRenameEnabled(bool enabled) { } } +void AbstractSingleFilePreview::setSelectable(bool selectable) { + _selectable = selectable && (_type == AttachControls::Type::Full); +} + +void AbstractSingleFilePreview::setSelectionMode(bool enabled) { + _selectionMode = enabled; +} + +void AbstractSingleFilePreview::setSelected( + bool selected, + anim::type animated) { + _selectCheck.setChecked(selected, animated); +} + +bool AbstractSingleFilePreview::selected() const { + return _selectCheck.checked(); +} + void AbstractSingleFilePreview::setDisplayName(const QString &displayName) { _data.name = displayName; updateTextWidthFor(_data); @@ -174,6 +194,14 @@ void AbstractSingleFilePreview::paintEvent(QPaintEvent *e) { style::rtlrect(x, y, st.thumbSize, st.thumbSize, width())); p.drawPixmap(rthumb.topLeft(), _data.fileThumb); } + const auto checkSize = _st.files.selectCheck.size; + const auto check = style::rtlrect( + x + st.thumbSize - checkSize, + y + st.thumbSize - checkSize, + checkSize, + checkSize, + width()); + _selectCheck.paint(p, check.x(), check.y(), width()); p.setFont(st::semiboldFont); p.setPen(_st.files.nameFg); p.drawTextLeft( @@ -297,7 +325,11 @@ void AbstractSingleFilePreview::setData(Data data) { } void AbstractSingleFilePreview::mousePressEvent(QMouseEvent *e) { - if (isOverName(e->pos())) { + if (e->button() != Qt::LeftButton) { + return; + } else if (selectingByClick(e->modifiers())) { + _selectPressed = true; + } else if (isOverName(e->pos())) { _namePressed = true; } } @@ -309,9 +341,13 @@ void AbstractSingleFilePreview::mouseMoveEvent(QMouseEvent *e) { } void AbstractSingleFilePreview::mouseReleaseEvent(QMouseEvent *e) { - if (base::take(_namePressed) - && (e->button() == Qt::LeftButton) - && isOverName(e->pos())) { + const auto selectPressed = base::take(_selectPressed); + const auto namePressed = base::take(_namePressed); + if (e->button() != Qt::LeftButton) { + return; + } else if (selectPressed && rect().contains(e->pos())) { + _selectRequests.fire({}); + } else if (namePressed && isOverName(e->pos())) { _renameRequests.fire({}); } } @@ -335,7 +371,13 @@ QRect AbstractSingleFilePreview::nameRect() const { } bool AbstractSingleFilePreview::isOverName(QPoint point) const { - return _renameEnabled && nameRect().contains(point); + return _renameEnabled && !_selectionMode && nameRect().contains(point); +} + +bool AbstractSingleFilePreview::selectingByClick( + Qt::KeyboardModifiers modifiers) const { + return _selectable + && (_selectionMode || modifiers.testFlag(Qt::ControlModifier)); } void AbstractSingleFilePreview::applyCursor(style::cursor cursor) { diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h index 11b7356f0f..6152c54a4c 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_abstract_single_file_preview.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/attach/attach_abstract_single_preview.h" #include "ui/chat/attach/attach_controls.h" +#include "ui/effects/round_checkbox.h" #include "base/object_ptr.h" namespace style { @@ -31,8 +32,14 @@ public: [[nodiscard]] rpl::producer<> deleteRequests() const override; [[nodiscard]] rpl::producer<> editRequests() const override; [[nodiscard]] rpl::producer<> renameRequests() const; + [[nodiscard]] rpl::producer<> selectRequests() const; [[nodiscard]] rpl::producer<> modifyRequests() const override; void setRenameEnabled(bool enabled); + void setSelectionMode(bool enabled); + void setSelected( + bool selected, + anim::type animated = anim::type::normal); + [[nodiscard]] bool selected() const; virtual void setDisplayName(const QString &displayName); virtual void setCaption(const TextWithTags &caption); @@ -56,6 +63,7 @@ protected: } void setData(Data data); + void setSelectable(bool selectable); private: void paintEvent(QPaintEvent *e) override; @@ -69,6 +77,8 @@ private: [[nodiscard]] QRect captionRect() const; [[nodiscard]] QRect nameRect() const; [[nodiscard]] bool isOverName(QPoint point) const; + [[nodiscard]] bool selectingByClick( + Qt::KeyboardModifiers modifiers) const; void applyCursor(style::cursor cursor); const style::ComposeControls &_st; @@ -80,10 +90,15 @@ private: object_ptr _editMedia = { nullptr }; object_ptr _deleteMedia = { nullptr }; rpl::event_stream<> _renameRequests; + rpl::event_stream<> _selectRequests; + RoundCheckbox _selectCheck; style::cursor _cursor = style::cur_default; bool _namePressed = false; bool _renameEnabled = false; + bool _selectable = false; + bool _selectionMode = false; + bool _selectPressed = false; }; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp index 4db1725555..b4c89a5949 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.cpp @@ -128,6 +128,16 @@ void AlbumPreview::toggleSpoilers(bool enabled) { } } +void AlbumPreview::setSelectionMode(bool enabled) { + _selectionMode = enabled; +} + +void AlbumPreview::setSelected(int index, bool selected) { + if (index >= 0 && index < _order.size()) { + _thumbs[_order[index]]->setSelected(selected); + } +} + std::vector AlbumPreview::takeOrder() { //Expects(_thumbs.size() == _order.size()); //Expects(_itemsShownDimensions.size() == _order.size()); @@ -184,12 +194,6 @@ void AlbumPreview::prepareThumbs(gsl::span items) { this, [=] { update(); }, [=](QRect rect) { update(rect); }, - // Bound to the thumb that owns the button, not to whatever is - // under the cursor when the callback runs: editing is delayed by - // the ripple hide duration, and keyboard activation has no - // cursor over the thumb at all. Bound by pointer and not by - // index, because takeOrder() permutes _thumbs in place - so the - // slot this one was built in can hold another thumb later. [=] { changeThumbByIndex(orderIndex(*self)); }, [=] { deleteThumbByIndex(orderIndex(*self)); })); *self = _thumbs.back().get(); @@ -515,6 +519,10 @@ void AlbumPreview::mousePressEvent(QMouseEvent *e) { const auto position = e->pos(); cancelDrag(); if (const auto thumb = findThumb(position)) { + if (selectingByClick(e, thumb)) { + _selectPressedThumb = thumb; + return; + } _draggedStartPosition = position; _pressedThumb = thumb; _pressedButtonType = thumb->buttonTypeFromPoint(position); @@ -615,6 +623,12 @@ void AlbumPreview::updateSuggestedDrag(QPoint position) { } void AlbumPreview::mouseReleaseEvent(QMouseEvent *e) { + if (const auto thumb = base::take(_selectPressedThumb)) { + if (e->button() == Qt::LeftButton && findThumb(e->pos()) == thumb) { + _thumbSelected.fire(orderIndex(thumb)); + } + return; + } if (_draggedThumb) { finishDrag(); _shrinkAnimation.start( @@ -635,6 +649,16 @@ void AlbumPreview::mouseReleaseEvent(QMouseEvent *e) { _pressedButtonType = AttachButtonType::None; } +bool AlbumPreview::selectingByClick( + QMouseEvent *e, + not_null thumb) const { + return !_sendWay.sendImagesAsPhotos() + && thumb->selectable() + && (e->button() == Qt::LeftButton) + && (_selectionMode + || e->modifiers().testFlag(Qt::ControlModifier)); +} + void AlbumPreview::switchToDrag() { _paintedAbove = _suggestedThumb diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h index e8950c0126..7a82bd4294 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_preview.h @@ -39,6 +39,8 @@ public: [[nodiscard]] base::flat_set collectSpoileredIndices(); [[nodiscard]] bool canHaveSpoiler(int index) const; void toggleSpoilers(bool enabled); + void setSelectionMode(bool enabled); + void setSelected(int index, bool selected); [[nodiscard]] std::vector takeOrder(); [[nodiscard]] rpl::producer thumbDeleted() const { @@ -50,6 +52,9 @@ public: [[nodiscard]] rpl::producer thumbModified() const { return _thumbModified.events(); } + [[nodiscard]] rpl::producer thumbSelected() const { + return _thumbSelected.events(); + } [[nodiscard]] rpl::producer<> orderUpdated() const { return _orderUpdated.events(); } @@ -81,6 +86,9 @@ private: AttachButtonType type); void switchToDrag(); + [[nodiscard]] bool selectingByClick( + QMouseEvent *e, + not_null thumb) const; void paintAlbum(Painter &p) const; void paintPhotos(Painter &p, QRect clip) const; @@ -113,6 +121,8 @@ private: AlbumThumbnail *_suggestedThumb = nullptr; AlbumThumbnail *_paintedAbove = nullptr; AlbumThumbnail *_pressedThumb = nullptr; + AlbumThumbnail *_selectPressedThumb = nullptr; + bool _selectionMode = false; QPoint _draggedStartPosition; base::Timer _dragTimer; @@ -121,6 +131,7 @@ private: rpl::event_stream _thumbDeleted; rpl::event_stream _thumbChanged; rpl::event_stream _thumbModified; + rpl::event_stream _thumbSelected; rpl::event_stream<> _orderUpdated; mutable Animations::Simple _thumbsHeightAnimation; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp index e36a3f249d..0a2dda3c99 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.cpp @@ -18,7 +18,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/ui_utility.h" #include "ui/painter.h" #include "ui/power_saving.h" -#include "base/call_delayed.h" #include "styles/style_chat.h" #include "styles/style_chat_helpers.h" #include "styles/style_boxes.h" @@ -53,7 +52,11 @@ AlbumThumbnail::AlbumThumbnail( , _ttlSeconds(file.ttlSeconds) , _isCompressedSticker(Core::IsMimeSticker(file.information->filemime)) , _repaint(std::move(repaint)) -, _repaintRect(std::move(repaintRect)) { +, _repaintRect(std::move(repaintRect)) +, _selectCheck(_st.files.selectCheck, _repaint) +, _selectable(!_isCompressedSticker + && !file.archive + && !file.path.isEmpty()) { Expects(!_fullPreview.isNull()); moveToLayout(layout); @@ -152,18 +155,14 @@ AlbumThumbnail::AlbumThumbnail( _editMedia.create(parent, _st.files.buttonFile); _deleteMedia.create(parent, _st.files.buttonFile); - const auto duration = st::historyAttach.ripple.hideDuration; - _editMedia->setClickedCallback([=] { - // Guarded by the button, which dies with this thumbnail, so the - // delayed edit is dropped instead of firing for a thumb that is gone. - base::call_delayed(duration, _editMedia.data(), editCallback); - }); + _editMedia->setClickedCallback(editCallback); _deleteMedia->setClickedCallback(deleteCallback); _editMedia->setIconOverride(&_st.files.buttonFileEdit); _deleteMedia->setIconOverride(&_st.files.buttonFileDelete); setSpoiler(file.spoiler); + setSelected(file.selected, anim::type::instant); setButtonVisible(false); } @@ -190,6 +189,18 @@ void AlbumThumbnail::setCaption(const TextWithTags &caption) { _repaint(); } +void AlbumThumbnail::setSelected(bool selected, anim::type animated) { + _selectCheck.setChecked(selected, animated); +} + +bool AlbumThumbnail::selected() const { + return _selectCheck.checked(); +} + +bool AlbumThumbnail::selectable() const { + return _selectable; +} + bool AlbumThumbnail::hasSpoiler() const { return _spoiler != nullptr; } @@ -586,6 +597,12 @@ void AlbumThumbnail::paintFile( const auto textLeft = left + st.thumbSize + st.thumbSkip; p.drawPixmap(left, top, _fileThumb); + const auto checkSize = _st.files.selectCheck.size; + _selectCheck.paint( + p, + left + st.thumbSize - checkSize, + top + st.thumbSize - checkSize, + outerWidth); p.setFont(st::semiboldFont); p.setPen(_st.files.nameFg); p.drawTextLeft( diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h index daacecb613..9e8f144031 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_album_thumbnail.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/attach/attach_controls.h" #include "ui/chat/attach/attach_send_files_way.h" #include "ui/effects/animations.h" +#include "ui/effects/round_checkbox.h" #include "ui/grouped_layout.h" #include "ui/round_rect.h" #include "base/object_ptr.h" @@ -42,6 +43,11 @@ public: void resetLayoutAnimation(); void setSpoiler(bool spoiler); + void setSelected( + bool selected, + anim::type animated = anim::type::normal); + [[nodiscard]] bool selected() const; + [[nodiscard]] bool selectable() const; void setCaption(const TextWithTags &caption); [[nodiscard]] bool hasSpoiler() const; @@ -134,6 +140,8 @@ private: QImage _cornerCache; Fn _repaint; Fn _repaintRect; + RoundCheckbox _selectCheck; + const bool _selectable = false; QRect _lastRectOfModify; QRect _lastRectOfButtons; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h index dae82beb91..5086e36e33 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_prepare.h @@ -66,6 +66,7 @@ struct PreparedFileArchive { QString folder; QString root; QStringList paths; + QStringList names; }; struct PreparedFile { @@ -117,6 +118,7 @@ struct PreparedFile { Type type = Type::File; crl::time ttlSeconds = 0; bool spoiler = false; + bool selected = false; bool sendLargePhotos = false; std::shared_ptr animationJob; std::shared_ptr archive; diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp index c4d1ba383d..aa496ecbda 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp @@ -118,6 +118,8 @@ void SingleFilePreview::preparePreview(const PreparedFile &file) { captionContext()); setData(std::move(data)); + setSelectable(!file.archive && !file.path.isEmpty()); + setSelected(file.selected, anim::type::instant); } } // namespace Ui