diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.cpp b/Telegram/SourceFiles/boxes/edit_caption_box.cpp index b2e36864a..93ca21033 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.cpp +++ b/Telegram/SourceFiles/boxes/edit_caption_box.cpp @@ -47,8 +47,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/widgets/checkbox.h" #include "ui/text/format_values.h" -#include "ui/special_buttons.h" #include "ui/text/text_options.h" +#include "ui/controls/emoji_button.h" #include "window/window_session_controller.h" #include "confirm_box.h" #include "apiwrap.h" diff --git a/Telegram/SourceFiles/boxes/send_files_box.cpp b/Telegram/SourceFiles/boxes/send_files_box.cpp index fcfc37789..a247b56b0 100644 --- a/Telegram/SourceFiles/boxes/send_files_box.cpp +++ b/Telegram/SourceFiles/boxes/send_files_box.cpp @@ -34,7 +34,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/text/format_values.h" #include "ui/grouped_layout.h" #include "ui/text/text_options.h" -#include "ui/special_buttons.h" +#include "ui/controls/emoji_button.h" #include "lottie/lottie_single_player.h" #include "data/data_document.h" #include "media/clip/media_clip_reader.h" diff --git a/Telegram/SourceFiles/config.h b/Telegram/SourceFiles/config.h index 40d01e9fc..5bc8f9c47 100644 --- a/Telegram/SourceFiles/config.h +++ b/Telegram/SourceFiles/config.h @@ -133,25 +133,3 @@ inline const QRegularExpression &cRussianLetters() { static QRegularExpression regexp(QString::fromUtf8("[а-яА-ЯёЁ]")); return regexp; } - -inline const QStringList &cImgExtensions() { - static QStringList result; - if (result.isEmpty()) { - result.reserve(4); - result.push_back(qsl(".jpg")); - result.push_back(qsl(".jpeg")); - result.push_back(qsl(".png")); - result.push_back(qsl(".gif")); - } - return result; -} - -inline const QStringList &cExtensionsForCompress() { - static QStringList result; - if (result.isEmpty()) { - result.push_back(qsl(".jpg")); - result.push_back(qsl(".jpeg")); - result.push_back(qsl(".png")); - } - return result; -} diff --git a/Telegram/SourceFiles/core/file_utilities.cpp b/Telegram/SourceFiles/core/file_utilities.cpp index edd0681c9..90560e7a6 100644 --- a/Telegram/SourceFiles/core/file_utilities.cpp +++ b/Telegram/SourceFiles/core/file_utilities.cpp @@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "base/unixtime.h" #include "ui/delayed_activation.h" +#include "ui/chat/attach/attach_extensions.h" #include "main/main_session.h" #include "mainwindow.h" @@ -316,8 +317,20 @@ QString AllFilesFilter() { #endif // Q_OS_WIN } +QString ImagesFilter() { + return u"Image files (*"_q + Ui::ImageExtensions().join(u" *"_q) + u")"_q; +} + +QString AllOrImagesFilter() { + return AllFilesFilter() + u";;"_q + ImagesFilter(); +} + +QString ImagesOrAllFilter() { + return ImagesFilter() + u";;"_q + AllFilesFilter(); +} + QString AlbumFilesFilter() { - return qsl("Image and Video Files (*.png *.jpg *.jpeg *.mp4 *.mov)"); + return u"Image and Video Files (*.png *.jpg *.jpeg *.mp4 *.mov)"_q; } namespace internal { diff --git a/Telegram/SourceFiles/core/file_utilities.h b/Telegram/SourceFiles/core/file_utilities.h index ecefe35bb..d44072bb1 100644 --- a/Telegram/SourceFiles/core/file_utilities.h +++ b/Telegram/SourceFiles/core/file_utilities.h @@ -89,8 +89,11 @@ void GetFolder( Fn callback, Fn failed = Fn()); -QString AllFilesFilter(); -QString AlbumFilesFilter(); +[[nodiscard]] QString AllFilesFilter(); +[[nodiscard]] QString ImagesFilter(); +[[nodiscard]] QString AllOrImagesFilter(); +[[nodiscard]] QString ImagesOrAllFilter(); +[[nodiscard]] QString AlbumFilesFilter(); namespace internal { diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 6d48a15f2..751f3023b 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -32,6 +32,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/chat/message_bar.h" #include "ui/image/image.h" #include "ui/special_buttons.h" +#include "ui/controls/emoji_button.h" +#include "ui/controls/send_button.h" #include "inline_bots/inline_bot_result.h" #include "base/event_filter.h" #include "base/unixtime.h" @@ -3276,10 +3278,7 @@ void HistoryWidget::chooseAttach() { return; } - const auto filter = FileDialog::AllFilesFilter() - + qsl(";;Image files (*") - + cImgExtensions().join(qsl(" *")) - + qsl(")"); + const auto filter = FileDialog::AllOrImagesFilter(); FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=]( FileDialog::OpenResult &&result) { diff --git a/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp index 154495ed4..6e3ec83d2 100644 --- a/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp @@ -32,11 +32,12 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "media/audio/media_audio_capture.h" #include "media/audio/media_audio.h" #include "styles/style_chat.h" -#include "ui/special_buttons.h" #include "ui/text/text_options.h" #include "ui/ui_utility.h" #include "ui/widgets/input_fields.h" #include "ui/text/format_values.h" +#include "ui/controls/emoji_button.h" +#include "ui/controls/send_button.h" #include "window/window_session_controller.h" #include "mainwindow.h" diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index d433276e1..41d644c40 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -526,11 +526,7 @@ void RepliesWidget::chooseAttach() { return; } - const auto filter = FileDialog::AllFilesFilter() - + qsl(";;Image files (*") - + cImgExtensions().join(qsl(" *")) - + qsl(")"); - + const auto filter = FileDialog::AllOrImagesFilter(); FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=]( FileDialog::OpenResult &&result) { if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index ce7b5e9f7..dca932b12 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -266,11 +266,7 @@ void ScheduledWidget::chooseAttach() { return; } - const auto filter = FileDialog::AllFilesFilter() - + qsl(";;Image files (*") - + cImgExtensions().join(qsl(" *")) - + qsl(")"); - + const auto filter = FileDialog::AllOrImagesFilter(); FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=]( FileDialog::OpenResult &&result) { if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { diff --git a/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp b/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp index 5148a10f9..6bdad1c90 100644 --- a/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp +++ b/Telegram/SourceFiles/passport/passport_panel_edit_scans.cpp @@ -853,10 +853,7 @@ void EditScans::ChooseScan( Fn errorCallback) { Expects(parent != nullptr); - const auto filter = FileDialog::AllFilesFilter() - + qsl(";;Image files (*") - + cImgExtensions().join(qsl(" *")) - + qsl(")"); + const auto filter = FileDialog::AllOrImagesFilter(); const auto guardedCallback = crl::guard(parent, doneCallback); const auto guardedError = crl::guard(parent, errorCallback); const auto onMainError = [=](ReadScanError error) { diff --git a/Telegram/SourceFiles/settings/settings_chat.cpp b/Telegram/SourceFiles/settings/settings_chat.cpp index 8633a560e..a27a33f60 100644 --- a/Telegram/SourceFiles/settings/settings_chat.cpp +++ b/Telegram/SourceFiles/settings/settings_chat.cpp @@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/checkbox.h" #include "ui/widgets/buttons.h" #include "ui/widgets/labels.h" +#include "ui/chat/attach/attach_extensions.h" #include "ui/layers/generic_box.h" #include "ui/effects/radial_animation.h" #include "ui/toast/toast.h" @@ -600,10 +601,9 @@ void BackgroundRow::updateImage() { void ChooseFromFile( not_null controller, not_null parent) { - const auto &imgExtensions = cImgExtensions(); auto filters = QStringList( qsl("Theme files (*.tdesktop-theme *.tdesktop-palette *") - + imgExtensions.join(qsl(" *")) + + Ui::ImageExtensions().join(qsl(" *")) + qsl(")")); filters.push_back(FileDialog::AllFilesFilter()); const auto callback = crl::guard(controller, [=]( diff --git a/Telegram/SourceFiles/settings/settings_information.cpp b/Telegram/SourceFiles/settings/settings_information.cpp index 476c6ba32..da2fd6461 100644 --- a/Telegram/SourceFiles/settings/settings_information.cpp +++ b/Telegram/SourceFiles/settings/settings_information.cpp @@ -63,11 +63,7 @@ void SetupPhoto( st::settingsInfoPhotoSet); upload->setFullRadius(true); upload->addClickHandler([=] { - const auto imageExtensions = cImgExtensions(); - const auto filter = qsl("Image files (*") - + imageExtensions.join(qsl(" *")) - + qsl(");;") - + FileDialog::AllFilesFilter(); + const auto filter = FileDialog::ImagesOrAllFilter(); const auto callback = [=](const FileDialog::OpenResult &result) { if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { return; diff --git a/Telegram/SourceFiles/storage/storage_media_prepare.cpp b/Telegram/SourceFiles/storage/storage_media_prepare.cpp index c67f36b34..6099d31df 100644 --- a/Telegram/SourceFiles/storage/storage_media_prepare.cpp +++ b/Telegram/SourceFiles/storage/storage_media_prepare.cpp @@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "storage/localimageloader.h" #include "core/mime_type.h" #include "ui/image/image_prepare.h" +#include "ui/chat/attach/attach_extensions.h" #include "app.h" #include @@ -194,7 +195,7 @@ MimeDataState ComputeMimeDataState(const QMimeData *data) { return MimeDataState::None; } - const auto imageExtensions = cImgExtensions(); + const auto imageExtensions = Ui::ImageExtensions(); auto files = QStringList(); auto allAreSmallImages = true; for (const auto &url : urls) { @@ -242,7 +243,7 @@ PreparedList PrepareMediaList(const QList &files, int previewWidth) { PreparedList PrepareMediaList(const QStringList &files, int previewWidth) { auto result = PreparedList(); result.files.reserve(files.size()); - const auto extensionsToCompress = cExtensionsForCompress(); + const auto extensionsToCompress = Ui::ExtensionsForCompression(); for (const auto &file : files) { const auto fileinfo = QFileInfo(file); const auto filesize = fileinfo.size(); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_extensions.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_extensions.cpp new file mode 100644 index 000000000..73fbaf25b --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/attach/attach_extensions.cpp @@ -0,0 +1,33 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "ui/chat/attach/attach_extensions.h" + +namespace Ui { + +const QStringList &ImageExtensions() { + static const auto result = QStringList{ + u".bmp"_q, + u".jpg"_q, + u".jpeg"_q, + u".png"_q, + u".gif"_q, + }; + return result; +} + +const QStringList &ExtensionsForCompression() { + static const auto result = QStringList{ + u".bmp"_q, + u".jpg"_q, + u".jpeg"_q, + u".png"_q, + }; + return result; +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_extensions.h b/Telegram/SourceFiles/ui/chat/attach/attach_extensions.h new file mode 100644 index 000000000..f49f685ae --- /dev/null +++ b/Telegram/SourceFiles/ui/chat/attach/attach_extensions.h @@ -0,0 +1,15 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +namespace Ui { + +[[nodiscard]] const QStringList &ImageExtensions(); +[[nodiscard]] const QStringList &ExtensionsForCompression(); + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/emoji_button.cpp b/Telegram/SourceFiles/ui/controls/emoji_button.cpp new file mode 100644 index 000000000..83b9ba31b --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/emoji_button.cpp @@ -0,0 +1,127 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "ui/controls/emoji_button.h" + +#include "ui/effects/radial_animation.h" +#include "ui/effects/ripple_animation.h" +#include "styles/style_chat.h" + +namespace Ui { +namespace { + +} // namespace + +EmojiButton::EmojiButton(QWidget *parent, const style::IconButton &st) +: RippleButton(parent, st.ripple) +, _st(st) { + resize(_st.width, _st.height); + setCursor(style::cur_pointer); +} + +void EmojiButton::paintEvent(QPaintEvent *e) { + Painter p(this); + + p.fillRect(e->rect(), st::historyComposeAreaBg); + paintRipple(p, _st.rippleAreaPosition.x(), _st.rippleAreaPosition.y(), _rippleOverride ? &(*_rippleOverride)->c : nullptr); + + const auto over = isOver(); + const auto loadingState = _loading + ? _loading->computeState() + : RadialState{ 0., 0, RadialState::kFull }; + if (loadingState.shown < 1.) { + p.setOpacity(1. - loadingState.shown); + + const auto icon = _iconOverride ? _iconOverride : &(over ? _st.iconOver : _st.icon); + auto position = _st.iconPosition; + if (position.x() < 0) { + position.setX((width() - icon->width()) / 2); + } + if (position.y() < 0) { + position.setY((height() - icon->height()) / 2); + } + icon->paint(p, position, width()); + + p.setOpacity(1.); + } + + QRect inner(QPoint((width() - st::historyEmojiCircle.width()) / 2, (height() - st::historyEmojiCircle.height()) / 2), st::historyEmojiCircle); + const auto color = (_colorOverride + ? *_colorOverride + : (over + ? st::historyEmojiCircleFgOver + : st::historyEmojiCircleFg)); + if (anim::Disabled() && _loading && _loading->animating()) { + anim::DrawStaticLoading( + p, + inner, + st::historyEmojiCircleLine, + color); + } else { + auto pen = color->p; + pen.setWidth(st::historyEmojiCircleLine); + pen.setCapStyle(Qt::RoundCap); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + + PainterHighQualityEnabler hq(p); + if (loadingState.arcLength < RadialState::kFull) { + p.drawArc(inner, loadingState.arcFrom, loadingState.arcLength); + } else { + p.drawEllipse(inner); + } + } +} + +void EmojiButton::loadingAnimationCallback() { + if (!anim::Disabled()) { + update(); + } +} + +void EmojiButton::setLoading(bool loading) { + if (loading && !_loading) { + _loading = std::make_unique( + [=] { loadingAnimationCallback(); }, + st::defaultInfiniteRadialAnimation); + } + if (loading) { + _loading->start(); + update(); + } else if (_loading) { + _loading->stop(); + update(); + } +} + +void EmojiButton::setColorOverrides(const style::icon *iconOverride, const style::color *colorOverride, const style::color *rippleOverride) { + _iconOverride = iconOverride; + _colorOverride = colorOverride; + _rippleOverride = rippleOverride; + update(); +} + +void EmojiButton::onStateChanged(State was, StateChangeSource source) { + RippleButton::onStateChanged(was, source); + auto wasOver = static_cast(was & StateFlag::Over); + if (isOver() != wasOver) { + update(); + } +} + +QPoint EmojiButton::prepareRippleStartPosition() const { + if (!_st.rippleAreaSize) { + return DisabledRippleStartPosition(); + } + return mapFromGlobal(QCursor::pos()) - _st.rippleAreaPosition; +} + +QImage EmojiButton::prepareRippleMask() const { + return RippleAnimation::ellipseMask(QSize(_st.rippleAreaSize, _st.rippleAreaSize)); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/emoji_button.h b/Telegram/SourceFiles/ui/controls/emoji_button.h new file mode 100644 index 000000000..a39d40c9c --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/emoji_button.h @@ -0,0 +1,46 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/widgets/buttons.h" + +namespace Ui { + +class InfiniteRadialAnimation; + +class EmojiButton final : public RippleButton { +public: + EmojiButton(QWidget *parent, const style::IconButton &st); + + void setLoading(bool loading); + void setColorOverrides( + const style::icon *iconOverride, + const style::color *colorOverride, + const style::color *rippleOverride); + +protected: + void paintEvent(QPaintEvent *e) override; + void onStateChanged(State was, StateChangeSource source) override; + + QImage prepareRippleMask() const override; + QPoint prepareRippleStartPosition() const override; + +private: + void loadingAnimationCallback(); + + const style::IconButton &_st; + + std::unique_ptr _loading; + + const style::icon *_iconOverride = nullptr; + const style::color *_colorOverride = nullptr; + const style::color *_rippleOverride = nullptr; + +}; + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/send_button.cpp b/Telegram/SourceFiles/ui/controls/send_button.cpp new file mode 100644 index 000000000..3c7df9e84 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/send_button.cpp @@ -0,0 +1,265 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "ui/controls/send_button.h" + +#include "ui/effects/ripple_animation.h" +#include "styles/style_chat.h" + +namespace Ui { +namespace { + +constexpr int kWideScale = 5; + +} // namespace + +SendButton::SendButton(QWidget *parent) +: RippleButton(parent, st::historyReplyCancel.ripple) { + resize(st::historySendSize); +} + +void SendButton::setType(Type type) { + Expects(isSlowmode() || type != Type::Slowmode); + + if (isSlowmode() && type != Type::Slowmode) { + _afterSlowmodeType = type; + return; + } + if (_type != type) { + _contentFrom = grabContent(); + _type = type; + _a_typeChanged.stop(); + _contentTo = grabContent(); + _a_typeChanged.start( + [=] { update(); }, + 0., + 1., + st::historyRecordVoiceDuration); + setPointerCursor(_type != Type::Slowmode); + update(); + } + if (_type != Type::Record) { + _recordActive = false; + _a_recordActive.stop(); + } +} + +void SendButton::setRecordActive(bool recordActive) { + if (_recordActive != recordActive) { + _recordActive = recordActive; + _a_recordActive.start( + [=] { recordAnimationCallback(); }, + _recordActive ? 0. : 1., + _recordActive ? 1. : 0, + st::historyRecordVoiceDuration); + update(); + } +} + +void SendButton::setSlowmodeDelay(int seconds) { + Expects(seconds >= 0 && seconds < kSlowmodeDelayLimit); + + if (_slowmodeDelay == seconds) { + return; + } + _slowmodeDelay = seconds; + _slowmodeDelayText = isSlowmode() + ? u"%1:%2"_q.arg(seconds / 60).arg(seconds % 60, 2, 10, QChar('0')) + : QString(); + setType(isSlowmode() ? Type::Slowmode : _afterSlowmodeType); + update(); +} + +void SendButton::finishAnimating() { + _a_typeChanged.stop(); + _a_recordActive.stop(); + update(); +} + +void SendButton::mouseMoveEvent(QMouseEvent *e) { + AbstractButton::mouseMoveEvent(e); + if (_recording) { + if (_recordUpdateCallback) { + _recordUpdateCallback(e->globalPos()); + } + } +} + +void SendButton::paintEvent(QPaintEvent *e) { + Painter p(this); + + auto over = (isDown() || isOver()); + auto changed = _a_typeChanged.value(1.); + if (changed < 1.) { + PainterHighQualityEnabler hq(p); + p.setOpacity(1. - changed); + auto targetRect = QRect((1 - kWideScale) / 2 * width(), (1 - kWideScale) / 2 * height(), kWideScale * width(), kWideScale * height()); + auto hiddenWidth = anim::interpolate(0, (1 - kWideScale) / 2 * width(), changed); + auto hiddenHeight = anim::interpolate(0, (1 - kWideScale) / 2 * height(), changed); + p.drawPixmap(targetRect.marginsAdded(QMargins(hiddenWidth, hiddenHeight, hiddenWidth, hiddenHeight)), _contentFrom); + p.setOpacity(changed); + auto shownWidth = anim::interpolate((1 - kWideScale) / 2 * width(), 0, changed); + auto shownHeight = anim::interpolate((1 - kWideScale) / 2 * height(), 0, changed); + p.drawPixmap(targetRect.marginsAdded(QMargins(shownWidth, shownHeight, shownWidth, shownHeight)), _contentTo); + return; + } + switch (_type) { + case Type::Record: paintRecord(p, over); break; + case Type::Save: paintSave(p, over); break; + case Type::Cancel: paintCancel(p, over); break; + case Type::Send: paintSend(p, over); break; + case Type::Schedule: paintSchedule(p, over); break; + case Type::Slowmode: paintSlowmode(p); break; + } +} + +void SendButton::paintRecord(Painter &p, bool over) { + auto recordActive = recordActiveRatio(); + if (!isDisabled()) { + auto rippleColor = anim::color(st::historyAttachEmoji.ripple.color, st::historyRecordVoiceRippleBgActive, recordActive); + paintRipple(p, (width() - st::historyAttachEmoji.rippleAreaSize) / 2, st::historyAttachEmoji.rippleAreaPosition.y(), &rippleColor); + } + + auto fastIcon = [&] { + if (isDisabled()) { + return &st::historyRecordVoice; + } else if (recordActive == 1.) { + return &st::historyRecordVoiceActive; + } else if (over) { + return &st::historyRecordVoiceOver; + } + return &st::historyRecordVoice; + }; + fastIcon()->paintInCenter(p, rect()); + if (!isDisabled() && recordActive > 0. && recordActive < 1.) { + p.setOpacity(recordActive); + st::historyRecordVoiceActive.paintInCenter(p, rect()); + p.setOpacity(1.); + } +} + +void SendButton::paintSave(Painter &p, bool over) { + const auto &saveIcon = over + ? st::historyEditSaveIconOver + : st::historyEditSaveIcon; + saveIcon.paint(p, st::historySendIconPosition, width()); +} + +void SendButton::paintCancel(Painter &p, bool over) { + paintRipple(p, (width() - st::historyAttachEmoji.rippleAreaSize) / 2, st::historyAttachEmoji.rippleAreaPosition.y()); + + const auto &cancelIcon = over + ? st::historyReplyCancelIconOver + : st::historyReplyCancelIcon; + cancelIcon.paintInCenter(p, rect()); +} + +void SendButton::paintSend(Painter &p, bool over) { + const auto &sendIcon = over + ? st::historySendIconOver + : st::historySendIcon; + if (isDisabled()) { + const auto color = st::historyRecordVoiceFg->c; + sendIcon.paint(p, st::historySendIconPosition, width(), color); + } else { + sendIcon.paint(p, st::historySendIconPosition, width()); + } +} + +void SendButton::paintSchedule(Painter &p, bool over) { + { + PainterHighQualityEnabler hq(p); + p.setPen(Qt::NoPen); + p.setBrush(over ? st::historySendIconFgOver : st::historySendIconFg); + p.drawEllipse( + st::historyScheduleIconPosition.x(), + st::historyScheduleIconPosition.y(), + st::historyScheduleIcon.width(), + st::historyScheduleIcon.height()); + } + st::historyScheduleIcon.paint( + p, + st::historyScheduleIconPosition, + width()); +} + +void SendButton::paintSlowmode(Painter &p) { + p.setFont(st::normalFont); + p.setPen(st::windowSubTextFg); + p.drawText( + rect().marginsRemoved(st::historySlowmodeCounterMargins), + _slowmodeDelayText, + style::al_center); +} + +void SendButton::onStateChanged(State was, StateChangeSource source) { + RippleButton::onStateChanged(was, source); + + auto down = (state() & StateFlag::Down); + if ((was & StateFlag::Down) != down) { + if (down) { + if (_type == Type::Record) { + _recording = true; + if (_recordStartCallback) { + _recordStartCallback(); + } + } + } else if (_recording) { + _recording = false; + if (_recordStopCallback) { + _recordStopCallback(_recordActive); + } + } + } +} + +bool SendButton::isSlowmode() const { + return (_slowmodeDelay > 0); +} + +QPixmap SendButton::grabContent() { + auto result = QImage( + kWideScale * size() * style::DevicePixelRatio(), + QImage::Format_ARGB32_Premultiplied); + result.setDevicePixelRatio(style::DevicePixelRatio()); + result.fill(Qt::transparent); + { + Painter p(&result); + p.drawPixmap( + (kWideScale - 1) / 2 * width(), + (kWideScale - 1) / 2 * height(), + GrabWidget(this)); + } + return Ui::PixmapFromImage(std::move(result)); +} + +QImage SendButton::prepareRippleMask() const { + auto size = (_type == Type::Record) + ? st::historyAttachEmoji.rippleAreaSize + : st::historyReplyCancel.rippleAreaSize; + return RippleAnimation::ellipseMask(QSize(size, size)); +} + +QPoint SendButton::prepareRippleStartPosition() const { + auto real = mapFromGlobal(QCursor::pos()); + auto size = (_type == Type::Record) + ? st::historyAttachEmoji.rippleAreaSize + : st::historyReplyCancel.rippleAreaSize; + auto y = (_type == Type::Record) + ? st::historyAttachEmoji.rippleAreaPosition.y() + : (height() - st::historyReplyCancel.rippleAreaSize) / 2; + return real - QPoint((width() - size) / 2, y); +} + +void SendButton::recordAnimationCallback() { + update(); + if (_recordAnimationCallback) { + _recordAnimationCallback(); + } +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/controls/send_button.h b/Telegram/SourceFiles/ui/controls/send_button.h new file mode 100644 index 000000000..ad6500699 --- /dev/null +++ b/Telegram/SourceFiles/ui/controls/send_button.h @@ -0,0 +1,92 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "ui/widgets/buttons.h" + +namespace Ui { + +class SendButton final : public RippleButton { +public: + SendButton(QWidget *parent); + + static constexpr auto kSlowmodeDelayLimit = 100 * 60; + + enum class Type { + Send, + Schedule, + Save, + Record, + Cancel, + Slowmode, + }; + [[nodiscard]] Type type() const { + return _type; + } + void setType(Type state); + void setRecordActive(bool recordActive); + void setSlowmodeDelay(int seconds); + void finishAnimating(); + + void setRecordStartCallback(Fn callback) { + _recordStartCallback = std::move(callback); + } + void setRecordUpdateCallback(Fn callback) { + _recordUpdateCallback = std::move(callback); + } + void setRecordStopCallback(Fn callback) { + _recordStopCallback = std::move(callback); + } + void setRecordAnimationCallback(Fn callback) { + _recordAnimationCallback = std::move(callback); + } + + [[nodiscard]] float64 recordActiveRatio() { + return _a_recordActive.value(_recordActive ? 1. : 0.); + } + +protected: + void mouseMoveEvent(QMouseEvent *e) override; + void paintEvent(QPaintEvent *e) override; + void onStateChanged(State was, StateChangeSource source) override; + + QImage prepareRippleMask() const override; + QPoint prepareRippleStartPosition() const override; + +private: + void recordAnimationCallback(); + [[nodiscard]] QPixmap grabContent(); + [[nodiscard]] bool isSlowmode() const; + + void paintRecord(Painter &p, bool over); + void paintSave(Painter &p, bool over); + void paintCancel(Painter &p, bool over); + void paintSend(Painter &p, bool over); + void paintSchedule(Painter &p, bool over); + void paintSlowmode(Painter &p); + + Type _type = Type::Send; + Type _afterSlowmodeType = Type::Send; + bool _recordActive = false; + QPixmap _contentFrom, _contentTo; + + Ui::Animations::Simple _a_typeChanged; + Ui::Animations::Simple _a_recordActive; + + bool _recording = false; + Fn _recordStartCallback; + Fn _recordStopCallback; + Fn _recordUpdateCallback; + Fn _recordAnimationCallback; + + int _slowmodeDelay = 0; + QString _slowmodeDelayText; + +}; + +} // namespace Ui diff --git a/Telegram/SourceFiles/ui/special_buttons.cpp b/Telegram/SourceFiles/ui/special_buttons.cpp index 29fce70ba..a353e5465 100644 --- a/Telegram/SourceFiles/ui/special_buttons.cpp +++ b/Telegram/SourceFiles/ui/special_buttons.cpp @@ -43,8 +43,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Ui { namespace { -constexpr int kWideScale = 5; - QString CropTitle(not_null peer) { if (peer->isChat() || peer->isMegagroup()) { return tr::lng_create_group_crop(tr::now); @@ -122,11 +120,7 @@ void ShowChoosePhotoBox( QPointer parent, const QString &title, Callback &&callback) { - auto imgExtensions = cImgExtensions(); - auto filter = qsl("Image files (*") - + imgExtensions.join(qsl(" *")) - + qsl(");;") - + FileDialog::AllFilesFilter(); + auto filter = FileDialog::ImagesOrAllFilter(); auto handleChosenPhoto = [ title, callback = std::forward(callback) @@ -185,342 +179,6 @@ void HistoryDownButton::setUnreadCount(int unreadCount) { } } -EmojiButton::EmojiButton(QWidget *parent, const style::IconButton &st) -: RippleButton(parent, st.ripple) -, _st(st) { - resize(_st.width, _st.height); - setCursor(style::cur_pointer); -} - -void EmojiButton::paintEvent(QPaintEvent *e) { - Painter p(this); - - p.fillRect(e->rect(), st::historyComposeAreaBg); - paintRipple(p, _st.rippleAreaPosition.x(), _st.rippleAreaPosition.y(), _rippleOverride ? &(*_rippleOverride)->c : nullptr); - - const auto over = isOver(); - const auto loadingState = _loading - ? _loading->computeState() - : Ui::RadialState{ 0., 0, FullArcLength }; - if (loadingState.shown < 1.) { - p.setOpacity(1. - loadingState.shown); - - const auto icon = _iconOverride ? _iconOverride : &(over ? _st.iconOver : _st.icon); - auto position = _st.iconPosition; - if (position.x() < 0) { - position.setX((width() - icon->width()) / 2); - } - if (position.y() < 0) { - position.setY((height() - icon->height()) / 2); - } - icon->paint(p, position, width()); - - p.setOpacity(1.); - } - - QRect inner(QPoint((width() - st::historyEmojiCircle.width()) / 2, (height() - st::historyEmojiCircle.height()) / 2), st::historyEmojiCircle); - const auto color = (_colorOverride - ? *_colorOverride - : (over - ? st::historyEmojiCircleFgOver - : st::historyEmojiCircleFg)); - if (anim::Disabled() && _loading && _loading->animating()) { - anim::DrawStaticLoading( - p, - inner, - st::historyEmojiCircleLine, - color); - } else { - auto pen = color->p; - pen.setWidth(st::historyEmojiCircleLine); - pen.setCapStyle(Qt::RoundCap); - p.setPen(pen); - p.setBrush(Qt::NoBrush); - - PainterHighQualityEnabler hq(p); - if (loadingState.arcLength < FullArcLength) { - p.drawArc(inner, loadingState.arcFrom, loadingState.arcLength); - } else { - p.drawEllipse(inner); - } - } -} - -void EmojiButton::loadingAnimationCallback() { - if (!anim::Disabled()) { - update(); - } -} - -void EmojiButton::setLoading(bool loading) { - if (loading && !_loading) { - _loading = std::make_unique( - [=] { loadingAnimationCallback(); }, - st::defaultInfiniteRadialAnimation); - } - if (loading) { - _loading->start(); - update(); - } else if (_loading) { - _loading->stop(); - update(); - } -} - -void EmojiButton::setColorOverrides(const style::icon *iconOverride, const style::color *colorOverride, const style::color *rippleOverride) { - _iconOverride = iconOverride; - _colorOverride = colorOverride; - _rippleOverride = rippleOverride; - update(); -} - -void EmojiButton::onStateChanged(State was, StateChangeSource source) { - RippleButton::onStateChanged(was, source); - auto wasOver = static_cast(was & StateFlag::Over); - if (isOver() != wasOver) { - update(); - } -} - -QPoint EmojiButton::prepareRippleStartPosition() const { - if (!_st.rippleAreaSize) { - return DisabledRippleStartPosition(); - } - return mapFromGlobal(QCursor::pos()) - _st.rippleAreaPosition; -} - -QImage EmojiButton::prepareRippleMask() const { - return RippleAnimation::ellipseMask(QSize(_st.rippleAreaSize, _st.rippleAreaSize)); -} - -SendButton::SendButton(QWidget *parent) : RippleButton(parent, st::historyReplyCancel.ripple) { - resize(st::historySendSize); -} - -void SendButton::setType(Type type) { - Expects(isSlowmode() || type != Type::Slowmode); - - if (isSlowmode() && type != Type::Slowmode) { - _afterSlowmodeType = type; - return; - } - if (_type != type) { - _contentFrom = grabContent(); - _type = type; - _a_typeChanged.stop(); - _contentTo = grabContent(); - _a_typeChanged.start([=] { update(); }, 0., 1., st::historyRecordVoiceDuration); - setPointerCursor(_type != Type::Slowmode); - update(); - } - if (_type != Type::Record) { - _recordActive = false; - _a_recordActive.stop(); - } -} - -void SendButton::setRecordActive(bool recordActive) { - if (_recordActive != recordActive) { - _recordActive = recordActive; - _a_recordActive.start([this] { recordAnimationCallback(); }, _recordActive ? 0. : 1., _recordActive ? 1. : 0, st::historyRecordVoiceDuration); - update(); - } -} - -void SendButton::setSlowmodeDelay(int seconds) { - Expects(seconds >= 0 && seconds < kSlowmodeDelayLimit); - - if (_slowmodeDelay == seconds) { - return; - } - _slowmodeDelay = seconds; - _slowmodeDelayText = isSlowmode() - ? qsl("%1:%2").arg(seconds / 60).arg(seconds % 60, 2, 10, QChar('0')) - : QString(); - setType(isSlowmode() ? Type::Slowmode : _afterSlowmodeType); - update(); -} - -void SendButton::finishAnimating() { - _a_typeChanged.stop(); - _a_recordActive.stop(); - update(); -} - -void SendButton::mouseMoveEvent(QMouseEvent *e) { - AbstractButton::mouseMoveEvent(e); - if (_recording) { - if (_recordUpdateCallback) { - _recordUpdateCallback(e->globalPos()); - } - } -} - -void SendButton::paintEvent(QPaintEvent *e) { - Painter p(this); - - auto over = (isDown() || isOver()); - auto changed = _a_typeChanged.value(1.); - if (changed < 1.) { - PainterHighQualityEnabler hq(p); - p.setOpacity(1. - changed); - auto targetRect = QRect((1 - kWideScale) / 2 * width(), (1 - kWideScale) / 2 * height(), kWideScale * width(), kWideScale * height()); - auto hiddenWidth = anim::interpolate(0, (1 - kWideScale) / 2 * width(), changed); - auto hiddenHeight = anim::interpolate(0, (1 - kWideScale) / 2 * height(), changed); - p.drawPixmap(targetRect.marginsAdded(QMargins(hiddenWidth, hiddenHeight, hiddenWidth, hiddenHeight)), _contentFrom); - p.setOpacity(changed); - auto shownWidth = anim::interpolate((1 - kWideScale) / 2 * width(), 0, changed); - auto shownHeight = anim::interpolate((1 - kWideScale) / 2 * height(), 0, changed); - p.drawPixmap(targetRect.marginsAdded(QMargins(shownWidth, shownHeight, shownWidth, shownHeight)), _contentTo); - return; - } - switch (_type) { - case Type::Record: paintRecord(p, over); break; - case Type::Save: paintSave(p, over); break; - case Type::Cancel: paintCancel(p, over); break; - case Type::Send: paintSend(p, over); break; - case Type::Schedule: paintSchedule(p, over); break; - case Type::Slowmode: paintSlowmode(p); break; - } -} - -void SendButton::paintRecord(Painter &p, bool over) { - auto recordActive = recordActiveRatio(); - if (!isDisabled()) { - auto rippleColor = anim::color(st::historyAttachEmoji.ripple.color, st::historyRecordVoiceRippleBgActive, recordActive); - paintRipple(p, (width() - st::historyAttachEmoji.rippleAreaSize) / 2, st::historyAttachEmoji.rippleAreaPosition.y(), &rippleColor); - } - - auto fastIcon = [&] { - if (isDisabled()) { - return &st::historyRecordVoice; - } else if (recordActive == 1.) { - return &st::historyRecordVoiceActive; - } else if (over) { - return &st::historyRecordVoiceOver; - } - return &st::historyRecordVoice; - }; - fastIcon()->paintInCenter(p, rect()); - if (!isDisabled() && recordActive > 0. && recordActive < 1.) { - p.setOpacity(recordActive); - st::historyRecordVoiceActive.paintInCenter(p, rect()); - p.setOpacity(1.); - } -} - -void SendButton::paintSave(Painter &p, bool over) { - const auto &saveIcon = over - ? st::historyEditSaveIconOver - : st::historyEditSaveIcon; - saveIcon.paint(p, st::historySendIconPosition, width()); -} - -void SendButton::paintCancel(Painter &p, bool over) { - paintRipple(p, (width() - st::historyAttachEmoji.rippleAreaSize) / 2, st::historyAttachEmoji.rippleAreaPosition.y()); - - const auto &cancelIcon = over - ? st::historyReplyCancelIconOver - : st::historyReplyCancelIcon; - cancelIcon.paintInCenter(p, rect()); -} - -void SendButton::paintSend(Painter &p, bool over) { - const auto &sendIcon = over - ? st::historySendIconOver - : st::historySendIcon; - if (isDisabled()) { - const auto color = st::historyRecordVoiceFg->c; - sendIcon.paint(p, st::historySendIconPosition, width(), color); - } else { - sendIcon.paint(p, st::historySendIconPosition, width()); - } -} - -void SendButton::paintSchedule(Painter &p, bool over) { - { - PainterHighQualityEnabler hq(p); - p.setPen(Qt::NoPen); - p.setBrush(over ? st::historySendIconFgOver : st::historySendIconFg); - p.drawEllipse( - st::historyScheduleIconPosition.x(), - st::historyScheduleIconPosition.y(), - st::historyScheduleIcon.width(), - st::historyScheduleIcon.height()); - } - st::historyScheduleIcon.paint( - p, - st::historyScheduleIconPosition, - width()); -} - -void SendButton::paintSlowmode(Painter &p) { - p.setFont(st::normalFont); - p.setPen(st::windowSubTextFg); - p.drawText( - rect().marginsRemoved(st::historySlowmodeCounterMargins), - _slowmodeDelayText, - style::al_center); -} - -void SendButton::onStateChanged(State was, StateChangeSource source) { - RippleButton::onStateChanged(was, source); - - auto down = (state() & StateFlag::Down); - if ((was & StateFlag::Down) != down) { - if (down) { - if (_type == Type::Record) { - _recording = true; - if (_recordStartCallback) { - _recordStartCallback(); - } - } - } else if (_recording) { - _recording = false; - if (_recordStopCallback) { - _recordStopCallback(_recordActive); - } - } - } -} - -bool SendButton::isSlowmode() const { - return (_slowmodeDelay > 0); -} - -QPixmap SendButton::grabContent() { - auto result = QImage(kWideScale * size() * cIntRetinaFactor(), QImage::Format_ARGB32_Premultiplied); - result.setDevicePixelRatio(cRetinaFactor()); - result.fill(Qt::transparent); - { - Painter p(&result); - p.drawPixmap( - (kWideScale - 1) / 2 * width(), - (kWideScale - 1) / 2 * height(), - GrabWidget(this)); - } - return App::pixmapFromImageInPlace(std::move(result)); -} - -QImage SendButton::prepareRippleMask() const { - auto size = (_type == Type::Record) ? st::historyAttachEmoji.rippleAreaSize : st::historyReplyCancel.rippleAreaSize; - return Ui::RippleAnimation::ellipseMask(QSize(size, size)); -} - -QPoint SendButton::prepareRippleStartPosition() const { - auto real = mapFromGlobal(QCursor::pos()); - auto size = (_type == Type::Record) ? st::historyAttachEmoji.rippleAreaSize : st::historyReplyCancel.rippleAreaSize; - auto y = (_type == Type::Record) ? st::historyAttachEmoji.rippleAreaPosition.y() : (height() - st::historyReplyCancel.rippleAreaSize) / 2; - return real - QPoint((width() - size) / 2, y); -} - -void SendButton::recordAnimationCallback() { - update(); - if (_recordAnimationCallback) { - _recordAnimationCallback(); - } -} - UserpicButton::UserpicButton( QWidget *parent, const QString &cropTitle, diff --git a/Telegram/SourceFiles/ui/special_buttons.h b/Telegram/SourceFiles/ui/special_buttons.h index 86d908e01..4cc13286f 100644 --- a/Telegram/SourceFiles/ui/special_buttons.h +++ b/Telegram/SourceFiles/ui/special_buttons.h @@ -34,8 +34,6 @@ struct Information; namespace Ui { -class InfiniteRadialAnimation; - class HistoryDownButton : public RippleButton { public: HistoryDownButton(QWidget *parent, const style::TwoIconButton &st); @@ -58,111 +56,6 @@ private: }; -class EmojiButton : public RippleButton { -public: - EmojiButton(QWidget *parent, const style::IconButton &st); - - void setLoading(bool loading); - void setColorOverrides(const style::icon *iconOverride, const style::color *colorOverride, const style::color *rippleOverride); - -protected: - void paintEvent(QPaintEvent *e) override; - void onStateChanged(State was, StateChangeSource source) override; - - QImage prepareRippleMask() const override; - QPoint prepareRippleStartPosition() const override; - -private: - void loadingAnimationCallback(); - - const style::IconButton &_st; - - std::unique_ptr _loading; - - const style::icon *_iconOverride = nullptr; - const style::color *_colorOverride = nullptr; - const style::color *_rippleOverride = nullptr; - -}; - -class SendButton : public RippleButton { -public: - SendButton(QWidget *parent); - - static constexpr auto kSlowmodeDelayLimit = 100 * 60; - - enum class Type { - Send, - Schedule, - Save, - Record, - Cancel, - Slowmode, - }; - Type type() const { - return _type; - } - void setType(Type state); - void setRecordActive(bool recordActive); - void setSlowmodeDelay(int seconds); - void finishAnimating(); - - void setRecordStartCallback(Fn callback) { - _recordStartCallback = std::move(callback); - } - void setRecordUpdateCallback(Fn callback) { - _recordUpdateCallback = std::move(callback); - } - void setRecordStopCallback(Fn callback) { - _recordStopCallback = std::move(callback); - } - void setRecordAnimationCallback(Fn callback) { - _recordAnimationCallback = std::move(callback); - } - - float64 recordActiveRatio() { - return _a_recordActive.value(_recordActive ? 1. : 0.); - } - -protected: - void mouseMoveEvent(QMouseEvent *e) override; - void paintEvent(QPaintEvent *e) override; - void onStateChanged(State was, StateChangeSource source) override; - - QImage prepareRippleMask() const override; - QPoint prepareRippleStartPosition() const override; - -private: - void recordAnimationCallback(); - QPixmap grabContent(); - bool isSlowmode() const; - - void paintRecord(Painter &p, bool over); - void paintSave(Painter &p, bool over); - void paintCancel(Painter &p, bool over); - void paintSend(Painter &p, bool over); - void paintSchedule(Painter &p, bool over); - void paintSlowmode(Painter &p); - - Type _type = Type::Send; - Type _afterSlowmodeType = Type::Send; - bool _recordActive = false; - QPixmap _contentFrom, _contentTo; - - Ui::Animations::Simple _a_typeChanged; - Ui::Animations::Simple _a_recordActive; - - bool _recording = false; - Fn _recordStartCallback; - Fn _recordStopCallback; - Fn _recordUpdateCallback; - Fn _recordAnimationCallback; - - int _slowmodeDelay = 0; - QString _slowmodeDelayText; - -}; - class UserpicButton : public RippleButton { public: enum class Role { diff --git a/Telegram/SourceFiles/ui/ui_pch.h b/Telegram/SourceFiles/ui/ui_pch.h index 07af4a4fb..52bc1e52a 100644 --- a/Telegram/SourceFiles/ui/ui_pch.h +++ b/Telegram/SourceFiles/ui/ui_pch.h @@ -19,6 +19,7 @@ #include #include #include +#include #include @@ -34,3 +35,4 @@ #include "ui/text/text.h" #include "ui/effects/animations.h" +#include "styles/palette.h" diff --git a/Telegram/cmake/td_ui.cmake b/Telegram/cmake/td_ui.cmake index 9c41becf5..b1feecb12 100644 --- a/Telegram/cmake/td_ui.cmake +++ b/Telegram/cmake/td_ui.cmake @@ -31,17 +31,24 @@ nice_target_sources(td_ui ${src_loc} PRIVATE ${style_files} - ui/ui_pch.h + ui/chat/attach/attach_extensions.cpp + ui/chat/attach/attach_extensions.h ui/chat/message_bar.cpp ui/chat/message_bar.h ui/chat/pinned_bar.cpp ui/chat/pinned_bar.h + ui/controls/emoji_button.cpp + ui/controls/emoji_button.h + ui/controls/send_button.cpp + ui/controls/send_button.h ui/text/format_values.cpp ui/text/format_values.h ui/text/text_options.cpp ui/text/text_options.h ui/toasts/common_toasts.cpp ui/toasts/common_toasts.h + + ui/ui_pch.h ) target_include_directories(td_ui