From 2cfdf1b32ee4160da4d0816f6b9ff26bdf41cd81 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 28 Apr 2026 20:50:59 +0700 Subject: [PATCH] Rename send as a file with extension. --- .../SourceFiles/boxes/edit_caption_box.cpp | 187 +++++++++++------- Telegram/SourceFiles/boxes/edit_caption_box.h | 4 + Telegram/SourceFiles/boxes/send_files_box.cpp | 113 ++++++++--- Telegram/SourceFiles/boxes/send_files_box.h | 6 + .../attach_abstract_single_file_preview.cpp | 65 ++++++ .../attach_abstract_single_file_preview.h | 13 ++ 6 files changed, 290 insertions(+), 98 deletions(-) diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index 78d47ba6c2..e5b740e259 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/event_filter.h" #include "boxes/premium_limits_box.h" #include "boxes/premium_preview_box.h" +#include "boxes/send_files_box.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "chat_helpers/field_autocomplete.h" #include "chat_helpers/message_field.h" @@ -545,6 +546,24 @@ void EditCaptionBox::rebuildPreview() { _content->heightValue( ) | rpl::start_to_stream(_contentHeight, _content->lifetime()); + if (const auto file = dynamic_cast( + _content.get())) { + file->setRenameEnabled(!_preparedList.files.empty()); + file->renameRequests( + ) | rpl::on_next([=] { + renameCurrentFile(); + }, _content->lifetime()); + } + + base::install_event_filter(_content.get(), [=](not_null e) { + if (e->type() == QEvent::ContextMenu) { + const auto mouse = static_cast(e.get()); + showMenu(mouse->globalPos(), false); + return base::EventFilterResult::Cancel; + } + return base::EventFilterResult::Continue; + }, _content->lifetime()); + _scroll->setOwnedWidget( object_ptr::fromRaw(_content.get())); @@ -728,81 +747,109 @@ void EditCaptionBox::setupControls() { } void EditCaptionBox::setupEditEventHandler() { - const auto menu - = lifetime().make_state>(); _editMediaClicks.events( ) | rpl::on_next([=] { - *menu = base::make_unique_q( - this, - st::popupMenuWithIcons); - (*menu)->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); - if (_isAllowedEditMedia) { - (*menu)->addAction(tr::lng_attach_replace(tr::now), [=] { - ChooseReplacement( - _controller, - _albumType, - crl::guard(this, [=](Ui::PreparedList &&list) { - setPreparedList(std::move(list)); - })); - }, &st::menuIconReplace); - } - using Type = Ui::PreparedFile::Type; - const auto canDraw = !_preparedList.files.empty() - ? (_preparedList.files.front().type == Type::Photo) - : (_isPhoto && !_asFile); - if (canDraw) { - (*menu)->addAction(tr::lng_context_draw(tr::now), [=] { - _photoEditorOpens.fire({}); - }, &st::menuIconDraw); - } - if (!_asFile && (_isPhoto || _isVideo)) { - if (hasSendLargePhotosOption()) { - const auto enabled = _sendLargePhotos; - Menu::AddCheckedAction( - menu->get(), - tr::lng_send_high_quality(tr::now), - [=] { - _sendLargePhotos = !enabled; - rebuildPreview(); - }, - &st::menuIconQualityHigh, - enabled); - } - if (_preparedList.hasSpoilerMenu(!_asFile)) { - const auto spoilered = hasSpoiler(); - Menu::AddCheckedAction( - menu->get(), - tr::lng_context_spoiler_effect(tr::now), - [=] { - _mediaEditManager.apply({ .type = spoilered - ? SendMenu::ActionType::SpoilerOff - : SendMenu::ActionType::SpoilerOn - }); - rebuildPreview(); - }, - &st::menuIconSpoiler, - spoilered); - } - if (_isVideo && !_preparedList.files.empty()) { - (*menu)->addAction(tr::lng_context_edit_cover(tr::now), [=] { - setupEditCoverHandler(); - }, &st::menuIconEdit); - if (_preparedList.files.front().videoCover != nullptr) { - (*menu)->addAction( - tr::lng_context_clear_cover(tr::now), - [=] { setupClearCoverHandler(); }, - &st::menuIconCancel); - } - } - } - if ((*menu)->empty()) { - *menu = nullptr; - } else { - (*menu)->popup(QCursor::pos()); - } + showMenu(QCursor::pos(), true); }, lifetime()); } +void EditCaptionBox::showMenu(QPoint globalPos, bool forceTopRight) { + _previewMenu = base::make_unique_q( + this, + st::popupMenuWithIcons); + if (forceTopRight) { + _previewMenu->setForcedOrigin(Ui::PanelAnimation::Origin::TopRight); + } + if (_isAllowedEditMedia) { + _previewMenu->addAction(tr::lng_attach_replace(tr::now), [=] { + ChooseReplacement( + _controller, + _albumType, + crl::guard(this, [=](Ui::PreparedList &&list) { + setPreparedList(std::move(list)); + })); + }, &st::menuIconReplace); + } + if (dynamic_cast(_content.get()) + && !_preparedList.files.empty()) { + _previewMenu->addAction(tr::lng_rename_file(tr::now), [=] { + renameCurrentFile(); + }, &st::menuIconEdit); + } + using Type = Ui::PreparedFile::Type; + const auto canDraw = !_preparedList.files.empty() + ? (_preparedList.files.front().type == Type::Photo) + : (_isPhoto && !_asFile); + if (canDraw) { + _previewMenu->addAction(tr::lng_context_draw(tr::now), [=] { + _photoEditorOpens.fire({}); + }, &st::menuIconDraw); + } + if (!_asFile && (_isPhoto || _isVideo)) { + if (hasSendLargePhotosOption()) { + const auto enabled = _sendLargePhotos; + Menu::AddCheckedAction( + _previewMenu.get(), + tr::lng_send_high_quality(tr::now), + [=] { + _sendLargePhotos = !enabled; + rebuildPreview(); + }, + &st::menuIconQualityHigh, + enabled); + } + if (_preparedList.hasSpoilerMenu(!_asFile)) { + const auto spoilered = hasSpoiler(); + Menu::AddCheckedAction( + _previewMenu.get(), + tr::lng_context_spoiler_effect(tr::now), + [=] { + _mediaEditManager.apply({ .type = spoilered + ? SendMenu::ActionType::SpoilerOff + : SendMenu::ActionType::SpoilerOn + }); + rebuildPreview(); + }, + &st::menuIconSpoiler, + spoilered); + } + if (_isVideo && !_preparedList.files.empty()) { + _previewMenu->addAction(tr::lng_context_edit_cover(tr::now), [=] { + setupEditCoverHandler(); + }, &st::menuIconEdit); + if (_preparedList.files.front().videoCover != nullptr) { + _previewMenu->addAction( + tr::lng_context_clear_cover(tr::now), + [=] { setupClearCoverHandler(); }, + &st::menuIconCancel); + } + } + } + if (_previewMenu->empty()) { + _previewMenu = nullptr; + } else { + _previewMenu->popup(globalPos); + } +} + +void EditCaptionBox::renameCurrentFile() { + if (_preparedList.files.empty()) { + return; + } + const auto &file = _preparedList.files.front(); + const auto allowExtensionEdit = file.path.isEmpty(); + _controller->show(Box(RenameFileBox, file.displayName, allowExtensionEdit, [=]( + QString displayName) { + _preparedList.files.front().displayName = displayName; + if (const auto filePreview = dynamic_cast( + _content.get())) { + filePreview->setDisplayName(displayName); + } else { + rebuildPreview(); + } + })); +} + void EditCaptionBox::setupPhotoEditorEventHandler() { const auto openedOnce = lifetime().make_state(false); _photoEditorOpens.events( diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.h b/Telegram/SourceFiles/boxes/edit_caption_box.h index f08621f894..7e11b5b2fe 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.h +++ b/Telegram/SourceFiles/boxes/edit_caption_box.h @@ -32,6 +32,7 @@ namespace Ui { class AbstractSinglePreview; class InputField; class EmojiButton; +class PopupMenu; class VerticalLayout; enum class AlbumType; } // namespace Ui @@ -90,6 +91,8 @@ protected: private: void rebuildPreview(); void setupEditEventHandler(); + void showMenu(QPoint globalPos, bool forceTopRight); + void renameCurrentFile(); void setupPhotoEditorEventHandler(); void setupEditCoverHandler(); void setupClearCoverHandler(); @@ -137,6 +140,7 @@ private: std::unique_ptr _autocomplete; base::unique_qptr _content; + base::unique_qptr _previewMenu; base::unique_qptr _emojiPanel; base::unique_qptr _emojiFilter; diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index 4e4c230d41..78b30651fa 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -113,9 +113,12 @@ void FileDialogCallback( callback(std::move(*list)); } +} // namespace + void RenameFileBox( not_null box, const QString ¤tName, + bool allowExtensionEdit, Fn apply) { box->setTitle(tr::lng_rename_file()); const auto field = box->addRow(object_ptr( @@ -123,19 +126,25 @@ void RenameFileBox( st::settingsDeviceName, rpl::single(QString()), currentName)); - const auto extension = [&] { - if (currentName.isEmpty()) { - return u".png"_q; - } - const auto dot = currentName.lastIndexOf('.'); - return (dot >= 0) ? currentName.mid(dot) : QString(); - }(); - const auto nameWithoutExt = extension.isEmpty() - ? currentName - : currentName.left(currentName.size() - extension.size()); - const auto maxNameLength = kMaxDisplayNameLength - extension.size(); - field->setMaxLength((maxNameLength > 0) ? maxNameLength : 0); - field->setText(nameWithoutExt); + QString extension; + if (allowExtensionEdit) { + field->setMaxLength(kMaxDisplayNameLength); + field->setText(currentName); + } else { + extension = [&] { + if (currentName.isEmpty()) { + return u".png"_q; + } + const auto dot = currentName.lastIndexOf('.'); + return (dot >= 0) ? currentName.mid(dot) : QString(); + }(); + const auto nameWithoutExt = extension.isEmpty() + ? currentName + : currentName.left(currentName.size() - extension.size()); + const auto maxNameLength = kMaxDisplayNameLength - extension.size(); + field->setMaxLength((maxNameLength > 0) ? maxNameLength : 0); + field->setText(nameWithoutExt); + } field->selectAll(); box->setFocusCallback([=] { field->setFocusFast(); @@ -146,12 +155,18 @@ void RenameFileBox( field->showError(); return; } - if ((newName.size() + extension.size()) > kMaxDisplayNameLength) { + if (allowExtensionEdit) { + if (newName.size() > kMaxDisplayNameLength) { + field->showError(); + return; + } + } else if ((newName.size() + extension.size()) + > kMaxDisplayNameLength) { field->showError(); return; } const auto weak = base::make_weak(box); - apply(newName + extension); + apply(allowExtensionEdit ? newName : (newName + extension)); if (const auto strong = weak.get()) { strong->closeBox(); } @@ -165,6 +180,8 @@ void RenameFileBox( }); } +namespace { + void EditFileCaptionBox( not_null box, const style::ComposeControls &st, @@ -416,11 +433,13 @@ SendFilesBox::Block::Block( media->setCanShowHighQualityBadge(first.canUseHighQualityPhoto()); _preview.reset(media); } else { - _preview.reset(Ui::CreateChild( + const auto single = Ui::CreateChild( parent.get(), st, first, - captionContext)); + captionContext); + single->setRenameEnabled(!SkipCaption(first, way)); + _preview.reset(single); } } _preview->show(); @@ -492,6 +511,19 @@ rpl::producer SendFilesBox::Block::itemModifyRequest() const { } } +rpl::producer SendFilesBox::Block::itemRenameRequest() const { + using namespace rpl::mappers; + + const auto preview = _preview.get(); + const auto from = _from; + if (_isAlbum || _isSingleMedia) { + return rpl::never(); + } else { + const auto single = static_cast(preview); + return single->renameRequests() | rpl::map_to(from); + } +} + rpl::producer<> SendFilesBox::Block::orderUpdated() const { if (_isAlbum) { const auto album = static_cast(_preview.get()); @@ -506,6 +538,10 @@ void SendFilesBox::Block::setSendWay(Ui::SendFilesWay way) { const auto media = static_cast( _preview.get()); media->setSendWay(way); + } else { + const auto single = static_cast( + _preview.get()); + single->setRenameEnabled(!SkipCaption((*_items)[_from], way)); } return; } @@ -1467,6 +1503,32 @@ void SendFilesBox::pushBlock(int from, int till) { entry.videoCover = nullptr; }); }; + const auto renameFile = [=](int fileIndex) { + if (fileIndex < 0 || fileIndex >= _list.files.size()) { + return; + } + const auto &file = _list.files[fileIndex]; + const auto canEditFileData = !SkipCaption( + file, + _sendWay.current()); + if (!canEditFileData) { + return; + } + const auto allowExtensionEdit = file.path.isEmpty(); + _show->show(Box( + RenameFileBox, + file.displayName, + allowExtensionEdit, + [=](QString newName) { + const auto displayName = std::move(newName); + _list.files[fileIndex].displayName = displayName; + if (!setDisplayNameInSingleFilePreview( + fileIndex, + displayName)) { + refreshAllAfterChanges(from); + } + })); + }; const auto showContextMenu = [=]( int fileIndex, QPoint globalPosition, @@ -1501,17 +1563,7 @@ void SendFilesBox::pushBlock(int from, int till) { _sendWay.current()); if (canEditFileData) { state->menu->addAction(tr::lng_rename_file(tr::now), [=] { - auto &file = _list.files[fileIndex]; - _show->show(Box(RenameFileBox, file.displayName, [=]( - QString newName) { - const auto displayName = std::move(newName); - _list.files[fileIndex].displayName = displayName; - if (!setDisplayNameInSingleFilePreview( - fileIndex, - displayName)) { - refreshAllAfterChanges(from); - } - })); + renameFile(fileIndex); }, &st::menuIconEdit); state->menu->addAction( tr::lng_context_upload_edit_caption(tr::now), @@ -1626,6 +1678,11 @@ void SendFilesBox::pushBlock(int from, int till) { openInPhotoEditor(index); }, widget->lifetime()); + block.itemRenameRequest( + ) | rpl::on_next([=](int index) { + renameFile(index); + }, widget->lifetime()); + block.orderUpdated() | rpl::on_next([=]{ if (_priceTag) { _priceTagBg = QImage(); diff --git a/Telegram/SourceFiles/boxes/send_files_box.h b/Telegram/SourceFiles/boxes/send_files_box.h index 90f8a143a9..f752bb1c7b 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.h +++ b/Telegram/SourceFiles/boxes/send_files_box.h @@ -93,6 +93,11 @@ using SendFilesCheck = Fn show, not_null peer); +void RenameFileBox( + not_null box, + const QString ¤tName, + bool allowExtensionEdit, + Fn apply); using SendFilesConfirmed = Fn, @@ -178,6 +183,7 @@ private: [[nodiscard]] rpl::producer itemDeleteRequest() const; [[nodiscard]] rpl::producer itemReplaceRequest() const; [[nodiscard]] rpl::producer itemModifyRequest() const; + [[nodiscard]] rpl::producer itemRenameRequest() const; [[nodiscard]] rpl::producer<> orderUpdated() const; void setSendWay(Ui::SendFilesWay way); 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 5cdf08fc80..76ea640353 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 @@ -56,6 +56,7 @@ AbstractSingleFilePreview::AbstractSingleFilePreview( _deleteMedia->hide(); _editMedia->hide(); } + setMouseTracking(true); } AbstractSingleFilePreview::~AbstractSingleFilePreview() = default; @@ -66,6 +67,10 @@ rpl::producer<> AbstractSingleFilePreview::editRequests() const { }) | rpl::flatten_latest(); } +rpl::producer<> AbstractSingleFilePreview::renameRequests() const { + return _renameRequests.events(); +} + rpl::producer<> AbstractSingleFilePreview::deleteRequests() const { return _deleteMedia->clicks() | rpl::to_empty; } @@ -74,6 +79,17 @@ rpl::producer<> AbstractSingleFilePreview::modifyRequests() const { return rpl::never<>(); } +void AbstractSingleFilePreview::setRenameEnabled(bool enabled) { + if (_renameEnabled == enabled) { + return; + } + _renameEnabled = enabled; + if (!_renameEnabled) { + _namePressed = false; + applyCursor(style::cur_default); + } +} + void AbstractSingleFilePreview::setDisplayName(const QString &displayName) { _data.name = displayName; updateTextWidthFor(_data); @@ -279,4 +295,53 @@ void AbstractSingleFilePreview::setData(Data data) { updateDataGeometry(); } +void AbstractSingleFilePreview::mousePressEvent(QMouseEvent *e) { + if (isOverName(e->pos())) { + _namePressed = true; + } +} + +void AbstractSingleFilePreview::mouseMoveEvent(QMouseEvent *e) { + applyCursor(isOverName(e->pos()) + ? style::cur_pointer + : style::cur_default); +} + +void AbstractSingleFilePreview::mouseReleaseEvent(QMouseEvent *e) { + if (base::take(_namePressed) + && (e->button() == Qt::LeftButton) + && isOverName(e->pos())) { + _renameRequests.fire({}); + } +} + +QRect AbstractSingleFilePreview::nameRect() const { + const auto w = width() + - st::boxPhotoPadding.left() + - st::boxPhotoPadding.right(); + const auto &st = !isThumbedLayout(_data) + ? st::attachPreviewLayout + : st::attachPreviewThumbLayout; + const auto nameleft = st.thumbSize + st.thumbSkip; + const auto nametop = st.nameTop; + const auto x = (width() - w) / 2, y = 0; + return style::rtlrect( + x + nameleft, + y + nametop, + _data.nameWidth, + st::semiboldFont->height, + width()); +} + +bool AbstractSingleFilePreview::isOverName(QPoint point) const { + return _renameEnabled && nameRect().contains(point); +} + +void AbstractSingleFilePreview::applyCursor(style::cursor cursor) { + if (_cursor != cursor) { + _cursor = cursor; + setCursor(_cursor); + } +} + } // namespace Ui 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 bfe05fde32..11b7356f0f 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 @@ -30,7 +30,9 @@ public: [[nodiscard]] rpl::producer<> deleteRequests() const override; [[nodiscard]] rpl::producer<> editRequests() const override; + [[nodiscard]] rpl::producer<> renameRequests() const; [[nodiscard]] rpl::producer<> modifyRequests() const override; + void setRenameEnabled(bool enabled); virtual void setDisplayName(const QString &displayName); virtual void setCaption(const TextWithTags &caption); @@ -58,10 +60,16 @@ protected: private: void paintEvent(QPaintEvent *e) override; void resizeEvent(QResizeEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; void updateTextWidthFor(Data &data); void updateDataGeometry(); [[nodiscard]] QRect captionRect() const; + [[nodiscard]] QRect nameRect() const; + [[nodiscard]] bool isOverName(QPoint point) const; + void applyCursor(style::cursor cursor); const style::ComposeControls &_st; const AttachControls::Type _type; @@ -71,6 +79,11 @@ private: object_ptr _editMedia = { nullptr }; object_ptr _deleteMedia = { nullptr }; + rpl::event_stream<> _renameRequests; + + style::cursor _cursor = style::cur_default; + bool _namePressed = false; + bool _renameEnabled = false; };