Rename send as a file with extension.

This commit is contained in:
John Preston
2026-04-28 20:50:59 +07:00
parent 6aa406901c
commit 2cfdf1b32e
6 changed files with 290 additions and 98 deletions
+117 -70
View File
@@ -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<Ui::AbstractSingleFilePreview*>(
_content.get())) {
file->setRenameEnabled(!_preparedList.files.empty());
file->renameRequests(
) | rpl::on_next([=] {
renameCurrentFile();
}, _content->lifetime());
}
base::install_event_filter(_content.get(), [=](not_null<QEvent*> e) {
if (e->type() == QEvent::ContextMenu) {
const auto mouse = static_cast<QContextMenuEvent*>(e.get());
showMenu(mouse->globalPos(), false);
return base::EventFilterResult::Cancel;
}
return base::EventFilterResult::Continue;
}, _content->lifetime());
_scroll->setOwnedWidget(
object_ptr<Ui::RpWidget>::fromRaw(_content.get()));
@@ -728,81 +747,109 @@ void EditCaptionBox::setupControls() {
}
void EditCaptionBox::setupEditEventHandler() {
const auto menu
= lifetime().make_state<base::unique_qptr<Ui::PopupMenu>>();
_editMediaClicks.events(
) | rpl::on_next([=] {
*menu = base::make_unique_q<Ui::PopupMenu>(
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<Ui::PopupMenu>(
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<Ui::AbstractSingleFilePreview*>(_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<Ui::AbstractSingleFilePreview*>(
_content.get())) {
filePreview->setDisplayName(displayName);
} else {
rebuildPreview();
}
}));
}
void EditCaptionBox::setupPhotoEditorEventHandler() {
const auto openedOnce = lifetime().make_state<bool>(false);
_photoEditorOpens.events(
@@ -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<ChatHelpers::FieldAutocomplete> _autocomplete;
base::unique_qptr<Ui::AbstractSinglePreview> _content;
base::unique_qptr<Ui::PopupMenu> _previewMenu;
base::unique_qptr<ChatHelpers::TabbedPanel> _emojiPanel;
base::unique_qptr<QObject> _emojiFilter;
+85 -28
View File
@@ -113,9 +113,12 @@ void FileDialogCallback(
callback(std::move(*list));
}
} // namespace
void RenameFileBox(
not_null<Ui::GenericBox*> box,
const QString &currentName,
bool allowExtensionEdit,
Fn<void(QString)> apply) {
box->setTitle(tr::lng_rename_file());
const auto field = box->addRow(object_ptr<Ui::InputField>(
@@ -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<Ui::GenericBox*> box,
const style::ComposeControls &st,
@@ -416,11 +433,13 @@ SendFilesBox::Block::Block(
media->setCanShowHighQualityBadge(first.canUseHighQualityPhoto());
_preview.reset(media);
} else {
_preview.reset(Ui::CreateChild<Ui::SingleFilePreview>(
const auto single = Ui::CreateChild<Ui::SingleFilePreview>(
parent.get(),
st,
first,
captionContext));
captionContext);
single->setRenameEnabled(!SkipCaption(first, way));
_preview.reset(single);
}
}
_preview->show();
@@ -492,6 +511,19 @@ rpl::producer<int> SendFilesBox::Block::itemModifyRequest() const {
}
}
rpl::producer<int> SendFilesBox::Block::itemRenameRequest() const {
using namespace rpl::mappers;
const auto preview = _preview.get();
const auto from = _from;
if (_isAlbum || _isSingleMedia) {
return rpl::never<int>();
} else {
const auto single = static_cast<Ui::SingleFilePreview*>(preview);
return single->renameRequests() | rpl::map_to(from);
}
}
rpl::producer<> SendFilesBox::Block::orderUpdated() const {
if (_isAlbum) {
const auto album = static_cast<Ui::AlbumPreview*>(_preview.get());
@@ -506,6 +538,10 @@ void SendFilesBox::Block::setSendWay(Ui::SendFilesWay way) {
const auto media = static_cast<Ui::SingleMediaPreview*>(
_preview.get());
media->setSendWay(way);
} else {
const auto single = static_cast<Ui::SingleFilePreview*>(
_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();
@@ -93,6 +93,11 @@ using SendFilesCheck = Fn<bool(
[[nodiscard]] SendFilesCheck DefaultCheckForPeer(
std::shared_ptr<ChatHelpers::Show> show,
not_null<PeerData*> peer);
void RenameFileBox(
not_null<Ui::GenericBox*> box,
const QString &currentName,
bool allowExtensionEdit,
Fn<void(QString)> apply);
using SendFilesConfirmed = Fn<void(
std::shared_ptr<Ui::PreparedBundle>,
@@ -178,6 +183,7 @@ private:
[[nodiscard]] rpl::producer<int> itemDeleteRequest() const;
[[nodiscard]] rpl::producer<int> itemReplaceRequest() const;
[[nodiscard]] rpl::producer<int> itemModifyRequest() const;
[[nodiscard]] rpl::producer<int> itemRenameRequest() const;
[[nodiscard]] rpl::producer<> orderUpdated() const;
void setSendWay(Ui::SendFilesWay way);
@@ -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
@@ -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<IconButton> _editMedia = { nullptr };
object_ptr<IconButton> _deleteMedia = { nullptr };
rpl::event_stream<> _renameRequests;
style::cursor _cursor = style::cur_default;
bool _namePressed = false;
bool _renameEnabled = false;
};