Added ability to unpack archives from send files box. (Combined commit.)

This commit is contained in:
23rd
2026-09-16 09:13:35 +03:00
parent 353be0fa1f
commit 1cb992e2bf
19 changed files with 447 additions and 58 deletions

View File

@@ -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

View File

@@ -545,6 +545,21 @@ rpl::producer<int> SendFilesBox::Block::itemRenameRequest() const {
}
}
rpl::producer<int> SendFilesBox::Block::itemSelectRequest() const {
using namespace rpl::mappers;
const auto preview = _preview.get();
const auto from = _from;
if (_isAlbum) {
const auto album = static_cast<Ui::AlbumPreview*>(preview);
return album->thumbSelected() | rpl::map(_1 + from);
} else if (_isSingleMedia) {
return rpl::never<int>();
}
const auto single = static_cast<Ui::SingleFilePreview*>(preview);
return single->selectRequests() | rpl::map_to(from);
}
rpl::producer<> SendFilesBox::Block::orderUpdated() const {
if (_isAlbum) {
const auto album = static_cast<Ui::AlbumPreview*>(_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<Ui::AlbumPreview*>(_preview.get());
album->setSelectionMode(enabled);
} else if (!_isSingleMedia) {
const auto single = static_cast<Ui::SingleFilePreview*>(
_preview.get());
single->setSelectionMode(enabled);
}
}
void SendFilesBox::Block::setSelected(int index, bool selected) {
if (_isAlbum) {
const auto album = static_cast<Ui::AlbumPreview*>(_preview.get());
album->setSelected(index - _from, selected);
} else if (!_isSingleMedia) {
const auto single = static_cast<Ui::SingleFilePreview*>(
_preview.get());
single->setSelected(selected);
}
}
void SendFilesBox::Block::applyChanges() {
if (!_isAlbum) {
if (_isSingleMedia) {
@@ -959,6 +996,171 @@ void SendFilesBox::refreshAllAfterChanges(int fromItem, Fn<void()> 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<int> SendFilesBox::archivableIndices(bool selectedOnly) const {
auto result = std::vector<int>();
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<int> &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<Ui::PopupMenu>(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);
}

View File

@@ -184,10 +184,13 @@ private:
[[nodiscard]] rpl::producer<int> itemReplaceRequest() const;
[[nodiscard]] rpl::producer<int> itemModifyRequest() const;
[[nodiscard]] rpl::producer<int> itemRenameRequest() const;
[[nodiscard]] rpl::producer<int> 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<void()> perform = nullptr);
void unpackArchive(int index);
[[nodiscard]] std::vector<int> archivableIndices(bool selectedOnly) const;
[[nodiscard]] bool hasSelection() const;
void toggleSelection(int index);
void updateSelectionMode();
[[nodiscard]] bool clearSelection();
void archiveFiles(const std::vector<int> &indices);
[[nodiscard]] bool canArchiveAll() const;
[[nodiscard]] bool hasArchives() const;
void unpackArchives();
[[nodiscard]] bool setDisplayNameInSingleFilePreview(
int fileIndex,
const QString &displayName);

View File

@@ -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 }};

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 }};

View File

@@ -273,10 +273,12 @@ void FillEntryDate(const QDateTime &modified, zip_fileinfo *result) {
}
[[nodiscard]] std::optional<ArchiveEntries> GatherFiles(
const QStringList &paths) {
const QStringList &paths,
const QStringList &names) {
auto result = ArchiveEntries();
auto counts = base::flat_map<QString, int>();
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<QUrl> &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<ArchiveEntries> GatherArchiveEntries(
const Ui::PreparedFileArchive &job) {
return job.folder.isEmpty()
? GatherFiles(job.paths)
? GatherFiles(job.paths, job.names)
: WalkFolder(job.folder, job.root);
}

View File

@@ -45,6 +45,9 @@ struct ArchiveWriteResult {
[[nodiscard]] Ui::PreparedFile PrepareFolderArchive(const QString &folder);
[[nodiscard]] Ui::PreparedFile PrepareFilesArchive(const QList<QUrl> &urls);
[[nodiscard]] Ui::PreparedFile PrepareFilesArchive(
const QStringList &paths,
const QStringList &names = {});
[[nodiscard]] std::optional<ArchiveEntries> GatherArchiveEntries(
const Ui::PreparedFileArchive &job);

View File

@@ -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) {

View File

@@ -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<IconButton> _editMedia = { nullptr };
object_ptr<IconButton> _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;
};

View File

@@ -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<int> AlbumPreview::takeOrder() {
//Expects(_thumbs.size() == _order.size());
//Expects(_itemsShownDimensions.size() == _order.size());
@@ -184,12 +194,6 @@ void AlbumPreview::prepareThumbs(gsl::span<Ui::PreparedFile> 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<AlbumThumbnail*> thumb) const {
return !_sendWay.sendImagesAsPhotos()
&& thumb->selectable()
&& (e->button() == Qt::LeftButton)
&& (_selectionMode
|| e->modifiers().testFlag(Qt::ControlModifier));
}
void AlbumPreview::switchToDrag() {
_paintedAbove
= _suggestedThumb

View File

@@ -39,6 +39,8 @@ public:
[[nodiscard]] base::flat_set<int> collectSpoileredIndices();
[[nodiscard]] bool canHaveSpoiler(int index) const;
void toggleSpoilers(bool enabled);
void setSelectionMode(bool enabled);
void setSelected(int index, bool selected);
[[nodiscard]] std::vector<int> takeOrder();
[[nodiscard]] rpl::producer<int> thumbDeleted() const {
@@ -50,6 +52,9 @@ public:
[[nodiscard]] rpl::producer<int> thumbModified() const {
return _thumbModified.events();
}
[[nodiscard]] rpl::producer<int> 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<AlbumThumbnail*> 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<int> _thumbDeleted;
rpl::event_stream<int> _thumbChanged;
rpl::event_stream<int> _thumbModified;
rpl::event_stream<int> _thumbSelected;
rpl::event_stream<> _orderUpdated;
mutable Animations::Simple _thumbsHeightAnimation;

View File

@@ -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(

View File

@@ -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<void()> _repaint;
Fn<void(QRect)> _repaintRect;
RoundCheckbox _selectCheck;
const bool _selectable = false;
QRect _lastRectOfModify;
QRect _lastRectOfButtons;

View File

@@ -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<Media::Encode::Job> animationJob;
std::shared_ptr<PreparedFileArchive> archive;

View File

@@ -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