Updated to 6.7.7.
Esse commit está contido em:
@@ -127,7 +127,7 @@ Locate the render tool (`codegen_style` with `--render-svg` mode):
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
if [[ "$OSTYPE" == darwin* ]]; then
|
if [[ "$OSTYPE" == darwin* ]]; then
|
||||||
ls out/Debug/codegen_style
|
ls out/Telegram/codegen/codegen/style/Debug/codegen_style
|
||||||
else
|
else
|
||||||
ls out/Telegram/codegen/codegen/style/Debug/codegen_style.exe
|
ls out/Telegram/codegen/codegen/style/Debug/codegen_style.exe
|
||||||
fi
|
fi
|
||||||
@@ -137,7 +137,7 @@ If missing, build it: `cmake --build out --config Debug --target codegen_style`
|
|||||||
|
|
||||||
Test on a known good SVG (use the appropriate binary path for the OS):
|
Test on a known good SVG (use the appropriate binary path for the OS):
|
||||||
```bash
|
```bash
|
||||||
CODEGEN=$(if [[ "$OSTYPE" == darwin* ]]; then echo out/Debug/codegen_style; else echo out/Telegram/codegen/codegen/style/Debug/codegen_style.exe; fi)
|
CODEGEN=$(if [[ "$OSTYPE" == darwin* ]]; then echo out/Telegram/codegen/codegen/style/Debug/codegen_style; else echo out/Telegram/codegen/codegen/style/Debug/codegen_style.exe; fi)
|
||||||
$CODEGEN --render-svg Telegram/Resources/icons/menu/tag_add.svg .ai/icon_{name}/test_render.png 512
|
$CODEGEN --render-svg Telegram/Resources/icons/menu/tag_add.svg .ai/icon_{name}/test_render.png 512
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -44,4 +44,4 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deploy
|
id: deploy
|
||||||
uses: actions/deploy-pages@v4
|
uses: actions/deploy-pages@v5
|
||||||
|
|||||||
@@ -196,6 +196,8 @@ PRIVATE
|
|||||||
api/api_statistics_data_deserialize.h
|
api/api_statistics_data_deserialize.h
|
||||||
api/api_statistics_sender.cpp
|
api/api_statistics_sender.cpp
|
||||||
api/api_statistics_sender.h
|
api/api_statistics_sender.h
|
||||||
|
api/api_stickers_creator.cpp
|
||||||
|
api/api_stickers_creator.h
|
||||||
api/api_suggest_post.cpp
|
api/api_suggest_post.cpp
|
||||||
api/api_suggest_post.h
|
api/api_suggest_post.h
|
||||||
api/api_text_entities.cpp
|
api/api_text_entities.cpp
|
||||||
@@ -361,6 +363,8 @@ PRIVATE
|
|||||||
boxes/send_gif_with_caption_box.h
|
boxes/send_gif_with_caption_box.h
|
||||||
boxes/send_files_box.cpp
|
boxes/send_files_box.cpp
|
||||||
boxes/send_files_box.h
|
boxes/send_files_box.h
|
||||||
|
boxes/send_files_box_reply_header.cpp
|
||||||
|
boxes/send_files_box_reply_header.h
|
||||||
boxes/share_box.cpp
|
boxes/share_box.cpp
|
||||||
boxes/share_box.h
|
boxes/share_box.h
|
||||||
boxes/star_gift_auction_box.cpp
|
boxes/star_gift_auction_box.cpp
|
||||||
@@ -377,6 +381,8 @@ PRIVATE
|
|||||||
boxes/star_gift_preview_box.h
|
boxes/star_gift_preview_box.h
|
||||||
boxes/star_gift_resale_box.cpp
|
boxes/star_gift_resale_box.cpp
|
||||||
boxes/star_gift_resale_box.h
|
boxes/star_gift_resale_box.h
|
||||||
|
boxes/sticker_creator_box.cpp
|
||||||
|
boxes/sticker_creator_box.h
|
||||||
boxes/sticker_set_box.cpp
|
boxes/sticker_set_box.cpp
|
||||||
boxes/sticker_set_box.h
|
boxes/sticker_set_box.h
|
||||||
boxes/stickers_box.cpp
|
boxes/stickers_box.cpp
|
||||||
@@ -474,6 +480,8 @@ PRIVATE
|
|||||||
chat_helpers/emoji_keywords.h
|
chat_helpers/emoji_keywords.h
|
||||||
chat_helpers/emoji_list_widget.cpp
|
chat_helpers/emoji_list_widget.cpp
|
||||||
chat_helpers/emoji_list_widget.h
|
chat_helpers/emoji_list_widget.h
|
||||||
|
chat_helpers/emoji_picker_overlay.cpp
|
||||||
|
chat_helpers/emoji_picker_overlay.h
|
||||||
chat_helpers/emoji_sets_manager.cpp
|
chat_helpers/emoji_sets_manager.cpp
|
||||||
chat_helpers/emoji_sets_manager.h
|
chat_helpers/emoji_sets_manager.h
|
||||||
chat_helpers/emoji_suggestions_widget.cpp
|
chat_helpers/emoji_suggestions_widget.cpp
|
||||||
@@ -510,6 +518,8 @@ PRIVATE
|
|||||||
chat_helpers/ttl_media_layer_widget.h
|
chat_helpers/ttl_media_layer_widget.h
|
||||||
core/application.cpp
|
core/application.cpp
|
||||||
core/application.h
|
core/application.h
|
||||||
|
core/proxy_rotation_manager.cpp
|
||||||
|
core/proxy_rotation_manager.h
|
||||||
core/cached_webview_availability.h
|
core/cached_webview_availability.h
|
||||||
core/bank_card_click_handler.cpp
|
core/bank_card_click_handler.cpp
|
||||||
core/bank_card_click_handler.h
|
core/bank_card_click_handler.h
|
||||||
@@ -1457,6 +1467,8 @@ PRIVATE
|
|||||||
mtproto/facade.h
|
mtproto/facade.h
|
||||||
mtproto/mtp_instance.cpp
|
mtproto/mtp_instance.cpp
|
||||||
mtproto/mtp_instance.h
|
mtproto/mtp_instance.h
|
||||||
|
mtproto/proxy_check.cpp
|
||||||
|
mtproto/proxy_check.h
|
||||||
mtproto/sender.h
|
mtproto/sender.h
|
||||||
mtproto/session.cpp
|
mtproto/session.cpp
|
||||||
mtproto/session.h
|
mtproto/session.h
|
||||||
|
|||||||
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 685 B |
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 1.1 KiB |
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 1.6 KiB |
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 769 B |
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 1.3 KiB |
Arquivo binário não exibido.
|
Depois Largura: | Altura: | Tamanho: 1.9 KiB |
@@ -1285,6 +1285,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_proxy_use_system_settings" = "Use system proxy settings";
|
"lng_proxy_use_system_settings" = "Use system proxy settings";
|
||||||
"lng_proxy_use_custom" = "Use custom proxy";
|
"lng_proxy_use_custom" = "Use custom proxy";
|
||||||
"lng_proxy_use_for_calls" = "Use proxy for calls";
|
"lng_proxy_use_for_calls" = "Use proxy for calls";
|
||||||
|
"lng_proxy_auto_switch" = "Auto-switch proxies";
|
||||||
|
"lng_proxy_auto_switch_about" = "You can choose how quickly the app should auto-connect to the nearest active proxy if the current one stops working.";
|
||||||
|
"lng_proxy_auto_switch_timeout#one" = "{count} s";
|
||||||
|
"lng_proxy_auto_switch_timeout#other" = "{count} s";
|
||||||
"lng_proxy_about" = "Proxy servers may be helpful in accessing Telegram if there is no connection in a specific region.";
|
"lng_proxy_about" = "Proxy servers may be helpful in accessing Telegram if there is no connection in a specific region.";
|
||||||
"lng_proxy_add" = "Add proxy";
|
"lng_proxy_add" = "Add proxy";
|
||||||
"lng_proxy_share" = "Share";
|
"lng_proxy_share" = "Share";
|
||||||
@@ -4545,10 +4549,42 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_stickers_context_edit_name" = "Edit name";
|
"lng_stickers_context_edit_name" = "Edit name";
|
||||||
"lng_stickers_context_delete" = "Delete sticker";
|
"lng_stickers_context_delete" = "Delete sticker";
|
||||||
"lng_stickers_context_delete_sure" = "Are you sure you want to delete the sticker from your sticker set?";
|
"lng_stickers_context_delete_sure" = "Are you sure you want to delete the sticker from your sticker set?";
|
||||||
|
"lng_stickers_context_delete_pack" = "Delete Pack";
|
||||||
|
"lng_stickers_context_delete_pack_everyone" = "Delete for Everyone";
|
||||||
|
"lng_stickers_context_delete_pack_self" = "Delete for Myself";
|
||||||
|
"lng_stickers_delete_pack_sure" = "Are you sure you want to delete this sticker set for everyone? This cannot be undone.";
|
||||||
|
"lng_stickers_bot_more_options" = "Check the {bot} bot for more options";
|
||||||
"lng_stickers_box_edit_name_title" = "Edit Sticker Set Name";
|
"lng_stickers_box_edit_name_title" = "Edit Sticker Set Name";
|
||||||
"lng_stickers_box_edit_name_about" = "Choose a name for your set.";
|
"lng_stickers_box_edit_name_about" = "Choose a name for your set.";
|
||||||
"lng_stickers_creator_badge" = "edit";
|
"lng_stickers_creator_badge" = "edit";
|
||||||
|
|
||||||
|
"lng_stickers_create_new" = "Create a New Sticker";
|
||||||
|
"lng_stickers_add_existing" = "Add an Existing Sticker";
|
||||||
|
"lng_stickers_add_to_set" = "Add to Sticker Set";
|
||||||
|
"lng_stickers_already_in_set" = "This Sticker is already in the Set.";
|
||||||
|
"lng_stickers_set_is_full" = "This Sticker Set is full.";
|
||||||
|
"lng_emoji_add_to_set" = "Add to Emoji Set";
|
||||||
|
"lng_emoji_already_in_set" = "This Emoji is already in the Set.";
|
||||||
|
"lng_emoji_set_is_full" = "This Emoji Set is full.";
|
||||||
|
"lng_emoji_added" = "Emoji added.";
|
||||||
|
"lng_stickers_pack_choose_emoji_title" = "Choose Emoji";
|
||||||
|
"lng_stickers_pack_choose_emoji_about" = "Pick an emoji that corresponds to this sticker.";
|
||||||
|
"lng_stickers_pick_existing_title" = "Choose Sticker";
|
||||||
|
"lng_stickers_pick_existing_about" = "Pick a sticker from your library to add it to this set.";
|
||||||
|
"lng_stickers_pick_existing_empty" = "You don't have any stickers yet.";
|
||||||
|
"lng_stickers_create_image_title" = "New Sticker";
|
||||||
|
"lng_stickers_create_image_about" = "Choose an image to add as a sticker.";
|
||||||
|
"lng_stickers_create_choose_image" = "Choose Image";
|
||||||
|
"lng_stickers_create_image_filter" = "Images";
|
||||||
|
"lng_stickers_create_open_failed" = "Could not load image.";
|
||||||
|
"lng_stickers_create_too_small" = "The image must be at least {size} pixels on each side.";
|
||||||
|
"lng_stickers_create_choose_emoji" = "Choose an emoji that corresponds to your sticker:";
|
||||||
|
"lng_stickers_create_emoji_about" = "Choose emojis that match your sticker";
|
||||||
|
"lng_stickers_create_emoji_required" = "Please choose an emoji.";
|
||||||
|
"lng_stickers_create_uploading" = "Uploading sticker…";
|
||||||
|
"lng_stickers_create_upload_failed" = "Sticker upload failed. Please try again.";
|
||||||
|
"lng_stickers_create_added" = "Sticker added.";
|
||||||
|
|
||||||
"lng_in_dlg_photo" = "Photo";
|
"lng_in_dlg_photo" = "Photo";
|
||||||
"lng_in_dlg_album" = "Album";
|
"lng_in_dlg_album" = "Album";
|
||||||
"lng_in_dlg_video" = "Video";
|
"lng_in_dlg_video" = "Video";
|
||||||
@@ -5655,6 +5691,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_mediaview_playback_speed" = "Playback speed: {speed}";
|
"lng_mediaview_playback_speed" = "Playback speed: {speed}";
|
||||||
"lng_mediaview_rotate_video" = "Rotate video";
|
"lng_mediaview_rotate_video" = "Rotate video";
|
||||||
"lng_mediaview_quality_auto" = "Auto";
|
"lng_mediaview_quality_auto" = "Auto";
|
||||||
|
"lng_mediaview_quality_original" = "Original ({quality}p)";
|
||||||
|
|
||||||
"lng_theme_preview_title" = "Theme Preview";
|
"lng_theme_preview_title" = "Theme Preview";
|
||||||
"lng_theme_preview_generating" = "Generating color theme preview...";
|
"lng_theme_preview_generating" = "Generating color theme preview...";
|
||||||
@@ -7228,6 +7265,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
"lng_photo_editor_menu_flip" = "Flip";
|
"lng_photo_editor_menu_flip" = "Flip";
|
||||||
"lng_photo_editor_menu_duplicate" = "Duplicate";
|
"lng_photo_editor_menu_duplicate" = "Duplicate";
|
||||||
|
|
||||||
|
"lng_photo_editor_text_style_plain" = "Plain";
|
||||||
|
"lng_photo_editor_text_style_framed" = "Framed";
|
||||||
|
"lng_photo_editor_text_style_semi_transparent" = "Semi-Transparent";
|
||||||
|
|
||||||
"lng_photo_editor_crop_original" = "Original";
|
"lng_photo_editor_crop_original" = "Original";
|
||||||
"lng_photo_editor_crop_square" = "Square";
|
"lng_photo_editor_crop_square" = "Square";
|
||||||
"lng_photo_editor_crop_free" = "Free";
|
"lng_photo_editor_crop_free" = "Free";
|
||||||
@@ -7964,4 +8005,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
|
|
||||||
"lng_mac_hold_to_quit" = "Hold {text} to Quit";
|
"lng_mac_hold_to_quit" = "Hold {text} to Quit";
|
||||||
|
|
||||||
|
"lng_sr_country_column_name" = "Country name";
|
||||||
|
"lng_sr_languages_column_native" = "Native name";
|
||||||
|
"lng_sr_languages_column_name" = "Language name";
|
||||||
|
|
||||||
// Keys finished
|
// Keys finished
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
|
||||||
ProcessorArchitecture="ARCHITECTURE"
|
ProcessorArchitecture="ARCHITECTURE"
|
||||||
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
|
||||||
Version="6.7.6.0" />
|
Version="6.7.7.0" />
|
||||||
<Properties>
|
<Properties>
|
||||||
<DisplayName>Telegram Desktop</DisplayName>
|
<DisplayName>Telegram Desktop</DisplayName>
|
||||||
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
<PublisherDisplayName>Telegram Messenger LLP</PublisherDisplayName>
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
|||||||
//
|
//
|
||||||
|
|
||||||
VS_VERSION_INFO VERSIONINFO
|
VS_VERSION_INFO VERSIONINFO
|
||||||
FILEVERSION 6,7,6,0
|
FILEVERSION 6,7,7,0
|
||||||
PRODUCTVERSION 6,7,6,0
|
PRODUCTVERSION 6,7,7,0
|
||||||
FILEFLAGSMASK 0x3fL
|
FILEFLAGSMASK 0x3fL
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
FILEFLAGS 0x1L
|
FILEFLAGS 0x1L
|
||||||
@@ -62,10 +62,10 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", ""
|
VALUE "CompanyName", ""
|
||||||
VALUE "FileDescription", "Telegram Desktop"
|
VALUE "FileDescription", "Telegram Desktop"
|
||||||
VALUE "FileVersion", "6.7.6.0"
|
VALUE "FileVersion", "6.7.7.0"
|
||||||
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
|
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
|
||||||
VALUE "ProductName", "Telegram Desktop"
|
VALUE "ProductName", "Telegram Desktop"
|
||||||
VALUE "ProductVersion", "6.7.6.0"
|
VALUE "ProductVersion", "6.7.7.0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
|||||||
//
|
//
|
||||||
|
|
||||||
VS_VERSION_INFO VERSIONINFO
|
VS_VERSION_INFO VERSIONINFO
|
||||||
FILEVERSION 6,7,6,0
|
FILEVERSION 6,7,7,0
|
||||||
PRODUCTVERSION 6,7,6,0
|
PRODUCTVERSION 6,7,7,0
|
||||||
FILEFLAGSMASK 0x3fL
|
FILEFLAGSMASK 0x3fL
|
||||||
#ifdef _DEBUG
|
#ifdef _DEBUG
|
||||||
FILEFLAGS 0x1L
|
FILEFLAGS 0x1L
|
||||||
@@ -53,10 +53,10 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "CompanyName", ""
|
VALUE "CompanyName", ""
|
||||||
VALUE "FileDescription", "Telegram Desktop Updater"
|
VALUE "FileDescription", "Telegram Desktop Updater"
|
||||||
VALUE "FileVersion", "6.7.6.0"
|
VALUE "FileVersion", "6.7.7.0"
|
||||||
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
|
VALUE "LegalCopyright", "Copyright (C) 2014-2026"
|
||||||
VALUE "ProductName", "Telegram Desktop"
|
VALUE "ProductName", "Telegram Desktop"
|
||||||
VALUE "ProductVersion", "6.7.6.0"
|
VALUE "ProductVersion", "6.7.7.0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
@@ -0,0 +1,525 @@
|
|||||||
|
/*
|
||||||
|
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 "api/api_stickers_creator.h"
|
||||||
|
|
||||||
|
#include "apiwrap.h"
|
||||||
|
#include "base/random.h"
|
||||||
|
#include "base/unixtime.h"
|
||||||
|
#include "chat_helpers/compose/compose_show.h"
|
||||||
|
#include "data/data_document.h"
|
||||||
|
#include "data/data_file_origin.h"
|
||||||
|
#include "data/data_session.h"
|
||||||
|
#include "data/stickers/data_stickers.h"
|
||||||
|
#include "data/stickers/data_stickers_set.h"
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
|
#include "main/main_session.h"
|
||||||
|
#include "menu/menu_action_with_thumbnail.h"
|
||||||
|
#include "storage/file_upload.h"
|
||||||
|
#include "storage/localimageloader.h"
|
||||||
|
#include "styles/style_menu_icons.h"
|
||||||
|
#include "styles/style_widgets.h"
|
||||||
|
#include "ui/dynamic_thumbnails.h"
|
||||||
|
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||||
|
#include "ui/widgets/menu/menu_common.h"
|
||||||
|
#include "ui/widgets/popup_menu.h"
|
||||||
|
|
||||||
|
namespace Api {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kStickerSide = 512;
|
||||||
|
|
||||||
|
[[nodiscard]] MTPInputStickerSetItem InputItem(
|
||||||
|
const MTPInputDocument &document,
|
||||||
|
const QString &emoji) {
|
||||||
|
return MTP_inputStickerSetItem(
|
||||||
|
MTP_flags(0),
|
||||||
|
document,
|
||||||
|
MTP_string(emoji),
|
||||||
|
MTPMaskCoords(),
|
||||||
|
MTPstring());
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::shared_ptr<FilePrepareResult> PrepareStickerWebp(
|
||||||
|
MTP::DcId dcId,
|
||||||
|
DocumentId id,
|
||||||
|
const QByteArray &bytes) {
|
||||||
|
const auto filename = u"sticker.webp"_q;
|
||||||
|
auto attributes = QVector<MTPDocumentAttribute>(
|
||||||
|
1,
|
||||||
|
MTP_documentAttributeFilename(MTP_string(filename)));
|
||||||
|
attributes.push_back(MTP_documentAttributeImageSize(
|
||||||
|
MTP_int(kStickerSide),
|
||||||
|
MTP_int(kStickerSide)));
|
||||||
|
|
||||||
|
auto result = MakePreparedFile({
|
||||||
|
.id = id,
|
||||||
|
.type = SendMediaType::File,
|
||||||
|
});
|
||||||
|
result->filename = filename;
|
||||||
|
result->filemime = u"image/webp"_q;
|
||||||
|
result->content = bytes;
|
||||||
|
result->filesize = bytes.size();
|
||||||
|
result->setFileData(bytes);
|
||||||
|
result->document = MTP_document(
|
||||||
|
MTP_flags(0),
|
||||||
|
MTP_long(id),
|
||||||
|
MTP_long(0),
|
||||||
|
MTP_bytes(),
|
||||||
|
MTP_int(base::unixtime::now()),
|
||||||
|
MTP_string("image/webp"),
|
||||||
|
MTP_long(bytes.size()),
|
||||||
|
MTP_vector<MTPPhotoSize>(),
|
||||||
|
MTPVector<MTPVideoSize>(),
|
||||||
|
MTP_int(dcId),
|
||||||
|
MTP_vector<MTPDocumentAttribute>(std::move(attributes)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FeedSetIfFull(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const MTPmessages_StickerSet &result) {
|
||||||
|
result.match([&](const MTPDmessages_stickerSet &data) {
|
||||||
|
session->data().stickers().feedSetFull(data);
|
||||||
|
session->data().stickers().notifyUpdated(
|
||||||
|
Data::StickersType::Stickers);
|
||||||
|
}, [](const auto &) {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Callback>
|
||||||
|
void EnumerateOwnedSets(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
Data::StickersType type,
|
||||||
|
Callback &&callback) {
|
||||||
|
const auto &stickers = session->data().stickers();
|
||||||
|
const auto &sets = stickers.sets();
|
||||||
|
const auto &order = (type == Data::StickersType::Emoji)
|
||||||
|
? stickers.emojiSetsOrder()
|
||||||
|
: stickers.setsOrder();
|
||||||
|
for (const auto setId : order) {
|
||||||
|
const auto it = sets.find(setId);
|
||||||
|
if (it == sets.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto set = it->second.get();
|
||||||
|
if (!(set->flags & Data::StickersSetFlag::AmCreator)
|
||||||
|
|| (set->type() != type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
using namespace Data;
|
||||||
|
if constexpr (std::is_same_v<
|
||||||
|
bool,
|
||||||
|
std::invoke_result_t<Callback, not_null<StickersSet*>>>) {
|
||||||
|
if (!callback(set)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callback(set);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FillChooseOwnedSetMenu(
|
||||||
|
not_null<Ui::PopupMenu*> menu,
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
not_null<DocumentData*> document,
|
||||||
|
Data::StickersType type) {
|
||||||
|
const auto session = &show->session();
|
||||||
|
const auto emoji = StickerEmojiOrDefault(document);
|
||||||
|
const auto isEmoji = (type == Data::StickersType::Emoji);
|
||||||
|
const auto maxCount = isEmoji
|
||||||
|
? kEmojiInOwnedSetMax
|
||||||
|
: kStickersInOwnedSetMax;
|
||||||
|
const auto fullMessage = isEmoji
|
||||||
|
? tr::lng_emoji_set_is_full
|
||||||
|
: tr::lng_stickers_set_is_full;
|
||||||
|
const auto addedMessage = isEmoji
|
||||||
|
? tr::lng_emoji_added
|
||||||
|
: tr::lng_stickers_create_added;
|
||||||
|
const auto alreadyMessage = isEmoji
|
||||||
|
? tr::lng_emoji_already_in_set
|
||||||
|
: tr::lng_stickers_already_in_set;
|
||||||
|
const auto failToast = [=](QString err) {
|
||||||
|
show->showToast(err.isEmpty()
|
||||||
|
? tr::lng_attach_failed(tr::now)
|
||||||
|
: err);
|
||||||
|
};
|
||||||
|
EnumerateOwnedSets(session, type, [&](not_null<Data::StickersSet*> set) {
|
||||||
|
const auto identifier = set->identifier();
|
||||||
|
const auto coverDocument = set->lookupThumbnailDocument();
|
||||||
|
auto thumbnail = coverDocument
|
||||||
|
? Ui::MakeDocumentThumbnail(
|
||||||
|
coverDocument,
|
||||||
|
Data::FileOriginStickerSet(set->id, set->accessHash))
|
||||||
|
: nullptr;
|
||||||
|
const auto targetSetId = set->id;
|
||||||
|
const auto handler = crl::guard(session, [=] {
|
||||||
|
const auto &map = session->data().stickers().sets();
|
||||||
|
const auto i = map.find(targetSetId);
|
||||||
|
if (i != map.end() && i->second->count >= maxCount) {
|
||||||
|
show->showToast(fullMessage(tr::now));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto oldCount = (i != map.end())
|
||||||
|
? i->second->count
|
||||||
|
: 0;
|
||||||
|
AddExistingStickerToSet(
|
||||||
|
session,
|
||||||
|
identifier,
|
||||||
|
document,
|
||||||
|
emoji,
|
||||||
|
crl::guard(session, [=](MTPmessages_StickerSet) {
|
||||||
|
const auto &map = session->data().stickers().sets();
|
||||||
|
const auto i = map.find(targetSetId);
|
||||||
|
const auto newCount = (i != map.end())
|
||||||
|
? i->second->count
|
||||||
|
: oldCount;
|
||||||
|
show->showToast(newCount > oldCount
|
||||||
|
? addedMessage(tr::now)
|
||||||
|
: alreadyMessage(tr::now));
|
||||||
|
}),
|
||||||
|
crl::guard(session, failToast));
|
||||||
|
});
|
||||||
|
const auto rawAction = Ui::Menu::CreateAction(
|
||||||
|
menu.get(),
|
||||||
|
set->title,
|
||||||
|
handler);
|
||||||
|
auto item = base::make_unique_q<Menu::ActionWithThumbnail>(
|
||||||
|
menu->menu(),
|
||||||
|
menu->menu()->st(),
|
||||||
|
rawAction,
|
||||||
|
std::move(thumbnail),
|
||||||
|
st::menuIconStickerAdd.width());
|
||||||
|
menu->addAction(std::move(item));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void AddExistingStickerToSet(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const StickerSetIdentifier &set,
|
||||||
|
not_null<DocumentData*> document,
|
||||||
|
const QString &emoji,
|
||||||
|
Fn<void(MTPmessages_StickerSet)> done,
|
||||||
|
Fn<void(QString)> fail) {
|
||||||
|
session->api().request(MTPstickers_AddStickerToSet(
|
||||||
|
Data::InputStickerSet(set),
|
||||||
|
InputItem(document->mtpInput(), emoji))
|
||||||
|
).done([=](const MTPmessages_StickerSet &result) {
|
||||||
|
FeedSetIfFull(session, result);
|
||||||
|
if (done) {
|
||||||
|
done(result);
|
||||||
|
}
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
if (fail) {
|
||||||
|
fail(error.type());
|
||||||
|
}
|
||||||
|
}).handleFloodErrors().send();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString StickerEmojiOrDefault(not_null<DocumentData*> document) {
|
||||||
|
if (const auto sticker = document->sticker()) {
|
||||||
|
if (!sticker->alt.isEmpty()) {
|
||||||
|
return sticker->alt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QString::fromUtf8("\xF0\x9F\x99\x82");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasOwnedStickerSets(not_null<Main::Session*> session) {
|
||||||
|
auto found = false;
|
||||||
|
EnumerateOwnedSets(
|
||||||
|
session,
|
||||||
|
Data::StickersType::Stickers,
|
||||||
|
[&](not_null<Data::StickersSet*>) {
|
||||||
|
found = true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HasOwnedEmojiSets(not_null<Main::Session*> session) {
|
||||||
|
auto found = false;
|
||||||
|
EnumerateOwnedSets(
|
||||||
|
session,
|
||||||
|
Data::StickersType::Emoji,
|
||||||
|
[&](not_null<Data::StickersSet*>) {
|
||||||
|
found = true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FillChooseStickerSetMenu(
|
||||||
|
not_null<Ui::PopupMenu*> menu,
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
not_null<DocumentData*> document) {
|
||||||
|
using namespace Data;
|
||||||
|
FillChooseOwnedSetMenu(menu, show, document, StickersType::Stickers);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FillChooseEmojiSetMenu(
|
||||||
|
not_null<Ui::PopupMenu*> menu,
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
not_null<DocumentData*> document) {
|
||||||
|
FillChooseOwnedSetMenu(menu, show, document, Data::StickersType::Emoji);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddAddToStickerSetAction(
|
||||||
|
const Ui::Menu::MenuCallback &addAction,
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
not_null<DocumentData*> document) {
|
||||||
|
const auto session = &show->session();
|
||||||
|
if (!HasOwnedStickerSets(session)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addAction({
|
||||||
|
.text = tr::lng_stickers_add_to_set(tr::now),
|
||||||
|
.icon = &st::menuIconStickerAdd,
|
||||||
|
.fillSubmenu = [show, document](not_null<Ui::PopupMenu*> submenu) {
|
||||||
|
FillChooseStickerSetMenu(submenu, show, document);
|
||||||
|
},
|
||||||
|
.submenuSt = &st::popupMenuWithIcons,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddAddToEmojiSetAction(
|
||||||
|
const Ui::Menu::MenuCallback &addAction,
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
not_null<DocumentData*> document) {
|
||||||
|
const auto session = &show->session();
|
||||||
|
if (!HasOwnedEmojiSets(session)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addAction({
|
||||||
|
.text = tr::lng_emoji_add_to_set(tr::now),
|
||||||
|
.icon = &st::menuIconEmoji,
|
||||||
|
.fillSubmenu = [show, document](not_null<Ui::PopupMenu*> submenu) {
|
||||||
|
FillChooseEmojiSetMenu(submenu, show, document);
|
||||||
|
},
|
||||||
|
.submenuSt = &st::popupMenuWithIcons,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeleteStickerSet(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const StickerSetIdentifier &set,
|
||||||
|
Fn<void()> done,
|
||||||
|
Fn<void(QString)> fail) {
|
||||||
|
session->api().request(MTPstickers_DeleteStickerSet(
|
||||||
|
Data::InputStickerSet(set))
|
||||||
|
).done([=] {
|
||||||
|
session->data().stickers().notifyUpdated(
|
||||||
|
Data::StickersType::Stickers);
|
||||||
|
if (done) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}).fail([=](const MTP::Error &error) {
|
||||||
|
if (fail) {
|
||||||
|
fail(error.type());
|
||||||
|
}
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
|
||||||
|
StickerUpload::StickerUpload(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
StickerSetIdentifier set,
|
||||||
|
QByteArray webpBytes,
|
||||||
|
QString emoji)
|
||||||
|
: _session(session)
|
||||||
|
, _set(std::move(set))
|
||||||
|
, _bytes(std::move(webpBytes))
|
||||||
|
, _emoji(std::move(emoji))
|
||||||
|
, _api(&session->mtp()) {
|
||||||
|
}
|
||||||
|
|
||||||
|
StickerUpload::~StickerUpload() {
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerUpload::start(
|
||||||
|
Fn<void(MTPmessages_StickerSet)> done,
|
||||||
|
Fn<void(QString)> fail,
|
||||||
|
Fn<void(int)> progress) {
|
||||||
|
Expects(!_uploadId);
|
||||||
|
|
||||||
|
_done = std::move(done);
|
||||||
|
_fail = std::move(fail);
|
||||||
|
_progress = std::move(progress);
|
||||||
|
|
||||||
|
_documentId = base::RandomValue<DocumentId>();
|
||||||
|
auto ready = PrepareStickerWebp(
|
||||||
|
_session->mtp().mainDcId(),
|
||||||
|
_documentId,
|
||||||
|
_bytes);
|
||||||
|
_uploadId = FullMsgId(
|
||||||
|
_session->userPeerId(),
|
||||||
|
_session->data().nextLocalMessageId());
|
||||||
|
|
||||||
|
const auto document = _session->data().document(_documentId);
|
||||||
|
document->uploadingData = std::make_unique<Data::UploadState>(
|
||||||
|
document->size > 0 ? document->size : int64(_bytes.size()));
|
||||||
|
|
||||||
|
_session->uploader().documentReady(
|
||||||
|
) | rpl::filter([=](const Storage::UploadedMedia &data) {
|
||||||
|
return data.fullId == _uploadId;
|
||||||
|
}) | rpl::on_next([=](const Storage::UploadedMedia &data) {
|
||||||
|
uploadReady(data.info.file);
|
||||||
|
}, _uploadLifetime);
|
||||||
|
|
||||||
|
_session->uploader().documentFailed(
|
||||||
|
) | rpl::filter([=](const FullMsgId &id) {
|
||||||
|
return id == _uploadId;
|
||||||
|
}) | rpl::on_next([=] {
|
||||||
|
uploadFailed();
|
||||||
|
}, _uploadLifetime);
|
||||||
|
|
||||||
|
if (_progress) {
|
||||||
|
_session->uploader().documentProgress(
|
||||||
|
) | rpl::filter([=](const FullMsgId &id) {
|
||||||
|
return id == _uploadId;
|
||||||
|
}) | rpl::on_next([=] {
|
||||||
|
uploadProgressed();
|
||||||
|
}, _uploadLifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
_session->uploader().upload(_uploadId, ready);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerUpload::cancel() {
|
||||||
|
if (_uploadId) {
|
||||||
|
_session->uploader().cancel(_uploadId);
|
||||||
|
_uploadId = FullMsgId();
|
||||||
|
}
|
||||||
|
if (_addRequestId) {
|
||||||
|
_api.request(_addRequestId).cancel();
|
||||||
|
_addRequestId = 0;
|
||||||
|
}
|
||||||
|
_uploadLifetime.destroy();
|
||||||
|
_done = nullptr;
|
||||||
|
_fail = nullptr;
|
||||||
|
_progress = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerUpload::uploadProgressed() {
|
||||||
|
if (!_progress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto document = _session->data().document(_documentId);
|
||||||
|
if (!document->uploading() || !document->uploadingData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto size = document->uploadingData->size;
|
||||||
|
if (size <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto percent = int(
|
||||||
|
(document->uploadingData->offset * 100) / size);
|
||||||
|
if (percent != _lastReportedPercent) {
|
||||||
|
_lastReportedPercent = percent;
|
||||||
|
_progress(percent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerUpload::uploadFailed() {
|
||||||
|
const auto fail = std::move(_fail);
|
||||||
|
cancel();
|
||||||
|
if (fail) {
|
||||||
|
fail(QString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerUpload::uploadReady(const MTPInputFile &file) {
|
||||||
|
_uploadLifetime.destroy();
|
||||||
|
_uploadId = FullMsgId();
|
||||||
|
|
||||||
|
auto attributes = QVector<MTPDocumentAttribute>();
|
||||||
|
attributes.push_back(MTP_documentAttributeSticker(
|
||||||
|
MTP_flags(0),
|
||||||
|
MTP_string(_emoji),
|
||||||
|
MTP_inputStickerSetEmpty(),
|
||||||
|
MTPMaskCoords()));
|
||||||
|
attributes.push_back(MTP_documentAttributeImageSize(
|
||||||
|
MTP_int(kStickerSide),
|
||||||
|
MTP_int(kStickerSide)));
|
||||||
|
|
||||||
|
const auto media = MTP_inputMediaUploadedDocument(
|
||||||
|
MTP_flags(0),
|
||||||
|
file,
|
||||||
|
MTPInputFile(),
|
||||||
|
MTP_string("image/webp"),
|
||||||
|
MTP_vector<MTPDocumentAttribute>(std::move(attributes)),
|
||||||
|
MTP_vector<MTPInputDocument>(),
|
||||||
|
MTPInputPhoto(),
|
||||||
|
MTP_int(0),
|
||||||
|
MTP_int(0));
|
||||||
|
|
||||||
|
_addRequestId = _api.request(MTPmessages_UploadMedia(
|
||||||
|
MTP_flags(0),
|
||||||
|
MTPstring(),
|
||||||
|
MTP_inputPeerSelf(),
|
||||||
|
media
|
||||||
|
)).done(crl::guard(this, [=](const MTPMessageMedia &result) {
|
||||||
|
_addRequestId = 0;
|
||||||
|
auto inputDoc = (MTPInputDocument*)(nullptr);
|
||||||
|
auto storage = MTPInputDocument();
|
||||||
|
result.match([&](const MTPDmessageMediaDocument &data) {
|
||||||
|
if (const auto doc = data.vdocument()) {
|
||||||
|
doc->match([&](const MTPDdocument &d) {
|
||||||
|
storage = MTP_inputDocument(
|
||||||
|
d.vid(),
|
||||||
|
d.vaccess_hash(),
|
||||||
|
d.vfile_reference());
|
||||||
|
inputDoc = &storage;
|
||||||
|
}, [](const auto &) {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [](const auto &) {
|
||||||
|
});
|
||||||
|
if (inputDoc) {
|
||||||
|
requestAddSticker(*inputDoc);
|
||||||
|
} else if (const auto fail = std::move(_fail)) {
|
||||||
|
cancel();
|
||||||
|
fail(QString());
|
||||||
|
}
|
||||||
|
})).fail(crl::guard(this, [=](const MTP::Error &error) {
|
||||||
|
_addRequestId = 0;
|
||||||
|
const auto fail = std::move(_fail);
|
||||||
|
const auto type = error.type();
|
||||||
|
cancel();
|
||||||
|
if (fail) {
|
||||||
|
fail(type);
|
||||||
|
}
|
||||||
|
})).handleFloodErrors().send();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerUpload::requestAddSticker(const MTPInputDocument &document) {
|
||||||
|
_addRequestId = _api.request(MTPstickers_AddStickerToSet(
|
||||||
|
Data::InputStickerSet(_set),
|
||||||
|
InputItem(document, _emoji))
|
||||||
|
).done(crl::guard(this, [=](const MTPmessages_StickerSet &result) {
|
||||||
|
_addRequestId = 0;
|
||||||
|
FeedSetIfFull(_session, result);
|
||||||
|
const auto done = std::move(_done);
|
||||||
|
cancel();
|
||||||
|
if (done) {
|
||||||
|
done(result);
|
||||||
|
}
|
||||||
|
})).fail(crl::guard(this, [=](const MTP::Error &error) {
|
||||||
|
_addRequestId = 0;
|
||||||
|
const auto fail = std::move(_fail);
|
||||||
|
const auto type = error.type();
|
||||||
|
cancel();
|
||||||
|
if (fail) {
|
||||||
|
fail(type);
|
||||||
|
}
|
||||||
|
})).handleFloodErrors().send();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Api
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
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 "base/weak_ptr.h"
|
||||||
|
#include "data/stickers/data_stickers.h"
|
||||||
|
#include "mtproto/sender.h"
|
||||||
|
|
||||||
|
class DocumentData;
|
||||||
|
|
||||||
|
namespace ChatHelpers {
|
||||||
|
class Show;
|
||||||
|
} // namespace ChatHelpers
|
||||||
|
|
||||||
|
namespace Main {
|
||||||
|
class Session;
|
||||||
|
} // namespace Main
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class PopupMenu;
|
||||||
|
namespace Menu {
|
||||||
|
struct MenuCallback;
|
||||||
|
} // namespace Menu
|
||||||
|
} // namespace Ui
|
||||||
|
|
||||||
|
namespace Api {
|
||||||
|
|
||||||
|
inline constexpr auto kStickersInOwnedSetMax = 120;
|
||||||
|
inline constexpr auto kEmojiInOwnedSetMax = 200;
|
||||||
|
|
||||||
|
void AddExistingStickerToSet(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const StickerSetIdentifier &set,
|
||||||
|
not_null<DocumentData*> document,
|
||||||
|
const QString &emoji,
|
||||||
|
Fn<void(MTPmessages_StickerSet)> done,
|
||||||
|
Fn<void(QString)> fail);
|
||||||
|
|
||||||
|
void DeleteStickerSet(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
const StickerSetIdentifier &set,
|
||||||
|
Fn<void()> done,
|
||||||
|
Fn<void(QString)> fail);
|
||||||
|
|
||||||
|
[[nodiscard]] bool HasOwnedStickerSets(not_null<Main::Session*> session);
|
||||||
|
[[nodiscard]] bool HasOwnedEmojiSets(not_null<Main::Session*> session);
|
||||||
|
|
||||||
|
[[nodiscard]] QString StickerEmojiOrDefault(
|
||||||
|
not_null<DocumentData*> document);
|
||||||
|
|
||||||
|
void FillChooseStickerSetMenu(
|
||||||
|
not_null<Ui::PopupMenu*> menu,
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
not_null<DocumentData*> document);
|
||||||
|
|
||||||
|
void FillChooseEmojiSetMenu(
|
||||||
|
not_null<Ui::PopupMenu*> menu,
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
not_null<DocumentData*> document);
|
||||||
|
|
||||||
|
void AddAddToStickerSetAction(
|
||||||
|
const Ui::Menu::MenuCallback &addAction,
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
not_null<DocumentData*> document);
|
||||||
|
|
||||||
|
void AddAddToEmojiSetAction(
|
||||||
|
const Ui::Menu::MenuCallback &addAction,
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
not_null<DocumentData*> document);
|
||||||
|
|
||||||
|
class StickerUpload final : public base::has_weak_ptr {
|
||||||
|
public:
|
||||||
|
StickerUpload(
|
||||||
|
not_null<Main::Session*> session,
|
||||||
|
StickerSetIdentifier set,
|
||||||
|
QByteArray webpBytes,
|
||||||
|
QString emoji);
|
||||||
|
~StickerUpload();
|
||||||
|
|
||||||
|
void start(
|
||||||
|
Fn<void(MTPmessages_StickerSet)> done,
|
||||||
|
Fn<void(QString)> fail,
|
||||||
|
Fn<void(int /*percent*/)> progress = nullptr);
|
||||||
|
|
||||||
|
void cancel();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void uploadReady(const MTPInputFile &file);
|
||||||
|
void uploadFailed();
|
||||||
|
void uploadProgressed();
|
||||||
|
void requestAddSticker(const MTPInputDocument &document);
|
||||||
|
|
||||||
|
const not_null<Main::Session*> _session;
|
||||||
|
StickerSetIdentifier _set;
|
||||||
|
QByteArray _bytes;
|
||||||
|
QString _emoji;
|
||||||
|
MTP::Sender _api;
|
||||||
|
rpl::lifetime _uploadLifetime;
|
||||||
|
FullMsgId _uploadId;
|
||||||
|
DocumentId _documentId = 0;
|
||||||
|
mtpRequestId _addRequestId = 0;
|
||||||
|
|
||||||
|
Fn<void(MTPmessages_StickerSet)> _done;
|
||||||
|
Fn<void(QString)> _fail;
|
||||||
|
Fn<void(int)> _progress;
|
||||||
|
int _lastReportedPercent = -1;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Api
|
||||||
@@ -59,6 +59,7 @@ boxPhotoTitlePosition: point(28px, 20px);
|
|||||||
boxPhotoPadding: margins(28px, 28px, 28px, 18px);
|
boxPhotoPadding: margins(28px, 28px, 28px, 18px);
|
||||||
boxPhotoCompressedSkip: 20px;
|
boxPhotoCompressedSkip: 20px;
|
||||||
boxPhotoCaptionSkip: 8px;
|
boxPhotoCaptionSkip: 8px;
|
||||||
|
boxPhotoCaptionReplyOverlap: 5px;
|
||||||
|
|
||||||
defaultChangeUserpicIcon: icon {{ "new_chat_photo", activeButtonFg }};
|
defaultChangeUserpicIcon: icon {{ "new_chat_photo", activeButtonFg }};
|
||||||
defaultUploadUserpicIcon: icon {{ "upload_chat_photo", msgDateImgFg }};
|
defaultUploadUserpicIcon: icon {{ "upload_chat_photo", msgDateImgFg }};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "main/main_account.h"
|
#include "main/main_account.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "mtproto/facade.h"
|
#include "mtproto/facade.h"
|
||||||
|
#include "mtproto/proxy_check.h"
|
||||||
#include "settings/settings_common.h"
|
#include "settings/settings_common.h"
|
||||||
#include "storage/localstorage.h"
|
#include "storage/localstorage.h"
|
||||||
#include "ui/basic_click_handlers.h"
|
#include "ui/basic_click_handlers.h"
|
||||||
@@ -32,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "ui/widgets/buttons.h"
|
#include "ui/widgets/buttons.h"
|
||||||
#include "ui/widgets/checkbox.h"
|
#include "ui/widgets/checkbox.h"
|
||||||
#include "ui/widgets/dropdown_menu.h"
|
#include "ui/widgets/dropdown_menu.h"
|
||||||
|
#include "ui/widgets/discrete_sliders.h"
|
||||||
#include "ui/widgets/fields/input_field.h"
|
#include "ui/widgets/fields/input_field.h"
|
||||||
#include "ui/widgets/fields/number_input.h"
|
#include "ui/widgets/fields/number_input.h"
|
||||||
#include "ui/widgets/fields/password_input.h"
|
#include "ui/widgets/fields/password_input.h"
|
||||||
@@ -62,22 +64,55 @@ constexpr auto kSaveSettingsDelayedTimeout = crl::time(1000);
|
|||||||
|
|
||||||
using ProxyData = MTP::ProxyData;
|
using ProxyData = MTP::ProxyData;
|
||||||
|
|
||||||
[[nodiscard]] std::vector<QString> ExtractUrlsSimple(const QString &input) {
|
[[nodiscard]] int ClosestProxyRotationTimeoutSection(int value) {
|
||||||
|
auto result = 0;
|
||||||
|
auto bestDistance = 0;
|
||||||
|
for (auto i = 0; i != int(Core::SettingsProxy::kProxyRotationTimeouts.size()); ++i) {
|
||||||
|
const auto current = Core::SettingsProxy::kProxyRotationTimeouts[i];
|
||||||
|
const auto distance = (current > value) ? (current - value) : (value - current);
|
||||||
|
if ((i == 0) || (distance < bestDistance)) {
|
||||||
|
result = i;
|
||||||
|
bestDistance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<QString> ExtractLinkCandidates(const QString &input) {
|
||||||
auto urls = std::vector<QString>();
|
auto urls = std::vector<QString>();
|
||||||
static auto urlRegex = QRegularExpression(R"((https?:\/\/[^\s]+))");
|
static const auto urlRegex = QRegularExpression(
|
||||||
|
R"((?:https?:\/\/[^\s]+|tg:\/\/[^\s]+|(?:www\.)?(?:t\.me|telegram\.me|telegram\.dog)\/[^\s]+))",
|
||||||
|
QRegularExpression::CaseInsensitiveOption);
|
||||||
|
|
||||||
auto it = urlRegex.globalMatch(input);
|
auto it = urlRegex.globalMatch(input);
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
urls.push_back(it.next().captured(1));
|
urls.push_back(it.next().captured(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
return urls;
|
return urls;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] QString ProxyDataToString(const ProxyData &proxy) {
|
[[nodiscard]] bool ProxyDataIsShareable(const ProxyData &proxy) {
|
||||||
using Type = ProxyData::Type;
|
using Type = ProxyData::Type;
|
||||||
return u"tg://"_q
|
return (proxy.type == Type::Socks5)
|
||||||
+ (proxy.type == Type::Socks5 ? "socks" : "proxy")
|
|| (proxy.type == Type::Mtproto);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QString ProxyDataToQueryPath(const ProxyData &proxy) {
|
||||||
|
using Type = ProxyData::Type;
|
||||||
|
const auto path = [&] {
|
||||||
|
switch (proxy.type) {
|
||||||
|
case Type::Socks5: return u"socks"_q;
|
||||||
|
case Type::Mtproto: return u"proxy"_q;
|
||||||
|
case Type::None:
|
||||||
|
case Type::Http: return QString();
|
||||||
|
}
|
||||||
|
Unexpected("Proxy type in ProxyDataToQueryPath.");
|
||||||
|
}();
|
||||||
|
if (path.isEmpty()) {
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
return path
|
||||||
+ "?server=" + proxy.host + "&port=" + QString::number(proxy.port)
|
+ "?server=" + proxy.host + "&port=" + QString::number(proxy.port)
|
||||||
+ ((proxy.type == Type::Socks5 && !proxy.user.isEmpty())
|
+ ((proxy.type == Type::Socks5 && !proxy.user.isEmpty())
|
||||||
? "&user=" + qthelp::url_encode(proxy.user) : "")
|
? "&user=" + qthelp::url_encode(proxy.user) : "")
|
||||||
@@ -87,6 +122,20 @@ using ProxyData = MTP::ProxyData;
|
|||||||
? "&secret=" + proxy.password : "");
|
? "&secret=" + proxy.password : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QString ProxyDataToLocalLink(const ProxyData &proxy) {
|
||||||
|
const auto queryPath = ProxyDataToQueryPath(proxy);
|
||||||
|
return queryPath.isEmpty() ? QString() : (u"tg://"_q + queryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QString ProxyDataToPublicLink(
|
||||||
|
const Main::Session &session,
|
||||||
|
const ProxyData &proxy) {
|
||||||
|
const auto queryPath = ProxyDataToQueryPath(proxy);
|
||||||
|
return queryPath.isEmpty()
|
||||||
|
? QString()
|
||||||
|
: session.createInternalLinkFull(queryPath);
|
||||||
|
}
|
||||||
|
|
||||||
[[nodiscard]] ProxyData ProxyDataFromFields(
|
[[nodiscard]] ProxyData ProxyDataFromFields(
|
||||||
ProxyData::Type type,
|
ProxyData::Type type,
|
||||||
const QMap<QString, QString> &fields) {
|
const QMap<QString, QString> &fields) {
|
||||||
@@ -110,7 +159,7 @@ void AddProxyFromClipboard(
|
|||||||
const auto socksString = u"socks"_q;
|
const auto socksString = u"socks"_q;
|
||||||
const auto protocol = u"tg://"_q;
|
const auto protocol = u"tg://"_q;
|
||||||
|
|
||||||
const auto maybeUrls = ExtractUrlsSimple(
|
const auto maybeUrls = ExtractLinkCandidates(
|
||||||
QGuiApplication::clipboard()->text());
|
QGuiApplication::clipboard()->text());
|
||||||
const auto isSingle = maybeUrls.size() == 1;
|
const auto isSingle = maybeUrls.size() == 1;
|
||||||
|
|
||||||
@@ -128,8 +177,8 @@ void AddProxyFromClipboard(
|
|||||||
protocol.size(),
|
protocol.size(),
|
||||||
8192);
|
8192);
|
||||||
|
|
||||||
if (local.startsWith(protocol + proxyString)
|
if (local.startsWith(protocol + proxyString, Qt::CaseInsensitive)
|
||||||
|| local.startsWith(protocol + socksString)) {
|
|| local.startsWith(protocol + socksString, Qt::CaseInsensitive)) {
|
||||||
|
|
||||||
using namespace qthelp;
|
using namespace qthelp;
|
||||||
const auto options = RegExOption::CaseInsensitive;
|
const auto options = RegExOption::CaseInsensitive;
|
||||||
@@ -188,7 +237,10 @@ void AddProxyFromClipboard(
|
|||||||
|
|
||||||
auto success = Result::Failed;
|
auto success = Result::Failed;
|
||||||
for (const auto &maybeUrl : maybeUrls) {
|
for (const auto &maybeUrl : maybeUrls) {
|
||||||
const auto result = proceedUrl(Core::TryConvertUrlToLocal(maybeUrl));
|
const auto trimmed = maybeUrl.trimmed();
|
||||||
|
const auto local = Core::TryConvertUrlToLocal(trimmed);
|
||||||
|
const auto check = local.isEmpty() ? trimmed : local;
|
||||||
|
const auto result = proceedUrl(check);
|
||||||
if (success != Result::Success) {
|
if (success != Result::Success) {
|
||||||
success = result;
|
success = result;
|
||||||
}
|
}
|
||||||
@@ -376,12 +428,16 @@ private:
|
|||||||
void setupButtons(int id, not_null<ProxyRow*> button);
|
void setupButtons(int id, not_null<ProxyRow*> button);
|
||||||
int rowHeight() const;
|
int rowHeight() const;
|
||||||
void refreshProxyForCalls();
|
void refreshProxyForCalls();
|
||||||
|
void refreshProxyRotation();
|
||||||
|
|
||||||
not_null<ProxiesBoxController*> _controller;
|
not_null<ProxiesBoxController*> _controller;
|
||||||
Core::SettingsProxy &_settings;
|
Core::SettingsProxy &_settings;
|
||||||
QPointer<Ui::Checkbox> _tryIPv6;
|
QPointer<Ui::Checkbox> _tryIPv6;
|
||||||
std::shared_ptr<Ui::RadioenumGroup<ProxyData::Settings>> _proxySettings;
|
std::shared_ptr<Ui::RadioenumGroup<ProxyData::Settings>> _proxySettings;
|
||||||
QPointer<Ui::SlideWrap<Ui::Checkbox>> _proxyForCalls;
|
QPointer<Ui::SlideWrap<Ui::Checkbox>> _proxyForCalls;
|
||||||
|
QPointer<Ui::SlideWrap<Ui::Checkbox>> _proxyRotation;
|
||||||
|
QPointer<Ui::SlideWrap<Ui::VerticalLayout>> _proxyRotationOptions;
|
||||||
|
QPointer<Ui::SettingsSlider> _proxyRotationTimeout;
|
||||||
QPointer<Ui::DividerLabel> _about;
|
QPointer<Ui::DividerLabel> _about;
|
||||||
base::unique_qptr<Ui::RpWidget> _noRows;
|
base::unique_qptr<Ui::RpWidget> _noRows;
|
||||||
object_ptr<Ui::VerticalLayout> _initialWrap;
|
object_ptr<Ui::VerticalLayout> _initialWrap;
|
||||||
@@ -900,6 +956,47 @@ void ProxiesBox::setupContent() {
|
|||||||
0,
|
0,
|
||||||
st::proxyTryIPv6Padding.right(),
|
st::proxyTryIPv6Padding.right(),
|
||||||
st::proxyTryIPv6Padding.top()));
|
st::proxyTryIPv6Padding.top()));
|
||||||
|
_proxyRotation = inner->add(
|
||||||
|
object_ptr<Ui::SlideWrap<Ui::Checkbox>>(
|
||||||
|
inner,
|
||||||
|
object_ptr<Ui::Checkbox>(
|
||||||
|
inner,
|
||||||
|
tr::lng_proxy_auto_switch(tr::now),
|
||||||
|
_settings.proxyRotationEnabled()),
|
||||||
|
style::margins(
|
||||||
|
0,
|
||||||
|
st::proxyUsePadding.top(),
|
||||||
|
0,
|
||||||
|
st::proxyUsePadding.bottom())),
|
||||||
|
style::margins(
|
||||||
|
st::proxyTryIPv6Padding.left(),
|
||||||
|
0,
|
||||||
|
st::proxyTryIPv6Padding.right(),
|
||||||
|
st::proxyTryIPv6Padding.top()));
|
||||||
|
_proxyRotationOptions = inner->add(
|
||||||
|
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
|
||||||
|
inner,
|
||||||
|
object_ptr<Ui::VerticalLayout>(inner)));
|
||||||
|
_proxyRotationTimeout = _proxyRotationOptions->entity()->add(
|
||||||
|
object_ptr<Ui::SettingsSlider>(
|
||||||
|
_proxyRotationOptions->entity(),
|
||||||
|
st::settingsSlider),
|
||||||
|
st::settingsBigScalePadding);
|
||||||
|
for (const auto seconds : Core::SettingsProxy::kProxyRotationTimeouts) {
|
||||||
|
_proxyRotationTimeout->addSection(
|
||||||
|
tr::lng_proxy_auto_switch_timeout(
|
||||||
|
tr::now,
|
||||||
|
lt_count,
|
||||||
|
seconds));
|
||||||
|
}
|
||||||
|
_proxyRotationTimeout->setActiveSectionFast(
|
||||||
|
ClosestProxyRotationTimeoutSection(_settings.proxyRotationTimeout()));
|
||||||
|
_proxyRotationOptions->entity()->add(
|
||||||
|
object_ptr<Ui::FlatLabel>(
|
||||||
|
_proxyRotationOptions->entity(),
|
||||||
|
tr::lng_proxy_auto_switch_about(tr::now),
|
||||||
|
st::boxDividerLabel),
|
||||||
|
st::proxyAboutPadding);
|
||||||
|
|
||||||
_about = inner->add(
|
_about = inner->add(
|
||||||
object_ptr<Ui::DividerLabel>(
|
object_ptr<Ui::DividerLabel>(
|
||||||
@@ -922,6 +1019,7 @@ void ProxiesBox::setupContent() {
|
|||||||
addNewProxy();
|
addNewProxy();
|
||||||
}
|
}
|
||||||
refreshProxyForCalls();
|
refreshProxyForCalls();
|
||||||
|
refreshProxyRotation();
|
||||||
});
|
});
|
||||||
_tryIPv6->checkedChanges(
|
_tryIPv6->checkedChanges(
|
||||||
) | rpl::on_next([=](bool checked) {
|
) | rpl::on_next([=](bool checked) {
|
||||||
@@ -931,18 +1029,33 @@ void ProxiesBox::setupContent() {
|
|||||||
_controller->proxySettingsValue(
|
_controller->proxySettingsValue(
|
||||||
) | rpl::on_next([=](ProxyData::Settings value) {
|
) | rpl::on_next([=](ProxyData::Settings value) {
|
||||||
_proxySettings->setValue(value);
|
_proxySettings->setValue(value);
|
||||||
|
refreshProxyForCalls();
|
||||||
|
refreshProxyRotation();
|
||||||
}, inner->lifetime());
|
}, inner->lifetime());
|
||||||
|
|
||||||
_proxyForCalls->entity()->checkedChanges(
|
_proxyForCalls->entity()->checkedChanges(
|
||||||
) | rpl::on_next([=](bool checked) {
|
) | rpl::on_next([=](bool checked) {
|
||||||
_controller->setProxyForCalls(checked);
|
_controller->setProxyForCalls(checked);
|
||||||
}, _proxyForCalls->lifetime());
|
}, _proxyForCalls->lifetime());
|
||||||
|
_proxyRotation->entity()->checkedChanges(
|
||||||
|
) | rpl::on_next([=](bool checked) {
|
||||||
|
_controller->setProxyRotationEnabled(checked);
|
||||||
|
refreshProxyRotation();
|
||||||
|
}, _proxyRotation->lifetime());
|
||||||
|
_proxyRotationTimeout->sectionActivated(
|
||||||
|
) | rpl::on_next([=](int section) {
|
||||||
|
_controller->setProxyRotationTimeout(
|
||||||
|
Core::SettingsProxy::kProxyRotationTimeouts[section]);
|
||||||
|
}, _proxyRotationTimeout->lifetime());
|
||||||
|
|
||||||
if (_rows.empty()) {
|
if (_rows.empty()) {
|
||||||
createNoRowsLabel();
|
createNoRowsLabel();
|
||||||
}
|
}
|
||||||
refreshProxyForCalls();
|
refreshProxyForCalls();
|
||||||
|
refreshProxyRotation();
|
||||||
_proxyForCalls->finishAnimating();
|
_proxyForCalls->finishAnimating();
|
||||||
|
_proxyRotation->finishAnimating();
|
||||||
|
_proxyRotationOptions->finishAnimating();
|
||||||
|
|
||||||
{
|
{
|
||||||
const auto wrap = inner->add(
|
const auto wrap = inner->add(
|
||||||
@@ -987,6 +1100,20 @@ void ProxiesBox::refreshProxyForCalls() {
|
|||||||
anim::type::normal);
|
anim::type::normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProxiesBox::refreshProxyRotation() {
|
||||||
|
if (!_proxyRotation || !_proxyRotationOptions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto visible = (_proxySettings->current()
|
||||||
|
== ProxyData::Settings::Enabled)
|
||||||
|
&& _settings.selected()
|
||||||
|
&& (_settings.list().size() > 1);
|
||||||
|
_proxyRotation->toggle(visible, anim::type::normal);
|
||||||
|
_proxyRotationOptions->toggle(
|
||||||
|
visible && _proxyRotation->entity()->checked(),
|
||||||
|
anim::type::normal);
|
||||||
|
}
|
||||||
|
|
||||||
int ProxiesBox::rowHeight() const {
|
int ProxiesBox::rowHeight() const {
|
||||||
return st::proxyRowPadding.top()
|
return st::proxyRowPadding.top()
|
||||||
+ st::semiboldFont->height
|
+ st::semiboldFont->height
|
||||||
@@ -1029,6 +1156,7 @@ void ProxiesBox::applyView(View &&view) {
|
|||||||
} else {
|
} else {
|
||||||
i->second->updateFields(std::move(view));
|
i->second->updateFields(std::move(view));
|
||||||
}
|
}
|
||||||
|
refreshProxyRotation();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProxiesBox::createNoRowsLabel() {
|
void ProxiesBox::createNoRowsLabel() {
|
||||||
@@ -1359,95 +1487,7 @@ void ProxyBox::addLabel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
using Connection = MTP::details::AbstractConnection;
|
using Connection = MTP::details::AbstractConnection;
|
||||||
using Checker = MTP::details::ConnectionPointer;
|
using Checker = MTP::ProxyCheckConnection;
|
||||||
|
|
||||||
void ResetProxyCheckers(Checker &v4, Checker &v6) {
|
|
||||||
v4 = nullptr;
|
|
||||||
v6 = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DropProxyChecker(Checker &v4, Checker &v6, not_null<Connection*> raw) {
|
|
||||||
if (v4.get() == raw) {
|
|
||||||
v4 = nullptr;
|
|
||||||
} else if (v6.get() == raw) {
|
|
||||||
v6 = nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] bool HasProxyCheckers(const Checker &v4, const Checker &v6) {
|
|
||||||
return v4 || v6;
|
|
||||||
}
|
|
||||||
|
|
||||||
void StartProxyCheck(
|
|
||||||
not_null<MTP::Instance*> mtproto,
|
|
||||||
const ProxyData &proxy,
|
|
||||||
Checker &v4,
|
|
||||||
Checker &v6,
|
|
||||||
Fn<void(Connection *raw, int ping)> done,
|
|
||||||
Fn<void(Connection *raw)> fail) {
|
|
||||||
using Variants = MTP::DcOptions::Variants;
|
|
||||||
|
|
||||||
ResetProxyCheckers(v4, v6);
|
|
||||||
const auto connType = (proxy.type == ProxyData::Type::Http)
|
|
||||||
? Variants::Http
|
|
||||||
: Variants::Tcp;
|
|
||||||
const auto dcId = mtproto->mainDcId();
|
|
||||||
const auto setup = [&](Checker &checker, const bytes::vector &secret) {
|
|
||||||
checker = Connection::Create(
|
|
||||||
mtproto,
|
|
||||||
connType,
|
|
||||||
QThread::currentThread(),
|
|
||||||
secret,
|
|
||||||
proxy);
|
|
||||||
const auto raw = checker.get();
|
|
||||||
raw->connect(raw, &Connection::connected, [=] {
|
|
||||||
if (done) {
|
|
||||||
done(raw, raw->pingTime());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const auto failed = [=] {
|
|
||||||
if (fail) {
|
|
||||||
fail(raw);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
raw->connect(raw, &Connection::disconnected, failed);
|
|
||||||
raw->connect(raw, &Connection::error, failed);
|
|
||||||
};
|
|
||||||
if (proxy.type == ProxyData::Type::Mtproto) {
|
|
||||||
const auto secret = proxy.secretFromMtprotoPassword();
|
|
||||||
setup(v4, secret);
|
|
||||||
v4->connectToServer(
|
|
||||||
proxy.host,
|
|
||||||
proxy.port,
|
|
||||||
secret,
|
|
||||||
dcId,
|
|
||||||
false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto options = mtproto->dcOptions().lookup(
|
|
||||||
dcId,
|
|
||||||
MTP::DcType::Regular,
|
|
||||||
true);
|
|
||||||
const auto tryConnect = [&](Checker &checker, Variants::Address address) {
|
|
||||||
const auto &list = options.data[address][connType];
|
|
||||||
if (list.empty()
|
|
||||||
|| ((address == Variants::IPv6)
|
|
||||||
&& !Core::App().settings().proxy().tryIPv6())) {
|
|
||||||
checker = nullptr;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto &endpoint = list.front();
|
|
||||||
setup(checker, endpoint.secret);
|
|
||||||
checker->connectToServer(
|
|
||||||
QString::fromStdString(endpoint.ip),
|
|
||||||
endpoint.port,
|
|
||||||
endpoint.secret,
|
|
||||||
dcId,
|
|
||||||
false);
|
|
||||||
};
|
|
||||||
tryConnect(v4, Variants::IPv4);
|
|
||||||
tryConnect(v6, Variants::IPv6);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -1609,18 +1649,19 @@ void ProxiesBoxController::ShowApplyConfirmation(
|
|||||||
};
|
};
|
||||||
statusLabel->setTextColorOverride(st::proxyRowStatusFg->c);
|
statusLabel->setTextColorOverride(st::proxyRowStatusFg->c);
|
||||||
relayout();
|
relayout();
|
||||||
StartProxyCheck(
|
MTP::StartProxyCheck(
|
||||||
&account->mtp(),
|
&account->mtp(),
|
||||||
proxy,
|
proxy,
|
||||||
|
Core::App().settings().proxy().tryIPv6(),
|
||||||
state->v4,
|
state->v4,
|
||||||
state->v6,
|
state->v6,
|
||||||
[=](Connection *raw, int ping) {
|
[=](Connection *raw, int ping) {
|
||||||
if (!weak || state->finished) {
|
if (!weak || state->finished) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DropProxyChecker(state->v4, state->v6, raw);
|
MTP::DropProxyChecker(state->v4, state->v6, raw);
|
||||||
state->finished = true;
|
state->finished = true;
|
||||||
ResetProxyCheckers(state->v4, state->v6);
|
MTP::ResetProxyCheckers(state->v4, state->v6);
|
||||||
state->statusValue = TextWithEntities{
|
state->statusValue = TextWithEntities{
|
||||||
tr::lng_proxy_box_table_available(
|
tr::lng_proxy_box_table_available(
|
||||||
tr::now,
|
tr::now,
|
||||||
@@ -1635,13 +1676,13 @@ void ProxiesBoxController::ShowApplyConfirmation(
|
|||||||
if (!weak || state->finished) {
|
if (!weak || state->finished) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DropProxyChecker(state->v4, state->v6, raw);
|
MTP::DropProxyChecker(state->v4, state->v6, raw);
|
||||||
if (!HasProxyCheckers(state->v4, state->v6)) {
|
if (!MTP::HasProxyCheckers(state->v4, state->v6)) {
|
||||||
state->finished = true;
|
state->finished = true;
|
||||||
setUnavailable();
|
setUnavailable();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!HasProxyCheckers(state->v4, state->v6)) {
|
if (!MTP::HasProxyCheckers(state->v4, state->v6)) {
|
||||||
state->finished = true;
|
state->finished = true;
|
||||||
setUnavailable();
|
setUnavailable();
|
||||||
}
|
}
|
||||||
@@ -1681,9 +1722,9 @@ void ProxiesBoxController::ShowApplyConfirmation(
|
|||||||
const auto enableButton = box->addButton(
|
const auto enableButton = box->addButton(
|
||||||
tr::lng_proxy_box_table_button(),
|
tr::lng_proxy_box_table_button(),
|
||||||
[=] {
|
[=] {
|
||||||
auto &proxies = Core::App().settings().proxy().list();
|
auto &settings = Core::App().settings().proxy();
|
||||||
if (!ranges::contains(proxies, proxy)) {
|
if (settings.indexInList(proxy) < 0) {
|
||||||
proxies.push_back(proxy);
|
settings.addToList(proxy);
|
||||||
}
|
}
|
||||||
Core::App().setCurrentProxy(
|
Core::App().setCurrentProxy(
|
||||||
proxy,
|
proxy,
|
||||||
@@ -1719,9 +1760,10 @@ auto ProxiesBoxController::proxySettingsValue() const
|
|||||||
void ProxiesBoxController::refreshChecker(Item &item) {
|
void ProxiesBoxController::refreshChecker(Item &item) {
|
||||||
item.state = ItemState::Checking;
|
item.state = ItemState::Checking;
|
||||||
const auto id = item.id;
|
const auto id = item.id;
|
||||||
StartProxyCheck(
|
MTP::StartProxyCheck(
|
||||||
&_account->mtp(),
|
&_account->mtp(),
|
||||||
item.data,
|
item.data,
|
||||||
|
Core::App().settings().proxy().tryIPv6(),
|
||||||
item.checker,
|
item.checker,
|
||||||
item.checkerv6,
|
item.checkerv6,
|
||||||
[=](Connection *raw, int pingTime) {
|
[=](Connection *raw, int pingTime) {
|
||||||
@@ -1732,8 +1774,8 @@ void ProxiesBoxController::refreshChecker(Item &item) {
|
|||||||
if (item == end(_list)) {
|
if (item == end(_list)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DropProxyChecker(item->checker, item->checkerv6, raw);
|
MTP::DropProxyChecker(item->checker, item->checkerv6, raw);
|
||||||
ResetProxyCheckers(item->checker, item->checkerv6);
|
MTP::ResetProxyCheckers(item->checker, item->checkerv6);
|
||||||
if (item->state == ItemState::Checking) {
|
if (item->state == ItemState::Checking) {
|
||||||
item->state = ItemState::Available;
|
item->state = ItemState::Available;
|
||||||
item->ping = pingTime;
|
item->ping = pingTime;
|
||||||
@@ -1748,14 +1790,14 @@ void ProxiesBoxController::refreshChecker(Item &item) {
|
|||||||
if (item == end(_list)) {
|
if (item == end(_list)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
DropProxyChecker(item->checker, item->checkerv6, raw);
|
MTP::DropProxyChecker(item->checker, item->checkerv6, raw);
|
||||||
if (!HasProxyCheckers(item->checker, item->checkerv6)
|
if (!MTP::HasProxyCheckers(item->checker, item->checkerv6)
|
||||||
&& item->state == ItemState::Checking) {
|
&& item->state == ItemState::Checking) {
|
||||||
item->state = ItemState::Unavailable;
|
item->state = ItemState::Unavailable;
|
||||||
updateView(*item);
|
updateView(*item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!HasProxyCheckers(item.checker, item.checkerv6)) {
|
if (!MTP::HasProxyCheckers(item.checker, item.checkerv6)) {
|
||||||
item.state = ItemState::Unavailable;
|
item.state = ItemState::Unavailable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1817,8 +1859,9 @@ void ProxiesBoxController::shareItem(int id, bool qr) {
|
|||||||
void ProxiesBoxController::shareItems() {
|
void ProxiesBoxController::shareItems() {
|
||||||
auto result = QString();
|
auto result = QString();
|
||||||
for (const auto &item : _list) {
|
for (const auto &item : _list) {
|
||||||
if (!item.deleted) {
|
if (!item.deleted && ProxyDataIsShareable(item.data)) {
|
||||||
result += ProxyDataToString(item.data) + '\n' + '\n';
|
result += ProxyDataToPublicLink(_account->session(), item.data)
|
||||||
|
+ '\n' + '\n';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result.isEmpty()) {
|
if (result.isEmpty()) {
|
||||||
@@ -1854,8 +1897,8 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) {
|
|||||||
item->deleted = deleted;
|
item->deleted = deleted;
|
||||||
|
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
auto &proxies = _settings.list();
|
const auto removed = _settings.removeFromList(item->data);
|
||||||
proxies.erase(ranges::remove(proxies, item->data), end(proxies));
|
Assert(removed);
|
||||||
|
|
||||||
if (item->data == _settings.selected()) {
|
if (item->data == _settings.selected()) {
|
||||||
_lastSelectedProxy = _settings.selected();
|
_lastSelectedProxy = _settings.selected();
|
||||||
@@ -1871,16 +1914,19 @@ void ProxiesBoxController::setDeleted(int id, bool deleted) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
auto &proxies = _settings.list();
|
if (_settings.indexInList(item->data) < 0) {
|
||||||
if (ranges::find(proxies, item->data) == end(proxies)) {
|
const auto &proxies = _settings.list();
|
||||||
auto insertBefore = item + 1;
|
auto insertBefore = item + 1;
|
||||||
while (insertBefore != end(_list) && insertBefore->deleted) {
|
while (insertBefore != end(_list) && insertBefore->deleted) {
|
||||||
++insertBefore;
|
++insertBefore;
|
||||||
}
|
}
|
||||||
auto insertBeforeIt = (insertBefore == end(_list))
|
const auto foundIndex = (insertBefore == end(_list))
|
||||||
? end(proxies)
|
? int(proxies.size())
|
||||||
: ranges::find(proxies, insertBefore->data);
|
: _settings.indexInList(insertBefore->data);
|
||||||
proxies.insert(insertBeforeIt, item->data);
|
const auto insertIndex = (foundIndex >= 0)
|
||||||
|
? foundIndex
|
||||||
|
: int(proxies.size());
|
||||||
|
_settings.insertToList(insertIndex, item->data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_settings.selected() && _lastSelectedProxy == item->data) {
|
if (!_settings.selected() && _lastSelectedProxy == item->data) {
|
||||||
@@ -1919,8 +1965,8 @@ object_ptr<Ui::BoxContent> ProxiesBoxController::editItemBox(int id) {
|
|||||||
void ProxiesBoxController::replaceItemWith(
|
void ProxiesBoxController::replaceItemWith(
|
||||||
std::vector<Item>::iterator which,
|
std::vector<Item>::iterator which,
|
||||||
std::vector<Item>::iterator with) {
|
std::vector<Item>::iterator with) {
|
||||||
auto &proxies = _settings.list();
|
const auto removed = _settings.removeFromList(which->data);
|
||||||
proxies.erase(ranges::remove(proxies, which->data), end(proxies));
|
Assert(removed);
|
||||||
|
|
||||||
_views.fire({ which->id });
|
_views.fire({ which->id });
|
||||||
_list.erase(which);
|
_list.erase(which);
|
||||||
@@ -1939,10 +1985,8 @@ void ProxiesBoxController::replaceItemValue(
|
|||||||
restoreItem(which->id);
|
restoreItem(which->id);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &proxies = _settings.list();
|
const auto replaced = _settings.replaceInList(which->data, proxy);
|
||||||
const auto i = ranges::find(proxies, which->data);
|
Assert(replaced);
|
||||||
Assert(i != end(proxies));
|
|
||||||
*i = proxy;
|
|
||||||
which->data = proxy;
|
which->data = proxy;
|
||||||
refreshChecker(*which);
|
refreshChecker(*which);
|
||||||
|
|
||||||
@@ -1978,8 +2022,7 @@ bool ProxiesBoxController::contains(const ProxyData &proxy) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProxiesBoxController::addNewItem(const ProxyData &proxy) {
|
void ProxiesBoxController::addNewItem(const ProxyData &proxy) {
|
||||||
auto &proxies = _settings.list();
|
_settings.addToList(proxy);
|
||||||
proxies.push_back(proxy);
|
|
||||||
|
|
||||||
_list.push_back({ ++_idCounter, proxy });
|
_list.push_back({ ++_idCounter, proxy });
|
||||||
refreshChecker(_list.back());
|
refreshChecker(_list.back());
|
||||||
@@ -2016,6 +2059,22 @@ void ProxiesBoxController::setProxyForCalls(bool enabled) {
|
|||||||
saveDelayed();
|
saveDelayed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProxiesBoxController::setProxyRotationEnabled(bool enabled) {
|
||||||
|
if (_settings.proxyRotationEnabled() == enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_settings.setProxyRotationEnabled(enabled);
|
||||||
|
saveDelayed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxiesBoxController::setProxyRotationTimeout(int value) {
|
||||||
|
if (_settings.proxyRotationTimeout() == value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_settings.setProxyRotationTimeout(value);
|
||||||
|
saveDelayed();
|
||||||
|
}
|
||||||
|
|
||||||
void ProxiesBoxController::setTryIPv6(bool enabled) {
|
void ProxiesBoxController::setTryIPv6(bool enabled) {
|
||||||
if (Core::App().settings().proxy().tryIPv6() == enabled) {
|
if (Core::App().settings().proxy().tryIPv6() == enabled) {
|
||||||
return;
|
return;
|
||||||
@@ -2027,6 +2086,7 @@ void ProxiesBoxController::setTryIPv6(bool enabled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProxiesBoxController::saveDelayed() {
|
void ProxiesBoxController::saveDelayed() {
|
||||||
|
Core::App().proxyRotationSettingsChanged();
|
||||||
_saveTimer.callOnce(kSaveSettingsDelayedTimeout);
|
_saveTimer.callOnce(kSaveSettingsDelayedTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2037,7 +2097,7 @@ auto ProxiesBoxController::views() const -> rpl::producer<ItemView> {
|
|||||||
rpl::producer<bool> ProxiesBoxController::listShareableChanges() const {
|
rpl::producer<bool> ProxiesBoxController::listShareableChanges() const {
|
||||||
return _views.events_starting_with(ItemView()) | rpl::map([=] {
|
return _views.events_starting_with(ItemView()) | rpl::map([=] {
|
||||||
for (const auto &item : _list) {
|
for (const auto &item : _list) {
|
||||||
if (!item.deleted) {
|
if (!item.deleted && ProxyDataIsShareable(item.data)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2064,8 +2124,7 @@ void ProxiesBoxController::updateView(const Item &item) {
|
|||||||
}
|
}
|
||||||
return ItemState::Connecting;
|
return ItemState::Connecting;
|
||||||
}();
|
}();
|
||||||
const auto supportsShare = (item.data.type == Type::Socks5)
|
const auto supportsShare = ProxyDataIsShareable(item.data);
|
||||||
|| (item.data.type == Type::Mtproto);
|
|
||||||
const auto supportsCalls = item.data.supportsCalls();
|
const auto supportsCalls = item.data.supportsCalls();
|
||||||
_views.fire({
|
_views.fire({
|
||||||
item.id,
|
item.id,
|
||||||
@@ -2082,18 +2141,19 @@ void ProxiesBoxController::updateView(const Item &item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ProxiesBoxController::share(const ProxyData &proxy, bool qr) {
|
void ProxiesBoxController::share(const ProxyData &proxy, bool qr) {
|
||||||
if (proxy.type == Type::Http) {
|
if (!ProxyDataIsShareable(proxy)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const auto link = ProxyDataToString(proxy);
|
const auto qrLink = ProxyDataToLocalLink(proxy);
|
||||||
|
const auto shareLink = ProxyDataToPublicLink(_account->session(), proxy);
|
||||||
if (qr) {
|
if (qr) {
|
||||||
_show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
|
_show->showBox(Box([=](not_null<Ui::GenericBox*> box) {
|
||||||
Ui::FillPeerQrBox(box, nullptr, link, rpl::single(QString()));
|
Ui::FillPeerQrBox(box, nullptr, qrLink, rpl::single(QString()));
|
||||||
box->setTitle(tr::lng_proxy_edit_share_qr_box_title());
|
box->setTitle(tr::lng_proxy_edit_share_qr_box_title());
|
||||||
}));
|
}));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
QGuiApplication::clipboard()->setText(link);
|
QGuiApplication::clipboard()->setText(shareLink);
|
||||||
_show->showToast(tr::lng_username_copied(tr::now));
|
_show->showToast(tr::lng_username_copied(tr::now));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ public:
|
|||||||
object_ptr<Ui::BoxContent> addNewItemBox();
|
object_ptr<Ui::BoxContent> addNewItemBox();
|
||||||
bool setProxySettings(ProxyData::Settings value);
|
bool setProxySettings(ProxyData::Settings value);
|
||||||
void setProxyForCalls(bool enabled);
|
void setProxyForCalls(bool enabled);
|
||||||
|
void setProxyRotationEnabled(bool enabled);
|
||||||
|
void setProxyRotationTimeout(int value);
|
||||||
void setTryIPv6(bool enabled);
|
void setTryIPv6(bool enabled);
|
||||||
rpl::producer<ProxyData::Settings> proxySettingsValue() const;
|
rpl::producer<ProxyData::Settings> proxySettingsValue() const;
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kMaxOptionsCount = TodoListData::kMaxOptions;
|
|
||||||
constexpr auto kWarnTitleLimit = 12;
|
constexpr auto kWarnTitleLimit = 12;
|
||||||
constexpr auto kWarnTaskLimit = 24;
|
constexpr auto kWarnTaskLimit = 24;
|
||||||
constexpr auto kErrorLimit = 99;
|
constexpr auto kErrorLimit = 99;
|
||||||
|
|||||||
@@ -7,52 +7,55 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
*/
|
*/
|
||||||
#include "boxes/language_box.h"
|
#include "boxes/language_box.h"
|
||||||
|
|
||||||
#include "data/data_peer_values.h"
|
#include "base/platform/base_platform_info.h"
|
||||||
#include "lang/lang_keys.h"
|
|
||||||
#include "ui/boxes/choose_language_box.h"
|
|
||||||
#include "ui/widgets/checkbox.h"
|
|
||||||
#include "ui/widgets/buttons.h"
|
|
||||||
#include "ui/widgets/labels.h"
|
|
||||||
#include "ui/widgets/multi_select.h"
|
|
||||||
#include "ui/widgets/scroll_area.h"
|
|
||||||
#include "ui/widgets/dropdown_menu.h"
|
|
||||||
#include "ui/widgets/box_content_divider.h"
|
|
||||||
#include "ui/text/text_entity.h"
|
|
||||||
#include "ui/wrap/vertical_layout.h"
|
|
||||||
#include "ui/wrap/slide_wrap.h"
|
|
||||||
#include "ui/effects/ripple_animation.h"
|
|
||||||
#include "ui/toast/toast.h"
|
|
||||||
#include "ui/text/text_options.h"
|
|
||||||
#include "ui/painter.h"
|
|
||||||
#include "ui/vertical_list.h"
|
|
||||||
#include "ui/ui_utility.h"
|
|
||||||
#include "storage/localstorage.h"
|
|
||||||
#include "boxes/abstract_box.h"
|
#include "boxes/abstract_box.h"
|
||||||
#include "boxes/premium_preview_box.h"
|
#include "boxes/premium_preview_box.h"
|
||||||
#include "boxes/translate_box.h"
|
#include "boxes/translate_box.h"
|
||||||
#include "ui/boxes/confirm_box.h"
|
|
||||||
#include "main/main_session.h"
|
|
||||||
#include "mainwidget.h"
|
|
||||||
#include "mainwindow.h"
|
|
||||||
#include "core/application.h"
|
#include "core/application.h"
|
||||||
#include "base/platform/base_platform_info.h"
|
#include "data/data_peer_values.h"
|
||||||
#include "lang/lang_instance.h"
|
|
||||||
#include "lang/lang_cloud_manager.h"
|
#include "lang/lang_cloud_manager.h"
|
||||||
|
#include "lang/lang_instance.h"
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
|
#include "main/main_session.h"
|
||||||
#include "platform/platform_translate_provider.h"
|
#include "platform/platform_translate_provider.h"
|
||||||
#include "settings/settings_common.h"
|
#include "settings/settings_common.h"
|
||||||
#include "spellcheck/spellcheck_types.h"
|
#include "spellcheck/spellcheck_types.h"
|
||||||
|
#include "storage/localstorage.h"
|
||||||
|
#include "ui/accessible/ui_accessible_item.h"
|
||||||
|
#include "ui/boxes/choose_language_box.h"
|
||||||
|
#include "ui/boxes/confirm_box.h"
|
||||||
|
#include "ui/effects/ripple_animation.h"
|
||||||
|
#include "ui/text/text_entity.h"
|
||||||
|
#include "ui/text/text_options.h"
|
||||||
|
#include "ui/toast/toast.h"
|
||||||
|
#include "ui/widgets/box_content_divider.h"
|
||||||
|
#include "ui/widgets/buttons.h"
|
||||||
|
#include "ui/widgets/checkbox.h"
|
||||||
|
#include "ui/widgets/dropdown_menu.h"
|
||||||
|
#include "ui/widgets/labels.h"
|
||||||
|
#include "ui/widgets/multi_select.h"
|
||||||
|
#include "ui/widgets/scroll_area.h"
|
||||||
|
#include "ui/wrap/slide_wrap.h"
|
||||||
|
#include "ui/wrap/vertical_layout.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
|
#include "ui/screen_reader_mode.h"
|
||||||
|
#include "ui/ui_utility.h"
|
||||||
|
#include "ui/vertical_list.h"
|
||||||
#include "window/window_controller.h"
|
#include "window/window_controller.h"
|
||||||
#include "window/window_session_controller.h"
|
#include "window/window_session_controller.h"
|
||||||
#include "styles/style_layers.h"
|
#include "mainwidget.h"
|
||||||
|
#include "mainwindow.h"
|
||||||
|
|
||||||
#include "styles/style_boxes.h"
|
#include "styles/style_boxes.h"
|
||||||
#include "styles/style_info.h"
|
|
||||||
#include "styles/style_passport.h"
|
|
||||||
#include "styles/style_chat_helpers.h"
|
#include "styles/style_chat_helpers.h"
|
||||||
|
#include "styles/style_info.h"
|
||||||
|
#include "styles/style_layers.h"
|
||||||
#include "styles/style_menu_icons.h"
|
#include "styles/style_menu_icons.h"
|
||||||
|
#include "styles/style_passport.h"
|
||||||
#include "styles/style_settings.h"
|
#include "styles/style_settings.h"
|
||||||
|
|
||||||
#include <QtGui/QGuiApplication>
|
|
||||||
#include <QtGui/QClipboard>
|
#include <QtGui/QClipboard>
|
||||||
|
#include <QtGui/QGuiApplication>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -71,11 +74,13 @@ public:
|
|||||||
|
|
||||||
int count() const;
|
int count() const;
|
||||||
int selected() const;
|
int selected() const;
|
||||||
|
int chosenIndex() const;
|
||||||
void setSelected(int selected);
|
void setSelected(int selected);
|
||||||
rpl::producer<bool> hasSelection() const;
|
rpl::producer<bool> hasSelection() const;
|
||||||
rpl::producer<bool> isEmpty() const;
|
rpl::producer<bool> isEmpty() const;
|
||||||
|
|
||||||
void activateSelected();
|
void activateSelected();
|
||||||
|
void selectSkip(int dir);
|
||||||
rpl::producer<Language> activations() const;
|
rpl::producer<Language> activations() const;
|
||||||
void changeChosen(const QString &chosen);
|
void changeChosen(const QString &chosen);
|
||||||
|
|
||||||
@@ -83,10 +88,24 @@ public:
|
|||||||
|
|
||||||
static int DefaultRowHeight();
|
static int DefaultRowHeight();
|
||||||
|
|
||||||
|
QAccessible::Role accessibilityRole() override;
|
||||||
|
Qt::FocusPolicy accessibilityFocusPolicy() override;
|
||||||
|
QAccessible::Role accessibilityChildRole() const override;
|
||||||
|
QAccessible::State accessibilityChildState(int index) const override;
|
||||||
|
int accessibilityChildCount() const override;
|
||||||
|
QString accessibilityChildName(int index) const override;
|
||||||
|
QRect accessibilityChildRect(int index) const override;
|
||||||
|
int accessibilityChildColumnCount(int row) const override;
|
||||||
|
QAccessible::Role accessibilityChildSubItemRole() const override;
|
||||||
|
QString accessibilityChildSubItemName(int row, int column) const override;
|
||||||
|
QString accessibilityChildSubItemValue(int row, int column) const override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
int resizeGetHeight(int newWidth) override;
|
int resizeGetHeight(int newWidth) override;
|
||||||
|
|
||||||
|
void focusInEvent(QFocusEvent *e) override;
|
||||||
void paintEvent(QPaintEvent *e) override;
|
void paintEvent(QPaintEvent *e) override;
|
||||||
|
void keyPressEvent(QKeyEvent *e) override;
|
||||||
void mouseMoveEvent(QMouseEvent *e) override;
|
void mouseMoveEvent(QMouseEvent *e) override;
|
||||||
void mousePressEvent(QMouseEvent *e) override;
|
void mousePressEvent(QMouseEvent *e) override;
|
||||||
void mouseReleaseEvent(QMouseEvent *e) override;
|
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||||
@@ -154,6 +173,13 @@ private:
|
|||||||
void repaintChecked(not_null<const Row*> row);
|
void repaintChecked(not_null<const Row*> row);
|
||||||
void activateByIndex(int index);
|
void activateByIndex(int index);
|
||||||
|
|
||||||
|
enum class Announce {
|
||||||
|
No,
|
||||||
|
OnChange,
|
||||||
|
Always,
|
||||||
|
};
|
||||||
|
void setSelected(int index, Announce announce);
|
||||||
|
|
||||||
void showMenu(int index);
|
void showMenu(int index);
|
||||||
void setForceRippled(not_null<Row*> row, bool rippled);
|
void setForceRippled(not_null<Row*> row, bool rippled);
|
||||||
bool canShare(not_null<const Row*> row) const;
|
bool canShare(not_null<const Row*> row) const;
|
||||||
@@ -183,6 +209,26 @@ private:
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] bool ForwardListNavigation(
|
||||||
|
not_null<QKeyEvent*> e,
|
||||||
|
not_null<Rows*> rows,
|
||||||
|
int pageHeight) {
|
||||||
|
const auto key = e->key();
|
||||||
|
if (key == Qt::Key_Down) {
|
||||||
|
rows->selectSkip(1);
|
||||||
|
} else if (key == Qt::Key_Up) {
|
||||||
|
rows->selectSkip(-1);
|
||||||
|
} else if (key == Qt::Key_PageDown || key == Qt::Key_PageUp) {
|
||||||
|
const auto perPage = std::max(
|
||||||
|
pageHeight / Rows::DefaultRowHeight(),
|
||||||
|
1);
|
||||||
|
rows->selectSkip((key == Qt::Key_PageDown) ? perPage : -perPage);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
class Content : public Ui::RpWidget {
|
class Content : public Ui::RpWidget {
|
||||||
public:
|
public:
|
||||||
Content(
|
Content(
|
||||||
@@ -288,6 +334,44 @@ Rows::Rows(
|
|||||||
resizeToWidth(width());
|
resizeToWidth(width());
|
||||||
setAttribute(Qt::WA_MouseTracking);
|
setAttribute(Qt::WA_MouseTracking);
|
||||||
update();
|
update();
|
||||||
|
|
||||||
|
setAccessibleName(tr::lng_languages(tr::now));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Rows::focusInEvent(QFocusEvent *e) {
|
||||||
|
if (selected() < 0 && count() > 0) {
|
||||||
|
const auto chosen = chosenIndex();
|
||||||
|
setSelected(chosen >= 0 ? chosen : 0, Announce::No);
|
||||||
|
}
|
||||||
|
RpWidget::focusInEvent(e);
|
||||||
|
const auto index = selected();
|
||||||
|
if (index >= 0) {
|
||||||
|
InvokeQueued(this, [=] {
|
||||||
|
if (selected() == index && hasFocus()) {
|
||||||
|
accessibilityChildFocused(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Rows::keyPressEvent(QKeyEvent *e) {
|
||||||
|
const auto pageHeight = window() ? window()->height() : height();
|
||||||
|
if (ForwardListNavigation(e, this, pageHeight)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto key = e->key();
|
||||||
|
if (key == Qt::Key_Home && count() > 0) {
|
||||||
|
setSelected(0, Announce::Always);
|
||||||
|
} else if (key == Qt::Key_End && count() > 0) {
|
||||||
|
setSelected(count() - 1, Announce::Always);
|
||||||
|
} else if (!e->isAutoRepeat()
|
||||||
|
&& (key == Qt::Key_Space
|
||||||
|
|| key == Qt::Key_Return
|
||||||
|
|| key == Qt::Key_Enter)) {
|
||||||
|
activateSelected();
|
||||||
|
} else {
|
||||||
|
RpWidget::keyPressEvent(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Rows::mouseMoveEvent(QMouseEvent *e) {
|
void Rows::mouseMoveEvent(QMouseEvent *e) {
|
||||||
@@ -555,7 +639,10 @@ void Rows::setForceRippled(not_null<Row*> row, bool rippled) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Rows::activateByIndex(int index) {
|
void Rows::activateByIndex(int index) {
|
||||||
|
_chosen = rowByIndex(index).data.id;
|
||||||
_activations.fire_copy(rowByIndex(index).data);
|
_activations.fire_copy(rowByIndex(index).data);
|
||||||
|
accessibilityChildStateChanged(index, { .checked = true });
|
||||||
|
accessibilityChildNameChanged(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Rows::leaveEventHook(QEvent *e) {
|
void Rows::leaveEventHook(QEvent *e) {
|
||||||
@@ -631,6 +718,15 @@ int Rows::selected() const {
|
|||||||
return indexFromSelection(_selected);
|
return indexFromSelection(_selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int Rows::chosenIndex() const {
|
||||||
|
for (auto i = 0, n = count(); i < n; ++i) {
|
||||||
|
if (rowByIndex(i).data.id == _chosen) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
void Rows::activateSelected() {
|
void Rows::activateSelected() {
|
||||||
const auto index = selected();
|
const auto index = selected();
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
@@ -638,24 +734,66 @@ void Rows::activateSelected() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Rows::selectSkip(int dir) {
|
||||||
|
const auto limit = count();
|
||||||
|
auto now = selected();
|
||||||
|
if (now < 0) {
|
||||||
|
now = chosenIndex();
|
||||||
|
}
|
||||||
|
if (now >= 0) {
|
||||||
|
const auto changed = now + dir;
|
||||||
|
if (changed < 0) {
|
||||||
|
setSelected(0, Announce::Always);
|
||||||
|
} else if (changed >= limit) {
|
||||||
|
setSelected(limit - 1, Announce::Always);
|
||||||
|
} else {
|
||||||
|
setSelected(changed, Announce::Always);
|
||||||
|
}
|
||||||
|
} else if (dir > 0) {
|
||||||
|
setSelected(0, Announce::Always);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rpl::producer<Language> Rows::activations() const {
|
rpl::producer<Language> Rows::activations() const {
|
||||||
return _activations.events();
|
return _activations.events();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Rows::changeChosen(const QString &chosen) {
|
void Rows::changeChosen(const QString &chosen) {
|
||||||
|
const auto oldIndex = chosenIndex();
|
||||||
|
_chosen = chosen;
|
||||||
for (const auto &row : _rows) {
|
for (const auto &row : _rows) {
|
||||||
row.check->setChecked(row.data.id == chosen, anim::type::normal);
|
row.check->setChecked(row.data.id == chosen, anim::type::normal);
|
||||||
}
|
}
|
||||||
|
const auto newIndex = chosenIndex();
|
||||||
|
if (newIndex != oldIndex && newIndex >= 0) {
|
||||||
|
accessibilityChildStateChanged(newIndex, { .checked = true });
|
||||||
|
accessibilityChildNameChanged(newIndex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Rows::setSelected(int selected) {
|
void Rows::setSelected(int selected) {
|
||||||
|
setSelected(selected, Announce::OnChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Rows::setSelected(int selected, Announce announce) {
|
||||||
_mouseSelection = false;
|
_mouseSelection = false;
|
||||||
const auto limit = count();
|
const auto limit = count();
|
||||||
if (selected >= 0 && selected < limit) {
|
const auto clamped = (selected >= 0 && selected < limit)
|
||||||
updateSelected(RowSelection{ selected });
|
? selected
|
||||||
|
: -1;
|
||||||
|
const auto changed = (indexFromSelection(_selected) != clamped)
|
||||||
|
|| (clamped < 0 && !v::is_null(_selected));
|
||||||
|
if (clamped >= 0) {
|
||||||
|
updateSelected(RowSelection{ clamped });
|
||||||
} else {
|
} else {
|
||||||
updateSelected({});
|
updateSelected({});
|
||||||
}
|
}
|
||||||
|
const auto shouldAnnounce = (announce == Announce::Always)
|
||||||
|
|| (announce == Announce::OnChange && changed);
|
||||||
|
if (shouldAnnounce && clamped >= 0) {
|
||||||
|
accessibilityChildNameChanged(clamped);
|
||||||
|
accessibilityChildFocused(clamped);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rpl::producer<bool> Rows::hasSelection() const {
|
rpl::producer<bool> Rows::hasSelection() const {
|
||||||
@@ -875,6 +1013,84 @@ void Rows::paintEvent(QPaintEvent *e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QAccessible::Role Rows::accessibilityRole() {
|
||||||
|
return QAccessible::List;
|
||||||
|
}
|
||||||
|
|
||||||
|
Qt::FocusPolicy Rows::accessibilityFocusPolicy() {
|
||||||
|
return Qt::TabFocus;
|
||||||
|
}
|
||||||
|
|
||||||
|
QAccessible::Role Rows::accessibilityChildRole() const {
|
||||||
|
return QAccessible::RadioButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
QAccessible::State Rows::accessibilityChildState(int index) const {
|
||||||
|
QAccessible::State state;
|
||||||
|
if (Ui::ScreenReaderModeActive()) {
|
||||||
|
state.focusable = true;
|
||||||
|
}
|
||||||
|
state.checkable = true;
|
||||||
|
state.checked = (index == chosenIndex());
|
||||||
|
if (index == selected()) {
|
||||||
|
state.active = true;
|
||||||
|
if (hasFocus()) {
|
||||||
|
state.focused = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Rows::accessibilityChildCount() const {
|
||||||
|
return count();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Rows::accessibilityChildName(int index) const {
|
||||||
|
if (index < 0 || index >= count()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const auto &row = rowByIndex(index);
|
||||||
|
return row.data.nativeName + u", "_q + row.data.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect Rows::accessibilityChildRect(int index) const {
|
||||||
|
if (index < 0 || index >= count()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const auto &row = rowByIndex(index);
|
||||||
|
return QRect(0, row.top, width(), row.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Rows::accessibilityChildColumnCount(int row) const {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
QAccessible::Role Rows::accessibilityChildSubItemRole() const {
|
||||||
|
return QAccessible::Cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Rows::accessibilityChildSubItemName(int row, int column) const {
|
||||||
|
if (column == 0) {
|
||||||
|
return tr::lng_sr_languages_column_native(tr::now);
|
||||||
|
} else if (column == 1) {
|
||||||
|
return tr::lng_sr_languages_column_name(tr::now);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Rows::accessibilityChildSubItemValue(int row, int column) const {
|
||||||
|
if (row < 0 || row >= count()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const auto &data = rowByIndex(row).data;
|
||||||
|
if (column == 0) {
|
||||||
|
return data.nativeName;
|
||||||
|
} else if (column == 1) {
|
||||||
|
return data.name;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
Content::Content(
|
Content::Content(
|
||||||
QWidget *parent,
|
QWidget *parent,
|
||||||
const Languages &recent,
|
const Languages &recent,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "boxes/premium_preview_box.h"
|
#include "boxes/premium_preview_box.h"
|
||||||
#include "boxes/send_gif_with_caption_box.h"
|
#include "boxes/send_gif_with_caption_box.h"
|
||||||
#include "boxes/send_credits_box.h"
|
#include "boxes/send_credits_box.h"
|
||||||
|
#include "boxes/send_files_box_reply_header.h"
|
||||||
#include "ui/effects/scroll_content_shadow.h"
|
#include "ui/effects/scroll_content_shadow.h"
|
||||||
#include "ui/widgets/fields/number_input.h"
|
#include "ui/widgets/fields/number_input.h"
|
||||||
#include "ui/widgets/checkbox.h"
|
#include "ui/widgets/checkbox.h"
|
||||||
@@ -429,6 +430,10 @@ int SendFilesBox::Block::fromIndex() const {
|
|||||||
return _from;
|
return _from;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SendFilesBox::Block::isSingleFile() const {
|
||||||
|
return !_isAlbum && !_isSingleMedia;
|
||||||
|
}
|
||||||
|
|
||||||
int SendFilesBox::Block::tillIndex() const {
|
int SendFilesBox::Block::tillIndex() const {
|
||||||
return _till;
|
return _till;
|
||||||
}
|
}
|
||||||
@@ -646,9 +651,58 @@ SendFilesBox::SendFilesBox(QWidget*, SendFilesBoxDescriptor &&descriptor)
|
|||||||
, _inner(
|
, _inner(
|
||||||
_scroll->setOwnedWidget(
|
_scroll->setOwnedWidget(
|
||||||
object_ptr<Ui::VerticalLayout>(_scroll.data()))) {
|
object_ptr<Ui::VerticalLayout>(_scroll.data()))) {
|
||||||
|
setReplyTo(descriptor.replyTo);
|
||||||
enqueueNextPrepare();
|
enqueueNextPrepare();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SendFilesBox::setReplyTo(FullReplyTo replyTo) {
|
||||||
|
if (_replyTo == replyTo) {
|
||||||
|
return;
|
||||||
|
} else if (!replyTo.messageId || !replyTo.messageId.peer) {
|
||||||
|
_replyTo = {};
|
||||||
|
if (_replyHeader) {
|
||||||
|
_replyHeader->hideAnimated();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_replyTo = replyTo;
|
||||||
|
if (_replyHeader) {
|
||||||
|
_replyHeader = nullptr;
|
||||||
|
_replyHeaderHeight = 0;
|
||||||
|
}
|
||||||
|
_replyHeader = std::make_unique<SendFiles::ReplyPillHeader>(
|
||||||
|
this,
|
||||||
|
_show,
|
||||||
|
std::move(replyTo));
|
||||||
|
_replyHeader->setRoundedShapeBelow(
|
||||||
|
!_blocks.empty() && !_blocks.front().isSingleFile());
|
||||||
|
_replyHeader->show();
|
||||||
|
_replyHeader->desiredHeight(
|
||||||
|
) | rpl::on_next([=](int height) {
|
||||||
|
if (_replyHeaderHeight.current() != height) {
|
||||||
|
_replyHeaderHeight = height;
|
||||||
|
updateBoxSize();
|
||||||
|
updateControlsGeometry();
|
||||||
|
}
|
||||||
|
}, _replyHeader->lifetime());
|
||||||
|
_replyHeader->closeRequests(
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
_replyTo = {};
|
||||||
|
if (_replyHeader) {
|
||||||
|
_replyHeader->hideAnimated();
|
||||||
|
}
|
||||||
|
}, _replyHeader->lifetime());
|
||||||
|
_replyHeader->hideFinished(
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
InvokeQueued(this, [=] {
|
||||||
|
_replyHeader = nullptr;
|
||||||
|
_replyHeaderHeight = 0;
|
||||||
|
updateBoxSize();
|
||||||
|
updateControlsGeometry();
|
||||||
|
});
|
||||||
|
}, _replyHeader->lifetime());
|
||||||
|
}
|
||||||
|
|
||||||
Fn<SendMenu::Details()> SendFilesBox::prepareSendMenuDetails(
|
Fn<SendMenu::Details()> SendFilesBox::prepareSendMenuDetails(
|
||||||
const SendFilesBoxDescriptor &descriptor) {
|
const SendFilesBoxDescriptor &descriptor) {
|
||||||
auto initial = descriptor.sendMenuDetails;
|
auto initial = descriptor.sendMenuDetails;
|
||||||
@@ -1219,6 +1273,10 @@ void SendFilesBox::generatePreviewFrom(int fromBlock) {
|
|||||||
if (albumStart >= 0) {
|
if (albumStart >= 0) {
|
||||||
pushBlock(albumStart, _list.files.size());
|
pushBlock(albumStart, _list.files.size());
|
||||||
}
|
}
|
||||||
|
if (_replyHeader) {
|
||||||
|
_replyHeader->setRoundedShapeBelow(
|
||||||
|
!_blocks.empty() && !_blocks.front().isSingleFile());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SendFilesBox::pushBlock(int from, int till) {
|
void SendFilesBox::pushBlock(int from, int till) {
|
||||||
@@ -2095,6 +2153,7 @@ void SendFilesBox::updateBoxSize() {
|
|||||||
if (!_caption->isHidden()) {
|
if (!_caption->isHidden()) {
|
||||||
footerHeight += st::boxPhotoCaptionSkip + _caption->height();
|
footerHeight += st::boxPhotoCaptionSkip + _caption->height();
|
||||||
}
|
}
|
||||||
|
footerHeight += _replyHeaderHeight.current();
|
||||||
const auto pairs = std::array<std::pair<RpWidget*, int>, 5>{ {
|
const auto pairs = std::array<std::pair<RpWidget*, int>, 5>{ {
|
||||||
{ _groupFiles.data(), st::boxPhotoCompressedSkip },
|
{ _groupFiles.data(), st::boxPhotoCompressedSkip },
|
||||||
{ _sendImagesAsPhotos.data(), st::boxPhotoCompressedSkip },
|
{ _sendImagesAsPhotos.data(), st::boxPhotoCompressedSkip },
|
||||||
@@ -2184,8 +2243,14 @@ void SendFilesBox::updateControlsGeometry() {
|
|||||||
bottom -= pair.second + pointer->heightNoMargins();
|
bottom -= pair.second + pointer->heightNoMargins();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_scroll->resize(width(), bottom - _titleHeight.current());
|
const auto replyH = _replyHeaderHeight.current();
|
||||||
_scroll->move(0, _titleHeight.current());
|
const auto replyTopOverlap = std::min(st::boxPhotoCaptionSkip, replyH);
|
||||||
|
const auto replyTop = _titleHeight.current() - replyTopOverlap;
|
||||||
|
if (_replyHeader) {
|
||||||
|
_replyHeader->setGeometry(0, replyTop, width(), replyH);
|
||||||
|
}
|
||||||
|
_scroll->resize(width(), bottom - replyTop - replyH);
|
||||||
|
_scroll->move(0, replyTop + replyH);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SendFilesBox::showFinished() {
|
void SendFilesBox::showFinished() {
|
||||||
@@ -2333,7 +2398,7 @@ void SendFilesBox::send(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_confirmedCallback(std::move(bundle), options);
|
_confirmedCallback(std::move(bundle), options, _replyTo);
|
||||||
}
|
}
|
||||||
closeBox();
|
closeBox();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "base/flags.h"
|
#include "base/flags.h"
|
||||||
|
#include "data/data_msg_id.h"
|
||||||
#include "ui/layers/box_content.h"
|
#include "ui/layers/box_content.h"
|
||||||
#include "ui/chat/attach/attach_prepare.h"
|
#include "ui/chat/attach/attach_prepare.h"
|
||||||
#include "ui/chat/attach/attach_send_files_way.h"
|
#include "ui/chat/attach/attach_send_files_way.h"
|
||||||
@@ -62,6 +63,10 @@ class CharactersLimitLabel;
|
|||||||
class ComposeAiButton;
|
class ComposeAiButton;
|
||||||
} // namespace HistoryView::Controls
|
} // namespace HistoryView::Controls
|
||||||
|
|
||||||
|
namespace SendFiles {
|
||||||
|
class ReplyPillHeader;
|
||||||
|
} // namespace SendFiles
|
||||||
|
|
||||||
enum class SendFilesAllow {
|
enum class SendFilesAllow {
|
||||||
OnlyOne = (1 << 0),
|
OnlyOne = (1 << 0),
|
||||||
Photos = (1 << 1),
|
Photos = (1 << 1),
|
||||||
@@ -91,7 +96,8 @@ using SendFilesCheck = Fn<bool(
|
|||||||
|
|
||||||
using SendFilesConfirmed = Fn<void(
|
using SendFilesConfirmed = Fn<void(
|
||||||
std::shared_ptr<Ui::PreparedBundle>,
|
std::shared_ptr<Ui::PreparedBundle>,
|
||||||
Api::SendOptions)>;
|
Api::SendOptions,
|
||||||
|
FullReplyTo)>;
|
||||||
|
|
||||||
struct SendFilesBoxDescriptor {
|
struct SendFilesBoxDescriptor {
|
||||||
std::shared_ptr<ChatHelpers::Show> show;
|
std::shared_ptr<ChatHelpers::Show> show;
|
||||||
@@ -105,6 +111,7 @@ struct SendFilesBoxDescriptor {
|
|||||||
const style::ComposeControls *stOverride = nullptr;
|
const style::ComposeControls *stOverride = nullptr;
|
||||||
SendFilesConfirmed confirmed;
|
SendFilesConfirmed confirmed;
|
||||||
Fn<void()> cancelled;
|
Fn<void()> cancelled;
|
||||||
|
FullReplyTo replyTo;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SendFilesBox : public Ui::BoxContent {
|
class SendFilesBox : public Ui::BoxContent {
|
||||||
@@ -129,6 +136,7 @@ public:
|
|||||||
void setCancelledCallback(Fn<void()> callback) {
|
void setCancelledCallback(Fn<void()> callback) {
|
||||||
_cancelledCallback = std::move(callback);
|
_cancelledCallback = std::move(callback);
|
||||||
}
|
}
|
||||||
|
void setReplyTo(FullReplyTo replyTo);
|
||||||
|
|
||||||
[[nodiscard]] rpl::producer<TextWithTags> takeTextWithTagsRequests() const;
|
[[nodiscard]] rpl::producer<TextWithTags> takeTextWithTagsRequests() const;
|
||||||
|
|
||||||
@@ -164,6 +172,7 @@ private:
|
|||||||
|
|
||||||
[[nodiscard]] int fromIndex() const;
|
[[nodiscard]] int fromIndex() const;
|
||||||
[[nodiscard]] int tillIndex() const;
|
[[nodiscard]] int tillIndex() const;
|
||||||
|
[[nodiscard]] bool isSingleFile() const;
|
||||||
[[nodiscard]] object_ptr<Ui::RpWidget> takeWidget();
|
[[nodiscard]] object_ptr<Ui::RpWidget> takeWidget();
|
||||||
|
|
||||||
[[nodiscard]] rpl::producer<int> itemDeleteRequest() const;
|
[[nodiscard]] rpl::producer<int> itemDeleteRequest() const;
|
||||||
@@ -316,6 +325,10 @@ private:
|
|||||||
rpl::variable<int> _footerHeight = 0;
|
rpl::variable<int> _footerHeight = 0;
|
||||||
rpl::lifetime _dimensionsLifetime;
|
rpl::lifetime _dimensionsLifetime;
|
||||||
|
|
||||||
|
std::unique_ptr<SendFiles::ReplyPillHeader> _replyHeader;
|
||||||
|
rpl::variable<int> _replyHeaderHeight = 0;
|
||||||
|
FullReplyTo _replyTo;
|
||||||
|
|
||||||
object_ptr<Ui::ScrollArea> _scroll;
|
object_ptr<Ui::ScrollArea> _scroll;
|
||||||
QPointer<Ui::VerticalLayout> _inner;
|
QPointer<Ui::VerticalLayout> _inner;
|
||||||
std::deque<Block> _blocks;
|
std::deque<Block> _blocks;
|
||||||
|
|||||||
@@ -0,0 +1,372 @@
|
|||||||
|
/*
|
||||||
|
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 "boxes/send_files_box_reply_header.h"
|
||||||
|
|
||||||
|
#include "chat_helpers/compose/compose_show.h"
|
||||||
|
#include "core/ui_integration.h"
|
||||||
|
#include "data/data_changes.h"
|
||||||
|
#include "data/data_media_types.h"
|
||||||
|
#include "data/data_session.h"
|
||||||
|
#include "history/history.h"
|
||||||
|
#include "history/history_item.h"
|
||||||
|
#include "history/view/history_view_reply.h"
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
|
#include "main/main_session.h"
|
||||||
|
#include "ui/chat/chat_style.h"
|
||||||
|
#include "ui/effects/spoiler_mess.h"
|
||||||
|
#include "ui/image/image.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
|
#include "ui/text/text_options.h"
|
||||||
|
#include "ui/power_saving.h"
|
||||||
|
#include "ui/widgets/buttons.h"
|
||||||
|
#include "window/window_session_controller.h"
|
||||||
|
#include "apiwrap.h"
|
||||||
|
#include "styles/style_boxes.h"
|
||||||
|
#include "styles/style_chat.h"
|
||||||
|
#include "styles/style_chat_helpers.h"
|
||||||
|
#include "styles/style_dialogs.h"
|
||||||
|
|
||||||
|
namespace SendFiles {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kAnimationDuration = crl::time(180);
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ReplyPillHeader::ReplyPillHeader(
|
||||||
|
QWidget *parent,
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
FullReplyTo replyTo)
|
||||||
|
: RpWidget(parent)
|
||||||
|
, _show(std::move(show))
|
||||||
|
, _data(&_show->session().data())
|
||||||
|
, _replyTo(std::move(replyTo))
|
||||||
|
, _cancel(Ui::CreateChild<Ui::IconButton>(this, st::sendFilesReplyCancel)) {
|
||||||
|
resize(
|
||||||
|
parent->width(),
|
||||||
|
st::boxPhotoCaptionSkip + st::historyReplyHeight);
|
||||||
|
|
||||||
|
_cancel->setAccessibleName(tr::lng_cancel(tr::now));
|
||||||
|
_cancel->setClickedCallback([=] {
|
||||||
|
hideAnimated();
|
||||||
|
});
|
||||||
|
|
||||||
|
setShownMessage(_data->message(_replyTo.messageId));
|
||||||
|
|
||||||
|
_data->session().changes().messageUpdates(
|
||||||
|
Data::MessageUpdate::Flag::Edited
|
||||||
|
| Data::MessageUpdate::Flag::Destroyed
|
||||||
|
) | rpl::filter([=](const Data::MessageUpdate &update) {
|
||||||
|
return (update.item == _shownMessage);
|
||||||
|
}) | rpl::on_next([=](const Data::MessageUpdate &update) {
|
||||||
|
if (update.flags & Data::MessageUpdate::Flag::Destroyed) {
|
||||||
|
_shownMessage = nullptr;
|
||||||
|
_shownMessageName.clear();
|
||||||
|
_shownMessageText.clear();
|
||||||
|
hideAnimated();
|
||||||
|
} else {
|
||||||
|
updateShownMessageText();
|
||||||
|
RpWidget::update();
|
||||||
|
}
|
||||||
|
}, lifetime());
|
||||||
|
|
||||||
|
animationCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplyPillHeader::~ReplyPillHeader() = default;
|
||||||
|
|
||||||
|
rpl::producer<> ReplyPillHeader::closeRequests() const {
|
||||||
|
return _closeRequests.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<> ReplyPillHeader::hideFinished() const {
|
||||||
|
return _hideFinished.value()
|
||||||
|
| rpl::filter(rpl::mappers::_1)
|
||||||
|
| rpl::to_empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<int> ReplyPillHeader::desiredHeight() const {
|
||||||
|
return _desiredHeight.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyPillHeader::setRoundedShapeBelow(bool value) {
|
||||||
|
if (_roundedShapeBelow == value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_roundedShapeBelow = value;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyPillHeader::hideAnimated() {
|
||||||
|
if (_hiding) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_hiding = true;
|
||||||
|
_closeRequests.fire({});
|
||||||
|
_showAnimation.start(
|
||||||
|
[=] { animationCallback(); },
|
||||||
|
1.,
|
||||||
|
0.,
|
||||||
|
kAnimationDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyPillHeader::animationCallback() {
|
||||||
|
const auto full = st::boxPhotoCaptionSkip + st::historyReplyHeight;
|
||||||
|
const auto value = _showAnimation.value(_hiding ? 0. : 1.);
|
||||||
|
_desiredHeight = int(base::SafeRound(full * value));
|
||||||
|
update();
|
||||||
|
if (_hiding && !_showAnimation.animating()) {
|
||||||
|
_hideFinished = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyPillHeader::resolveMessageData() {
|
||||||
|
const auto id = _replyTo.messageId;
|
||||||
|
if (!id || !id.peer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto peer = _data->peer(id.peer);
|
||||||
|
const auto itemId = id.msg;
|
||||||
|
const auto callback = crl::guard(this, [=] {
|
||||||
|
if (!_shownMessage) {
|
||||||
|
if (const auto message = _data->message(peer, itemId)) {
|
||||||
|
setShownMessage(message);
|
||||||
|
} else {
|
||||||
|
hideAnimated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_data->session().api().requestMessageData(peer, itemId, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyPillHeader::setShownMessage(HistoryItem *item) {
|
||||||
|
_shownMessage = item;
|
||||||
|
if (item) {
|
||||||
|
updateShownMessageText();
|
||||||
|
const auto context = Core::TextContext({
|
||||||
|
.session = &item->history()->session(),
|
||||||
|
.customEmojiLoopLimit = 1,
|
||||||
|
});
|
||||||
|
_shownMessageName.setMarkedText(
|
||||||
|
st::fwdTextStyle,
|
||||||
|
HistoryView::Reply::ComposePreviewName(
|
||||||
|
item->history(),
|
||||||
|
item,
|
||||||
|
_replyTo),
|
||||||
|
Ui::NameTextOptions(),
|
||||||
|
context);
|
||||||
|
} else {
|
||||||
|
_shownMessageName.clear();
|
||||||
|
_shownMessageText.clear();
|
||||||
|
resolveMessageData();
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyPillHeader::updateShownMessageText() {
|
||||||
|
Expects(_shownMessage != nullptr);
|
||||||
|
|
||||||
|
const auto context = Core::TextContext({
|
||||||
|
.session = &_data->session(),
|
||||||
|
.repaint = [=] { customEmojiRepaint(); },
|
||||||
|
});
|
||||||
|
_shownMessageText.setMarkedText(
|
||||||
|
st::messageTextStyle,
|
||||||
|
(_replyTo.quote.empty()
|
||||||
|
? _shownMessage->inReplyText()
|
||||||
|
: _replyTo.quote),
|
||||||
|
Ui::DialogTextOptions(),
|
||||||
|
context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyPillHeader::customEmojiRepaint() {
|
||||||
|
if (_repaintScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_repaintScheduled = true;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyPillHeader::resizeEvent(QResizeEvent *e) {
|
||||||
|
_cancel->moveToRight(
|
||||||
|
st::boxPhotoPadding.right() + st::sendBoxAlbumGroupSkipRight,
|
||||||
|
(st::historyReplyHeight - _cancel->height()) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplyPillHeader::paintEvent(QPaintEvent *e) {
|
||||||
|
_repaintScheduled = false;
|
||||||
|
|
||||||
|
Painter p(this);
|
||||||
|
p.setInactive(_show->paused(Window::GifPauseReason::Layer));
|
||||||
|
|
||||||
|
const auto left = st::boxPhotoPadding.left();
|
||||||
|
const auto right = st::boxPhotoPadding.right();
|
||||||
|
const auto bottomSkip = st::boxPhotoCaptionSkip;
|
||||||
|
const auto pillHeight = height() - bottomSkip;
|
||||||
|
if (pillHeight <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto pillRect = QRect(
|
||||||
|
left,
|
||||||
|
0,
|
||||||
|
width() - left - right,
|
||||||
|
pillHeight);
|
||||||
|
if (pillRect.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(st::windowBgOver);
|
||||||
|
const auto topRadius = st::bubbleRadiusLarge;
|
||||||
|
const auto bottomRadius = _roundedShapeBelow
|
||||||
|
? st::bubbleRadiusSmall
|
||||||
|
: st::bubbleRadiusLarge;
|
||||||
|
const auto rectF = QRectF(pillRect);
|
||||||
|
auto path = QPainterPath();
|
||||||
|
path.moveTo(rectF.left() + topRadius, rectF.top());
|
||||||
|
path.lineTo(rectF.right() - topRadius, rectF.top());
|
||||||
|
path.arcTo(
|
||||||
|
rectF.right() - 2 * topRadius,
|
||||||
|
rectF.top(),
|
||||||
|
2 * topRadius,
|
||||||
|
2 * topRadius,
|
||||||
|
90, -90);
|
||||||
|
path.lineTo(rectF.right(), rectF.bottom() - bottomRadius);
|
||||||
|
path.arcTo(
|
||||||
|
rectF.right() - 2 * bottomRadius,
|
||||||
|
rectF.bottom() - 2 * bottomRadius,
|
||||||
|
2 * bottomRadius,
|
||||||
|
2 * bottomRadius,
|
||||||
|
0, -90);
|
||||||
|
path.lineTo(rectF.left() + bottomRadius, rectF.bottom());
|
||||||
|
path.arcTo(
|
||||||
|
rectF.left(),
|
||||||
|
rectF.bottom() - 2 * bottomRadius,
|
||||||
|
2 * bottomRadius,
|
||||||
|
2 * bottomRadius,
|
||||||
|
270, -90);
|
||||||
|
path.lineTo(rectF.left(), rectF.top() + topRadius);
|
||||||
|
path.arcTo(
|
||||||
|
rectF.left(),
|
||||||
|
rectF.top(),
|
||||||
|
2 * topRadius,
|
||||||
|
2 * topRadius,
|
||||||
|
180, -90);
|
||||||
|
path.closeSubpath();
|
||||||
|
p.fillPath(path, st::windowBgOver);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto iconPos = st::sendFilesReplyIconPosition
|
||||||
|
+ QPoint(pillRect.left(), pillRect.top());
|
||||||
|
if (!_replyTo.quote.empty()) {
|
||||||
|
st::historyQuoteIcon.paint(p, iconPos, width());
|
||||||
|
} else {
|
||||||
|
st::historyReplyIcon.paint(p, iconPos, width());
|
||||||
|
// Remove 'settings' mini-icon.
|
||||||
|
p.fillRect(
|
||||||
|
QRect(
|
||||||
|
QPoint(style::ConvertScale(16), style::ConvertScale(5))
|
||||||
|
+ iconPos,
|
||||||
|
QSize(style::ConvertScale(11), style::ConvertScale(8))),
|
||||||
|
st::windowBgOver);
|
||||||
|
p.fillRect(
|
||||||
|
QRect(
|
||||||
|
QPoint(style::ConvertScale(22), style::ConvertScale(13))
|
||||||
|
+ iconPos,
|
||||||
|
QSize(style::ConvertScale(5), style::ConvertScale(2))),
|
||||||
|
st::windowBgOver);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto replySkip = st::historyReplySkip;
|
||||||
|
const auto textLeft = pillRect.left() + replySkip;
|
||||||
|
const auto availableWidth = _cancel->x() - textLeft;
|
||||||
|
if (availableWidth <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto pillCenterY = pillRect.top()
|
||||||
|
+ st::historyReplyHeight / 2;
|
||||||
|
|
||||||
|
if (!_shownMessage) {
|
||||||
|
p.setFont(st::msgDateFont);
|
||||||
|
p.setPen(st::historyComposeAreaFgService);
|
||||||
|
const auto top = pillCenterY - st::msgDateFont->height / 2;
|
||||||
|
p.drawText(
|
||||||
|
textLeft,
|
||||||
|
top + st::msgDateFont->ascent,
|
||||||
|
st::msgDateFont->elided(
|
||||||
|
tr::lng_profile_loading(tr::now),
|
||||||
|
availableWidth));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto media = _shownMessage->media();
|
||||||
|
const auto hasPreview = media && media->hasReplyPreview();
|
||||||
|
const auto preview = hasPreview ? media->replyPreview() : nullptr;
|
||||||
|
const auto spoilered = media && media->hasSpoiler();
|
||||||
|
if (!spoilered) {
|
||||||
|
_previewSpoiler = nullptr;
|
||||||
|
} else if (!_previewSpoiler) {
|
||||||
|
_previewSpoiler = std::make_unique<Ui::SpoilerAnimation>([=] {
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const auto previewSkipValue = st::historyReplyPreview + st::msgReplyBarSkip;
|
||||||
|
const auto previewSkip = (hasPreview && preview) ? previewSkipValue : 0;
|
||||||
|
const auto contentLeft = textLeft + previewSkip;
|
||||||
|
const auto contentAvailable = availableWidth - previewSkip;
|
||||||
|
|
||||||
|
if (preview) {
|
||||||
|
const auto to = QRect(
|
||||||
|
textLeft,
|
||||||
|
pillCenterY - st::historyReplyPreview / 2,
|
||||||
|
st::historyReplyPreview,
|
||||||
|
st::historyReplyPreview);
|
||||||
|
p.drawPixmap(to.x(), to.y(), preview->pixSingle(
|
||||||
|
preview->size() / style::DevicePixelRatio(),
|
||||||
|
{
|
||||||
|
.options = Images::Option::RoundSmall,
|
||||||
|
.outer = to.size(),
|
||||||
|
}));
|
||||||
|
if (_previewSpoiler) {
|
||||||
|
Ui::FillSpoilerRect(
|
||||||
|
p,
|
||||||
|
to,
|
||||||
|
Ui::DefaultImageSpoiler().frame(
|
||||||
|
_previewSpoiler->index(crl::now(), p.inactive())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.setPen(st::historyReplyNameFg);
|
||||||
|
p.setFont(st::msgServiceNameFont);
|
||||||
|
_shownMessageName.drawElided(
|
||||||
|
p,
|
||||||
|
contentLeft,
|
||||||
|
pillRect.top() + st::msgReplyPadding.top(),
|
||||||
|
contentAvailable);
|
||||||
|
|
||||||
|
p.setPen(st::historyComposeAreaFg);
|
||||||
|
_shownMessageText.draw(p, {
|
||||||
|
.position = QPoint(
|
||||||
|
contentLeft,
|
||||||
|
pillRect.top()
|
||||||
|
+ st::msgReplyPadding.top()
|
||||||
|
+ st::msgServiceNameFont->height),
|
||||||
|
.availableWidth = contentAvailable,
|
||||||
|
.palette = &st::historyComposeAreaPalette,
|
||||||
|
.spoiler = Ui::Text::DefaultSpoilerCache(),
|
||||||
|
.now = crl::now(),
|
||||||
|
.pausedEmoji = p.inactive() || On(PowerSaving::kEmojiChat),
|
||||||
|
.pausedSpoiler = p.inactive() || On(PowerSaving::kChatSpoiler),
|
||||||
|
.elisionLines = 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace SendFiles
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
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/effects/animations.h"
|
||||||
|
#include "ui/rp_widget.h"
|
||||||
|
#include "ui/text/text.h"
|
||||||
|
|
||||||
|
class HistoryItem;
|
||||||
|
|
||||||
|
namespace ChatHelpers {
|
||||||
|
class Show;
|
||||||
|
} // namespace ChatHelpers
|
||||||
|
|
||||||
|
namespace Data {
|
||||||
|
class Session;
|
||||||
|
} // namespace Data
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class IconButton;
|
||||||
|
class SpoilerAnimation;
|
||||||
|
} // namespace Ui
|
||||||
|
|
||||||
|
namespace SendFiles {
|
||||||
|
|
||||||
|
class ReplyPillHeader final : public Ui::RpWidget {
|
||||||
|
public:
|
||||||
|
ReplyPillHeader(
|
||||||
|
QWidget *parent,
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
FullReplyTo replyTo);
|
||||||
|
~ReplyPillHeader();
|
||||||
|
|
||||||
|
[[nodiscard]] rpl::producer<> closeRequests() const;
|
||||||
|
[[nodiscard]] rpl::producer<> hideFinished() const;
|
||||||
|
[[nodiscard]] rpl::producer<int> desiredHeight() const;
|
||||||
|
|
||||||
|
void setRoundedShapeBelow(bool value);
|
||||||
|
void hideAnimated();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *e) override;
|
||||||
|
void resizeEvent(QResizeEvent *e) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void resolveMessageData();
|
||||||
|
void setShownMessage(HistoryItem *item);
|
||||||
|
void updateShownMessageText();
|
||||||
|
void customEmojiRepaint();
|
||||||
|
void animationCallback();
|
||||||
|
|
||||||
|
const std::shared_ptr<ChatHelpers::Show> _show;
|
||||||
|
const not_null<Data::Session*> _data;
|
||||||
|
const FullReplyTo _replyTo;
|
||||||
|
const not_null<Ui::IconButton*> _cancel;
|
||||||
|
|
||||||
|
HistoryItem *_shownMessage = nullptr;
|
||||||
|
Ui::Text::String _shownMessageName;
|
||||||
|
Ui::Text::String _shownMessageText;
|
||||||
|
std::unique_ptr<Ui::SpoilerAnimation> _previewSpoiler;
|
||||||
|
bool _repaintScheduled = false;
|
||||||
|
|
||||||
|
Ui::Animations::Simple _showAnimation;
|
||||||
|
rpl::variable<int> _desiredHeight = 0;
|
||||||
|
rpl::event_stream<> _closeRequests;
|
||||||
|
rpl::variable<bool> _hideFinished = false;
|
||||||
|
bool _hiding = false;
|
||||||
|
bool _roundedShapeBelow = true;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace SendFiles
|
||||||
@@ -55,7 +55,6 @@ constexpr auto kSuccessFadeInDuration = crl::time(300);
|
|||||||
constexpr auto kSuccessExpandDuration = crl::time(400);
|
constexpr auto kSuccessExpandDuration = crl::time(400);
|
||||||
constexpr auto kSuccessExpandStart = crl::time(100);
|
constexpr auto kSuccessExpandStart = crl::time(100);
|
||||||
constexpr auto kProgressFadeInDuration = crl::time(300);
|
constexpr auto kProgressFadeInDuration = crl::time(300);
|
||||||
constexpr auto kFailureFadeInDuration = crl::time(300);
|
|
||||||
|
|
||||||
[[nodiscard]] QString FormatPercent(int permille) {
|
[[nodiscard]] QString FormatPercent(int permille) {
|
||||||
const auto rounded = (permille + 5) / 10;
|
const auto rounded = (permille + 5) / 10;
|
||||||
|
|||||||
@@ -0,0 +1,389 @@
|
|||||||
|
/*
|
||||||
|
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 "boxes/sticker_creator_box.h"
|
||||||
|
|
||||||
|
#include "api/api_stickers_creator.h"
|
||||||
|
#include "chat_helpers/compose/compose_show.h"
|
||||||
|
#include "chat_helpers/emoji_picker_overlay.h"
|
||||||
|
#include "core/file_utilities.h"
|
||||||
|
#include "editor/editor_layer_widget.h"
|
||||||
|
#include "editor/photo_editor.h"
|
||||||
|
#include "editor/photo_editor_common.h"
|
||||||
|
#include "editor/scene/scene.h"
|
||||||
|
#include "editor/scene/scene_item_image.h"
|
||||||
|
#include "info/channel_statistics/boosts/giveaway/boost_badge.h"
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
|
#include "main/main_session.h"
|
||||||
|
#include "ui/emoji_config.h"
|
||||||
|
#include "ui/image/image.h"
|
||||||
|
#include "ui/image/image_prepare.h"
|
||||||
|
#include "ui/layers/generic_box.h"
|
||||||
|
#include "ui/layers/layer_widget.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
|
#include "ui/rp_widget.h"
|
||||||
|
#include "ui/vertical_list.h"
|
||||||
|
#include "ui/widgets/buttons.h"
|
||||||
|
#include "ui/wrap/vertical_layout.h"
|
||||||
|
#include "window/window_controller.h"
|
||||||
|
#include "window/window_session_controller.h"
|
||||||
|
#include "styles/style_boxes.h"
|
||||||
|
#include "styles/style_chat_helpers.h"
|
||||||
|
#include "styles/style_editor.h"
|
||||||
|
#include "styles/style_layers.h"
|
||||||
|
|
||||||
|
#include <QtCore/QBuffer>
|
||||||
|
#include <QtGui/QImageReader>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kStickerSide = 512;
|
||||||
|
constexpr auto kPreviewSide = 256;
|
||||||
|
constexpr auto kWebpQuality = 95;
|
||||||
|
constexpr auto kMaxEmojis = 7;
|
||||||
|
|
||||||
|
[[nodiscard]] QImage LoadImageFromFile(const QString &path) {
|
||||||
|
auto reader = QImageReader(path);
|
||||||
|
reader.setAutoTransform(true);
|
||||||
|
auto image = reader.read();
|
||||||
|
if (image.format() != QImage::Format_ARGB32_Premultiplied
|
||||||
|
&& image.format() != QImage::Format_ARGB32) {
|
||||||
|
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PreviewWidget final : public Ui::RpWidget {
|
||||||
|
public:
|
||||||
|
PreviewWidget(QWidget *parent, QImage image)
|
||||||
|
: RpWidget(parent)
|
||||||
|
, _image(std::move(image)) {
|
||||||
|
resize(kPreviewSide, kPreviewSide);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *e) override {
|
||||||
|
auto p = QPainter(this);
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
const auto target = QRect(0, 0, width(), height());
|
||||||
|
p.drawImage(target, _image);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
const QImage _image;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
void OpenPhotoEditorForSticker(
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
QImage image,
|
||||||
|
Fn<void(QImage&&)> onDone) {
|
||||||
|
if (image.isNull()) {
|
||||||
|
show->showToast(tr::lng_stickers_create_open_failed(tr::now));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto sessionController = show->resolveWindow();
|
||||||
|
if (!sessionController) {
|
||||||
|
show->showToast(tr::lng_stickers_create_open_failed(tr::now));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto windowController = &sessionController->window();
|
||||||
|
const auto parentWidget = sessionController->widget();
|
||||||
|
|
||||||
|
if (image.width() <= 0
|
||||||
|
|| image.height() <= 0
|
||||||
|
|| (image.width() > 10 * image.height())
|
||||||
|
|| (image.height() > 10 * image.width())) {
|
||||||
|
show->showToast(tr::lng_stickers_create_open_failed(tr::now));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto canvas = QImage(
|
||||||
|
kStickerSide,
|
||||||
|
kStickerSide,
|
||||||
|
QImage::Format_ARGB32_Premultiplied);
|
||||||
|
canvas.fill(Qt::transparent);
|
||||||
|
const auto baseImage = std::make_shared<Image>(std::move(canvas));
|
||||||
|
|
||||||
|
auto scene = std::make_shared<Editor::Scene>(
|
||||||
|
QRectF(0, 0, kStickerSide, kStickerSide));
|
||||||
|
|
||||||
|
const auto userPixmap = QPixmap::fromImage(std::move(image));
|
||||||
|
const auto userSize = userPixmap.size();
|
||||||
|
const auto fitted = userSize.scaled(
|
||||||
|
QSize(kStickerSide, kStickerSide),
|
||||||
|
Qt::KeepAspectRatio);
|
||||||
|
const auto handle = st::photoEditorItemHandleSize;
|
||||||
|
const auto itemSize = (userSize.width() >= userSize.height())
|
||||||
|
? int((fitted.height() + handle)
|
||||||
|
* userSize.width() / float64(userSize.height()))
|
||||||
|
: (fitted.width() + handle);
|
||||||
|
auto itemData = Editor::ItemBase::Data{
|
||||||
|
.initialZoom = 1.0,
|
||||||
|
.zPtr = scene->lastZ(),
|
||||||
|
.size = itemSize,
|
||||||
|
.x = kStickerSide / 2,
|
||||||
|
.y = kStickerSide / 2,
|
||||||
|
.imageSize = userSize,
|
||||||
|
};
|
||||||
|
auto imageItem = std::make_shared<Editor::ItemImage>(
|
||||||
|
QPixmap(userPixmap),
|
||||||
|
std::move(itemData));
|
||||||
|
scene->addItem(std::move(imageItem));
|
||||||
|
|
||||||
|
auto modifications = Editor::PhotoModifications{
|
||||||
|
.crop = QRect(0, 0, kStickerSide, kStickerSide),
|
||||||
|
.paint = std::move(scene),
|
||||||
|
};
|
||||||
|
|
||||||
|
auto editor = base::make_unique_q<Editor::PhotoEditor>(
|
||||||
|
parentWidget,
|
||||||
|
windowController,
|
||||||
|
baseImage,
|
||||||
|
std::move(modifications),
|
||||||
|
Editor::EditorData{
|
||||||
|
.exactSize = QSize(kStickerSide, kStickerSide),
|
||||||
|
.cropType = Editor::EditorData::CropType::RoundedRect,
|
||||||
|
.keepAspectRatio = true,
|
||||||
|
.fixedCrop = true,
|
||||||
|
});
|
||||||
|
const auto raw = editor.get();
|
||||||
|
|
||||||
|
auto applyModifications = [=, done = std::move(onDone)](
|
||||||
|
const Editor::PhotoModifications &mods) mutable {
|
||||||
|
auto result = Editor::ImageModified(baseImage->original(), mods);
|
||||||
|
if (result.size() != QSize(kStickerSide, kStickerSide)) {
|
||||||
|
result = result.scaled(
|
||||||
|
kStickerSide,
|
||||||
|
kStickerSide,
|
||||||
|
Qt::IgnoreAspectRatio,
|
||||||
|
Qt::SmoothTransformation);
|
||||||
|
}
|
||||||
|
done(std::move(result));
|
||||||
|
};
|
||||||
|
|
||||||
|
auto layer = std::make_unique<Editor::LayerWidget>(
|
||||||
|
parentWidget,
|
||||||
|
std::move(editor));
|
||||||
|
Editor::InitEditorLayer(layer.get(), raw, std::move(applyModifications));
|
||||||
|
windowController->showLayer(
|
||||||
|
std::move(layer),
|
||||||
|
Ui::LayerOption::KeepOther);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] QByteArray EncodeWebp(QImage image) {
|
||||||
|
if (image.size() != QSize(kStickerSide, kStickerSide)) {
|
||||||
|
image = image.scaled(
|
||||||
|
kStickerSide,
|
||||||
|
kStickerSide,
|
||||||
|
Qt::IgnoreAspectRatio,
|
||||||
|
Qt::SmoothTransformation);
|
||||||
|
}
|
||||||
|
if (image.format() != QImage::Format_ARGB32) {
|
||||||
|
image = image.convertToFormat(QImage::Format_ARGB32);
|
||||||
|
}
|
||||||
|
auto bytes = QByteArray();
|
||||||
|
auto buffer = QBuffer(&bytes);
|
||||||
|
buffer.open(QIODevice::WriteOnly);
|
||||||
|
image.save(&buffer, "WEBP", kWebpQuality);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace Api {
|
||||||
|
|
||||||
|
void CreateStickerBox(
|
||||||
|
not_null<Ui::GenericBox*> box,
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
StickerSetIdentifier set,
|
||||||
|
QImage image,
|
||||||
|
Fn<void(MTPmessages_StickerSet)> done) {
|
||||||
|
struct State {
|
||||||
|
rpl::variable<bool> uploading = false;
|
||||||
|
std::unique_ptr<StickerUpload> upload;
|
||||||
|
QPointer<Ui::RoundButton> addButton;
|
||||||
|
};
|
||||||
|
const auto state = box->lifetime().make_state<State>();
|
||||||
|
const auto session = &show->session();
|
||||||
|
|
||||||
|
box->setTitle(tr::lng_stickers_create_image_title());
|
||||||
|
|
||||||
|
const auto inner = box->verticalLayout();
|
||||||
|
|
||||||
|
auto pickerDescriptor = ChatHelpers::EmojiPickerOverlayDescriptor{
|
||||||
|
.aboutText = tr::lng_stickers_create_emoji_about(tr::now),
|
||||||
|
.maxSelected = kMaxEmojis,
|
||||||
|
.allowExpand = true,
|
||||||
|
};
|
||||||
|
const auto metrics = ChatHelpers::EmojiPickerOverlay::EstimateMetrics(
|
||||||
|
pickerDescriptor.aboutText);
|
||||||
|
const auto pickerCollapsed = metrics.collapsedHeight;
|
||||||
|
const auto pickerTotalExpanded = metrics.totalExpandedHeight;
|
||||||
|
const auto shadowExt = metrics.shadowExtent;
|
||||||
|
|
||||||
|
constexpr auto kStickerOverlap = 24;
|
||||||
|
const auto stickerTop = shadowExt.top()
|
||||||
|
+ pickerCollapsed
|
||||||
|
- kStickerOverlap;
|
||||||
|
const auto holderHeight = std::max(
|
||||||
|
stickerTop + kPreviewSide,
|
||||||
|
pickerTotalExpanded);
|
||||||
|
|
||||||
|
const auto previewHolder = inner->add(
|
||||||
|
object_ptr<Ui::RpWidget>(inner),
|
||||||
|
QMargins(0, 0, 0, 0),
|
||||||
|
style::al_top);
|
||||||
|
previewHolder->resize(st::boxWideWidth, holderHeight);
|
||||||
|
const auto preview = Ui::CreateChild<PreviewWidget>(
|
||||||
|
previewHolder,
|
||||||
|
image);
|
||||||
|
|
||||||
|
const auto picker = Ui::CreateChild<ChatHelpers::EmojiPickerOverlay>(
|
||||||
|
previewHolder,
|
||||||
|
std::move(pickerDescriptor));
|
||||||
|
|
||||||
|
auto layoutOverlay = [=] {
|
||||||
|
const auto bubbleW = std::min(
|
||||||
|
previewHolder->width()
|
||||||
|
- 2 * st::boxRowPadding.left()
|
||||||
|
- shadowExt.left() - shadowExt.right(),
|
||||||
|
int(kPreviewSide * 1.1));
|
||||||
|
const auto totalW = bubbleW + shadowExt.left() + shadowExt.right();
|
||||||
|
const auto x = (previewHolder->width() - totalW) / 2;
|
||||||
|
picker->setGeometry(x, 0, totalW, pickerTotalExpanded);
|
||||||
|
picker->raise();
|
||||||
|
};
|
||||||
|
|
||||||
|
previewHolder->widthValue(
|
||||||
|
) | rpl::on_next([=](int width) {
|
||||||
|
preview->move((width - kPreviewSide) / 2, stickerTop);
|
||||||
|
layoutOverlay();
|
||||||
|
}, preview->lifetime());
|
||||||
|
|
||||||
|
Ui::AddSkip(inner);
|
||||||
|
|
||||||
|
const auto startUpload = [=, set = std::move(set), done = std::move(done)](
|
||||||
|
) mutable {
|
||||||
|
if (state->uploading.current()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto emoji = QString();
|
||||||
|
for (const auto one : picker->selected()) {
|
||||||
|
emoji.append(one->text());
|
||||||
|
}
|
||||||
|
if (emoji.isEmpty()) {
|
||||||
|
show->showToast(
|
||||||
|
tr::lng_stickers_create_emoji_required(tr::now));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto bytes = EncodeWebp(image);
|
||||||
|
if (bytes.isEmpty()) {
|
||||||
|
show->showToast(
|
||||||
|
tr::lng_stickers_create_upload_failed(tr::now));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto lockedWidth = state->addButton
|
||||||
|
? state->addButton->width()
|
||||||
|
: 0;
|
||||||
|
state->uploading = true;
|
||||||
|
if (state->addButton && lockedWidth > 0) {
|
||||||
|
state->addButton->resizeToWidth(lockedWidth);
|
||||||
|
}
|
||||||
|
state->upload = std::make_unique<StickerUpload>(
|
||||||
|
session,
|
||||||
|
set,
|
||||||
|
bytes,
|
||||||
|
emoji);
|
||||||
|
|
||||||
|
const auto doneCallback = done;
|
||||||
|
state->upload->start(
|
||||||
|
crl::guard(box, [=](MTPmessages_StickerSet result) {
|
||||||
|
state->upload = nullptr;
|
||||||
|
state->uploading = false;
|
||||||
|
show->showToast(tr::lng_stickers_create_added(tr::now));
|
||||||
|
if (doneCallback) {
|
||||||
|
doneCallback(result);
|
||||||
|
}
|
||||||
|
box->closeBox();
|
||||||
|
}),
|
||||||
|
crl::guard(box, [=](QString err) {
|
||||||
|
state->upload = nullptr;
|
||||||
|
state->uploading = false;
|
||||||
|
show->showToast(err.isEmpty()
|
||||||
|
? tr::lng_stickers_create_upload_failed(tr::now)
|
||||||
|
: err);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto addButton = box->addButton(
|
||||||
|
rpl::conditional(
|
||||||
|
state->uploading.value(),
|
||||||
|
rpl::single(QString()),
|
||||||
|
tr::lng_box_done()),
|
||||||
|
startUpload);
|
||||||
|
state->addButton = addButton;
|
||||||
|
box->addButton(tr::lng_cancel(), [=] { box->closeBox(); });
|
||||||
|
|
||||||
|
{
|
||||||
|
using namespace Info::Statistics;
|
||||||
|
const auto loadingAnimation = InfiniteRadialAnimationWidget(
|
||||||
|
addButton,
|
||||||
|
addButton->height() / 2,
|
||||||
|
&st::editStickerSetNameLoading);
|
||||||
|
AddChildToWidgetCenter(addButton, loadingAnimation);
|
||||||
|
loadingAnimation->showOn(state->uploading.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
box->setWidth(st::boxWideWidth);
|
||||||
|
|
||||||
|
box->boxClosing(
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
state->upload = nullptr;
|
||||||
|
}, box->lifetime());
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenCreateStickerFlow(
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
StickerSetIdentifier set,
|
||||||
|
Fn<void(MTPmessages_StickerSet)> done) {
|
||||||
|
const auto parent = QPointer<QWidget>(show->toastParent());
|
||||||
|
|
||||||
|
const auto onChosen = [=, set = std::move(set), done = std::move(done)](
|
||||||
|
FileDialog::OpenResult &&result) mutable {
|
||||||
|
if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto path = result.paths.isEmpty()
|
||||||
|
? QString()
|
||||||
|
: result.paths.front();
|
||||||
|
auto image = path.isEmpty()
|
||||||
|
? QImage::fromData(result.remoteContent)
|
||||||
|
: LoadImageFromFile(path);
|
||||||
|
OpenPhotoEditorForSticker(
|
||||||
|
show,
|
||||||
|
std::move(image),
|
||||||
|
[=, set = std::move(set), done = std::move(done)](
|
||||||
|
QImage &&prepared) mutable {
|
||||||
|
show->showBox(Box(
|
||||||
|
CreateStickerBox,
|
||||||
|
show,
|
||||||
|
std::move(set),
|
||||||
|
std::move(prepared),
|
||||||
|
std::move(done)));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
FileDialog::GetOpenPath(
|
||||||
|
parent,
|
||||||
|
tr::lng_stickers_create_choose_image(tr::now),
|
||||||
|
FileDialog::ImagesFilter(),
|
||||||
|
std::move(onChosen));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Api
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
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 "data/stickers/data_stickers.h"
|
||||||
|
|
||||||
|
#include <QtGui/QImage>
|
||||||
|
|
||||||
|
namespace ChatHelpers {
|
||||||
|
class Show;
|
||||||
|
} // namespace ChatHelpers
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class GenericBox;
|
||||||
|
} // namespace Ui
|
||||||
|
|
||||||
|
namespace Api {
|
||||||
|
|
||||||
|
void CreateStickerBox(
|
||||||
|
not_null<Ui::GenericBox*> box,
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
StickerSetIdentifier set,
|
||||||
|
QImage image,
|
||||||
|
Fn<void(MTPmessages_StickerSet)> done);
|
||||||
|
|
||||||
|
void OpenCreateStickerFlow(
|
||||||
|
std::shared_ptr<ChatHelpers::Show> show,
|
||||||
|
StickerSetIdentifier set,
|
||||||
|
Fn<void(MTPmessages_StickerSet)> done = nullptr);
|
||||||
|
|
||||||
|
} // namespace Api
|
||||||
@@ -8,14 +8,17 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "boxes/sticker_set_box.h"
|
#include "boxes/sticker_set_box.h"
|
||||||
|
|
||||||
#include "api/api_common.h"
|
#include "api/api_common.h"
|
||||||
|
#include "api/api_stickers_creator.h"
|
||||||
#include "api/api_toggling_media.h"
|
#include "api/api_toggling_media.h"
|
||||||
#include "apiwrap.h"
|
#include "apiwrap.h"
|
||||||
#include "base/unixtime.h"
|
#include "base/unixtime.h"
|
||||||
#include "boxes/premium_preview_box.h"
|
#include "boxes/premium_preview_box.h"
|
||||||
|
#include "boxes/sticker_creator_box.h"
|
||||||
#include "chat_helpers/compose/compose_show.h"
|
#include "chat_helpers/compose/compose_show.h"
|
||||||
#include "chat_helpers/stickers_list_widget.h"
|
#include "chat_helpers/stickers_list_widget.h"
|
||||||
#include "chat_helpers/stickers_lottie.h"
|
#include "chat_helpers/stickers_lottie.h"
|
||||||
#include "core/application.h"
|
#include "core/application.h"
|
||||||
|
#include "core/click_handler_types.h"
|
||||||
#include "data/data_document.h"
|
#include "data/data_document.h"
|
||||||
#include "data/data_document_media.h"
|
#include "data/data_document_media.h"
|
||||||
#include "data/data_file_origin.h"
|
#include "data/data_file_origin.h"
|
||||||
@@ -52,11 +55,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "ui/widgets/buttons.h"
|
#include "ui/widgets/buttons.h"
|
||||||
#include "ui/widgets/fields/input_field.h"
|
#include "ui/widgets/fields/input_field.h"
|
||||||
#include "ui/widgets/gradient_round_button.h"
|
#include "ui/widgets/gradient_round_button.h"
|
||||||
|
#include "ui/widgets/menu/menu_action.h"
|
||||||
#include "ui/widgets/menu/menu_add_action_callback.h"
|
#include "ui/widgets/menu/menu_add_action_callback.h"
|
||||||
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
||||||
|
#include "ui/widgets/menu/menu_multiline_action.h"
|
||||||
|
#include "base/event_filter.h"
|
||||||
|
#include "chat_helpers/tabbed_panel.h"
|
||||||
|
#include "chat_helpers/tabbed_selector.h"
|
||||||
|
#include "ui/widgets/inner_dropdown.h"
|
||||||
#include "ui/widgets/popup_menu.h"
|
#include "ui/widgets/popup_menu.h"
|
||||||
#include "ui/widgets/scroll_area.h"
|
#include "ui/widgets/scroll_area.h"
|
||||||
#include "window/window_session_controller.h"
|
#include "window/window_session_controller.h"
|
||||||
|
#include "styles/style_chat.h"
|
||||||
#include "styles/style_layers.h"
|
#include "styles/style_layers.h"
|
||||||
#include "styles/style_chat_helpers.h"
|
#include "styles/style_chat_helpers.h"
|
||||||
#include "styles/style_info.h"
|
#include "styles/style_info.h"
|
||||||
@@ -325,6 +335,7 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void applySet(const TLStickerSet &set);
|
void applySet(const TLStickerSet &set);
|
||||||
|
void setOuterContainer(QPointer<QWidget> container);
|
||||||
|
|
||||||
~Inner();
|
~Inner();
|
||||||
|
|
||||||
@@ -382,6 +393,16 @@ private:
|
|||||||
void startOverAnimation(int index, float64 from, float64 to);
|
void startOverAnimation(int index, float64 from, float64 to);
|
||||||
int stickerFromGlobalPos(const QPoint &p) const;
|
int stickerFromGlobalPos(const QPoint &p) const;
|
||||||
|
|
||||||
|
[[nodiscard]] bool hasAddCell() const;
|
||||||
|
[[nodiscard]] int totalCellsCount() const;
|
||||||
|
[[nodiscard]] QRect addCellRect() const;
|
||||||
|
[[nodiscard]] bool addCellFromGlobalPos(const QPoint &p) const;
|
||||||
|
void setAddCellHovered(bool hovered);
|
||||||
|
void paintAddCell(QPainter &p) const;
|
||||||
|
void showAddMenu(QPoint globalPos);
|
||||||
|
void startAddExistingStickerFlow();
|
||||||
|
void startCreateNewStickerFlow();
|
||||||
|
|
||||||
void installDone(const MTPmessages_StickerSetInstallResult &result);
|
void installDone(const MTPmessages_StickerSetInstallResult &result);
|
||||||
|
|
||||||
void requestReorder(not_null<DocumentData*> document, int index);
|
void requestReorder(not_null<DocumentData*> document, int index);
|
||||||
@@ -465,6 +486,8 @@ private:
|
|||||||
mtpRequestId _installRequest = 0;
|
mtpRequestId _installRequest = 0;
|
||||||
|
|
||||||
int _selected = -1;
|
int _selected = -1;
|
||||||
|
bool _addCellHovered = false;
|
||||||
|
bool _addCellPressed = false;
|
||||||
|
|
||||||
base::Timer _previewTimer;
|
base::Timer _previewTimer;
|
||||||
int _previewShown = -1;
|
int _previewShown = -1;
|
||||||
@@ -472,6 +495,8 @@ private:
|
|||||||
bool _previewLocked = false;
|
bool _previewLocked = false;
|
||||||
|
|
||||||
base::unique_qptr<Ui::PopupMenu> _menu;
|
base::unique_qptr<Ui::PopupMenu> _menu;
|
||||||
|
base::unique_qptr<ChatHelpers::TabbedPanel> _pickerPanel;
|
||||||
|
QPointer<QWidget> _outerContainer;
|
||||||
|
|
||||||
rpl::event_stream<uint64> _setInstalled;
|
rpl::event_stream<uint64> _setInstalled;
|
||||||
rpl::event_stream<uint64> _setArchived;
|
rpl::event_stream<uint64> _setArchived;
|
||||||
@@ -525,6 +550,7 @@ void StickerSetBox::prepare() {
|
|||||||
_inner = setInnerWidget(
|
_inner = setInnerWidget(
|
||||||
object_ptr<Inner>(this, _show, _set, _type),
|
object_ptr<Inner>(this, _show, _set, _type),
|
||||||
st::stickersScroll);
|
st::stickersScroll);
|
||||||
|
_inner->setOuterContainer(getDelegate()->outerContainer());
|
||||||
if (const auto previewId = base::take(_previewDocumentId)) {
|
if (const auto previewId = base::take(_previewDocumentId)) {
|
||||||
_inner->showPreviewForDocument(previewId);
|
_inner->showPreviewForDocument(previewId);
|
||||||
}
|
}
|
||||||
@@ -771,6 +797,98 @@ void StickerSetBox::updateButtons() {
|
|||||||
&st::menuIconReorder);
|
&st::menuIconReorder);
|
||||||
});
|
});
|
||||||
}();
|
}();
|
||||||
|
const auto fillSetCreatorFooter = [&] {
|
||||||
|
using Filler = Fn<void(not_null<Ui::PopupMenu*>)>;
|
||||||
|
if (!_inner->amSetCreator()) {
|
||||||
|
return Filler(nullptr);
|
||||||
|
}
|
||||||
|
const auto data = &_session->data();
|
||||||
|
return Filler([=, show = _show, set = _set](
|
||||||
|
not_null<Ui::PopupMenu*> menu) {
|
||||||
|
const auto weak = base::weak_qptr<StickerSetBox>(this);
|
||||||
|
const auto deleteEveryone = [=] {
|
||||||
|
const auto confirm = [=](Fn<void()> close) {
|
||||||
|
Api::DeleteStickerSet(
|
||||||
|
&data->session(),
|
||||||
|
set,
|
||||||
|
[=] {
|
||||||
|
if (const auto strong = weak.get()) {
|
||||||
|
strong->closeBox();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[=](const QString &error) {
|
||||||
|
show->showToast(error);
|
||||||
|
});
|
||||||
|
close();
|
||||||
|
};
|
||||||
|
show->showBox(Ui::MakeConfirmBox({
|
||||||
|
.text = tr::lng_stickers_delete_pack_sure(tr::now),
|
||||||
|
.confirmed = confirm,
|
||||||
|
.confirmText
|
||||||
|
= tr::lng_stickers_remove_pack_confirm(),
|
||||||
|
.confirmStyle = &st::attentionBoxButton,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
const auto deleteSelf = [show, inner = _inner] {
|
||||||
|
const auto raw = inner.data();
|
||||||
|
if (!raw) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto box = ChatHelpers::MakeConfirmRemoveSetBox(
|
||||||
|
&show->session(),
|
||||||
|
st::boxLabel,
|
||||||
|
raw->setId());
|
||||||
|
if (box) {
|
||||||
|
show->showBox(std::move(box));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const auto deleteAction = menu->addAction(
|
||||||
|
base::make_unique_q<Ui::Menu::Action>(
|
||||||
|
menu->menu(),
|
||||||
|
st::menuWithIconsAttention,
|
||||||
|
Ui::Menu::CreateAction(
|
||||||
|
menu->menu().get(),
|
||||||
|
tr::lng_stickers_context_delete_pack(tr::now),
|
||||||
|
nullptr),
|
||||||
|
&st::menuIconDeleteAttention,
|
||||||
|
&st::menuIconDeleteAttention));
|
||||||
|
deleteAction->setMenu(
|
||||||
|
Ui::CreateChild<QMenu>(menu->menu().get()));
|
||||||
|
const auto sub = menu->ensureSubmenu(
|
||||||
|
deleteAction,
|
||||||
|
st::popupMenuWithIcons);
|
||||||
|
const auto addSub = Ui::Menu::CreateAddActionCallback(sub);
|
||||||
|
addSub({
|
||||||
|
.text = tr::lng_stickers_context_delete_pack_everyone(
|
||||||
|
tr::now),
|
||||||
|
.handler = deleteEveryone,
|
||||||
|
.icon = &st::menuIconDeleteAttention,
|
||||||
|
.isAttention = true,
|
||||||
|
});
|
||||||
|
sub->addAction(
|
||||||
|
tr::lng_stickers_context_delete_pack_self(tr::now),
|
||||||
|
deleteSelf,
|
||||||
|
&st::menuIconRemove);
|
||||||
|
menu->addSeparator(&st::expandedMenuSeparator);
|
||||||
|
auto item = base::make_unique_q<Ui::Menu::MultilineAction>(
|
||||||
|
menu->menu(),
|
||||||
|
st::defaultMenu,
|
||||||
|
st::historyHasCustomEmoji,
|
||||||
|
QPoint(
|
||||||
|
st::defaultMenu.itemPadding.left(),
|
||||||
|
st::defaultMenu.itemPadding.top()),
|
||||||
|
tr::lng_stickers_bot_more_options(
|
||||||
|
tr::now,
|
||||||
|
lt_bot,
|
||||||
|
Ui::Text::Colorized(tr::bold(u"@stickers"_q)),
|
||||||
|
Ui::Text::RichLangValue));
|
||||||
|
item->clicks(
|
||||||
|
) | rpl::on_next([] {
|
||||||
|
UrlClickHandler::Open(u"https://t.me/stickers"_q);
|
||||||
|
}, item->lifetime());
|
||||||
|
menu->addAction(std::move(item));
|
||||||
|
});
|
||||||
|
}();
|
||||||
if (_inner->notInstalled()) {
|
if (_inner->notInstalled()) {
|
||||||
if (!_session->premium()
|
if (!_session->premium()
|
||||||
&& _session->premiumPossible()
|
&& _session->premiumPossible()
|
||||||
@@ -818,6 +936,9 @@ void StickerSetBox::updateButtons() {
|
|||||||
: tr::lng_stickers_share_pack)(tr::now),
|
: tr::lng_stickers_share_pack)(tr::now),
|
||||||
[=] { share(); closeBox(); },
|
[=] { share(); closeBox(); },
|
||||||
&st::menuIconShare);
|
&st::menuIconShare);
|
||||||
|
if (fillSetCreatorFooter) {
|
||||||
|
fillSetCreatorFooter(*menu);
|
||||||
|
}
|
||||||
(*menu)->popup(QCursor::pos());
|
(*menu)->popup(QCursor::pos());
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -869,6 +990,9 @@ void StickerSetBox::updateButtons() {
|
|||||||
: tr::lng_stickers_archive_pack(tr::now)),
|
: tr::lng_stickers_archive_pack(tr::now)),
|
||||||
archive,
|
archive,
|
||||||
&st::menuIconArchive);
|
&st::menuIconArchive);
|
||||||
|
if (fillSetCreatorFooter) {
|
||||||
|
fillSetCreatorFooter(*menu);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(*menu)->popup(QCursor::pos());
|
(*menu)->popup(QCursor::pos());
|
||||||
return true;
|
return true;
|
||||||
@@ -1030,15 +1154,15 @@ void StickerSetBox::Inner::applySet(const TLStickerSet &set) {
|
|||||||
_errors.fire(Error::NotFound);
|
_errors.fire(Error::NotFound);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_loaded = true;
|
||||||
_perRow = isEmojiSet() ? kEmojiPerRow : kStickersPerRow;
|
_perRow = isEmojiSet() ? kEmojiPerRow : kStickersPerRow;
|
||||||
_rowsCount = (_pack.size() + _perRow - 1) / _perRow;
|
|
||||||
_singleSize = isEmojiSet() ? st::emojiSetSize : st::stickersSize;
|
_singleSize = isEmojiSet() ? st::emojiSetSize : st::stickersSize;
|
||||||
|
_rowsCount = (totalCellsCount() + _perRow - 1) / _perRow;
|
||||||
|
|
||||||
resize(
|
resize(
|
||||||
_padding.left() + _perRow * _singleSize.width(),
|
_padding.left() + _perRow * _singleSize.width(),
|
||||||
_padding.top() + _rowsCount * _singleSize.height() + _padding.bottom());
|
_padding.top() + _rowsCount * _singleSize.height() + _padding.bottom());
|
||||||
|
|
||||||
_loaded = true;
|
|
||||||
if (const auto previewId = base::take(_previewDocumentId)) {
|
if (const auto previewId = base::take(_previewDocumentId)) {
|
||||||
showPreviewForDocument(previewId);
|
showPreviewForDocument(previewId);
|
||||||
}
|
}
|
||||||
@@ -1169,6 +1293,10 @@ void StickerSetBox::Inner::mousePressEvent(QMouseEvent *e) {
|
|||||||
if (e->button() != Qt::LeftButton) {
|
if (e->button() != Qt::LeftButton) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (addCellFromGlobalPos(e->globalPos())) {
|
||||||
|
_addCellPressed = !_dragging.enabled;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const auto index = stickerFromGlobalPos(e->globalPos());
|
const auto index = stickerFromGlobalPos(e->globalPos());
|
||||||
if (index < 0 || index >= _pack.size()) {
|
if (index < 0 || index >= _pack.size()) {
|
||||||
return;
|
return;
|
||||||
@@ -1307,6 +1435,7 @@ void StickerSetBox::Inner::showPreviewForDocument(DocumentId documentId) {
|
|||||||
|
|
||||||
void StickerSetBox::Inner::leaveEventHook(QEvent *e) {
|
void StickerSetBox::Inner::leaveEventHook(QEvent *e) {
|
||||||
setSelected(-1);
|
setSelected(-1);
|
||||||
|
setAddCellHovered(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void StickerSetBox::Inner::requestReorder(
|
void StickerSetBox::Inner::requestReorder(
|
||||||
@@ -1399,6 +1528,13 @@ void StickerSetBox::Inner::mouseReleaseEvent(QMouseEvent *e) {
|
|||||||
_previewShown = -1;
|
_previewShown = -1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (_addCellPressed) {
|
||||||
|
_addCellPressed = false;
|
||||||
|
if (addCellFromGlobalPos(e->globalPos())) {
|
||||||
|
showAddMenu(e->globalPos());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_previewTimer.isActive()) {
|
if (!_previewTimer.isActive()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1470,6 +1606,12 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
|
|||||||
}
|
}
|
||||||
}, &st::menuIconCopy);
|
}, &st::menuIconCopy);
|
||||||
}
|
}
|
||||||
|
if (!amSetCreator()) {
|
||||||
|
Api::AddAddToEmojiSetAction(
|
||||||
|
Ui::Menu::CreateAddActionCallback(_menu.get()),
|
||||||
|
_show,
|
||||||
|
_pack[index]);
|
||||||
|
}
|
||||||
} else if (details.type != SendMenu::Type::Disabled) {
|
} else if (details.type != SendMenu::Type::Disabled) {
|
||||||
const auto document = _pack[index];
|
const auto document = _pack[index];
|
||||||
const auto send = crl::guard(this, [=](Api::SendOptions options) {
|
const auto send = crl::guard(this, [=](Api::SendOptions options) {
|
||||||
@@ -1501,6 +1643,12 @@ void StickerSetBox::Inner::contextMenuEvent(QContextMenuEvent *e) {
|
|||||||
(isFaved
|
(isFaved
|
||||||
? &st::menuIconUnfave
|
? &st::menuIconUnfave
|
||||||
: &st::menuIconFave));
|
: &st::menuIconFave));
|
||||||
|
if (!amSetCreator()) {
|
||||||
|
Api::AddAddToStickerSetAction(
|
||||||
|
Ui::Menu::CreateAddActionCallback(_menu.get()),
|
||||||
|
_show,
|
||||||
|
document);
|
||||||
|
}
|
||||||
if (amSetCreator()) {
|
if (amSetCreator()) {
|
||||||
const auto addAction = Ui::Menu::CreateAddActionCallback(
|
const auto addAction = Ui::Menu::CreateAddActionCallback(
|
||||||
_menu.get());
|
_menu.get());
|
||||||
@@ -1653,8 +1801,27 @@ void StickerSetBox::Inner::fillDeleteStickerBox(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void StickerSetBox::Inner::updateSelected() {
|
void StickerSetBox::Inner::updateSelected() {
|
||||||
auto selected = stickerFromGlobalPos(QCursor::pos());
|
const auto cursor = QCursor::pos();
|
||||||
|
const auto onAddCell = addCellFromGlobalPos(cursor);
|
||||||
|
const auto selected = onAddCell
|
||||||
|
? -1
|
||||||
|
: stickerFromGlobalPos(cursor);
|
||||||
setSelected(setType() == Data::StickersType::Masks ? -1 : selected);
|
setSelected(setType() == Data::StickersType::Masks ? -1 : selected);
|
||||||
|
setAddCellHovered(onAddCell);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerSetBox::Inner::setAddCellHovered(bool hovered) {
|
||||||
|
if (_addCellHovered == hovered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_addCellHovered = hovered;
|
||||||
|
if (hasAddCell()) {
|
||||||
|
setCursor((hovered && !_dragging.enabled)
|
||||||
|
? style::cur_pointer
|
||||||
|
: style::cur_default);
|
||||||
|
const auto rect = addCellRect();
|
||||||
|
rtlupdate(rect.x(), rect.y(), rect.width(), rect.height());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void StickerSetBox::Inner::setSelected(int selected) {
|
void StickerSetBox::Inner::setSelected(int selected) {
|
||||||
@@ -1793,6 +1960,10 @@ void StickerSetBox::Inner::paintEvent(QPaintEvent *e) {
|
|||||||
paintSticker(p, _dragging.index, pos, paused, now);
|
paintSticker(p, _dragging.index, pos, paused, now);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasAddCell()) {
|
||||||
|
paintAddCell(p);
|
||||||
|
}
|
||||||
|
|
||||||
if (_lottiePlayer && !paused) {
|
if (_lottiePlayer && !paused) {
|
||||||
_lottiePlayer->markFrameShown();
|
_lottiePlayer->markFrameShown();
|
||||||
}
|
}
|
||||||
@@ -2202,4 +2373,205 @@ void StickerSetBox::Inner::repaintItems(crl::time now) {
|
|||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool StickerSetBox::Inner::hasAddCell() const {
|
||||||
|
return _loaded
|
||||||
|
&& _amSetCreator
|
||||||
|
&& (setType() == Data::StickersType::Stickers)
|
||||||
|
&& !_pack.isEmpty()
|
||||||
|
&& (_pack.size() < Api::kStickersInOwnedSetMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
int StickerSetBox::Inner::totalCellsCount() const {
|
||||||
|
return _pack.size() + (hasAddCell() ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect StickerSetBox::Inner::addCellRect() const {
|
||||||
|
const auto index = _pack.size();
|
||||||
|
const auto row = index / _perRow;
|
||||||
|
const auto column = index % _perRow;
|
||||||
|
return QRect(
|
||||||
|
_padding.left() + column * _singleSize.width(),
|
||||||
|
_padding.top() + row * _singleSize.height(),
|
||||||
|
_singleSize.width(),
|
||||||
|
_singleSize.height());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StickerSetBox::Inner::addCellFromGlobalPos(const QPoint &p) const {
|
||||||
|
if (!hasAddCell()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto local = mapFromGlobal(p);
|
||||||
|
if (rtl()) {
|
||||||
|
local.setX(width() - local.x());
|
||||||
|
}
|
||||||
|
const auto rect = addCellRect();
|
||||||
|
return rect.contains(local);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerSetBox::Inner::paintAddCell(QPainter &p) const {
|
||||||
|
const auto ltrRect = addCellRect();
|
||||||
|
const auto rect = rtl()
|
||||||
|
? QRect(
|
||||||
|
width() - ltrRect.x() - ltrRect.width(),
|
||||||
|
ltrRect.y(),
|
||||||
|
ltrRect.width(),
|
||||||
|
ltrRect.height())
|
||||||
|
: ltrRect;
|
||||||
|
const auto inner = QRect(
|
||||||
|
rect::center(rect) - QPoint(
|
||||||
|
st::stickersAddCellBgRadius,
|
||||||
|
st::stickersAddCellBgRadius),
|
||||||
|
Size(st::stickersAddCellBgRadius * 2));
|
||||||
|
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
const auto base = st::windowSubTextFg->c;
|
||||||
|
const auto bgAlpha = (_addCellHovered && !_dragging.enabled)
|
||||||
|
? 0.22
|
||||||
|
: 0.12;
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(anim::with_alpha(base, bgAlpha));
|
||||||
|
p.drawEllipse(inner);
|
||||||
|
|
||||||
|
const auto plusHalf = st::stickersAddCellPlusSize / 2;
|
||||||
|
const auto thickness = st::stickersAddCellPlusThickness;
|
||||||
|
const auto center = rect.center();
|
||||||
|
const auto plusH = QRectF(
|
||||||
|
center.x() - plusHalf,
|
||||||
|
center.y() - thickness / 2.,
|
||||||
|
plusHalf * 2,
|
||||||
|
thickness);
|
||||||
|
const auto plusV = QRectF(
|
||||||
|
center.x() - thickness / 2.,
|
||||||
|
center.y() - plusHalf,
|
||||||
|
thickness,
|
||||||
|
plusHalf * 2);
|
||||||
|
const auto radius = thickness / 2.;
|
||||||
|
p.setBrush(base);
|
||||||
|
p.drawRoundedRect(plusH, radius, radius);
|
||||||
|
p.drawRoundedRect(plusV, radius, radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerSetBox::Inner::showAddMenu(QPoint globalPos) {
|
||||||
|
if (_dragging.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_menu = base::make_unique_q<Ui::PopupMenu>(
|
||||||
|
this,
|
||||||
|
st::popupMenuWithIcons);
|
||||||
|
_menu->addAction(
|
||||||
|
tr::lng_stickers_create_new(tr::now),
|
||||||
|
crl::guard(this, [=] { startCreateNewStickerFlow(); }),
|
||||||
|
&st::menuIconStickerCreate);
|
||||||
|
_menu->addAction(
|
||||||
|
tr::lng_stickers_add_existing(tr::now),
|
||||||
|
crl::guard(this, [=] { startAddExistingStickerFlow(); }),
|
||||||
|
&st::menuIconStickerAdd);
|
||||||
|
_menu->popup(globalPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerSetBox::Inner::setOuterContainer(QPointer<QWidget> container) {
|
||||||
|
_outerContainer = std::move(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerSetBox::Inner::startAddExistingStickerFlow() {
|
||||||
|
if (!hasAddCell() || !_outerContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto container = _outerContainer.data();
|
||||||
|
const auto identifier = StickerSetIdentifier{
|
||||||
|
.id = _setId,
|
||||||
|
.accessHash = _setAccessHash,
|
||||||
|
.shortName = _setShortName,
|
||||||
|
};
|
||||||
|
const auto session = _session;
|
||||||
|
const auto show = _show;
|
||||||
|
|
||||||
|
using Selector = ChatHelpers::TabbedSelector;
|
||||||
|
_pickerPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(
|
||||||
|
container,
|
||||||
|
ChatHelpers::TabbedPanelDescriptor{
|
||||||
|
.ownedSelector = object_ptr<Selector>(
|
||||||
|
nullptr,
|
||||||
|
ChatHelpers::TabbedSelectorDescriptor{
|
||||||
|
.show = _show,
|
||||||
|
.st = st::defaultComposeControls.tabbed,
|
||||||
|
.level = Window::GifPauseReason::Layer,
|
||||||
|
.mode = Selector::Mode::StickersOnly,
|
||||||
|
.excludeStickerSetId = _setId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const auto panel = _pickerPanel.get();
|
||||||
|
panel->setDesiredHeightValues(
|
||||||
|
1.,
|
||||||
|
st::emojiPanMinHeight / 2,
|
||||||
|
st::emojiPanMinHeight);
|
||||||
|
panel->setDropDown(true);
|
||||||
|
panel->setShowAnimationOrigin(Ui::PanelAnimation::Origin::TopLeft);
|
||||||
|
panel->hide();
|
||||||
|
|
||||||
|
panel->selector()->fileChosen(
|
||||||
|
) | rpl::on_next([=, this](const ChatHelpers::FileChosen &chosen) {
|
||||||
|
const auto document = chosen.document;
|
||||||
|
if (_pickerPanel) {
|
||||||
|
_pickerPanel->hideAnimated();
|
||||||
|
}
|
||||||
|
const auto emoji = Api::StickerEmojiOrDefault(document);
|
||||||
|
Api::AddExistingStickerToSet(
|
||||||
|
session,
|
||||||
|
identifier,
|
||||||
|
document,
|
||||||
|
emoji,
|
||||||
|
crl::guard(this, [=, this](MTPmessages_StickerSet result) {
|
||||||
|
applySet(result);
|
||||||
|
show->showToast(
|
||||||
|
tr::lng_stickers_create_added(tr::now));
|
||||||
|
}),
|
||||||
|
crl::guard(this, [=](QString err) {
|
||||||
|
show->showToast(err.isEmpty()
|
||||||
|
? tr::lng_attach_failed(tr::now)
|
||||||
|
: err);
|
||||||
|
}));
|
||||||
|
}, panel->lifetime());
|
||||||
|
|
||||||
|
const auto reposition = [=] {
|
||||||
|
const auto size = container->size();
|
||||||
|
const auto margins = st::emojiPanMargins;
|
||||||
|
const auto panelWidth = st::emojiPanWidth
|
||||||
|
+ margins.left()
|
||||||
|
+ margins.right();
|
||||||
|
const auto panelHeight = st::emojiPanMinHeight
|
||||||
|
+ margins.top()
|
||||||
|
+ margins.bottom();
|
||||||
|
const auto top = std::max(0, (size.height() - panelHeight) / 2);
|
||||||
|
const auto right = (size.width() + panelWidth) / 2;
|
||||||
|
panel->moveTopRight(top, right);
|
||||||
|
};
|
||||||
|
base::install_event_filter(panel, container, [=](
|
||||||
|
not_null<QEvent*> event) {
|
||||||
|
const auto type = event->type();
|
||||||
|
if (type == QEvent::Move || type == QEvent::Resize) {
|
||||||
|
crl::on_main(panel, reposition);
|
||||||
|
}
|
||||||
|
return base::EventFilterResult::Continue;
|
||||||
|
});
|
||||||
|
reposition();
|
||||||
|
panel->showAnimated();
|
||||||
|
}
|
||||||
|
|
||||||
|
void StickerSetBox::Inner::startCreateNewStickerFlow() {
|
||||||
|
if (!hasAddCell()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto identifier = StickerSetIdentifier{
|
||||||
|
.id = _setId,
|
||||||
|
.accessHash = _setAccessHash,
|
||||||
|
.shortName = _setShortName,
|
||||||
|
};
|
||||||
|
const auto onDone = crl::guard(this, [=, this](
|
||||||
|
MTPmessages_StickerSet result) {
|
||||||
|
applySet(result);
|
||||||
|
});
|
||||||
|
Api::OpenCreateStickerFlow(_show, identifier, onDone);
|
||||||
|
}
|
||||||
|
|
||||||
StickerSetBox::Inner::~Inner() = default;
|
StickerSetBox::Inner::~Inner() = default;
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ callBodyLayout: CallBodyLayout {
|
|||||||
participantsTop: 294px;
|
participantsTop: 294px;
|
||||||
muteStroke: 3px;
|
muteStroke: 3px;
|
||||||
muteSize: 36px;
|
muteSize: 36px;
|
||||||
mutePosition: point(142px, 135px);
|
mutePosition: point(140px, 135px);
|
||||||
}
|
}
|
||||||
callBodyWithPreview: CallBodyLayout {
|
callBodyWithPreview: CallBodyLayout {
|
||||||
height: 185px;
|
height: 185px;
|
||||||
@@ -641,7 +641,6 @@ groupCallMenuAbout: FlatLabel(defaultFlatLabel) {
|
|||||||
textFg: groupCallMemberNotJoinedStatus;
|
textFg: groupCallMemberNotJoinedStatus;
|
||||||
palette: groupCallTextPalette;
|
palette: groupCallTextPalette;
|
||||||
minWidth: 200px;
|
minWidth: 200px;
|
||||||
maxHeight: 92px;
|
|
||||||
}
|
}
|
||||||
callDeviceSelectionLabel: FlatLabel(defaultSubsectionTitle) {
|
callDeviceSelectionLabel: FlatLabel(defaultSubsectionTitle) {
|
||||||
textFg: groupCallActiveFg;
|
textFg: groupCallActiveFg;
|
||||||
|
|||||||
@@ -66,13 +66,28 @@ AboutItem::AboutItem(
|
|||||||
, _dummyAction(new QAction(parent)) {
|
, _dummyAction(new QAction(parent)) {
|
||||||
setPointerCursor(false);
|
setPointerCursor(false);
|
||||||
|
|
||||||
|
_text->setSelectable(true);
|
||||||
|
|
||||||
|
const auto added = st.itemPadding.left() + st.itemPadding.right();
|
||||||
|
|
||||||
|
sizeValue(
|
||||||
|
) | rpl::on_next([=](const QSize &s) {
|
||||||
|
if (s.width() <= added) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_text->resizeToWidth(s.width() - added);
|
||||||
|
_text->moveToLeft(st.itemPadding.left(), st.itemPadding.top());
|
||||||
|
}, lifetime());
|
||||||
|
|
||||||
|
_text->heightValue(
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
resize(width(), contentHeight());
|
||||||
|
}, lifetime());
|
||||||
|
|
||||||
|
_text->resizeToWidth(parent->width() - added);
|
||||||
fitToMenuWidth();
|
fitToMenuWidth();
|
||||||
enableMouseSelecting();
|
enableMouseSelecting();
|
||||||
enableMouseSelecting(_text.get());
|
enableMouseSelecting(_text.get());
|
||||||
|
|
||||||
_text->setSelectable(true);
|
|
||||||
_text->resizeToWidth(st::groupCallMenuAbout.minWidth);
|
|
||||||
_text->moveToLeft(st.itemPadding.left(), st.itemPadding.top());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
not_null<QAction*> AboutItem::action() const {
|
not_null<QAction*> AboutItem::action() const {
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
namespace Calls::Group {
|
namespace Calls::Group {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kPasswordCharAmount = 24;
|
|
||||||
|
|
||||||
void StartWithBox(
|
void StartWithBox(
|
||||||
not_null<Ui::GenericBox*> box,
|
not_null<Ui::GenericBox*> box,
|
||||||
Fn<void()> done,
|
Fn<void()> done,
|
||||||
|
|||||||
@@ -424,6 +424,58 @@ stickersScroll: ScrollArea(boxScroll) {
|
|||||||
stickersRowDisabledOpacity: 0.4;
|
stickersRowDisabledOpacity: 0.4;
|
||||||
stickersRowDuration: 200;
|
stickersRowDuration: 200;
|
||||||
|
|
||||||
|
stickersAddCellPlusSize: 22px;
|
||||||
|
stickersAddCellPlusThickness: 2px;
|
||||||
|
stickersAddCellBgRadius: 28px;
|
||||||
|
|
||||||
|
stickersEmojiPickerExpandedRadius: 20px;
|
||||||
|
stickersEmojiPickerBg: emojiPanBg;
|
||||||
|
stickersEmojiPickerShadow: windowShadowFg;
|
||||||
|
stickersEmojiPickerPadding: margins(12px, 8px, 12px, 0px);
|
||||||
|
stickersEmojiPickerItemSize: 30px;
|
||||||
|
stickersEmojiPickerItemSkip: 4px;
|
||||||
|
stickersEmojiPickerStripHeight: 40px;
|
||||||
|
stickersEmojiPickerExpandedHeight: 220px;
|
||||||
|
stickersEmojiPickerStripBubble: icon{
|
||||||
|
{ "chat/reactions_bubble_shadow", windowShadowFg },
|
||||||
|
{ "chat/reactions_bubble", emojiPanBg },
|
||||||
|
};
|
||||||
|
stickersEmojiPickerStripBubbleRight: 20px;
|
||||||
|
stickersEmojiPickerSelectedBg: windowBgActive;
|
||||||
|
stickersEmojiPickerSelectedFg: windowBgActive;
|
||||||
|
stickersEmojiPickerHeaderFg: windowSubTextFg;
|
||||||
|
stickersEmojiPickerScroll: ScrollArea(boxScroll) {
|
||||||
|
width: 14px;
|
||||||
|
deltax: 5px;
|
||||||
|
deltat: 4px;
|
||||||
|
deltab: 18px;
|
||||||
|
}
|
||||||
|
stickersEmojiPickerAbout: FlatLabel(defaultFlatLabel) {
|
||||||
|
minWidth: 100px;
|
||||||
|
align: align(top);
|
||||||
|
textFg: windowSubTextFg;
|
||||||
|
style: TextStyle(defaultTextStyle) {
|
||||||
|
font: font(12px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stickersEmojiPickerSectionHeader: FlatLabel(defaultFlatLabel) {
|
||||||
|
minWidth: 10px;
|
||||||
|
align: align(topleft);
|
||||||
|
textFg: windowSubTextFg;
|
||||||
|
style: TextStyle(defaultTextStyle) {
|
||||||
|
font: font(12px semibold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stickersEmojiPickerExpandIcon: icon {{ "intro_country_dropdown", windowSubTextFg }};
|
||||||
|
stickersEmojiPickerCollapseIcon: icon {{ "intro_country_dropdown-flip_vertical", windowSubTextFg }};
|
||||||
|
stickersEmojiPickerExpandSize: 24px;
|
||||||
|
stickersEmojiPickerExpandBg: windowBgRipple;
|
||||||
|
stickersEmojiPickerBoxShadow: BoxShadow {
|
||||||
|
blurRadius: 20px;
|
||||||
|
offset: point(0px, 6px);
|
||||||
|
opacity: 0.22;
|
||||||
|
}
|
||||||
|
|
||||||
emojiStatusDefault: icon {{ "emoji/stickers_premium", emojiIconFg }};
|
emojiStatusDefault: icon {{ "emoji/stickers_premium", emojiIconFg }};
|
||||||
|
|
||||||
filtersRemove: IconButton(stickersRemove) {
|
filtersRemove: IconButton(stickersRemove) {
|
||||||
@@ -609,7 +661,7 @@ sendBoxAlbumSmallGroupSize: size(30px, 25px);
|
|||||||
sendBoxAlbumSmallGroupCircleSize: 27px;
|
sendBoxAlbumSmallGroupCircleSize: 27px;
|
||||||
|
|
||||||
sendBoxFileGroupSkipTop: 2px;
|
sendBoxFileGroupSkipTop: 2px;
|
||||||
sendBoxFileGroupSkipRight: 5px;
|
sendBoxFileGroupSkipRight: 1px;
|
||||||
sendBoxFileGroupEditInternalSkip: -1px;
|
sendBoxFileGroupEditInternalSkip: -1px;
|
||||||
|
|
||||||
sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) {
|
sendBoxAlbumGroupButtonFile: IconButton(editMediaButton) {
|
||||||
@@ -622,7 +674,7 @@ sendBoxAlbumGroupDeleteButtonIconFile: icon {{ "send_media/send_media_cross", me
|
|||||||
|
|
||||||
sendBoxAlbumButtonMediaMore: icon {{ "send_media/send_media_more", roundedFg }};
|
sendBoxAlbumButtonMediaMore: icon {{ "send_media/send_media_more", roundedFg }};
|
||||||
sendBoxAlbumGroupButtonMediaMore: icon {{ "send_media/send_media_more", roundedFg, point(4px, 1px) }};
|
sendBoxAlbumGroupButtonMediaMore: icon {{ "send_media/send_media_more", roundedFg, point(4px, 1px) }};
|
||||||
sendBoxAlbumGroupButtonMediaDelete: icon {{ "send_media/send_media_cross", roundedFg, point(-2px, 1px) }};
|
sendBoxAlbumGroupButtonMediaDelete: icon {{ "send_media/send_media_cross", roundedFg }};
|
||||||
|
|
||||||
defaultComposeIcons: ComposeIcons {
|
defaultComposeIcons: ComposeIcons {
|
||||||
settings: icon {{ "emoji/emoji_settings", emojiIconFg }};
|
settings: icon {{ "emoji/emoji_settings", emojiIconFg }};
|
||||||
@@ -1033,6 +1085,19 @@ historyPinnedShowAll: IconButton(historyReplyCancel) {
|
|||||||
icon: icon {{ "pinned_show_all", historyReplyCancelFg }};
|
icon: icon {{ "pinned_show_all", historyReplyCancelFg }};
|
||||||
iconOver: icon {{ "pinned_show_all", historyReplyCancelFgOver }};
|
iconOver: icon {{ "pinned_show_all", historyReplyCancelFgOver }};
|
||||||
}
|
}
|
||||||
|
sendFilesReplyIconPosition: point(11px, 7px);
|
||||||
|
sendFilesReplyCancelSize: 24px;
|
||||||
|
sendFilesReplyCancel: IconButton(editMediaButton) {
|
||||||
|
width: sendFilesReplyCancelSize;
|
||||||
|
height: sendFilesReplyCancelSize;
|
||||||
|
icon: icon {{ "send_media/send_media_cross", historyReplyCancelFg }};
|
||||||
|
iconOver: icon {{ "send_media/send_media_cross", historyReplyCancelFgOver }};
|
||||||
|
ripple: RippleAnimation(defaultRippleAnimation) {
|
||||||
|
color: windowBgRipple;
|
||||||
|
}
|
||||||
|
|
||||||
|
rippleAreaSize: sendFilesReplyCancelSize;
|
||||||
|
}
|
||||||
historyPinnedBotButton: RoundButton(defaultActiveButton) {
|
historyPinnedBotButton: RoundButton(defaultActiveButton) {
|
||||||
width: -34px;
|
width: -34px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
|
|||||||
@@ -670,12 +670,13 @@ void EmojiListWidget::applyNextSearchQuery() {
|
|||||||
}
|
}
|
||||||
const auto guard = gsl::finally([&] { finish(); });
|
const auto guard = gsl::finally([&] { finish(); });
|
||||||
auto plain = collectPlainSearchResults();
|
auto plain = collectPlainSearchResults();
|
||||||
if (_searchEmoji == _searchEmojiPrevious) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_searchEmoticon = QString();
|
_searchEmoticon = QString();
|
||||||
for (const auto emoji : plain) {
|
{
|
||||||
_searchEmoticon += emoji->text();
|
auto exactSet = base::flat_set<EmojiPtr>();
|
||||||
|
const auto exact = SearchEmoji(_searchQuery, exactSet, true);
|
||||||
|
for (const auto emoji : exact) {
|
||||||
|
_searchEmoticon += emoji->text();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_searchResults.clear();
|
_searchResults.clear();
|
||||||
_searchCustomIds.clear();
|
_searchCustomIds.clear();
|
||||||
@@ -690,9 +691,6 @@ void EmojiListWidget::applyNextSearchQuery() {
|
|||||||
if (_mode != Mode::Full || session().premium()) {
|
if (_mode != Mode::Full || session().premium()) {
|
||||||
appendPremiumSearchResults();
|
appendPremiumSearchResults();
|
||||||
}
|
}
|
||||||
if (_mode == Mode::Full) {
|
|
||||||
appendLocalPackSearchResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
_searchQueryText = ranges::accumulate(
|
_searchQueryText = ranges::accumulate(
|
||||||
_searchQuery,
|
_searchQuery,
|
||||||
@@ -822,62 +820,6 @@ void EmojiListWidget::appendPremiumSearchResults() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EmojiListWidget::appendLocalPackSearchResults() {
|
|
||||||
const auto text = _searchQueryText.toLower();
|
|
||||||
if (text.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto test = session().isTestMode();
|
|
||||||
const auto &sets = session().data().stickers().sets();
|
|
||||||
const auto processSet = [&](uint64 setId) {
|
|
||||||
const auto it = sets.find(setId);
|
|
||||||
if (it == sets.end()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto set = it->second.get();
|
|
||||||
if (!(set->flags & Data::StickersSetFlag::Emoji)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto title = set->title.toLower();
|
|
||||||
if (!title.startsWith(text)
|
|
||||||
&& !title.contains(u' ' + text)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto &list = set->stickers.empty()
|
|
||||||
? set->covers
|
|
||||||
: set->stickers;
|
|
||||||
for (const auto document : list) {
|
|
||||||
if (_searchResults.size() >= kCustomSearchLimit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const auto sticker = document->sticker();
|
|
||||||
if (!sticker) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto id = document->id;
|
|
||||||
if (!_searchCustomIds.emplace(id).second) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const auto statusId = EmojiStatusId{ id };
|
|
||||||
_searchResults.push_back({
|
|
||||||
.custom = resolveCustomEmoji(
|
|
||||||
statusId,
|
|
||||||
document,
|
|
||||||
SearchEmojiSectionSetId()),
|
|
||||||
.id = { RecentEmojiDocument{ .id = id, .test = test } },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for (const auto setId
|
|
||||||
: session().data().stickers().emojiSetsOrder()) {
|
|
||||||
processSet(setId);
|
|
||||||
}
|
|
||||||
for (const auto setId
|
|
||||||
: session().data().stickers().featuredEmojiSetsOrder()) {
|
|
||||||
processSet(setId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void EmojiListWidget::toggleSearchLoading(bool loading) {
|
void EmojiListWidget::toggleSearchLoading(bool loading) {
|
||||||
if (_search) {
|
if (_search) {
|
||||||
_search->setLoading(loading);
|
_search->setLoading(loading);
|
||||||
@@ -1131,9 +1073,6 @@ void EmojiListWidget::showSearchResults() {
|
|||||||
appendPremiumSearchResults();
|
appendPremiumSearchResults();
|
||||||
}
|
}
|
||||||
fillCloudSearchResults();
|
fillCloudSearchResults();
|
||||||
if (_mode == Mode::Full) {
|
|
||||||
appendLocalPackSearchResults();
|
|
||||||
}
|
|
||||||
fillCloudSearchSets();
|
fillCloudSearchSets();
|
||||||
|
|
||||||
resizeToWidth(width());
|
resizeToWidth(width());
|
||||||
|
|||||||
@@ -299,7 +299,6 @@ private:
|
|||||||
void setupSearch();
|
void setupSearch();
|
||||||
[[nodiscard]] std::vector<EmojiPtr> collectPlainSearchResults();
|
[[nodiscard]] std::vector<EmojiPtr> collectPlainSearchResults();
|
||||||
void appendPremiumSearchResults();
|
void appendPremiumSearchResults();
|
||||||
void appendLocalPackSearchResults();
|
|
||||||
void sendSearchRequest();
|
void sendSearchRequest();
|
||||||
void sendSearchSetsRequest(const QString &query);
|
void sendSearchSetsRequest(const QString &query);
|
||||||
void requestSearchCloud(
|
void requestSearchCloud(
|
||||||
|
|||||||
@@ -0,0 +1,691 @@
|
|||||||
|
/*
|
||||||
|
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 "chat_helpers/emoji_picker_overlay.h"
|
||||||
|
|
||||||
|
#include "ui/abstract_button.h"
|
||||||
|
#include "ui/emoji_config.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
|
#include "ui/widgets/labels.h"
|
||||||
|
#include "ui/widgets/scroll_area.h"
|
||||||
|
#include "styles/style_chat_helpers.h"
|
||||||
|
|
||||||
|
#include <QtCore/QEvent>
|
||||||
|
#include <QtGui/QMouseEvent>
|
||||||
|
#include <QtGui/QPainter>
|
||||||
|
|
||||||
|
namespace ChatHelpers {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<EmojiPtr> BuildAllEmojis() {
|
||||||
|
using Section = Ui::Emoji::Section;
|
||||||
|
auto result = std::vector<EmojiPtr>();
|
||||||
|
for (auto i = int(Section::People); i <= int(Section::Symbols); ++i) {
|
||||||
|
const auto section = Ui::Emoji::GetSection(Section(i));
|
||||||
|
result.reserve(result.size() + section.size());
|
||||||
|
for (const auto emoji : section) {
|
||||||
|
result.push_back(emoji);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<EmojiPtr> DefaultRecentVector() {
|
||||||
|
const auto src = Ui::Emoji::GetDefaultRecent();
|
||||||
|
return std::vector<EmojiPtr>(src.begin(), src.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class EmojiPickerOverlay::Strip final : public Ui::RpWidget {
|
||||||
|
public:
|
||||||
|
Strip(QWidget *parent, not_null<EmojiPickerOverlay*> owner);
|
||||||
|
|
||||||
|
void refresh();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *e) override;
|
||||||
|
void mouseMoveEvent(QMouseEvent *e) override;
|
||||||
|
void mousePressEvent(QMouseEvent *e) override;
|
||||||
|
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||||
|
void leaveEventHook(QEvent *e) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Cell {
|
||||||
|
EmojiPtr emoji = nullptr;
|
||||||
|
QRect rect;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] int cellAtPoint(QPoint p) const;
|
||||||
|
void updateHover(int index);
|
||||||
|
|
||||||
|
const not_null<EmojiPickerOverlay*> _owner;
|
||||||
|
std::vector<Cell> _cells;
|
||||||
|
int _hover = -1;
|
||||||
|
int _pressed = -1;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class EmojiPickerOverlay::Grid final : public Ui::RpWidget {
|
||||||
|
public:
|
||||||
|
Grid(QWidget *parent, not_null<EmojiPickerOverlay*> owner);
|
||||||
|
|
||||||
|
void setEmojis(std::vector<EmojiPtr> emojis);
|
||||||
|
int resizeGetHeight(int newWidth) override;
|
||||||
|
void refresh();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *e) override;
|
||||||
|
void mouseMoveEvent(QMouseEvent *e) override;
|
||||||
|
void mousePressEvent(QMouseEvent *e) override;
|
||||||
|
void mouseReleaseEvent(QMouseEvent *e) override;
|
||||||
|
void leaveEventHook(QEvent *e) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Cell {
|
||||||
|
EmojiPtr emoji = nullptr;
|
||||||
|
QRect rect;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] int cellAtPoint(QPoint p) const;
|
||||||
|
void relayoutCells();
|
||||||
|
void updateHover(int index);
|
||||||
|
|
||||||
|
const not_null<EmojiPickerOverlay*> _owner;
|
||||||
|
std::vector<EmojiPtr> _emojis;
|
||||||
|
std::vector<Cell> _cells;
|
||||||
|
int _columns = 0;
|
||||||
|
int _hover = -1;
|
||||||
|
int _pressed = -1;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void DrawEmojiCell(
|
||||||
|
QPainter &p,
|
||||||
|
const QRect &cell,
|
||||||
|
EmojiPtr emoji,
|
||||||
|
bool selected,
|
||||||
|
bool hovered) {
|
||||||
|
if (selected) {
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(st::stickersEmojiPickerSelectedBg);
|
||||||
|
p.drawEllipse(cell);
|
||||||
|
} else if (hovered) {
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(anim::with_alpha(st::windowSubTextFg->c, 0.12));
|
||||||
|
p.drawEllipse(cell);
|
||||||
|
}
|
||||||
|
if (!emoji) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto esize = Ui::Emoji::GetSizeLarge();
|
||||||
|
const auto dpr = style::DevicePixelRatio();
|
||||||
|
const auto pixelSize = esize / dpr;
|
||||||
|
const auto drawSize = std::min(
|
||||||
|
pixelSize,
|
||||||
|
st::stickersEmojiPickerItemSize - 4);
|
||||||
|
const auto x = cell.x() + (cell.width() - drawSize) / 2;
|
||||||
|
const auto y = cell.y() + (cell.height() - drawSize) / 2;
|
||||||
|
if (drawSize == pixelSize) {
|
||||||
|
Ui::Emoji::Draw(p, emoji, esize, x, y);
|
||||||
|
} else {
|
||||||
|
const auto target = QRect(x, y, drawSize, drawSize);
|
||||||
|
auto buffer = QImage(
|
||||||
|
QSize(pixelSize, pixelSize) * dpr,
|
||||||
|
QImage::Format_ARGB32_Premultiplied);
|
||||||
|
buffer.fill(Qt::transparent);
|
||||||
|
buffer.setDevicePixelRatio(dpr);
|
||||||
|
{
|
||||||
|
auto q = QPainter(&buffer);
|
||||||
|
Ui::Emoji::Draw(q, emoji, esize, 0, 0);
|
||||||
|
}
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
p.drawImage(target, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
EmojiPickerOverlay::Strip::Strip(
|
||||||
|
QWidget *parent,
|
||||||
|
not_null<EmojiPickerOverlay*> owner)
|
||||||
|
: RpWidget(parent)
|
||||||
|
, _owner(owner) {
|
||||||
|
setMouseTracking(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Strip::refresh() {
|
||||||
|
const auto &sel = _owner->_selectedList;
|
||||||
|
const auto &recent = _owner->_recent;
|
||||||
|
const auto item = st::stickersEmojiPickerItemSize;
|
||||||
|
const auto skip = st::stickersEmojiPickerItemSkip;
|
||||||
|
const auto w = width();
|
||||||
|
if (w <= 0 || item <= 0) {
|
||||||
|
_cells.clear();
|
||||||
|
update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto capacity = std::max(1, (w + skip) / (item + skip));
|
||||||
|
|
||||||
|
auto order = std::vector<EmojiPtr>();
|
||||||
|
order.reserve(sel.size() + recent.size());
|
||||||
|
for (const auto emoji : sel) {
|
||||||
|
order.push_back(emoji);
|
||||||
|
}
|
||||||
|
for (const auto emoji : recent) {
|
||||||
|
const auto already = std::find(sel.begin(), sel.end(), emoji)
|
||||||
|
!= sel.end();
|
||||||
|
if (!already) {
|
||||||
|
order.push_back(emoji);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (int(order.size()) > capacity) {
|
||||||
|
order.resize(capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
_cells.clear();
|
||||||
|
_cells.reserve(order.size());
|
||||||
|
auto x = 0;
|
||||||
|
const auto y = (height() - item) / 2;
|
||||||
|
for (const auto emoji : order) {
|
||||||
|
_cells.push_back({ emoji, QRect(x, y, item, item) });
|
||||||
|
x += item + skip;
|
||||||
|
}
|
||||||
|
_hover = -1;
|
||||||
|
_pressed = -1;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
int EmojiPickerOverlay::Strip::cellAtPoint(QPoint p) const {
|
||||||
|
for (auto i = 0; i != int(_cells.size()); ++i) {
|
||||||
|
if (_cells[i].rect.contains(p)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Strip::updateHover(int index) {
|
||||||
|
if (_hover == index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_hover = index;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Strip::paintEvent(QPaintEvent *e) {
|
||||||
|
auto p = QPainter(this);
|
||||||
|
for (auto i = 0; i != int(_cells.size()); ++i) {
|
||||||
|
const auto &cell = _cells[i];
|
||||||
|
const auto selected = _owner->_selectedList.end()
|
||||||
|
!= std::find(
|
||||||
|
_owner->_selectedList.begin(),
|
||||||
|
_owner->_selectedList.end(),
|
||||||
|
cell.emoji);
|
||||||
|
DrawEmojiCell(p, cell.rect, cell.emoji, selected, i == _hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Strip::mouseMoveEvent(QMouseEvent *e) {
|
||||||
|
updateHover(cellAtPoint(e->pos()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Strip::mousePressEvent(QMouseEvent *e) {
|
||||||
|
_pressed = cellAtPoint(e->pos());
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Strip::mouseReleaseEvent(QMouseEvent *e) {
|
||||||
|
const auto released = cellAtPoint(e->pos());
|
||||||
|
const auto index = _pressed;
|
||||||
|
_pressed = -1;
|
||||||
|
if (released == index && index >= 0 && index < int(_cells.size())) {
|
||||||
|
_owner->toggleEmoji(_cells[index].emoji, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Strip::leaveEventHook(QEvent *e) {
|
||||||
|
updateHover(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
EmojiPickerOverlay::Grid::Grid(
|
||||||
|
QWidget *parent,
|
||||||
|
not_null<EmojiPickerOverlay*> owner)
|
||||||
|
: RpWidget(parent)
|
||||||
|
, _owner(owner) {
|
||||||
|
setMouseTracking(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Grid::setEmojis(std::vector<EmojiPtr> emojis) {
|
||||||
|
_emojis = std::move(emojis);
|
||||||
|
relayoutCells();
|
||||||
|
}
|
||||||
|
|
||||||
|
int EmojiPickerOverlay::Grid::resizeGetHeight(int newWidth) {
|
||||||
|
resize(newWidth, 0);
|
||||||
|
relayoutCells();
|
||||||
|
return height();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Grid::refresh() {
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Grid::relayoutCells() {
|
||||||
|
const auto item = st::stickersEmojiPickerItemSize;
|
||||||
|
const auto skip = st::stickersEmojiPickerItemSkip;
|
||||||
|
const auto w = width();
|
||||||
|
_columns = std::max(1, (w + skip) / (item + skip));
|
||||||
|
_cells.clear();
|
||||||
|
_cells.reserve(_emojis.size());
|
||||||
|
auto col = 0;
|
||||||
|
auto row = 0;
|
||||||
|
for (const auto emoji : _emojis) {
|
||||||
|
const auto x = col * (item + skip);
|
||||||
|
const auto y = row * (item + skip);
|
||||||
|
_cells.push_back({ emoji, QRect(x, y, item, item) });
|
||||||
|
if (++col >= _columns) {
|
||||||
|
col = 0;
|
||||||
|
++row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto fullRows = row + (col > 0 ? 1 : 0);
|
||||||
|
const auto h = fullRows > 0
|
||||||
|
? (fullRows * item + (fullRows - 1) * skip)
|
||||||
|
: 0;
|
||||||
|
resize(w, h);
|
||||||
|
_hover = -1;
|
||||||
|
_pressed = -1;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
int EmojiPickerOverlay::Grid::cellAtPoint(QPoint p) const {
|
||||||
|
for (auto i = 0; i != int(_cells.size()); ++i) {
|
||||||
|
if (_cells[i].rect.contains(p)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Grid::updateHover(int index) {
|
||||||
|
if (_hover == index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_hover = index;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Grid::paintEvent(QPaintEvent *e) {
|
||||||
|
auto p = QPainter(this);
|
||||||
|
const auto clip = e->rect();
|
||||||
|
for (auto i = 0; i != int(_cells.size()); ++i) {
|
||||||
|
const auto &cell = _cells[i];
|
||||||
|
if (!cell.rect.intersects(clip)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto selected = _owner->_selectedList.end()
|
||||||
|
!= std::find(
|
||||||
|
_owner->_selectedList.begin(),
|
||||||
|
_owner->_selectedList.end(),
|
||||||
|
cell.emoji);
|
||||||
|
DrawEmojiCell(p, cell.rect, cell.emoji, selected, i == _hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Grid::mouseMoveEvent(QMouseEvent *e) {
|
||||||
|
updateHover(cellAtPoint(e->pos()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Grid::mousePressEvent(QMouseEvent *e) {
|
||||||
|
_pressed = cellAtPoint(e->pos());
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Grid::mouseReleaseEvent(QMouseEvent *e) {
|
||||||
|
const auto released = cellAtPoint(e->pos());
|
||||||
|
const auto index = _pressed;
|
||||||
|
_pressed = -1;
|
||||||
|
if (released == index && index >= 0 && index < int(_cells.size())) {
|
||||||
|
_owner->toggleEmoji(_cells[index].emoji, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::Grid::leaveEventHook(QEvent *e) {
|
||||||
|
updateHover(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
EmojiPickerOverlay::Metrics EmojiPickerOverlay::EstimateMetrics(
|
||||||
|
const QString &aboutText) {
|
||||||
|
const auto tailHeight = st::stickersEmojiPickerStripBubble.height();
|
||||||
|
const auto shadowExtent = Ui::BoxShadow::ExtendFor(
|
||||||
|
st::stickersEmojiPickerBoxShadow);
|
||||||
|
const auto &pad = st::stickersEmojiPickerPadding;
|
||||||
|
auto about = Ui::FlatLabel(
|
||||||
|
nullptr,
|
||||||
|
aboutText,
|
||||||
|
st::stickersEmojiPickerAbout);
|
||||||
|
const auto collapsedHeight = pad.top()
|
||||||
|
+ about.height()
|
||||||
|
+ st::stickersEmojiPickerStripHeight
|
||||||
|
+ pad.bottom();
|
||||||
|
const auto expandedHeight = collapsedHeight
|
||||||
|
+ st::stickersEmojiPickerExpandedHeight;
|
||||||
|
const auto shadowAndTail = shadowExtent.top()
|
||||||
|
+ shadowExtent.bottom()
|
||||||
|
+ tailHeight;
|
||||||
|
return {
|
||||||
|
.shadowExtent = shadowExtent,
|
||||||
|
.tailHeight = tailHeight,
|
||||||
|
.collapsedHeight = collapsedHeight,
|
||||||
|
.expandedHeight = expandedHeight,
|
||||||
|
.totalCollapsedHeight = collapsedHeight + shadowAndTail,
|
||||||
|
.totalExpandedHeight = expandedHeight + shadowAndTail,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
EmojiPickerOverlay::EmojiPickerOverlay(
|
||||||
|
QWidget *parent,
|
||||||
|
EmojiPickerOverlayDescriptor descriptor)
|
||||||
|
: RpWidget(parent)
|
||||||
|
, _aboutText(std::move(descriptor.aboutText))
|
||||||
|
, _recent(descriptor.recent.empty()
|
||||||
|
? DefaultRecentVector()
|
||||||
|
: std::move(descriptor.recent))
|
||||||
|
, _maxSelected(descriptor.maxSelected)
|
||||||
|
, _allowExpand(descriptor.allowExpand)
|
||||||
|
, _selectedList(std::move(descriptor.initialSelected))
|
||||||
|
, _shadow(st::stickersEmojiPickerBoxShadow) {
|
||||||
|
_allForGrid = BuildAllEmojis();
|
||||||
|
|
||||||
|
_about = std::make_unique<Ui::FlatLabel>(
|
||||||
|
this,
|
||||||
|
_aboutText,
|
||||||
|
st::stickersEmojiPickerAbout);
|
||||||
|
|
||||||
|
_strip = Ui::CreateChild<Strip>(this, this);
|
||||||
|
|
||||||
|
if (_allowExpand) {
|
||||||
|
_expandButton = Ui::CreateChild<Ui::AbstractButton>(this);
|
||||||
|
_expandButton->resize(
|
||||||
|
st::stickersEmojiPickerExpandSize,
|
||||||
|
st::stickersEmojiPickerExpandSize);
|
||||||
|
_expandButton->setClickedCallback([=] {
|
||||||
|
setExpanded(!_expanded.current());
|
||||||
|
});
|
||||||
|
_expandButton->paintRequest(
|
||||||
|
) | rpl::on_next([=](const QRect &clip) {
|
||||||
|
auto p = QPainter(_expandButton);
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(st::stickersEmojiPickerExpandBg);
|
||||||
|
p.drawEllipse(_expandButton->rect());
|
||||||
|
const auto &icon = _expanded.current()
|
||||||
|
? st::stickersEmojiPickerCollapseIcon
|
||||||
|
: st::stickersEmojiPickerExpandIcon;
|
||||||
|
const auto x = (_expandButton->width() - icon.width()) / 2;
|
||||||
|
const auto y = (_expandButton->height() - icon.height()) / 2;
|
||||||
|
icon.paint(p, x, y, _expandButton->width());
|
||||||
|
}, _expandButton->lifetime());
|
||||||
|
|
||||||
|
_scroll = std::make_unique<Ui::ScrollArea>(
|
||||||
|
this,
|
||||||
|
st::stickersEmojiPickerScroll);
|
||||||
|
_scroll->setFrameStyle(QFrame::NoFrame);
|
||||||
|
_scroll->hide();
|
||||||
|
const auto gridPtr = _scroll->setOwnedWidget(
|
||||||
|
object_ptr<Grid>(_scroll.get(), this));
|
||||||
|
_grid = gridPtr.data();
|
||||||
|
_grid->setEmojis(_allForGrid);
|
||||||
|
}
|
||||||
|
|
||||||
|
_selectedVar = _selectedList;
|
||||||
|
resize(width(), totalExpandedHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
QMargins EmojiPickerOverlay::shadowExtent() const {
|
||||||
|
return _shadow.extend();
|
||||||
|
}
|
||||||
|
|
||||||
|
int EmojiPickerOverlay::totalCollapsedHeight() const {
|
||||||
|
const auto ext = _shadow.extend();
|
||||||
|
return collapsedHeight() + ext.top() + ext.bottom() + tailHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
int EmojiPickerOverlay::totalExpandedHeight() const {
|
||||||
|
const auto ext = _shadow.extend();
|
||||||
|
return expandedHeight() + ext.top() + ext.bottom() + tailHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect EmojiPickerOverlay::bubbleRect() const {
|
||||||
|
const auto ext = _shadow.extend();
|
||||||
|
return QRect(
|
||||||
|
ext.left(),
|
||||||
|
ext.top(),
|
||||||
|
width() - ext.left() - ext.right(),
|
||||||
|
height() - ext.top() - ext.bottom() - tailHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
QRect EmojiPickerOverlay::bubbleShownRect() const {
|
||||||
|
const auto r = bubbleRect();
|
||||||
|
return QRect(r.x(), r.y(), r.width(), currentShownHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
EmojiPickerOverlay::~EmojiPickerOverlay() = default;
|
||||||
|
|
||||||
|
const std::vector<EmojiPtr> &EmojiPickerOverlay::selected() const {
|
||||||
|
return _selectedList;
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<std::vector<EmojiPtr>>
|
||||||
|
EmojiPickerOverlay::selectedValue() const {
|
||||||
|
return _selectedVar.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::setExpanded(bool expanded) {
|
||||||
|
if (!_allowExpand || _expanded.current() == expanded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startExpandAnimation(expanded);
|
||||||
|
_expanded = expanded;
|
||||||
|
if (_expandButton) {
|
||||||
|
_expandButton->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::startExpandAnimation(bool expanded) {
|
||||||
|
const auto from = _expandAnim.value(expanded ? 0. : 1.);
|
||||||
|
const auto to = expanded ? 1. : 0.;
|
||||||
|
_expandAnim.start(
|
||||||
|
[=] { applyExpandProgress(); },
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
st::slideWrapDuration,
|
||||||
|
anim::easeOutCirc);
|
||||||
|
applyExpandProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
float64 EmojiPickerOverlay::currentExpandValue() const {
|
||||||
|
return _expandAnim.value(_expanded.current() ? 1. : 0.);
|
||||||
|
}
|
||||||
|
|
||||||
|
int EmojiPickerOverlay::currentShownHeight() const {
|
||||||
|
const auto progress = currentExpandValue();
|
||||||
|
return anim::interpolate(
|
||||||
|
collapsedHeight(),
|
||||||
|
expandedHeight(),
|
||||||
|
progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::applyExpandProgress() {
|
||||||
|
if (_scroll) {
|
||||||
|
const auto progress = currentExpandValue();
|
||||||
|
_scroll->setVisible(progress > 0.);
|
||||||
|
}
|
||||||
|
relayout();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EmojiPickerOverlay::expanded() const {
|
||||||
|
return _expanded.current();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<bool> EmojiPickerOverlay::expandedValue() const {
|
||||||
|
return _expanded.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
int EmojiPickerOverlay::collapsedHeight() const {
|
||||||
|
const auto &pad = st::stickersEmojiPickerPadding;
|
||||||
|
const auto aboutH = _about ? _about->height() : 0;
|
||||||
|
return pad.top()
|
||||||
|
+ aboutH
|
||||||
|
+ st::stickersEmojiPickerStripHeight
|
||||||
|
+ pad.bottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
int EmojiPickerOverlay::expandedHeight() const {
|
||||||
|
return collapsedHeight() + st::stickersEmojiPickerExpandedHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::paintEvent(QPaintEvent *e) {
|
||||||
|
auto p = QPainter(this);
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
const auto progress = currentExpandValue();
|
||||||
|
const auto shown = bubbleShownRect();
|
||||||
|
const auto radius = st::stickersEmojiPickerExpandedRadius;
|
||||||
|
|
||||||
|
_shadow.paint(p, shown, radius);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(st::stickersEmojiPickerBg);
|
||||||
|
p.drawRoundedRect(shown, radius, radius);
|
||||||
|
|
||||||
|
if (progress < 1.) {
|
||||||
|
paintTailBubble(p, shown, 1. - progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::paintTailBubble(
|
||||||
|
QPainter &p,
|
||||||
|
const QRect &bubble,
|
||||||
|
float64 opacity) {
|
||||||
|
const auto &icon = st::stickersEmojiPickerStripBubble;
|
||||||
|
const auto offsetRight = st::stickersEmojiPickerStripBubbleRight;
|
||||||
|
const auto x = bubble.right() + 1 - offsetRight - icon.width();
|
||||||
|
const auto y = bubble.bottom() + 1;
|
||||||
|
if (opacity >= 1.) {
|
||||||
|
icon.paint(p, x, y, width());
|
||||||
|
} else {
|
||||||
|
p.save();
|
||||||
|
p.setOpacity(opacity);
|
||||||
|
icon.paint(p, x, y, width());
|
||||||
|
p.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::resizeEvent(QResizeEvent *e) {
|
||||||
|
relayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::mousePressEvent(QMouseEvent *e) {
|
||||||
|
if (!bubbleShownRect().contains(e->pos())) {
|
||||||
|
e->ignore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int EmojiPickerOverlay::tailHeight() const {
|
||||||
|
return st::stickersEmojiPickerStripBubble.height();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::relayout() {
|
||||||
|
const auto &pad = st::stickersEmojiPickerPadding;
|
||||||
|
const auto bubble = bubbleRect();
|
||||||
|
const auto bubbleShown = currentShownHeight();
|
||||||
|
if (_about) {
|
||||||
|
_about->resizeToWidth(bubble.width() - pad.left() - pad.right());
|
||||||
|
_about->moveToLeft(bubble.left() + pad.left(), bubble.top() + pad.top());
|
||||||
|
}
|
||||||
|
const auto aboutBottom = _about
|
||||||
|
? (_about->y() + _about->height())
|
||||||
|
: (bubble.top() + pad.top());
|
||||||
|
|
||||||
|
const auto stripTop = aboutBottom;
|
||||||
|
const auto stripH = st::stickersEmojiPickerStripHeight;
|
||||||
|
const auto expandSize = _expandButton
|
||||||
|
? _expandButton->width()
|
||||||
|
: 0;
|
||||||
|
const auto expandGap = _expandButton
|
||||||
|
? st::stickersEmojiPickerItemSkip
|
||||||
|
: 0;
|
||||||
|
const auto stripW = bubble.width()
|
||||||
|
- pad.left()
|
||||||
|
- pad.right()
|
||||||
|
- expandSize
|
||||||
|
- expandGap;
|
||||||
|
_strip->setGeometry(bubble.left() + pad.left(), stripTop, stripW, stripH);
|
||||||
|
_strip->refresh();
|
||||||
|
|
||||||
|
if (_expandButton) {
|
||||||
|
const auto bx = bubble.right() + 1 - pad.right() - expandSize;
|
||||||
|
const auto by = stripTop + (stripH - expandSize) / 2;
|
||||||
|
_expandButton->moveToLeft(bx, by);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_scroll) {
|
||||||
|
const auto scrollTop = stripTop + stripH;
|
||||||
|
const auto bubbleBottom = bubble.top() + bubbleShown;
|
||||||
|
const auto scrollH = std::max(
|
||||||
|
0,
|
||||||
|
bubbleBottom - scrollTop - pad.bottom());
|
||||||
|
const auto scrollContentWidth = bubble.width()
|
||||||
|
- pad.left()
|
||||||
|
- pad.right();
|
||||||
|
const auto scrollAreaWidth = scrollContentWidth
|
||||||
|
+ pad.right();
|
||||||
|
_scroll->setGeometry(
|
||||||
|
bubble.left() + pad.left(),
|
||||||
|
scrollTop,
|
||||||
|
scrollAreaWidth,
|
||||||
|
scrollH);
|
||||||
|
if (_grid) {
|
||||||
|
_grid->resizeGetHeight(scrollContentWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::toggleEmoji(EmojiPtr emoji, bool fromGrid) {
|
||||||
|
if (!emoji) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto it = std::find(
|
||||||
|
_selectedList.begin(),
|
||||||
|
_selectedList.end(),
|
||||||
|
emoji);
|
||||||
|
if (it != _selectedList.end()) {
|
||||||
|
_selectedList.erase(it);
|
||||||
|
} else {
|
||||||
|
if (_maxSelected > 0 && int(_selectedList.size()) >= _maxSelected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_selectedList.push_back(emoji);
|
||||||
|
}
|
||||||
|
notifySelectionChanged();
|
||||||
|
if (fromGrid) {
|
||||||
|
setExpanded(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EmojiPickerOverlay::notifySelectionChanged() {
|
||||||
|
_selectedVar = _selectedList;
|
||||||
|
if (_strip) {
|
||||||
|
_strip->refresh();
|
||||||
|
}
|
||||||
|
if (_grid) {
|
||||||
|
_grid->refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ChatHelpers
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
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/rp_widget.h"
|
||||||
|
#include "ui/effects/animations.h"
|
||||||
|
#include "ui/emoji_config.h"
|
||||||
|
#include "ui/widgets/shadow.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class AbstractButton;
|
||||||
|
class FlatLabel;
|
||||||
|
class ScrollArea;
|
||||||
|
} // namespace Ui
|
||||||
|
|
||||||
|
namespace ChatHelpers {
|
||||||
|
|
||||||
|
struct EmojiPickerOverlayDescriptor {
|
||||||
|
QString aboutText;
|
||||||
|
std::vector<EmojiPtr> recent;
|
||||||
|
int maxSelected = 0;
|
||||||
|
bool allowExpand = true;
|
||||||
|
std::vector<EmojiPtr> initialSelected;
|
||||||
|
};
|
||||||
|
|
||||||
|
class EmojiPickerOverlay final : public Ui::RpWidget {
|
||||||
|
public:
|
||||||
|
EmojiPickerOverlay(
|
||||||
|
QWidget *parent,
|
||||||
|
EmojiPickerOverlayDescriptor descriptor);
|
||||||
|
~EmojiPickerOverlay();
|
||||||
|
|
||||||
|
struct Metrics {
|
||||||
|
QMargins shadowExtent;
|
||||||
|
int tailHeight = 0;
|
||||||
|
int collapsedHeight = 0;
|
||||||
|
int expandedHeight = 0;
|
||||||
|
int totalCollapsedHeight = 0;
|
||||||
|
int totalExpandedHeight = 0;
|
||||||
|
};
|
||||||
|
[[nodiscard]] static Metrics EstimateMetrics(const QString &aboutText);
|
||||||
|
|
||||||
|
[[nodiscard]] const std::vector<EmojiPtr> &selected() const;
|
||||||
|
[[nodiscard]] rpl::producer<std::vector<EmojiPtr>> selectedValue() const;
|
||||||
|
|
||||||
|
void setExpanded(bool expanded);
|
||||||
|
[[nodiscard]] bool expanded() const;
|
||||||
|
[[nodiscard]] rpl::producer<bool> expandedValue() const;
|
||||||
|
|
||||||
|
[[nodiscard]] int collapsedHeight() const;
|
||||||
|
[[nodiscard]] int expandedHeight() const;
|
||||||
|
[[nodiscard]] QMargins shadowExtent() const;
|
||||||
|
[[nodiscard]] int totalCollapsedHeight() const;
|
||||||
|
[[nodiscard]] int totalExpandedHeight() const;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *e) override;
|
||||||
|
void resizeEvent(QResizeEvent *e) override;
|
||||||
|
void mousePressEvent(QMouseEvent *e) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
class Strip;
|
||||||
|
class Grid;
|
||||||
|
|
||||||
|
void relayout();
|
||||||
|
void toggleEmoji(EmojiPtr emoji, bool fromGrid);
|
||||||
|
void notifySelectionChanged();
|
||||||
|
void startExpandAnimation(bool expanded);
|
||||||
|
void applyExpandProgress();
|
||||||
|
void paintTailBubble(QPainter &p, const QRect &bubble, float64 opacity);
|
||||||
|
[[nodiscard]] float64 currentExpandValue() const;
|
||||||
|
[[nodiscard]] int currentShownHeight() const;
|
||||||
|
[[nodiscard]] int tailHeight() const;
|
||||||
|
[[nodiscard]] QRect bubbleRect() const;
|
||||||
|
[[nodiscard]] QRect bubbleShownRect() const;
|
||||||
|
|
||||||
|
const QString _aboutText;
|
||||||
|
const std::vector<EmojiPtr> _recent;
|
||||||
|
const int _maxSelected;
|
||||||
|
const bool _allowExpand;
|
||||||
|
|
||||||
|
std::vector<EmojiPtr> _allForGrid;
|
||||||
|
|
||||||
|
std::vector<EmojiPtr> _selectedList;
|
||||||
|
rpl::variable<std::vector<EmojiPtr>> _selectedVar;
|
||||||
|
rpl::variable<bool> _expanded = false;
|
||||||
|
|
||||||
|
std::unique_ptr<Ui::FlatLabel> _about;
|
||||||
|
Strip *_strip = nullptr;
|
||||||
|
Ui::AbstractButton *_expandButton = nullptr;
|
||||||
|
std::unique_ptr<Ui::ScrollArea> _scroll;
|
||||||
|
Grid *_grid = nullptr;
|
||||||
|
Ui::Animations::Simple _expandAnim;
|
||||||
|
Ui::BoxShadow _shadow;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ChatHelpers
|
||||||
@@ -146,7 +146,8 @@ rpl::producer<std::vector<GifSection>> GifSectionsValue(
|
|||||||
|
|
||||||
[[nodiscard]] std::vector<EmojiPtr> SearchEmoji(
|
[[nodiscard]] std::vector<EmojiPtr> SearchEmoji(
|
||||||
const std::vector<QString> &query,
|
const std::vector<QString> &query,
|
||||||
base::flat_set<EmojiPtr> &outResultSet) {
|
base::flat_set<EmojiPtr> &outResultSet,
|
||||||
|
bool exact) {
|
||||||
auto result = std::vector<EmojiPtr>();
|
auto result = std::vector<EmojiPtr>();
|
||||||
const auto pushPlain = [&](EmojiPtr emoji) {
|
const auto pushPlain = [&](EmojiPtr emoji) {
|
||||||
if (result.size() < kEmojiSearchLimit
|
if (result.size() < kEmojiSearchLimit
|
||||||
@@ -170,7 +171,7 @@ rpl::producer<std::vector<GifSection>> GifSectionsValue(
|
|||||||
refreshed = true;
|
refreshed = true;
|
||||||
keywords.refresh();
|
keywords.refresh();
|
||||||
}
|
}
|
||||||
const auto list = keywords.queryMine(entry);
|
const auto list = keywords.queryMine(entry, exact);
|
||||||
for (const auto &entry : list) {
|
for (const auto &entry : list) {
|
||||||
pushPlain(entry.emoji);
|
pushPlain(entry.emoji);
|
||||||
if (result.size() >= kEmojiSearchLimit) {
|
if (result.size() >= kEmojiSearchLimit) {
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ struct GifSection {
|
|||||||
|
|
||||||
[[nodiscard]] std::vector<EmojiPtr> SearchEmoji(
|
[[nodiscard]] std::vector<EmojiPtr> SearchEmoji(
|
||||||
const std::vector<QString> &query,
|
const std::vector<QString> &query,
|
||||||
base::flat_set<EmojiPtr> &outResultSet);
|
base::flat_set<EmojiPtr> &outResultSet,
|
||||||
|
bool exact = false);
|
||||||
|
|
||||||
struct StickerIcon {
|
struct StickerIcon {
|
||||||
explicit StickerIcon(uint64 setId);
|
explicit StickerIcon(uint64 setId);
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ StickersListWidget::StickersListWidget(
|
|||||||
, _section(Section::Stickers)
|
, _section(Section::Stickers)
|
||||||
, _isMasks(_mode == Mode::Masks)
|
, _isMasks(_mode == Mode::Masks)
|
||||||
, _isEffects(_mode == Mode::MessageEffects)
|
, _isEffects(_mode == Mode::MessageEffects)
|
||||||
|
, _excludeSetId(descriptor.excludeSetId)
|
||||||
, _updateItemsTimer([=] { updateItems(); })
|
, _updateItemsTimer([=] { updateItems(); })
|
||||||
, _updateSetsTimer([=] { updateSets(); })
|
, _updateSetsTimer([=] { updateSets(); })
|
||||||
, _trendingAddBgOver(
|
, _trendingAddBgOver(
|
||||||
@@ -2580,6 +2581,9 @@ bool StickersListWidget::appendSet(
|
|||||||
uint64 setId,
|
uint64 setId,
|
||||||
bool externalLayout,
|
bool externalLayout,
|
||||||
AppendSkip skip) {
|
AppendSkip skip) {
|
||||||
|
if (_excludeSetId && setId == _excludeSetId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const auto &sets = session().data().stickers().sets();
|
const auto &sets = session().data().stickers().sets();
|
||||||
auto it = sets.find(setId);
|
auto it = sets.find(setId);
|
||||||
if (it == sets.cend()
|
if (it == sets.cend()
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ struct StickersListDescriptor {
|
|||||||
std::vector<StickerCustomRecentDescriptor> customRecentList;
|
std::vector<StickerCustomRecentDescriptor> customRecentList;
|
||||||
const style::EmojiPan *st = nullptr;
|
const style::EmojiPan *st = nullptr;
|
||||||
ComposeFeatures features;
|
ComposeFeatures features;
|
||||||
|
uint64 excludeSetId = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class StickersListWidget final : public TabbedSelector::Inner {
|
class StickersListWidget final : public TabbedSelector::Inner {
|
||||||
@@ -419,6 +420,7 @@ private:
|
|||||||
Section _section = Section::Stickers;
|
Section _section = Section::Stickers;
|
||||||
const bool _isMasks;
|
const bool _isMasks;
|
||||||
const bool _isEffects;
|
const bool _isEffects;
|
||||||
|
const uint64 _excludeSetId = 0;
|
||||||
|
|
||||||
base::Timer _updateItemsTimer;
|
base::Timer _updateItemsTimer;
|
||||||
base::Timer _updateSetsTimer;
|
base::Timer _updateSetsTimer;
|
||||||
|
|||||||
@@ -192,6 +192,10 @@ void TabbedPanel::setDesiredHeightValues(
|
|||||||
updateContentHeight();
|
updateContentHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TabbedPanel::setShowAnimationOrigin(Ui::PanelAnimation::Origin origin) {
|
||||||
|
_showAnimationOrigin = origin;
|
||||||
|
}
|
||||||
|
|
||||||
void TabbedPanel::setDropDown(bool dropDown) {
|
void TabbedPanel::setDropDown(bool dropDown) {
|
||||||
selector()->setDropDown(dropDown);
|
selector()->setDropDown(dropDown);
|
||||||
_dropDown = dropDown;
|
_dropDown = dropDown;
|
||||||
@@ -380,11 +384,12 @@ void TabbedPanel::startShowAnimation() {
|
|||||||
if (!_a_show.animating()) {
|
if (!_a_show.animating()) {
|
||||||
auto image = grabForAnimation();
|
auto image = grabForAnimation();
|
||||||
|
|
||||||
|
const auto origin = _showAnimationOrigin.value_or(_dropDown
|
||||||
|
? Ui::PanelAnimation::Origin::TopRight
|
||||||
|
: Ui::PanelAnimation::Origin::BottomRight);
|
||||||
_showAnimation = std::make_unique<Ui::PanelAnimation>(
|
_showAnimation = std::make_unique<Ui::PanelAnimation>(
|
||||||
_selector->st().showAnimation,
|
_selector->st().showAnimation,
|
||||||
(_dropDown
|
origin);
|
||||||
? Ui::PanelAnimation::Origin::TopRight
|
|
||||||
: Ui::PanelAnimation::Origin::BottomRight));
|
|
||||||
auto inner = rect().marginsRemoved(st::emojiPanMargins);
|
auto inner = rect().marginsRemoved(st::emojiPanMargins);
|
||||||
_showAnimation->setFinalImage(
|
_showAnimation->setFinalImage(
|
||||||
std::move(image),
|
std::move(image),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "ui/effects/animations.h"
|
#include "ui/effects/animations.h"
|
||||||
|
#include "ui/effects/panel_animation.h"
|
||||||
#include "ui/rp_widget.h"
|
#include "ui/rp_widget.h"
|
||||||
#include "ui/widgets/shadow.h"
|
#include "ui/widgets/shadow.h"
|
||||||
#include "base/timer.h"
|
#include "base/timer.h"
|
||||||
@@ -17,9 +18,6 @@ namespace Window {
|
|||||||
class SessionController;
|
class SessionController;
|
||||||
} // namespace Window
|
} // namespace Window
|
||||||
|
|
||||||
namespace Ui {
|
|
||||||
class PanelAnimation;
|
|
||||||
} // namespace Ui
|
|
||||||
|
|
||||||
namespace ChatHelpers {
|
namespace ChatHelpers {
|
||||||
|
|
||||||
@@ -57,6 +55,7 @@ public:
|
|||||||
int minHeight,
|
int minHeight,
|
||||||
int maxHeight);
|
int maxHeight);
|
||||||
void setDropDown(bool dropDown);
|
void setDropDown(bool dropDown);
|
||||||
|
void setShowAnimationOrigin(Ui::PanelAnimation::Origin origin);
|
||||||
|
|
||||||
void hideFast();
|
void hideFast();
|
||||||
bool hiding() const {
|
bool hiding() const {
|
||||||
@@ -122,6 +121,7 @@ private:
|
|||||||
|
|
||||||
bool _shouldFinishHide = false;
|
bool _shouldFinishHide = false;
|
||||||
bool _dropDown = false;
|
bool _dropDown = false;
|
||||||
|
std::optional<Ui::PanelAnimation::Origin> _showAnimationOrigin;
|
||||||
|
|
||||||
bool _hiding = false;
|
bool _hiding = false;
|
||||||
bool _hideAfterSlide = false;
|
bool _hideAfterSlide = false;
|
||||||
|
|||||||
@@ -381,6 +381,7 @@ TabbedSelector::TabbedSelector(
|
|||||||
, _show(std::move(descriptor.show))
|
, _show(std::move(descriptor.show))
|
||||||
, _level(descriptor.level)
|
, _level(descriptor.level)
|
||||||
, _customTextColor(std::move(descriptor.customTextColor))
|
, _customTextColor(std::move(descriptor.customTextColor))
|
||||||
|
, _excludeStickerSetId(descriptor.excludeStickerSetId)
|
||||||
, _mode(descriptor.mode)
|
, _mode(descriptor.mode)
|
||||||
, _panelRounding(Ui::PrepareCornerPixmaps(st::emojiPanRadius, _st.bg))
|
, _panelRounding(Ui::PrepareCornerPixmaps(st::emojiPanRadius, _st.bg))
|
||||||
, _categoriesRounding(
|
, _categoriesRounding(
|
||||||
@@ -644,6 +645,7 @@ TabbedSelector::Tab TabbedSelector::createTab(SelectorTab type, int index) {
|
|||||||
.paused = paused,
|
.paused = paused,
|
||||||
.st = &_st,
|
.st = &_st,
|
||||||
.features = _features,
|
.features = _features,
|
||||||
|
.excludeSetId = _excludeStickerSetId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
case SelectorTab::Gifs: {
|
case SelectorTab::Gifs: {
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ struct TabbedSelectorDescriptor {
|
|||||||
TabbedSelectorMode mode = TabbedSelectorMode::Full;
|
TabbedSelectorMode mode = TabbedSelectorMode::Full;
|
||||||
Fn<QColor()> customTextColor;
|
Fn<QColor()> customTextColor;
|
||||||
ComposeFeatures features;
|
ComposeFeatures features;
|
||||||
|
uint64 excludeStickerSetId = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class TabbedSearchType {
|
enum class TabbedSearchType {
|
||||||
@@ -295,6 +296,7 @@ private:
|
|||||||
const std::shared_ptr<Show> _show;
|
const std::shared_ptr<Show> _show;
|
||||||
const PauseReason _level = {};
|
const PauseReason _level = {};
|
||||||
const Fn<QColor()> _customTextColor;
|
const Fn<QColor()> _customTextColor;
|
||||||
|
const uint64 _excludeStickerSetId = 0;
|
||||||
|
|
||||||
Ui::Controls::SwipeBackResult _swipeBackData;
|
Ui::Controls::SwipeBackResult _swipeBackData;
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "core/sandbox.h"
|
#include "core/sandbox.h"
|
||||||
#include "core/local_url_handlers.h"
|
#include "core/local_url_handlers.h"
|
||||||
#include "core/launcher.h"
|
#include "core/launcher.h"
|
||||||
|
#include "core/proxy_rotation_manager.h"
|
||||||
#include "core/ui_integration.h"
|
#include "core/ui_integration.h"
|
||||||
#include "chat_helpers/emoji_keywords.h"
|
#include "chat_helpers/emoji_keywords.h"
|
||||||
#include "chat_helpers/stickers_emoji_image_loader.h"
|
#include "chat_helpers/stickers_emoji_image_loader.h"
|
||||||
@@ -146,6 +147,7 @@ struct Application::Private {
|
|||||||
base::Timer quitTimer;
|
base::Timer quitTimer;
|
||||||
UiIntegration uiIntegration;
|
UiIntegration uiIntegration;
|
||||||
Settings settings;
|
Settings settings;
|
||||||
|
std::unique_ptr<ProxyRotationManager> proxyRotation;
|
||||||
};
|
};
|
||||||
|
|
||||||
Application::Application()
|
Application::Application()
|
||||||
@@ -173,6 +175,7 @@ Application::Application()
|
|||||||
, _setupEmailLock(false)
|
, _setupEmailLock(false)
|
||||||
, _autoLockTimer([=] { checkAutoLock(); }) {
|
, _autoLockTimer([=] { checkAutoLock(); }) {
|
||||||
Ui::Integration::Set(&_private->uiIntegration);
|
Ui::Integration::Set(&_private->uiIntegration);
|
||||||
|
_private->proxyRotation = std::make_unique<ProxyRotationManager>();
|
||||||
|
|
||||||
_platformIntegration->init();
|
_platformIntegration->init();
|
||||||
|
|
||||||
@@ -234,6 +237,7 @@ Application::~Application() {
|
|||||||
// Domain::finish() and there is a violation on Ensures(started()).
|
// Domain::finish() and there is a violation on Ensures(started()).
|
||||||
closeAdditionalWindows();
|
closeAdditionalWindows();
|
||||||
|
|
||||||
|
_private->proxyRotation = nullptr;
|
||||||
_domain->finish();
|
_domain->finish();
|
||||||
|
|
||||||
Local::finish();
|
Local::finish();
|
||||||
@@ -835,6 +839,17 @@ void Application::setCurrentProxy(
|
|||||||
refreshGlobalProxy();
|
refreshGlobalProxy();
|
||||||
_proxyChanges.fire({ was, now });
|
_proxyChanges.fire({ was, now });
|
||||||
my.connectionTypeChangesNotify();
|
my.connectionTypeChangesNotify();
|
||||||
|
proxyRotationSettingsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::proxyRotationSettingsChanged() {
|
||||||
|
_private->proxyRotation->settingsChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::checkProxyRotation(
|
||||||
|
not_null<Main::Account*> account,
|
||||||
|
int32 state) {
|
||||||
|
_private->proxyRotation->handleConnectionStateChanged(account, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Application::proxyChanges() const -> rpl::producer<ProxyChange> {
|
auto Application::proxyChanges() const -> rpl::producer<ProxyChange> {
|
||||||
|
|||||||
@@ -219,6 +219,8 @@ public:
|
|||||||
void setCurrentProxy(
|
void setCurrentProxy(
|
||||||
const MTP::ProxyData &proxy,
|
const MTP::ProxyData &proxy,
|
||||||
MTP::ProxyData::Settings settings);
|
MTP::ProxyData::Settings settings);
|
||||||
|
void proxyRotationSettingsChanged();
|
||||||
|
void checkProxyRotation(not_null<Main::Account*> account, int32 state);
|
||||||
[[nodiscard]] rpl::producer<ProxyChange> proxyChanges() const;
|
[[nodiscard]] rpl::producer<ProxyChange> proxyChanges() const;
|
||||||
void badMtprotoConfigurationError();
|
void badMtprotoConfigurationError();
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "base/platform/base_platform_info.h"
|
#include "base/platform/base_platform_info.h"
|
||||||
#include "storage/serialize_common.h"
|
#include "storage/serialize_common.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -88,6 +90,22 @@ namespace {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<int> NormalizeProxyRotationPreferredIndices(
|
||||||
|
std::vector<int> indices,
|
||||||
|
int listSize) {
|
||||||
|
auto filtered = std::vector<int>();
|
||||||
|
filtered.reserve(indices.size());
|
||||||
|
for (const auto index : indices) {
|
||||||
|
if (index < 0
|
||||||
|
|| index >= listSize
|
||||||
|
|| ranges::contains(filtered, index)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
filtered.push_back(index);
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
SettingsProxy::SettingsProxy()
|
SettingsProxy::SettingsProxy()
|
||||||
@@ -108,7 +126,7 @@ QByteArray SettingsProxy::serialize() const {
|
|||||||
0,
|
0,
|
||||||
ranges::plus(),
|
ranges::plus(),
|
||||||
&Serialize::bytearraySize)
|
&Serialize::bytearraySize)
|
||||||
+ 1 * sizeof(qint32); // _checkIpWarningShown
|
+ (4 + int(_proxyRotationPreferredIndices.size())) * sizeof(qint32);
|
||||||
auto stream = Serialize::ByteArrayWriter(size);
|
auto stream = Serialize::ByteArrayWriter(size);
|
||||||
stream
|
stream
|
||||||
<< qint32(_tryIPv6 ? 1 : 0)
|
<< qint32(_tryIPv6 ? 1 : 0)
|
||||||
@@ -119,7 +137,14 @@ QByteArray SettingsProxy::serialize() const {
|
|||||||
for (const auto &i : serializedList) {
|
for (const auto &i : serializedList) {
|
||||||
stream << i;
|
stream << i;
|
||||||
}
|
}
|
||||||
stream << qint32(_checkIpWarningShown ? 1 : 0);
|
stream
|
||||||
|
<< qint32(_checkIpWarningShown ? 1 : 0)
|
||||||
|
<< qint32(_proxyRotationEnabled ? 1 : 0)
|
||||||
|
<< qint32(_proxyRotationTimeout)
|
||||||
|
<< qint32(_proxyRotationPreferredIndices.size());
|
||||||
|
for (const auto index : _proxyRotationPreferredIndices) {
|
||||||
|
stream << qint32(index);
|
||||||
|
}
|
||||||
return std::move(stream).result();
|
return std::move(stream).result();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +160,7 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
|
|||||||
auto settings = ProxySettingsToInt(_settings);
|
auto settings = ProxySettingsToInt(_settings);
|
||||||
auto listCount = qint32(_list.size());
|
auto listCount = qint32(_list.size());
|
||||||
auto selectedProxy = QByteArray();
|
auto selectedProxy = QByteArray();
|
||||||
|
auto list = std::vector<MTP::ProxyData>();
|
||||||
|
|
||||||
if (!stream.atEnd()) {
|
if (!stream.atEnd()) {
|
||||||
stream
|
stream
|
||||||
@@ -144,10 +170,14 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
|
|||||||
>> selectedProxy
|
>> selectedProxy
|
||||||
>> listCount;
|
>> listCount;
|
||||||
if (stream.ok()) {
|
if (stream.ok()) {
|
||||||
|
if (listCount < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
list.reserve(listCount);
|
||||||
for (auto i = 0; i != listCount; ++i) {
|
for (auto i = 0; i != listCount; ++i) {
|
||||||
QByteArray data;
|
QByteArray data;
|
||||||
stream >> data;
|
stream >> data;
|
||||||
_list.push_back(DeserializeProxyData(data));
|
list.push_back(DeserializeProxyData(data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,6 +186,30 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
|
|||||||
if (!stream.atEnd()) {
|
if (!stream.atEnd()) {
|
||||||
stream >> checkIpWarningShown;
|
stream >> checkIpWarningShown;
|
||||||
}
|
}
|
||||||
|
auto proxyRotationEnabled = qint32(_proxyRotationEnabled ? 1 : 0);
|
||||||
|
if (!stream.atEnd()) {
|
||||||
|
stream >> proxyRotationEnabled;
|
||||||
|
}
|
||||||
|
auto proxyRotationTimeout = qint32(_proxyRotationTimeout);
|
||||||
|
if (!stream.atEnd()) {
|
||||||
|
stream >> proxyRotationTimeout;
|
||||||
|
}
|
||||||
|
auto preferredCount = qint32(0);
|
||||||
|
auto preferredIndices = std::vector<int>();
|
||||||
|
if (!stream.atEnd()) {
|
||||||
|
stream >> preferredCount;
|
||||||
|
if (stream.ok()) {
|
||||||
|
if (preferredCount < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
preferredIndices.reserve(preferredCount);
|
||||||
|
for (auto i = 0; i != preferredCount; ++i) {
|
||||||
|
auto index = qint32(0);
|
||||||
|
stream >> index;
|
||||||
|
preferredIndices.push_back(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!stream.ok()) {
|
if (!stream.ok()) {
|
||||||
LOG(("App Error: "
|
LOG(("App Error: "
|
||||||
@@ -166,8 +220,12 @@ bool SettingsProxy::setFromSerialized(const QByteArray &serialized) {
|
|||||||
_tryIPv6 = (tryIPv6 == 1);
|
_tryIPv6 = (tryIPv6 == 1);
|
||||||
_useProxyForCalls = (useProxyForCalls == 1);
|
_useProxyForCalls = (useProxyForCalls == 1);
|
||||||
_checkIpWarningShown = (checkIpWarningShown == 1);
|
_checkIpWarningShown = (checkIpWarningShown == 1);
|
||||||
|
_proxyRotationEnabled = (proxyRotationEnabled == 1);
|
||||||
|
setProxyRotationTimeout(proxyRotationTimeout);
|
||||||
_settings = IntToProxySettings(settings);
|
_settings = IntToProxySettings(settings);
|
||||||
_selected = DeserializeProxyData(selectedProxy);
|
_selected = DeserializeProxyData(selectedProxy);
|
||||||
|
_list = std::move(list);
|
||||||
|
setProxyRotationPreferredIndices(std::move(preferredIndices));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -192,6 +250,32 @@ void SettingsProxy::setCheckIpWarningShown(bool value) {
|
|||||||
_checkIpWarningShown = value;
|
_checkIpWarningShown = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::vector<int> &SettingsProxy::proxyRotationPreferredIndices() const {
|
||||||
|
return _proxyRotationPreferredIndices;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsProxy::setProxyRotationPreferredIndices(std::vector<int> value) {
|
||||||
|
_proxyRotationPreferredIndices = NormalizeProxyRotationPreferredIndices(
|
||||||
|
std::move(value),
|
||||||
|
int(_list.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsProxy::promoteProxyRotationPreferredIndex(int index) {
|
||||||
|
if (index < 0 || index >= int(_list.size())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto &indices = _proxyRotationPreferredIndices;
|
||||||
|
const auto i = ranges::find(indices, index);
|
||||||
|
if (i == begin(indices)) {
|
||||||
|
return false;
|
||||||
|
} else if (i != end(indices)) {
|
||||||
|
std::rotate(begin(indices), i, std::next(i));
|
||||||
|
} else {
|
||||||
|
indices.insert(begin(indices), index);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool SettingsProxy::tryIPv6() const {
|
bool SettingsProxy::tryIPv6() const {
|
||||||
return _tryIPv6;
|
return _tryIPv6;
|
||||||
}
|
}
|
||||||
@@ -208,6 +292,24 @@ void SettingsProxy::setUseProxyForCalls(bool value) {
|
|||||||
_useProxyForCalls = value;
|
_useProxyForCalls = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SettingsProxy::proxyRotationEnabled() const {
|
||||||
|
return _proxyRotationEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsProxy::setProxyRotationEnabled(bool value) {
|
||||||
|
_proxyRotationEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SettingsProxy::proxyRotationTimeout() const {
|
||||||
|
return _proxyRotationTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsProxy::setProxyRotationTimeout(int value) {
|
||||||
|
_proxyRotationTimeout = (value > 0)
|
||||||
|
? value
|
||||||
|
: kDefaultProxyRotationTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
MTP::ProxyData::Settings SettingsProxy::settings() const {
|
MTP::ProxyData::Settings SettingsProxy::settings() const {
|
||||||
return _settings;
|
return _settings;
|
||||||
}
|
}
|
||||||
@@ -232,6 +334,62 @@ std::vector<MTP::ProxyData> &SettingsProxy::list() {
|
|||||||
return _list;
|
return _list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SettingsProxy::setList(std::vector<MTP::ProxyData> value) {
|
||||||
|
_list = std::move(value);
|
||||||
|
_proxyRotationPreferredIndices.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsProxy::addToList(MTP::ProxyData value) {
|
||||||
|
_list.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsProxy::insertToList(int index, MTP::ProxyData value) {
|
||||||
|
index = std::clamp(index, 0, int(_list.size()));
|
||||||
|
for (auto &existing : _proxyRotationPreferredIndices) {
|
||||||
|
if (existing >= index) {
|
||||||
|
++existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_list.insert(begin(_list) + index, std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsProxy::removeFromList(const MTP::ProxyData &value) {
|
||||||
|
const auto i = ranges::find(_list, value);
|
||||||
|
if (i == end(_list)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto index = int(i - begin(_list));
|
||||||
|
_list.erase(i);
|
||||||
|
for (auto &existing : _proxyRotationPreferredIndices) {
|
||||||
|
if (existing > index) {
|
||||||
|
--existing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_proxyRotationPreferredIndices.erase(
|
||||||
|
std::remove(
|
||||||
|
begin(_proxyRotationPreferredIndices),
|
||||||
|
end(_proxyRotationPreferredIndices),
|
||||||
|
index),
|
||||||
|
end(_proxyRotationPreferredIndices));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SettingsProxy::replaceInList(
|
||||||
|
const MTP::ProxyData &was,
|
||||||
|
MTP::ProxyData value) {
|
||||||
|
const auto i = ranges::find(_list, was);
|
||||||
|
if (i == end(_list)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*i = std::move(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int SettingsProxy::indexInList(const MTP::ProxyData &value) const {
|
||||||
|
const auto i = ranges::find(_list, value);
|
||||||
|
return (i == end(_list)) ? -1 : int(i - begin(_list));
|
||||||
|
}
|
||||||
|
|
||||||
rpl::producer<> SettingsProxy::connectionTypeValue() const {
|
rpl::producer<> SettingsProxy::connectionTypeValue() const {
|
||||||
return _connectionTypeChanges.events_starting_with({});
|
return _connectionTypeChanges.events_starting_with({});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
|
|
||||||
#include "mtproto/mtproto_proxy_data.h"
|
#include "mtproto/mtproto_proxy_data.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
|
|
||||||
class SettingsProxy final {
|
class SettingsProxy final {
|
||||||
public:
|
public:
|
||||||
|
static constexpr auto kProxyRotationTimeouts = std::array{
|
||||||
|
5,
|
||||||
|
10,
|
||||||
|
15,
|
||||||
|
30,
|
||||||
|
60,
|
||||||
|
};
|
||||||
|
static constexpr auto kDefaultProxyRotationTimeout = 10;
|
||||||
|
|
||||||
SettingsProxy();
|
SettingsProxy();
|
||||||
|
|
||||||
[[nodiscard]] bool isEnabled() const;
|
[[nodiscard]] bool isEnabled() const;
|
||||||
@@ -29,6 +40,12 @@ public:
|
|||||||
[[nodiscard]] bool useProxyForCalls() const;
|
[[nodiscard]] bool useProxyForCalls() const;
|
||||||
void setUseProxyForCalls(bool value);
|
void setUseProxyForCalls(bool value);
|
||||||
|
|
||||||
|
[[nodiscard]] bool proxyRotationEnabled() const;
|
||||||
|
void setProxyRotationEnabled(bool value);
|
||||||
|
|
||||||
|
[[nodiscard]] int proxyRotationTimeout() const;
|
||||||
|
void setProxyRotationTimeout(int value);
|
||||||
|
|
||||||
[[nodiscard]] MTP::ProxyData::Settings settings() const;
|
[[nodiscard]] MTP::ProxyData::Settings settings() const;
|
||||||
void setSettings(MTP::ProxyData::Settings value);
|
void setSettings(MTP::ProxyData::Settings value);
|
||||||
|
|
||||||
@@ -38,8 +55,20 @@ public:
|
|||||||
[[nodiscard]] bool checkIpWarningShown() const;
|
[[nodiscard]] bool checkIpWarningShown() const;
|
||||||
void setCheckIpWarningShown(bool value);
|
void setCheckIpWarningShown(bool value);
|
||||||
|
|
||||||
|
[[nodiscard]] const std::vector<int> &proxyRotationPreferredIndices() const;
|
||||||
|
void setProxyRotationPreferredIndices(std::vector<int> value);
|
||||||
|
[[nodiscard]] bool promoteProxyRotationPreferredIndex(int index);
|
||||||
|
|
||||||
[[nodiscard]] const std::vector<MTP::ProxyData> &list() const;
|
[[nodiscard]] const std::vector<MTP::ProxyData> &list() const;
|
||||||
[[nodiscard]] std::vector<MTP::ProxyData> &list();
|
[[nodiscard]] std::vector<MTP::ProxyData> &list();
|
||||||
|
void setList(std::vector<MTP::ProxyData> value);
|
||||||
|
void addToList(MTP::ProxyData value);
|
||||||
|
void insertToList(int index, MTP::ProxyData value);
|
||||||
|
[[nodiscard]] bool removeFromList(const MTP::ProxyData &value);
|
||||||
|
[[nodiscard]] bool replaceInList(
|
||||||
|
const MTP::ProxyData &was,
|
||||||
|
MTP::ProxyData value);
|
||||||
|
[[nodiscard]] int indexInList(const MTP::ProxyData &value) const;
|
||||||
|
|
||||||
[[nodiscard]] QByteArray serialize() const;
|
[[nodiscard]] QByteArray serialize() const;
|
||||||
bool setFromSerialized(const QByteArray &serialized);
|
bool setFromSerialized(const QByteArray &serialized);
|
||||||
@@ -47,14 +76,16 @@ public:
|
|||||||
private:
|
private:
|
||||||
bool _tryIPv6 = false;
|
bool _tryIPv6 = false;
|
||||||
bool _useProxyForCalls = false;
|
bool _useProxyForCalls = false;
|
||||||
|
bool _proxyRotationEnabled = false;
|
||||||
bool _checkIpWarningShown = false;
|
bool _checkIpWarningShown = false;
|
||||||
|
int _proxyRotationTimeout = kDefaultProxyRotationTimeout;
|
||||||
MTP::ProxyData::Settings _settings = MTP::ProxyData::Settings::System;
|
MTP::ProxyData::Settings _settings = MTP::ProxyData::Settings::System;
|
||||||
MTP::ProxyData _selected;
|
MTP::ProxyData _selected;
|
||||||
std::vector<MTP::ProxyData> _list;
|
std::vector<MTP::ProxyData> _list;
|
||||||
|
std::vector<int> _proxyRotationPreferredIndices;
|
||||||
|
|
||||||
rpl::event_stream<> _connectionTypeChanges;
|
rpl::event_stream<> _connectionTypeChanges;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Core
|
} // namespace Core
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
/*
|
||||||
|
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 "core/proxy_rotation_manager.h"
|
||||||
|
|
||||||
|
#include "core/application.h"
|
||||||
|
#include "core/core_settings.h"
|
||||||
|
#include "main/main_account.h"
|
||||||
|
#include "main/main_domain.h"
|
||||||
|
#include "mtproto/facade.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kProxyRotationCheckInterval = 2 * crl::time(1000);
|
||||||
|
constexpr auto kProxyRotationCheckLifetime = 20 * crl::time(1000);
|
||||||
|
constexpr auto kProxyRotationMaxActiveChecks = 10;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ProxyRotationManager::ProxyRotationManager()
|
||||||
|
: _checkTimer([=] { runChecks(); })
|
||||||
|
, _switchTimer([=] { switchTimerDone(); }) {
|
||||||
|
App().domain().accountsChanges(
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
stopChecking();
|
||||||
|
reevaluate();
|
||||||
|
}, _lifetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::settingsChanged() {
|
||||||
|
stopChecking();
|
||||||
|
pruneRemovedEntries();
|
||||||
|
reevaluate();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::handleConnectionStateChanged(
|
||||||
|
not_null<Main::Account*> account,
|
||||||
|
int32 state) {
|
||||||
|
(void)account;
|
||||||
|
(void)state;
|
||||||
|
reevaluate();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProxyRotationManager::shouldObserve() const {
|
||||||
|
const auto &settings = App().settings().proxy();
|
||||||
|
return settings.isEnabled()
|
||||||
|
&& settings.selected()
|
||||||
|
&& settings.proxyRotationEnabled()
|
||||||
|
&& (settings.list().size() > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<not_null<Main::Account*>> ProxyRotationManager::productionAccounts() const {
|
||||||
|
auto result = std::vector<not_null<Main::Account*>>();
|
||||||
|
for (const auto &entry : App().domain().accounts()) {
|
||||||
|
const auto account = entry.account.get();
|
||||||
|
if (!account->sessionExists() || account->mtp().isTestMode()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.push_back(account);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
not_null<Main::Account*> ProxyRotationManager::accountForChecks() const {
|
||||||
|
if (App().someSessionExists()
|
||||||
|
&& App().activeAccount().sessionExists()
|
||||||
|
&& !App().activeAccount().mtp().isTestMode()) {
|
||||||
|
return &App().activeAccount();
|
||||||
|
}
|
||||||
|
const auto accounts = productionAccounts();
|
||||||
|
Expects(!accounts.empty());
|
||||||
|
return accounts.front();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ProxyRotationManager::find(
|
||||||
|
const MTP::ProxyData &proxy) -> Entry* {
|
||||||
|
const auto i = ranges::find(
|
||||||
|
_entries,
|
||||||
|
proxy,
|
||||||
|
[](const Entry &entry) { return entry.proxy; });
|
||||||
|
return (i == end(_entries)) ? nullptr : &*i;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ProxyRotationManager::ensure(
|
||||||
|
const MTP::ProxyData &proxy) -> Entry& {
|
||||||
|
if (const auto entry = find(proxy)) {
|
||||||
|
return *entry;
|
||||||
|
}
|
||||||
|
_entries.push_back({ .proxy = proxy });
|
||||||
|
return _entries.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::reevaluate() {
|
||||||
|
if (!shouldObserve()) {
|
||||||
|
stopChecking();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto accounts = productionAccounts();
|
||||||
|
if (accounts.empty()) {
|
||||||
|
stopChecking();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto stateProj = [](not_null<Main::Account*> account) {
|
||||||
|
return account->mtp().dcstate();
|
||||||
|
};
|
||||||
|
if (ranges::contains(accounts, MTP::ConnectedState, stateProj)) {
|
||||||
|
stopChecking();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startChecking();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::startChecking() {
|
||||||
|
if (_checking) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_checking = true;
|
||||||
|
_waitingToSwitch = false;
|
||||||
|
_switchStartedAt = crl::now();
|
||||||
|
updateProbeOrder();
|
||||||
|
runChecks();
|
||||||
|
const auto timeout = App().settings().proxy().proxyRotationTimeout();
|
||||||
|
_switchTimer.callOnce(timeout * crl::time(1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::stopChecking() {
|
||||||
|
_checkTimer.cancel();
|
||||||
|
_switchTimer.cancel();
|
||||||
|
_checking = false;
|
||||||
|
_waitingToSwitch = false;
|
||||||
|
_switchStartedAt = 0;
|
||||||
|
_probeOrder.clear();
|
||||||
|
_nextCheckIndex = 0;
|
||||||
|
clearPendingChecks();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::pruneRemovedEntries() {
|
||||||
|
const auto &settings = App().settings().proxy();
|
||||||
|
_entries.erase(
|
||||||
|
std::remove_if(begin(_entries), end(_entries), [&](const Entry &entry) {
|
||||||
|
return (settings.indexInList(entry.proxy) < 0);
|
||||||
|
}),
|
||||||
|
end(_entries));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::updateProbeOrder() {
|
||||||
|
const auto &settings = App().settings().proxy();
|
||||||
|
const auto currentIndex = settings.indexInList(settings.selected());
|
||||||
|
_probeOrder.clear();
|
||||||
|
_probeOrder.reserve(settings.list().size());
|
||||||
|
for (const auto index : settings.proxyRotationPreferredIndices()) {
|
||||||
|
if (index == currentIndex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_probeOrder.push_back(index);
|
||||||
|
}
|
||||||
|
for (auto i = 0, count = int(settings.list().size()); i != count; ++i) {
|
||||||
|
if (i == currentIndex || ranges::contains(_probeOrder, i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_probeOrder.push_back(i);
|
||||||
|
}
|
||||||
|
_nextCheckIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::continueChecking(crl::time delay) {
|
||||||
|
if (!_checking) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_checkTimer.isActive()) {
|
||||||
|
_checkTimer.cancel();
|
||||||
|
}
|
||||||
|
_checkTimer.callOnce(delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::runChecks() {
|
||||||
|
if (!_checking) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!shouldObserve()) {
|
||||||
|
stopChecking();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto accounts = productionAccounts();
|
||||||
|
if (accounts.empty()
|
||||||
|
|| ranges::contains(
|
||||||
|
accounts,
|
||||||
|
MTP::ConnectedState,
|
||||||
|
[](not_null<Main::Account*> account) {
|
||||||
|
return account->mtp().dcstate();
|
||||||
|
})) {
|
||||||
|
stopChecking();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pruneExpiredChecks();
|
||||||
|
startNextCheck();
|
||||||
|
continueChecking(kProxyRotationCheckInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::pruneExpiredChecks() {
|
||||||
|
const auto now = crl::now();
|
||||||
|
for (auto &entry : _entries) {
|
||||||
|
if (!entry.checking
|
||||||
|
|| (now - entry.startedAt < kProxyRotationCheckLifetime)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
MTP::ResetProxyCheckers(entry.v4, entry.v6);
|
||||||
|
entry.checking = false;
|
||||||
|
entry.startedAt = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::startNextCheck() {
|
||||||
|
if (_probeOrder.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ranges::count(_entries, true, &Entry::checking)
|
||||||
|
>= kProxyRotationMaxActiveChecks) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto &settings = App().settings().proxy();
|
||||||
|
auto attemptsLeft = int(_probeOrder.size());
|
||||||
|
while (attemptsLeft-- > 0) {
|
||||||
|
if (_nextCheckIndex >= int(_probeOrder.size())) {
|
||||||
|
_nextCheckIndex = 0;
|
||||||
|
}
|
||||||
|
const auto listIndex = _probeOrder[_nextCheckIndex++];
|
||||||
|
if (listIndex < 0 || listIndex >= int(settings.list().size())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto &proxy = settings.list()[listIndex];
|
||||||
|
auto &entry = ensure(proxy);
|
||||||
|
if (entry.checking) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
entry.checking = true;
|
||||||
|
entry.startedAt = crl::now();
|
||||||
|
MTP::StartProxyCheck(
|
||||||
|
&accountForChecks()->mtp(),
|
||||||
|
proxy,
|
||||||
|
settings.tryIPv6(),
|
||||||
|
entry.v4,
|
||||||
|
entry.v6,
|
||||||
|
[=](MTP::details::AbstractConnection *raw, int ping) {
|
||||||
|
checkDone(proxy, raw, ping);
|
||||||
|
},
|
||||||
|
[=](MTP::details::AbstractConnection *raw) {
|
||||||
|
checkFailed(proxy, raw);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::switchTimerDone() {
|
||||||
|
if (!_checking || !shouldSwitchToAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_waitingToSwitch = !switchToAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::clearPendingChecks() {
|
||||||
|
for (auto &entry : _entries) {
|
||||||
|
MTP::ResetProxyCheckers(entry.v4, entry.v6);
|
||||||
|
entry.checking = false;
|
||||||
|
entry.startedAt = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::checkDone(
|
||||||
|
const MTP::ProxyData &proxy,
|
||||||
|
not_null<MTP::details::AbstractConnection*> raw,
|
||||||
|
int ping) {
|
||||||
|
const auto entry = find(proxy);
|
||||||
|
if (!entry
|
||||||
|
|| !entry->checking
|
||||||
|
|| ((entry->v4.get() != raw) && (entry->v6.get() != raw))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MTP::DropProxyChecker(entry->v4, entry->v6, raw);
|
||||||
|
MTP::ResetProxyCheckers(entry->v4, entry->v6);
|
||||||
|
entry->checking = false;
|
||||||
|
entry->startedAt = 0;
|
||||||
|
entry->availableAt = crl::now();
|
||||||
|
const auto proxySettings = &App().settings().proxy();
|
||||||
|
if (const auto index = proxySettings->indexInList(proxy); index >= 0) {
|
||||||
|
if (proxySettings->promoteProxyRotationPreferredIndex(index)) {
|
||||||
|
App().saveSettingsDelayed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateProbeOrder();
|
||||||
|
if (_waitingToSwitch && shouldSwitchToAvailable()) {
|
||||||
|
_waitingToSwitch = !switchToAvailable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProxyRotationManager::checkFailed(
|
||||||
|
const MTP::ProxyData &proxy,
|
||||||
|
not_null<MTP::details::AbstractConnection*> raw) {
|
||||||
|
const auto entry = find(proxy);
|
||||||
|
if (!entry
|
||||||
|
|| !entry->checking
|
||||||
|
|| ((entry->v4.get() != raw) && (entry->v6.get() != raw))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MTP::DropProxyChecker(entry->v4, entry->v6, raw);
|
||||||
|
if (MTP::HasProxyCheckers(entry->v4, entry->v6)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entry->checking = false;
|
||||||
|
entry->startedAt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProxyRotationManager::switchToAvailable() {
|
||||||
|
if (!_checking) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto &settings = App().settings().proxy();
|
||||||
|
for (const auto index : _probeOrder) {
|
||||||
|
if (index < 0 || index >= int(settings.list().size())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto &proxy = settings.list()[index];
|
||||||
|
const auto entry = find(proxy);
|
||||||
|
if (!entry || entry->checking || !entry->availableAt) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry->availableAt < _switchStartedAt) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_waitingToSwitch = false;
|
||||||
|
App().setCurrentProxy(proxy, MTP::ProxyData::Settings::Enabled);
|
||||||
|
App().saveSettingsDelayed();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProxyRotationManager::shouldSwitchToAvailable() const {
|
||||||
|
if (!_checking || !shouldObserve()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto accounts = productionAccounts();
|
||||||
|
return !accounts.empty()
|
||||||
|
&& !ranges::contains(
|
||||||
|
accounts,
|
||||||
|
MTP::ConnectedState,
|
||||||
|
[](not_null<Main::Account*> account) {
|
||||||
|
return account->mtp().dcstate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Core
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
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 "base/timer.h"
|
||||||
|
#include "mtproto/proxy_check.h"
|
||||||
|
|
||||||
|
#include <rpl/lifetime.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Main {
|
||||||
|
class Account;
|
||||||
|
} // namespace Main
|
||||||
|
|
||||||
|
namespace Core {
|
||||||
|
|
||||||
|
class ProxyRotationManager final {
|
||||||
|
public:
|
||||||
|
ProxyRotationManager();
|
||||||
|
|
||||||
|
void settingsChanged();
|
||||||
|
void handleConnectionStateChanged(
|
||||||
|
not_null<Main::Account*> account,
|
||||||
|
int32 state);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Entry {
|
||||||
|
MTP::ProxyData proxy;
|
||||||
|
MTP::ProxyCheckConnection v4;
|
||||||
|
MTP::ProxyCheckConnection v6;
|
||||||
|
bool checking = false;
|
||||||
|
crl::time startedAt = 0;
|
||||||
|
crl::time availableAt = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] bool shouldObserve() const;
|
||||||
|
[[nodiscard]] std::vector<not_null<Main::Account*>> productionAccounts() const;
|
||||||
|
[[nodiscard]] not_null<Main::Account*> accountForChecks() const;
|
||||||
|
[[nodiscard]] Entry *find(const MTP::ProxyData &proxy);
|
||||||
|
[[nodiscard]] Entry &ensure(const MTP::ProxyData &proxy);
|
||||||
|
|
||||||
|
void reevaluate();
|
||||||
|
void startChecking();
|
||||||
|
void stopChecking();
|
||||||
|
void pruneRemovedEntries();
|
||||||
|
void updateProbeOrder();
|
||||||
|
void continueChecking(crl::time delay);
|
||||||
|
void runChecks();
|
||||||
|
void pruneExpiredChecks();
|
||||||
|
void startNextCheck();
|
||||||
|
void switchTimerDone();
|
||||||
|
void clearPendingChecks();
|
||||||
|
void checkDone(
|
||||||
|
const MTP::ProxyData &proxy,
|
||||||
|
not_null<MTP::details::AbstractConnection*> raw,
|
||||||
|
int ping);
|
||||||
|
void checkFailed(
|
||||||
|
const MTP::ProxyData &proxy,
|
||||||
|
not_null<MTP::details::AbstractConnection*> raw);
|
||||||
|
[[nodiscard]] bool switchToAvailable();
|
||||||
|
[[nodiscard]] bool shouldSwitchToAvailable() const;
|
||||||
|
|
||||||
|
base::Timer _checkTimer;
|
||||||
|
base::Timer _switchTimer;
|
||||||
|
std::vector<Entry> _entries;
|
||||||
|
std::vector<int> _probeOrder;
|
||||||
|
int _nextCheckIndex = 0;
|
||||||
|
bool _checking = false;
|
||||||
|
bool _waitingToSwitch = false;
|
||||||
|
crl::time _switchStartedAt = 0;
|
||||||
|
rpl::lifetime _lifetime;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Core
|
||||||
@@ -24,7 +24,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
|
|||||||
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
|
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
|
||||||
constexpr auto AppName = "Telegram Desktop"_cs;
|
constexpr auto AppName = "Telegram Desktop"_cs;
|
||||||
constexpr auto AppFile = "Telegram"_cs;
|
constexpr auto AppFile = "Telegram"_cs;
|
||||||
constexpr auto AppVersion = 6007006;
|
constexpr auto AppVersion = 6007007;
|
||||||
constexpr auto AppVersionStr = "6.7.6";
|
constexpr auto AppVersionStr = "6.7.7";
|
||||||
constexpr auto AppBetaVersion = false;
|
constexpr auto AppBetaVersion = true;
|
||||||
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;
|
||||||
|
|||||||
@@ -88,6 +88,16 @@ void UpdateStickerSetIdentifier(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] int ResolveAttributeVsTranscodeQuality(
|
||||||
|
int attributesQuality,
|
||||||
|
int transcodeMax) {
|
||||||
|
return (transcodeMax > 0
|
||||||
|
&& (attributesQuality < transcodeMax
|
||||||
|
|| attributesQuality > transcodeMax * 1.5))
|
||||||
|
? transcodeMax
|
||||||
|
: attributesQuality;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
QString FileNameUnsafe(
|
QString FileNameUnsafe(
|
||||||
@@ -547,8 +557,7 @@ void DocumentData::setVideoQualities(
|
|||||||
return document->isVideoFile()
|
return document->isVideoFile()
|
||||||
&& !document->dimensions.isEmpty()
|
&& !document->dimensions.isEmpty()
|
||||||
&& !document->inappPlaybackFailed()
|
&& !document->inappPlaybackFailed()
|
||||||
&& document->useStreamingLoader()
|
&& document->useStreamingLoader();
|
||||||
&& document->canBeStreamed();
|
|
||||||
};
|
};
|
||||||
ranges::sort(
|
ranges::sort(
|
||||||
qualities,
|
qualities,
|
||||||
@@ -578,18 +587,58 @@ void DocumentData::setVideoQualities(
|
|||||||
}
|
}
|
||||||
qualities.erase(qualities.begin() + count, qualities.end());
|
qualities.erase(qualities.begin() + count, qualities.end());
|
||||||
if (!qualities.empty()) {
|
if (!qualities.empty()) {
|
||||||
if (const auto mine = resolveVideoQuality()) {
|
auto transcodeMax = 0;
|
||||||
if (mine > qualities.front()->resolveVideoQuality()) {
|
for (const auto &quality : qualities) {
|
||||||
qualities.insert(begin(qualities), this);
|
const auto qres = quality->resolveVideoQuality();
|
||||||
|
if (qres > transcodeMax) {
|
||||||
|
transcodeMax = qres;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const auto attributesSize = isVideoFile() ? dimensions : QSize();
|
||||||
|
const auto attributesQuality = attributesSize.isEmpty()
|
||||||
|
? 0
|
||||||
|
: std::min(attributesSize.width(), attributesSize.height());
|
||||||
|
auto mine = ResolveAttributeVsTranscodeQuality(
|
||||||
|
attributesQuality,
|
||||||
|
transcodeMax);
|
||||||
|
if (mine) {
|
||||||
|
qualities.insert(begin(qualities), this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
data->qualities = std::move(qualities);
|
data->qualities = std::move(qualities);
|
||||||
}
|
}
|
||||||
|
|
||||||
int DocumentData::resolveVideoQuality() const {
|
int DocumentData::resolveVideoQuality() const {
|
||||||
const auto size = isVideoFile() ? dimensions : QSize();
|
if (const auto data = video()) {
|
||||||
return size.isEmpty() ? 0 : std::min(size.width(), size.height());
|
if (!data->realVideoSize.isEmpty()) {
|
||||||
|
const auto size = data->realVideoSize;
|
||||||
|
return std::min(size.width(), size.height());
|
||||||
|
}
|
||||||
|
const auto attributesSize = isVideoFile() ? dimensions : QSize();
|
||||||
|
const auto attributesQuality = attributesSize.isEmpty()
|
||||||
|
? 0
|
||||||
|
: std::min(attributesSize.width(), attributesSize.height());
|
||||||
|
if (!data->qualities.empty()) {
|
||||||
|
auto transcodeMax = 0;
|
||||||
|
for (const auto &quality : data->qualities) {
|
||||||
|
if (quality != this) {
|
||||||
|
const auto qres = quality->resolveVideoQuality();
|
||||||
|
if (qres > transcodeMax) {
|
||||||
|
transcodeMax = qres;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (transcodeMax > 0) {
|
||||||
|
return ResolveAttributeVsTranscodeQuality(
|
||||||
|
attributesQuality,
|
||||||
|
transcodeMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto attributesSize = isVideoFile() ? dimensions : QSize();
|
||||||
|
return attributesSize.isEmpty()
|
||||||
|
? 0
|
||||||
|
: std::min(attributesSize.width(), attributesSize.height());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto DocumentData::resolveQualities(HistoryItem *context) const
|
auto DocumentData::resolveQualities(HistoryItem *context) const
|
||||||
@@ -611,19 +660,28 @@ not_null<DocumentData*> DocumentData::chooseQuality(
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
const auto height = int(request.height);
|
const auto height = int(request.height);
|
||||||
auto closest = this;
|
if (request.original) {
|
||||||
auto closestAbs = std::abs(height - resolveVideoQuality());
|
return this;
|
||||||
auto closestSize = size;
|
}
|
||||||
|
|
||||||
|
auto closest = (DocumentData*)nullptr;
|
||||||
|
auto closestAbs = -1;
|
||||||
|
auto closestSize = -1;
|
||||||
|
|
||||||
for (const auto &quality : list) {
|
for (const auto &quality : list) {
|
||||||
const auto abs = std::abs(height - quality->resolveVideoQuality());
|
const auto qres = quality->resolveVideoQuality();
|
||||||
if (abs < closestAbs
|
const auto abs = std::abs(height - qres);
|
||||||
|| (abs == closestAbs && quality->size < closestSize)) {
|
if (!closest
|
||||||
|
|| abs < closestAbs
|
||||||
|
|| (abs == closestAbs && (quality->size < closestSize
|
||||||
|
|| (closest == this && quality != this)))) {
|
||||||
closest = quality;
|
closest = quality;
|
||||||
closestAbs = abs;
|
closestAbs = abs;
|
||||||
closestSize = quality->size;
|
closestSize = quality->size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return closest;
|
|
||||||
|
return closest ? closest : this;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DocumentData::validateLottieSticker() {
|
void DocumentData::validateLottieSticker() {
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ struct VoiceData : public DocumentAdditionalData {
|
|||||||
struct VideoData : public DocumentAdditionalData {
|
struct VideoData : public DocumentAdditionalData {
|
||||||
QString codec;
|
QString codec;
|
||||||
std::vector<not_null<DocumentData*>> qualities;
|
std::vector<not_null<DocumentData*>> qualities;
|
||||||
|
QSize realVideoSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
using RoundData = VoiceData;
|
using RoundData = VoiceData;
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ Image *DocumentMedia::goodThumbnail() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DocumentMedia::setGoodThumbnail(QImage thumbnail) {
|
void DocumentMedia::setGoodThumbnail(QImage thumbnail) {
|
||||||
if (!(_flags & Flag::GoodThumbnailWanted)) {
|
if (!(_flags & Flag::GoodThumbnailWanted) || thumbnail.isNull()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_goodThumbnail = std::make_unique<Image>(std::move(thumbnail));
|
_goodThumbnail = std::make_unique<Image>(std::move(thumbnail));
|
||||||
|
|||||||
@@ -4174,6 +4174,21 @@ void Session::webpageApplyFields(
|
|||||||
auto iv = (data.vcached_page() && !IgnoreIv(type))
|
auto iv = (data.vcached_page() && !IgnoreIv(type))
|
||||||
? std::make_unique<Iv::Data>(data, *data.vcached_page())
|
? std::make_unique<Iv::Data>(data, *data.vcached_page())
|
||||||
: nullptr;
|
: nullptr;
|
||||||
|
const auto resolvedPhoto = story
|
||||||
|
? story->photo()
|
||||||
|
: photo
|
||||||
|
? processPhoto(*photo).get()
|
||||||
|
: nullptr;
|
||||||
|
const auto resolvedDocument = story
|
||||||
|
? story->document()
|
||||||
|
: document
|
||||||
|
? processDocument(*document).get()
|
||||||
|
: lookupThemeDocument();
|
||||||
|
const auto photoIsVideoCover = data.is_video_cover_photo()
|
||||||
|
|| (resolvedDocument
|
||||||
|
&& resolvedPhoto
|
||||||
|
&& resolvedDocument->isVideoFile()
|
||||||
|
&& !resolvedDocument->hasThumbnail());
|
||||||
webpageApplyFields(
|
webpageApplyFields(
|
||||||
page,
|
page,
|
||||||
type,
|
type,
|
||||||
@@ -4183,16 +4198,8 @@ void Session::webpageApplyFields(
|
|||||||
qs(data.vtitle().value_or_empty()),
|
qs(data.vtitle().value_or_empty()),
|
||||||
(story ? story->caption() : description),
|
(story ? story->caption() : description),
|
||||||
storyId,
|
storyId,
|
||||||
(story
|
resolvedPhoto,
|
||||||
? story->photo()
|
resolvedDocument,
|
||||||
: photo
|
|
||||||
? processPhoto(*photo).get()
|
|
||||||
: nullptr),
|
|
||||||
(story
|
|
||||||
? story->document()
|
|
||||||
: document
|
|
||||||
? processDocument(*document).get()
|
|
||||||
: lookupThemeDocument()),
|
|
||||||
WebPageCollage(this, data),
|
WebPageCollage(this, data),
|
||||||
std::move(iv),
|
std::move(iv),
|
||||||
lookupStickerSet(),
|
lookupStickerSet(),
|
||||||
@@ -4201,7 +4208,7 @@ void Session::webpageApplyFields(
|
|||||||
data.vduration().value_or_empty(),
|
data.vduration().value_or_empty(),
|
||||||
qs(data.vauthor().value_or_empty()),
|
qs(data.vauthor().value_or_empty()),
|
||||||
data.is_has_large_media(),
|
data.is_has_large_media(),
|
||||||
data.is_video_cover_photo(),
|
photoIsVideoCover,
|
||||||
pendingTill);
|
pendingTill);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kShortPollTimeout = 30 * crl::time(1000);
|
|
||||||
|
|
||||||
const TodoListItem *ItemById(const std::vector<TodoListItem> &list, int id) {
|
const TodoListItem *ItemById(const std::vector<TodoListItem> &list, int id) {
|
||||||
const auto i = ranges::find(list, id, &TodoListItem::id);
|
const auto i = ranges::find(list, id, &TodoListItem::id);
|
||||||
return (i != end(list)) ? &*i : nullptr;
|
return (i != end(list)) ? &*i : nullptr;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
|
|
||||||
#include "dialogs/dialogs_key.h"
|
#include "dialogs/dialogs_key.h"
|
||||||
#include "dialogs/dialogs_indexed_list.h"
|
#include "dialogs/dialogs_indexed_list.h"
|
||||||
|
#include "base/unixtime.h"
|
||||||
#include "data/data_changes.h"
|
#include "data/data_changes.h"
|
||||||
#include "data/data_session.h"
|
#include "data/data_session.h"
|
||||||
#include "data/data_folder.h"
|
#include "data/data_folder.h"
|
||||||
@@ -20,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "mainwidget.h"
|
#include "mainwidget.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
#include "main/main_session_settings.h"
|
#include "main/main_session_settings.h"
|
||||||
|
#include "ui/text/format_values.h"
|
||||||
#include "ui/text/text_options.h"
|
#include "ui/text/text_options.h"
|
||||||
#include "ui/ui_utility.h"
|
#include "ui/ui_utility.h"
|
||||||
#include "history/history.h"
|
#include "history/history.h"
|
||||||
@@ -320,6 +322,33 @@ const Ui::Text::String &Entry::chatListNameText() const {
|
|||||||
return _chatListNameText;
|
return _chatListNameText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DateText ResolveDateText(
|
||||||
|
DateTextCache &cache,
|
||||||
|
TimeId date,
|
||||||
|
crl::time now) {
|
||||||
|
static crl::time LastNow = 0;
|
||||||
|
static int LastTodaySerial = 0;
|
||||||
|
if (!now || LastNow != now) {
|
||||||
|
LastNow = now;
|
||||||
|
LastTodaySerial = int(QDate::currentDate().toJulianDay());
|
||||||
|
}
|
||||||
|
if (cache.messageTimeId != date
|
||||||
|
|| cache.todaySerial != LastTodaySerial) {
|
||||||
|
const auto qdt = base::unixtime::parse(date);
|
||||||
|
cache.text = Ui::FormatDialogsDate(qdt);
|
||||||
|
cache.width = st::dialogsDateFont->width(cache.text);
|
||||||
|
cache.messageTimeId = date;
|
||||||
|
cache.todaySerial = LastTodaySerial;
|
||||||
|
}
|
||||||
|
return { cache.text, cache.width };
|
||||||
|
}
|
||||||
|
|
||||||
|
DateText Entry::chatListTimestampText(
|
||||||
|
TimeId date,
|
||||||
|
crl::time now) const {
|
||||||
|
return ResolveDateText(_chatListDateCache, date, now);
|
||||||
|
}
|
||||||
|
|
||||||
void Entry::setChatListExistence(bool exists) {
|
void Entry::setChatListExistence(bool exists) {
|
||||||
if (exists && _sortKeyInChatList) {
|
if (exists && _sortKeyInChatList) {
|
||||||
owner().refreshChatListEntry(this);
|
owner().refreshChatListEntry(this);
|
||||||
|
|||||||
@@ -52,6 +52,23 @@ class MainList;
|
|||||||
CountInBadge count = CountInBadge::Default,
|
CountInBadge count = CountInBadge::Default,
|
||||||
IncludeInBadge include = IncludeInBadge::Default);
|
IncludeInBadge include = IncludeInBadge::Default);
|
||||||
|
|
||||||
|
struct DateTextCache {
|
||||||
|
QString text;
|
||||||
|
TimeId messageTimeId = 0;
|
||||||
|
int todaySerial = 0;
|
||||||
|
int width = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DateText {
|
||||||
|
const QString &text;
|
||||||
|
int width = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] DateText ResolveDateText(
|
||||||
|
DateTextCache &cache,
|
||||||
|
TimeId date,
|
||||||
|
crl::time now);
|
||||||
|
|
||||||
class Entry : public base::has_weak_ptr {
|
class Entry : public base::has_weak_ptr {
|
||||||
public:
|
public:
|
||||||
enum class Type : uchar {
|
enum class Type : uchar {
|
||||||
@@ -154,6 +171,9 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] const Ui::Text::String &chatListNameText() const;
|
[[nodiscard]] const Ui::Text::String &chatListNameText() const;
|
||||||
|
[[nodiscard]] DateText chatListTimestampText(
|
||||||
|
TimeId date,
|
||||||
|
crl::time now) const;
|
||||||
[[nodiscard]] Ui::PeerBadge &chatListPeerBadge() const {
|
[[nodiscard]] Ui::PeerBadge &chatListPeerBadge() const {
|
||||||
return _chatListPeerBadge;
|
return _chatListPeerBadge;
|
||||||
}
|
}
|
||||||
@@ -194,6 +214,7 @@ private:
|
|||||||
mutable Ui::PeerBadge _chatListPeerBadge;
|
mutable Ui::PeerBadge _chatListPeerBadge;
|
||||||
mutable Ui::Text::String _chatListNameText;
|
mutable Ui::Text::String _chatListNameText;
|
||||||
mutable int _chatListNameVersion = 0;
|
mutable int _chatListNameVersion = 0;
|
||||||
|
mutable DateTextCache _chatListDateCache;
|
||||||
TimeId _timeId = 0;
|
TimeId _timeId = 0;
|
||||||
Flags _flags;
|
Flags _flags;
|
||||||
|
|
||||||
|
|||||||
@@ -764,4 +764,10 @@ const Ui::Text::String &FakeRow::name() const {
|
|||||||
return _name;
|
return _name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DateText FakeRow::dateText(
|
||||||
|
TimeId date,
|
||||||
|
crl::time now) const {
|
||||||
|
return ResolveDateText(_dateCache, date, now);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Dialogs
|
} // namespace Dialogs
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "ui/text/text.h"
|
#include "ui/text/text.h"
|
||||||
#include "ui/unread_badge.h"
|
#include "ui/unread_badge.h"
|
||||||
#include "ui/userpic_view.h"
|
#include "ui/userpic_view.h"
|
||||||
|
#include "dialogs/dialogs_entry.h"
|
||||||
#include "dialogs/dialogs_key.h"
|
#include "dialogs/dialogs_key.h"
|
||||||
#include "dialogs/ui/dialogs_message_view.h"
|
#include "dialogs/ui/dialogs_message_view.h"
|
||||||
|
|
||||||
@@ -239,6 +240,9 @@ public:
|
|||||||
return _badge;
|
return _badge;
|
||||||
}
|
}
|
||||||
[[nodiscard]] const Ui::Text::String &name() const;
|
[[nodiscard]] const Ui::Text::String &name() const;
|
||||||
|
[[nodiscard]] DateText dateText(
|
||||||
|
TimeId date,
|
||||||
|
crl::time now) const;
|
||||||
|
|
||||||
void invalidateTopic();
|
void invalidateTopic();
|
||||||
|
|
||||||
@@ -252,6 +256,7 @@ private:
|
|||||||
mutable Ui::MessageView _itemView;
|
mutable Ui::MessageView _itemView;
|
||||||
mutable Ui::PeerBadge _badge;
|
mutable Ui::PeerBadge _badge;
|
||||||
mutable Ui::Text::String _name;
|
mutable Ui::Text::String _name;
|
||||||
|
mutable DateTextCache _dateCache;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "history/history_unread_things.h"
|
#include "history/history_unread_things.h"
|
||||||
#include "history/view/history_view_item_preview.h"
|
#include "history/view/history_view_item_preview.h"
|
||||||
#include "history/view/history_view_send_action.h"
|
#include "history/view/history_view_send_action.h"
|
||||||
#include "lang/lang_instance.h"
|
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
#include "lottie/lottie_icon.h"
|
#include "lottie/lottie_icon.h"
|
||||||
#include "main/main_session.h"
|
#include "main/main_session.h"
|
||||||
@@ -90,8 +89,11 @@ void PaintRowTopRight(
|
|||||||
QPainter &p,
|
QPainter &p,
|
||||||
const QString &text,
|
const QString &text,
|
||||||
QRect &rectForName,
|
QRect &rectForName,
|
||||||
const PaintContext &context) {
|
const PaintContext &context,
|
||||||
const auto width = st::dialogsDateFont->width(text);
|
int precomputedWidth = -1) {
|
||||||
|
const auto width = (precomputedWidth >= 0)
|
||||||
|
? precomputedWidth
|
||||||
|
: st::dialogsDateFont->width(text);
|
||||||
rectForName.setWidth(rectForName.width() - width - st::dialogsDateSkip);
|
rectForName.setWidth(rectForName.width() - width - st::dialogsDateSkip);
|
||||||
p.setFont(st::dialogsDateFont);
|
p.setFont(st::dialogsDateFont);
|
||||||
p.setPen(context.active
|
p.setPen(context.active
|
||||||
@@ -388,6 +390,19 @@ enum class Flag {
|
|||||||
};
|
};
|
||||||
inline constexpr bool is_flag_type(Flag) { return true; }
|
inline constexpr bool is_flag_type(Flag) { return true; }
|
||||||
|
|
||||||
|
void PaintDialogDate(
|
||||||
|
QPainter &p,
|
||||||
|
not_null<const Entry*> entry,
|
||||||
|
const FakeRow *fakeRow,
|
||||||
|
TimeId date,
|
||||||
|
QRect &rectForName,
|
||||||
|
const PaintContext &context) {
|
||||||
|
const auto resolved = fakeRow
|
||||||
|
? fakeRow->dateText(date, context.now)
|
||||||
|
: entry->chatListTimestampText(date, context.now);
|
||||||
|
PaintRowTopRight(p, resolved.text, rectForName, context, resolved.width);
|
||||||
|
}
|
||||||
|
|
||||||
template <typename PaintItemCallback>
|
template <typename PaintItemCallback>
|
||||||
void PaintRow(
|
void PaintRow(
|
||||||
Painter &p,
|
Painter &p,
|
||||||
@@ -402,8 +417,9 @@ void PaintRow(
|
|||||||
const HiddenSenderInfo *hiddenSenderInfo,
|
const HiddenSenderInfo *hiddenSenderInfo,
|
||||||
HistoryItem *item,
|
HistoryItem *item,
|
||||||
const Data::Draft *draft,
|
const Data::Draft *draft,
|
||||||
QDateTime date,
|
TimeId date,
|
||||||
const PaintContext &context,
|
const PaintContext &context,
|
||||||
|
const FakeRow *fakeRow,
|
||||||
BadgesState badgesState,
|
BadgesState badgesState,
|
||||||
base::flags<Flag> flags,
|
base::flags<Flag> flags,
|
||||||
PaintItemCallback &&paintItemCallback) {
|
PaintItemCallback &&paintItemCallback) {
|
||||||
@@ -597,8 +613,7 @@ void PaintRow(
|
|||||||
|| (supportMode
|
|| (supportMode
|
||||||
&& entry->session().supportHelper().isOccupiedBySomeone(history))) {
|
&& entry->session().supportHelper().isOccupiedBySomeone(history))) {
|
||||||
if (!promoted) {
|
if (!promoted) {
|
||||||
const auto dateString = Ui::FormatDialogsDate(date);
|
PaintDialogDate(p, entry, fakeRow, date, rectForName, context);
|
||||||
PaintRowTopRight(p, dateString, rectForName, context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto availableWidth = namewidth;
|
auto availableWidth = namewidth;
|
||||||
@@ -729,8 +744,7 @@ void PaintRow(
|
|||||||
}
|
}
|
||||||
} else if (!item->isEmpty()) {
|
} else if (!item->isEmpty()) {
|
||||||
if ((thread || sublist) && !promoted) {
|
if ((thread || sublist) && !promoted) {
|
||||||
const auto dateString = Ui::FormatDialogsDate(date);
|
PaintDialogDate(p, entry, fakeRow, date, rectForName, context);
|
||||||
PaintRowTopRight(p, dateString, rectForName, context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paintItemCallback(nameleft, namewidth);
|
paintItemCallback(nameleft, namewidth);
|
||||||
@@ -1083,18 +1097,10 @@ void RowPainter::Paint(
|
|||||||
}
|
}
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}();
|
}();
|
||||||
const auto displayDate = [&] {
|
const auto displayDate = [&]() -> TimeId {
|
||||||
if (item) {
|
const auto itemDate = item ? item->date() : TimeId(0);
|
||||||
if (cloudDraft) {
|
const auto draftDate = cloudDraft ? cloudDraft->date : TimeId(0);
|
||||||
return (item->date() > cloudDraft->date)
|
return std::max(itemDate, draftDate);
|
||||||
? ItemDateTime(item)
|
|
||||||
: base::unixtime::parse(cloudDraft->date);
|
|
||||||
}
|
|
||||||
return ItemDateTime(item);
|
|
||||||
}
|
|
||||||
return cloudDraft
|
|
||||||
? base::unixtime::parse(cloudDraft->date)
|
|
||||||
: QDateTime();
|
|
||||||
}();
|
}();
|
||||||
const auto displayPinnedIcon = badgesState.empty()
|
const auto displayPinnedIcon = badgesState.empty()
|
||||||
&& entry->isPinnedDialog(context.filter)
|
&& entry->isPinnedDialog(context.filter)
|
||||||
@@ -1194,6 +1200,7 @@ void RowPainter::Paint(
|
|||||||
cloudDraft,
|
cloudDraft,
|
||||||
displayDate,
|
displayDate,
|
||||||
context,
|
context,
|
||||||
|
nullptr,
|
||||||
badgesState,
|
badgesState,
|
||||||
flags,
|
flags,
|
||||||
paintItemCallback);
|
paintItemCallback);
|
||||||
@@ -1303,8 +1310,9 @@ void RowPainter::Paint(
|
|||||||
hiddenSenderInfo,
|
hiddenSenderInfo,
|
||||||
item,
|
item,
|
||||||
cloudDraft,
|
cloudDraft,
|
||||||
ItemDateTime(item),
|
item->date(),
|
||||||
context,
|
context,
|
||||||
|
row,
|
||||||
badgesState,
|
badgesState,
|
||||||
flags,
|
flags,
|
||||||
paintItemCallback);
|
paintItemCallback);
|
||||||
|
|||||||
@@ -351,6 +351,7 @@ ColorPicker::ColorPicker(
|
|||||||
button->show();
|
button->show();
|
||||||
}
|
}
|
||||||
const auto setToolRequest = [=](Brush::Tool tool) {
|
const auto setToolRequest = [=](Brush::Tool tool) {
|
||||||
|
_toolClicks.fire({});
|
||||||
setTool(tool);
|
setTool(tool);
|
||||||
};
|
};
|
||||||
if (_toolButtons.size() >= 5) {
|
if (_toolButtons.size() >= 5) {
|
||||||
@@ -566,10 +567,28 @@ void ColorPicker::setTool(Brush::Tool tool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ColorPicker::storeCurrentBrush() {
|
void ColorPicker::storeCurrentBrush() {
|
||||||
|
if (_toolSelectionSuppressed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
NormalizeBrushColor(_brush);
|
NormalizeBrushColor(_brush);
|
||||||
_toolBrushes[ToolIndex(_brush.tool)] = _brush;
|
_toolBrushes[ToolIndex(_brush.tool)] = _brush;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ColorPicker::setColor(const QColor &color) {
|
||||||
|
_brush.color = color;
|
||||||
|
updateColorButtonColor(color, true);
|
||||||
|
if (_paletteVisible) {
|
||||||
|
rebuildPalette();
|
||||||
|
} else {
|
||||||
|
_colorButton->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorPicker::setToolSelectionVisible(bool visible) {
|
||||||
|
_toolSelectionSuppressed = !visible;
|
||||||
|
_toolSelection->setVisible(visible);
|
||||||
|
}
|
||||||
|
|
||||||
void ColorPicker::updateColorButtonColor(const QColor &color, bool animated) {
|
void ColorPicker::updateColorButtonColor(const QColor &color, bool animated) {
|
||||||
const auto hasValid = _colorButtonFrom.isValid() && _colorButtonTo.isValid();
|
const auto hasValid = _colorButtonFrom.isValid() && _colorButtonTo.isValid();
|
||||||
const auto from = hasValid ? colorButtonColor() : color;
|
const auto from = hasValid ? colorButtonColor() : color;
|
||||||
@@ -632,11 +651,14 @@ void ColorPicker::setVisible(bool visible) {
|
|||||||
_paletteWrap->setVisible(visible && _paletteVisible);
|
_paletteWrap->setVisible(visible && _paletteVisible);
|
||||||
_sizeControlHoverArea->setVisible(visible);
|
_sizeControlHoverArea->setVisible(visible);
|
||||||
_sizeControl->setVisible(visible);
|
_sizeControl->setVisible(visible);
|
||||||
_toolSelection->setVisible(visible && !_paletteVisible);
|
const auto showTools = visible
|
||||||
|
&& !_paletteVisible
|
||||||
|
&& !_toolSelectionSuppressed;
|
||||||
|
_toolSelection->setVisible(showTools);
|
||||||
for (const auto &button : _toolButtons) {
|
for (const auto &button : _toolButtons) {
|
||||||
button->setVisible(visible && !_paletteVisible);
|
button->setVisible(visible && !_paletteVisible);
|
||||||
}
|
}
|
||||||
if (visible && !_paletteVisible) {
|
if (showTools) {
|
||||||
updateToolSelection(false);
|
updateToolSelection(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,6 +667,10 @@ rpl::producer<Brush> ColorPicker::saveBrushRequests() const {
|
|||||||
return _saveBrushRequests.events_starting_with_copy(_brush);
|
return _saveBrushRequests.events_starting_with_copy(_brush);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rpl::producer<> ColorPicker::toolClicks() const {
|
||||||
|
return _toolClicks.events();
|
||||||
|
}
|
||||||
|
|
||||||
bool ColorPicker::preventHandleKeyPress() const {
|
bool ColorPicker::preventHandleKeyPress() const {
|
||||||
return _sizeControl->isVisible()
|
return _sizeControl->isVisible()
|
||||||
&& (_sizeControlAnimation.animating() || _sizeDown.pressed);
|
&& (_sizeControlAnimation.animating() || _sizeDown.pressed);
|
||||||
@@ -785,13 +811,14 @@ void ColorPicker::setPaletteVisible(bool visible) {
|
|||||||
_paletteVisible = visible;
|
_paletteVisible = visible;
|
||||||
_paletteWrap->setVisible(visible);
|
_paletteWrap->setVisible(visible);
|
||||||
_colorButton->setVisible(!visible);
|
_colorButton->setVisible(!visible);
|
||||||
_toolSelection->setVisible(!visible);
|
const auto showTools = !visible && !_toolSelectionSuppressed;
|
||||||
|
_toolSelection->setVisible(showTools);
|
||||||
for (const auto &button : _toolButtons) {
|
for (const auto &button : _toolButtons) {
|
||||||
button->setVisible(!visible);
|
button->setVisible(!visible);
|
||||||
}
|
}
|
||||||
if (visible) {
|
if (visible) {
|
||||||
rebuildPalette();
|
rebuildPalette();
|
||||||
} else {
|
} else if (showTools) {
|
||||||
updateToolSelection(false);
|
updateToolSelection(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,12 @@ public:
|
|||||||
void moveLine(const QPoint &position);
|
void moveLine(const QPoint &position);
|
||||||
void setCanvasRect(const QRect &rect);
|
void setCanvasRect(const QRect &rect);
|
||||||
void setVisible(bool visible);
|
void setVisible(bool visible);
|
||||||
|
void setColor(const QColor &color);
|
||||||
|
void setToolSelectionVisible(bool visible);
|
||||||
bool preventHandleKeyPress() const;
|
bool preventHandleKeyPress() const;
|
||||||
|
|
||||||
rpl::producer<Brush> saveBrushRequests() const;
|
rpl::producer<Brush> saveBrushRequests() const;
|
||||||
|
rpl::producer<> toolClicks() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void paintSizeControl(QPainter &p);
|
void paintSizeControl(QPainter &p);
|
||||||
@@ -75,6 +78,7 @@ private:
|
|||||||
int y = 0;
|
int y = 0;
|
||||||
bool pressed = false;
|
bool pressed = false;
|
||||||
} _sizeDown;
|
} _sizeDown;
|
||||||
|
bool _toolSelectionSuppressed = false;
|
||||||
bool _sizeHoverAreaHovered = false;
|
bool _sizeHoverAreaHovered = false;
|
||||||
bool _sizeControlHovered = false;
|
bool _sizeControlHovered = false;
|
||||||
bool _sizeControlExpanded = false;
|
bool _sizeControlExpanded = false;
|
||||||
@@ -94,6 +98,7 @@ private:
|
|||||||
Ui::Animations::Simple _toolSelectionAnimation;
|
Ui::Animations::Simple _toolSelectionAnimation;
|
||||||
|
|
||||||
rpl::event_stream<Brush> _saveBrushRequests;
|
rpl::event_stream<Brush> _saveBrushRequests;
|
||||||
|
rpl::event_stream<> _toolClicks;
|
||||||
|
|
||||||
std::vector<base::unique_qptr<Ui::ColorSample>> _paletteButtons;
|
std::vector<base::unique_qptr<Ui::ColorSample>> _paletteButtons;
|
||||||
base::unique_qptr<Ui::AbstractButton> _palettePlus;
|
base::unique_qptr<Ui::AbstractButton> _palettePlus;
|
||||||
|
|||||||
@@ -177,6 +177,10 @@ void Crop::paintFrame(QPainter &p) {
|
|||||||
p.save();
|
p.save();
|
||||||
p.setRenderHint(QPainter::Antialiasing, true);
|
p.setRenderHint(QPainter::Antialiasing, true);
|
||||||
p.fillPath(frameShape, st::photoCropPointFg);
|
p.fillPath(frameShape, st::photoCropPointFg);
|
||||||
|
if (_data.fixedCrop) {
|
||||||
|
p.restore();
|
||||||
|
return;
|
||||||
|
}
|
||||||
{
|
{
|
||||||
const auto cornerLength = std::min(
|
const auto cornerLength = std::min(
|
||||||
float64(st::photoEditorCropPointSize * 2),
|
float64(st::photoEditorCropPointSize * 2),
|
||||||
@@ -286,6 +290,10 @@ void Crop::convertCropPaintToOriginal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Crop::updateEdges() {
|
void Crop::updateEdges() {
|
||||||
|
if (_data.fixedCrop) {
|
||||||
|
_edges.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const auto &s = _pointSize;
|
const auto &s = _pointSize;
|
||||||
const auto &m = _edgePointMargins;
|
const auto &m = _edgePointMargins;
|
||||||
const auto &r = _cropPaint;
|
const auto &r = _cropPaint;
|
||||||
@@ -338,6 +346,9 @@ Qt::Edges Crop::mouseState(const QPoint &p) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Crop::mousePressEvent(QMouseEvent *e) {
|
void Crop::mousePressEvent(QMouseEvent *e) {
|
||||||
|
if (_data.fixedCrop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
computeDownState(e->pos());
|
computeDownState(e->pos());
|
||||||
if (_down.edge) {
|
if (_down.edge) {
|
||||||
setGridVisible(true, false);
|
setGridVisible(true, false);
|
||||||
@@ -345,6 +356,9 @@ void Crop::mousePressEvent(QMouseEvent *e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Crop::mouseReleaseEvent(QMouseEvent *e) {
|
void Crop::mouseReleaseEvent(QMouseEvent *e) {
|
||||||
|
if (_data.fixedCrop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const auto hadEdge = bool(_down.edge);
|
const auto hadEdge = bool(_down.edge);
|
||||||
if (hadEdge) {
|
if (hadEdge) {
|
||||||
setGridVisible(false, true);
|
setGridVisible(false, true);
|
||||||
@@ -474,6 +488,9 @@ void Crop::performMove(const QPoint &pos) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Crop::mouseMoveEvent(QMouseEvent *e) {
|
void Crop::mouseMoveEvent(QMouseEvent *e) {
|
||||||
|
if (_data.fixedCrop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const auto pos = e->pos();
|
const auto pos = e->pos();
|
||||||
const auto pressedEdge = _down.edge;
|
const auto pressedEdge = _down.edge;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "editor/scene/scene_item_canvas.h"
|
#include "editor/scene/scene_item_canvas.h"
|
||||||
#include "editor/scene/scene_item_image.h"
|
#include "editor/scene/scene_item_image.h"
|
||||||
#include "editor/scene/scene_item_sticker.h"
|
#include "editor/scene/scene_item_sticker.h"
|
||||||
|
#include "editor/scene/scene_item_text.h"
|
||||||
#include "editor/scene/scene.h"
|
#include "editor/scene/scene.h"
|
||||||
#include "lang/lang_keys.h"
|
#include "lang/lang_keys.h"
|
||||||
#include "lottie/lottie_single_player.h"
|
#include "lottie/lottie_single_player.h"
|
||||||
@@ -35,6 +36,9 @@ constexpr auto kMinCanvasZoom = 1.;
|
|||||||
constexpr auto kMaxCanvasZoom = 8.;
|
constexpr auto kMaxCanvasZoom = 8.;
|
||||||
constexpr auto kCanvasZoomStep = 1.15;
|
constexpr auto kCanvasZoomStep = 1.15;
|
||||||
constexpr auto kZoomEpsilon = 0.0001;
|
constexpr auto kZoomEpsilon = 0.0001;
|
||||||
|
constexpr auto kMinItemZoom = 0.1;
|
||||||
|
constexpr auto kMaxItemZoom = 10.;
|
||||||
|
constexpr auto kCanvasZoomStepFine = 1.015;
|
||||||
|
|
||||||
std::shared_ptr<Scene> EnsureScene(
|
std::shared_ptr<Scene> EnsureScene(
|
||||||
PhotoModifications &mods,
|
PhotoModifications &mods,
|
||||||
@@ -54,17 +58,30 @@ Paint::Paint(
|
|||||||
PhotoModifications &modifications,
|
PhotoModifications &modifications,
|
||||||
const QSize &imageSize,
|
const QSize &imageSize,
|
||||||
std::shared_ptr<Controllers> controllers,
|
std::shared_ptr<Controllers> controllers,
|
||||||
Fn<QImage(QRect)> blurSource)
|
Fn<QImage(QRect)> blurSource,
|
||||||
|
bool fixedCrop)
|
||||||
: RpWidget(parent)
|
: RpWidget(parent)
|
||||||
, _controllers(controllers)
|
, _controllers(controllers)
|
||||||
, _scene(EnsureScene(modifications, imageSize))
|
, _scene(EnsureScene(modifications, imageSize))
|
||||||
, _view(base::make_unique_q<QGraphicsView>(_scene.get(), this))
|
, _view(base::make_unique_q<QGraphicsView>(_scene.get(), this))
|
||||||
, _viewport(_view->viewport())
|
, _viewport(_view->viewport())
|
||||||
, _imageSize(imageSize) {
|
, _imageSize(imageSize)
|
||||||
|
, _fixedCrop(fixedCrop) {
|
||||||
Expects(modifications.paint != nullptr);
|
Expects(modifications.paint != nullptr);
|
||||||
|
|
||||||
_scene->setBlurSource(std::move(blurSource));
|
_scene->setBlurSource(std::move(blurSource));
|
||||||
|
|
||||||
|
{
|
||||||
|
constexpr auto kDefaultFontSizeDivisor = 15.;
|
||||||
|
const auto shortSide = std::min(
|
||||||
|
imageSize.width(),
|
||||||
|
imageSize.height());
|
||||||
|
_scene->setTextDefaults(
|
||||||
|
QColor(255, 255, 255),
|
||||||
|
shortSide / kDefaultFontSizeDivisor,
|
||||||
|
int(TextStyle::Plain));
|
||||||
|
}
|
||||||
|
|
||||||
keepResult();
|
keepResult();
|
||||||
|
|
||||||
_view->show();
|
_view->show();
|
||||||
@@ -77,9 +94,17 @@ Paint::Paint(
|
|||||||
_viewport->setAttribute(Qt::WA_TranslucentBackground, true);
|
_viewport->setAttribute(Qt::WA_TranslucentBackground, true);
|
||||||
_viewport->installEventFilter(this);
|
_viewport->installEventFilter(this);
|
||||||
|
|
||||||
|
_scene->textEditStates(
|
||||||
|
) | rpl::on_next([=](bool editing) {
|
||||||
|
_textEditing = editing;
|
||||||
|
}, lifetime());
|
||||||
|
|
||||||
// Undo / Redo.
|
// Undo / Redo.
|
||||||
controllers->undoController->performRequestChanges(
|
controllers->undoController->performRequestChanges(
|
||||||
) | rpl::on_next([=](const Undo &command) {
|
) | rpl::on_next([=](const Undo &command) {
|
||||||
|
if (_textEditing.current()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (command == Undo::Undo) {
|
if (command == Undo::Undo) {
|
||||||
_scene->performUndo();
|
_scene->performUndo();
|
||||||
} else {
|
} else {
|
||||||
@@ -91,16 +116,22 @@ Paint::Paint(
|
|||||||
}, lifetime());
|
}, lifetime());
|
||||||
|
|
||||||
controllers->undoController->setCanPerformChanges(rpl::merge(
|
controllers->undoController->setCanPerformChanges(rpl::merge(
|
||||||
_hasUndo.value() | rpl::map([](bool enable) {
|
rpl::combine(
|
||||||
|
_hasUndo.value(),
|
||||||
|
_textEditing.value()
|
||||||
|
) | rpl::map([](bool enable, bool editing) {
|
||||||
return UndoController::EnableRequest{
|
return UndoController::EnableRequest{
|
||||||
.command = Undo::Undo,
|
.command = Undo::Undo,
|
||||||
.enable = enable,
|
.enable = enable && !editing,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
_hasRedo.value() | rpl::map([](bool enable) {
|
rpl::combine(
|
||||||
|
_hasRedo.value(),
|
||||||
|
_textEditing.value()
|
||||||
|
) | rpl::map([](bool enable, bool editing) {
|
||||||
return UndoController::EnableRequest{
|
return UndoController::EnableRequest{
|
||||||
.command = Undo::Redo,
|
.command = Undo::Redo,
|
||||||
.enable = enable,
|
.enable = enable && !editing,
|
||||||
};
|
};
|
||||||
})));
|
})));
|
||||||
|
|
||||||
@@ -139,6 +170,51 @@ Paint::Paint(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Paint::zoomSceneItems(float64 wheelDelta, bool fine) {
|
||||||
|
if (!wheelDelta) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto step = wheelDelta
|
||||||
|
/ float64(QWheelEvent::DefaultDeltasPerStep);
|
||||||
|
const auto base = fine ? kCanvasZoomStepFine : kCanvasZoomStep;
|
||||||
|
const auto factor = std::pow(base, step);
|
||||||
|
const auto center = rect::center(_scene->sceneRect());
|
||||||
|
auto applied = false;
|
||||||
|
for (const auto &item : _scene->items()) {
|
||||||
|
const auto raw = item.get();
|
||||||
|
const auto oldScale = raw->scale();
|
||||||
|
const auto newScale = std::clamp(
|
||||||
|
oldScale * factor,
|
||||||
|
kMinItemZoom,
|
||||||
|
kMaxItemZoom);
|
||||||
|
if (std::abs(newScale - oldScale) < kZoomEpsilon) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto ratio = newScale / oldScale;
|
||||||
|
raw->setScale(newScale);
|
||||||
|
const auto pos = raw->pos();
|
||||||
|
raw->setPos(center + (pos - center) * ratio);
|
||||||
|
applied = true;
|
||||||
|
}
|
||||||
|
return applied;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Paint::panSceneItems(QPointF sceneDelta) {
|
||||||
|
if (sceneDelta.isNull()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const auto &item : _scene->items()) {
|
||||||
|
item->setPos(item->pos() + sceneDelta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QPointF Paint::mapWidgetDeltaToScene(QPoint delta) const {
|
||||||
|
if (!_view) {
|
||||||
|
return QPointF(delta);
|
||||||
|
}
|
||||||
|
return _view->mapToScene(delta) - _view->mapToScene(QPoint());
|
||||||
|
}
|
||||||
|
|
||||||
Paint::~Paint() {
|
Paint::~Paint() {
|
||||||
if (_viewport) {
|
if (_viewport) {
|
||||||
_viewport->removeEventFilter(this);
|
_viewport->removeEventFilter(this);
|
||||||
@@ -234,6 +310,38 @@ void Paint::applyBrush(const Brush &brush) {
|
|||||||
brush.tool);
|
brush.tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Paint::createTextItem() {
|
||||||
|
_scene->createTextAtCenter();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Paint::clearSelection() {
|
||||||
|
_scene->clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Paint::setTextColor(const QColor &color) {
|
||||||
|
_scene->setTextColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Paint::setSelectedTextColor(const QColor &color) {
|
||||||
|
_scene->setSelectedTextColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<QColor> Paint::textColorRequests() const {
|
||||||
|
return _scene->textColorRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<QColor> Paint::textItemSelections() const {
|
||||||
|
return _scene->textItemSelections();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<> Paint::textItemDeselections() const {
|
||||||
|
return _scene->textItemDeselections();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<bool> Paint::textEditStates() const {
|
||||||
|
return _scene->textEditStates();
|
||||||
|
}
|
||||||
|
|
||||||
void Paint::handleMimeData(const QMimeData *data) {
|
void Paint::handleMimeData(const QMimeData *data) {
|
||||||
const auto add = [&](QImage image) {
|
const auto add = [&](QImage image) {
|
||||||
if (image.isNull()) {
|
if (image.isNull()) {
|
||||||
@@ -330,11 +438,18 @@ bool Paint::eventFilter(QObject *obj, QEvent *e) {
|
|||||||
}
|
}
|
||||||
if (e->type() == QEvent::Wheel) {
|
if (e->type() == QEvent::Wheel) {
|
||||||
const auto wheel = static_cast<QWheelEvent*>(e);
|
const auto wheel = static_cast<QWheelEvent*>(e);
|
||||||
const auto delta = wheel->angleDelta().y();
|
const auto raw = wheel->angleDelta();
|
||||||
|
const auto delta = raw.y() ? raw.y() : raw.x();
|
||||||
if (!delta) {
|
if (!delta) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_fixedCrop) {
|
||||||
|
zoomSceneItems(
|
||||||
|
delta,
|
||||||
|
wheel->modifiers().testFlag(Qt::ShiftModifier));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const auto step = delta / float64(QWheelEvent::DefaultDeltasPerStep);
|
const auto step = delta / float64(QWheelEvent::DefaultDeltasPerStep);
|
||||||
const auto factor = std::pow(kCanvasZoomStep, step);
|
const auto factor = std::pow(kCanvasZoomStep, step);
|
||||||
const auto newZoom = std::clamp(
|
const auto newZoom = std::clamp(
|
||||||
@@ -363,7 +478,8 @@ bool Paint::eventFilter(QObject *obj, QEvent *e) {
|
|||||||
const auto mouse = static_cast<QMouseEvent*>(e);
|
const auto mouse = static_cast<QMouseEvent*>(e);
|
||||||
if (mouse->button() == Qt::MiddleButton) {
|
if (mouse->button() == Qt::MiddleButton) {
|
||||||
_pan = {
|
_pan = {
|
||||||
.active = (_transform.userZoom > kMinCanvasZoom),
|
.active = (_fixedCrop
|
||||||
|
|| _transform.userZoom > kMinCanvasZoom),
|
||||||
.point = mouse->pos(),
|
.point = mouse->pos(),
|
||||||
};
|
};
|
||||||
if (_pan.active) {
|
if (_pan.active) {
|
||||||
@@ -378,7 +494,9 @@ bool Paint::eventFilter(QObject *obj, QEvent *e) {
|
|||||||
const auto delta = point - _pan.point;
|
const auto delta = point - _pan.point;
|
||||||
_pan.point = point;
|
_pan.point = point;
|
||||||
|
|
||||||
if (_transform.userZoom > kMinCanvasZoom) {
|
if (_fixedCrop) {
|
||||||
|
panSceneItems(mapWidgetDeltaToScene(delta));
|
||||||
|
} else if (_transform.userZoom > kMinCanvasZoom) {
|
||||||
view->horizontalScrollBar()->setValue(
|
view->horizontalScrollBar()->setValue(
|
||||||
view->horizontalScrollBar()->value() - delta.x());
|
view->horizontalScrollBar()->value() - delta.x());
|
||||||
view->verticalScrollBar()->setValue(
|
view->verticalScrollBar()->setValue(
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ public:
|
|||||||
PhotoModifications &modifications,
|
PhotoModifications &modifications,
|
||||||
const QSize &imageSize,
|
const QSize &imageSize,
|
||||||
std::shared_ptr<Controllers> controllers,
|
std::shared_ptr<Controllers> controllers,
|
||||||
Fn<QImage(QRect)> blurSource);
|
Fn<QImage(QRect)> blurSource,
|
||||||
|
bool fixedCrop = false);
|
||||||
~Paint() override;
|
~Paint() override;
|
||||||
|
|
||||||
[[nodiscard]] std::shared_ptr<Scene> saveScene() const;
|
[[nodiscard]] std::shared_ptr<Scene> saveScene() const;
|
||||||
@@ -41,10 +42,24 @@ public:
|
|||||||
void keepResult();
|
void keepResult();
|
||||||
void updateUndoState();
|
void updateUndoState();
|
||||||
|
|
||||||
|
void createTextItem();
|
||||||
|
void clearSelection();
|
||||||
|
void setTextColor(const QColor &color);
|
||||||
|
void setSelectedTextColor(const QColor &color);
|
||||||
|
|
||||||
|
[[nodiscard]] rpl::producer<QColor> textColorRequests() const;
|
||||||
|
[[nodiscard]] rpl::producer<QColor> textItemSelections() const;
|
||||||
|
[[nodiscard]] rpl::producer<> textItemDeselections() const;
|
||||||
|
[[nodiscard]] rpl::producer<bool> textEditStates() const;
|
||||||
|
|
||||||
void handleMimeData(const QMimeData *data);
|
void handleMimeData(const QMimeData *data);
|
||||||
void paintImage(QPainter &p, const QPixmap &image) const;
|
void paintImage(QPainter &p, const QPixmap &image) const;
|
||||||
void resetView();
|
void resetView();
|
||||||
|
|
||||||
|
bool zoomSceneItems(float64 wheelDelta, bool fine = false);
|
||||||
|
void panSceneItems(QPointF sceneDelta);
|
||||||
|
[[nodiscard]] QPointF mapWidgetDeltaToScene(QPoint delta) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool eventFilter(QObject *obj, QEvent *e) override;
|
bool eventFilter(QObject *obj, QEvent *e) override;
|
||||||
void updateViewGeometry();
|
void updateViewGeometry();
|
||||||
@@ -64,6 +79,7 @@ private:
|
|||||||
const base::unique_qptr<QGraphicsView> _view;
|
const base::unique_qptr<QGraphicsView> _view;
|
||||||
QPointer<QWidget> _viewport;
|
QPointer<QWidget> _viewport;
|
||||||
const QSize _imageSize;
|
const QSize _imageSize;
|
||||||
|
const bool _fixedCrop = false;
|
||||||
QRect _imageGeometry;
|
QRect _imageGeometry;
|
||||||
QRect _outerGeometry;
|
QRect _outerGeometry;
|
||||||
|
|
||||||
@@ -84,6 +100,7 @@ private:
|
|||||||
|
|
||||||
rpl::variable<bool> _hasUndo = true;
|
rpl::variable<bool> _hasUndo = true;
|
||||||
rpl::variable<bool> _hasRedo = true;
|
rpl::variable<bool> _hasRedo = true;
|
||||||
|
rpl::variable<bool> _textEditing = false;
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ struct BrushState {
|
|||||||
const auto tool = ToolFromSerialized(entryTool);
|
const auto tool = ToolFromSerialized(entryTool);
|
||||||
const auto index = ToolIndex(tool);
|
const auto index = ToolIndex(tool);
|
||||||
if (version == kBrushesVersion && size > 0) {
|
if (version == kBrushesVersion && size > 0) {
|
||||||
result.brushes[index].sizeRatio = size / float(kPrecision);
|
result.brushes[index].sizeRatio = size / float64(kPrecision);
|
||||||
}
|
}
|
||||||
if (color.isValid()) {
|
if (color.isValid()) {
|
||||||
result.brushes[index].color = color;
|
result.brushes[index].color = color;
|
||||||
@@ -237,6 +237,7 @@ PhotoEditor::PhotoEditor(
|
|||||||
std::move(show),
|
std::move(show),
|
||||||
_brushes,
|
_brushes,
|
||||||
_brushTool)) {
|
_brushTool)) {
|
||||||
|
_modifications.cropType = data.cropType;
|
||||||
|
|
||||||
sizeValue(
|
sizeValue(
|
||||||
) | rpl::on_next([=](const QSize &size) {
|
) | rpl::on_next([=](const QSize &size) {
|
||||||
@@ -302,6 +303,11 @@ PhotoEditor::PhotoEditor(
|
|||||||
};
|
};
|
||||||
}, lifetime());
|
}, lifetime());
|
||||||
|
|
||||||
|
_controls->textRequests(
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
_content->createTextItem();
|
||||||
|
}, lifetime());
|
||||||
|
|
||||||
_controls->doneRequests(
|
_controls->doneRequests(
|
||||||
) | rpl::on_next([=] {
|
) | rpl::on_next([=] {
|
||||||
const auto mode = _mode.current().mode;
|
const auto mode = _mode.current().mode;
|
||||||
@@ -336,18 +342,64 @@ PhotoEditor::PhotoEditor(
|
|||||||
}
|
}
|
||||||
}, lifetime());
|
}, lifetime());
|
||||||
|
|
||||||
|
_colorPicker->toolClicks(
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
_content->clearSelection();
|
||||||
|
}, lifetime());
|
||||||
|
|
||||||
_colorPicker->saveBrushRequests(
|
_colorPicker->saveBrushRequests(
|
||||||
) | rpl::on_next([=](const Brush &brush) {
|
) | rpl::on_next([=](const Brush &brush) {
|
||||||
_content->applyBrush(brush);
|
if (_textItemSelected || _textEditing) {
|
||||||
|
_content->setSelectedTextColor(brush.color);
|
||||||
|
_content->setTextColor(brush.color);
|
||||||
|
} else {
|
||||||
|
_content->applyBrush(brush);
|
||||||
|
_content->setTextColor(brush.color);
|
||||||
|
|
||||||
_brushTool = brush.tool;
|
_brushTool = brush.tool;
|
||||||
_brushes[ToolIndex(brush.tool)] = brush;
|
_brushes[ToolIndex(brush.tool)] = brush;
|
||||||
const auto serialized = Serialize(_brushes, _brushTool);
|
const auto serialized = Serialize(_brushes, _brushTool);
|
||||||
if (Core::App().settings().photoEditorBrush() != serialized) {
|
if (Core::App().settings().photoEditorBrush() != serialized) {
|
||||||
Core::App().settings().setPhotoEditorBrush(serialized);
|
Core::App().settings().setPhotoEditorBrush(serialized);
|
||||||
Core::App().saveSettingsDelayed();
|
Core::App().saveSettingsDelayed();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, lifetime());
|
}, lifetime());
|
||||||
|
|
||||||
|
_content->textEditStates(
|
||||||
|
) | rpl::on_next([=](bool editing) {
|
||||||
|
_textEditing = editing;
|
||||||
|
if (_textEditing) {
|
||||||
|
_colorPicker->setToolSelectionVisible(false);
|
||||||
|
} else if (!_textItemSelected) {
|
||||||
|
const auto &brush = _brushes[ToolIndex(_brushTool)];
|
||||||
|
_colorPicker->setColor(brush.color);
|
||||||
|
_colorPicker->setToolSelectionVisible(true);
|
||||||
|
}
|
||||||
|
}, lifetime());
|
||||||
|
|
||||||
|
_content->textColorRequests(
|
||||||
|
) | rpl::on_next([=](const QColor &color) {
|
||||||
|
_colorPicker->setColor(color);
|
||||||
|
}, lifetime());
|
||||||
|
|
||||||
|
_content->textItemSelections(
|
||||||
|
) | rpl::on_next([=](const QColor &color) {
|
||||||
|
_textItemSelected = true;
|
||||||
|
_colorPicker->setToolSelectionVisible(false);
|
||||||
|
_colorPicker->setColor(color);
|
||||||
|
}, lifetime());
|
||||||
|
|
||||||
|
_content->textItemDeselections(
|
||||||
|
) | rpl::on_next([=] {
|
||||||
|
_textItemSelected = false;
|
||||||
|
if (_textEditing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto &brush = _brushes[ToolIndex(_brushTool)];
|
||||||
|
_colorPicker->setColor(brush.color);
|
||||||
|
_colorPicker->setToolSelectionVisible(true);
|
||||||
|
}, lifetime());
|
||||||
}
|
}
|
||||||
|
|
||||||
void PhotoEditor::keyPressEvent(QKeyEvent *e) {
|
void PhotoEditor::keyPressEvent(QKeyEvent *e) {
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ private:
|
|||||||
.mode = PhotoEditorMode::Mode::Transform,
|
.mode = PhotoEditorMode::Mode::Transform,
|
||||||
.action = PhotoEditorMode::Action::None,
|
.action = PhotoEditorMode::Action::None,
|
||||||
};
|
};
|
||||||
|
bool _textItemSelected = false;
|
||||||
|
bool _textEditing = false;
|
||||||
rpl::event_stream<PhotoModifications> _done;
|
rpl::event_stream<PhotoModifications> _done;
|
||||||
rpl::event_stream<> _cancel;
|
rpl::event_stream<> _cancel;
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,40 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
|
|
||||||
#include "editor/scene/scene.h"
|
#include "editor/scene/scene.h"
|
||||||
#include "ui/painter.h"
|
#include "ui/painter.h"
|
||||||
|
#include "ui/userpic_view.h"
|
||||||
|
|
||||||
namespace Editor {
|
namespace Editor {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void ApplyShapeMask(QImage &image, EditorData::CropType type) {
|
||||||
|
if (type == EditorData::CropType::Rect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||||
|
image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||||
|
}
|
||||||
|
auto mask = QImage(image.size(), QImage::Format_ARGB32_Premultiplied);
|
||||||
|
mask.fill(Qt::transparent);
|
||||||
|
{
|
||||||
|
auto p = QPainter(&mask);
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(Qt::white);
|
||||||
|
const auto rect = QRectF(QPointF(), QSizeF(image.size()));
|
||||||
|
if (type == EditorData::CropType::Ellipse) {
|
||||||
|
p.drawEllipse(rect);
|
||||||
|
} else {
|
||||||
|
const auto radius = std::min(rect.width(), rect.height())
|
||||||
|
* Ui::ForumUserpicRadiusMultiplier();
|
||||||
|
p.drawRoundedRect(rect, radius, radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
auto p = QPainter(&image);
|
||||||
|
p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
|
||||||
|
p.drawImage(0, 0, mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
QImage ImageModified(QImage image, const PhotoModifications &mods) {
|
QImage ImageModified(QImage image, const PhotoModifications &mods) {
|
||||||
Expects(!image.isNull());
|
Expects(!image.isNull());
|
||||||
@@ -32,6 +64,7 @@ QImage ImageModified(QImage image, const PhotoModifications &mods) {
|
|||||||
auto cropped = mods.crop.isValid()
|
auto cropped = mods.crop.isValid()
|
||||||
? image.copy(mods.crop)
|
? image.copy(mods.crop)
|
||||||
: image;
|
: image;
|
||||||
|
ApplyShapeMask(cropped, mods.cropType);
|
||||||
QTransform transform;
|
QTransform transform;
|
||||||
if (mods.flipped) {
|
if (mods.flipped) {
|
||||||
transform.scale(-1, 1);
|
transform.scale(-1, 1);
|
||||||
@@ -43,7 +76,11 @@ QImage ImageModified(QImage image, const PhotoModifications &mods) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool PhotoModifications::empty() const {
|
bool PhotoModifications::empty() const {
|
||||||
return !angle && !flipped && !crop.isValid() && !paint;
|
return !angle
|
||||||
|
&& !flipped
|
||||||
|
&& !crop.isValid()
|
||||||
|
&& cropType == EditorData::CropType::Rect
|
||||||
|
&& !paint;
|
||||||
}
|
}
|
||||||
|
|
||||||
PhotoModifications::operator bool() const {
|
PhotoModifications::operator bool() const {
|
||||||
|
|||||||
@@ -11,18 +11,6 @@ namespace Editor {
|
|||||||
|
|
||||||
class Scene;
|
class Scene;
|
||||||
|
|
||||||
struct PhotoModifications {
|
|
||||||
int angle = 0;
|
|
||||||
bool flipped = false;
|
|
||||||
QRect crop;
|
|
||||||
std::shared_ptr<Scene> paint = nullptr;
|
|
||||||
|
|
||||||
[[nodiscard]] bool empty() const;
|
|
||||||
[[nodiscard]] explicit operator bool() const;
|
|
||||||
~PhotoModifications();
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
struct EditorData {
|
struct EditorData {
|
||||||
enum class CropType {
|
enum class CropType {
|
||||||
Rect,
|
Rect,
|
||||||
@@ -35,6 +23,20 @@ struct EditorData {
|
|||||||
QSize exactSize;
|
QSize exactSize;
|
||||||
CropType cropType = CropType::Rect;
|
CropType cropType = CropType::Rect;
|
||||||
bool keepAspectRatio = false;
|
bool keepAspectRatio = false;
|
||||||
|
bool fixedCrop = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PhotoModifications {
|
||||||
|
int angle = 0;
|
||||||
|
bool flipped = false;
|
||||||
|
QRect crop;
|
||||||
|
EditorData::CropType cropType = EditorData::CropType::Rect;
|
||||||
|
std::shared_ptr<Scene> paint = nullptr;
|
||||||
|
|
||||||
|
[[nodiscard]] bool empty() const;
|
||||||
|
[[nodiscard]] explicit operator bool() const;
|
||||||
|
~PhotoModifications();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
[[nodiscard]] QImage ImageModified(
|
[[nodiscard]] QImage ImageModified(
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "media/view/media_view_pip.h"
|
#include "media/view/media_view_pip.h"
|
||||||
#include "storage/storage_media_prepare.h"
|
#include "storage/storage_media_prepare.h"
|
||||||
|
|
||||||
|
#include <QtGui/QMouseEvent>
|
||||||
|
#include <QtGui/QWheelEvent>
|
||||||
|
|
||||||
namespace Editor {
|
namespace Editor {
|
||||||
|
|
||||||
using Media::View::FlipSizeByRotation;
|
using Media::View::FlipSizeByRotation;
|
||||||
@@ -26,6 +29,7 @@ PhotoEditorContent::PhotoEditorContent(
|
|||||||
EditorData data)
|
EditorData data)
|
||||||
: RpWidget(parent)
|
: RpWidget(parent)
|
||||||
, _photoSize(photo->size())
|
, _photoSize(photo->size())
|
||||||
|
, _fixedCrop(data.fixedCrop)
|
||||||
, _paint(base::make_unique_q<Paint>(
|
, _paint(base::make_unique_q<Paint>(
|
||||||
this,
|
this,
|
||||||
modifications,
|
modifications,
|
||||||
@@ -42,7 +46,8 @@ PhotoEditorContent::PhotoEditorContent(
|
|||||||
auto result = img.copy(pixelRect.intersected(img.rect()));
|
auto result = img.copy(pixelRect.intersected(img.rect()));
|
||||||
result.setDevicePixelRatio(dpr);
|
result.setDevicePixelRatio(dpr);
|
||||||
return result;
|
return result;
|
||||||
}))
|
},
|
||||||
|
data.fixedCrop))
|
||||||
, _crop(base::make_unique_q<Crop>(
|
, _crop(base::make_unique_q<Crop>(
|
||||||
this,
|
this,
|
||||||
modifications,
|
modifications,
|
||||||
@@ -110,6 +115,48 @@ PhotoEditorContent::PhotoEditorContent(
|
|||||||
}, lifetime());
|
}, lifetime());
|
||||||
|
|
||||||
setupDragArea();
|
setupDragArea();
|
||||||
|
|
||||||
|
if (_fixedCrop) {
|
||||||
|
const auto pan = _crop->lifetime().make_state<
|
||||||
|
std::optional<QPoint>
|
||||||
|
>();
|
||||||
|
_crop->events(
|
||||||
|
) | rpl::on_next([=](not_null<QEvent*> e) {
|
||||||
|
const auto type = e->type();
|
||||||
|
if (type == QEvent::Wheel) {
|
||||||
|
const auto wheel = static_cast<QWheelEvent*>(e.get());
|
||||||
|
const auto raw = wheel->angleDelta();
|
||||||
|
_paint->zoomSceneItems(
|
||||||
|
raw.y() ? raw.y() : raw.x(),
|
||||||
|
wheel->modifiers().testFlag(Qt::ShiftModifier));
|
||||||
|
e->accept();
|
||||||
|
} else if (type == QEvent::MouseButtonPress) {
|
||||||
|
const auto mouse = static_cast<QMouseEvent*>(e.get());
|
||||||
|
if (mouse->button() == Qt::MiddleButton) {
|
||||||
|
*pan = mouse->pos();
|
||||||
|
_crop->setCursor(Qt::ClosedHandCursor);
|
||||||
|
e->accept();
|
||||||
|
}
|
||||||
|
} else if (type == QEvent::MouseMove) {
|
||||||
|
if (pan->has_value()) {
|
||||||
|
const auto mouse = static_cast<QMouseEvent*>(e.get());
|
||||||
|
const auto point = mouse->pos();
|
||||||
|
const auto delta = point - **pan;
|
||||||
|
*pan = point;
|
||||||
|
_paint->panSceneItems(
|
||||||
|
_paint->mapWidgetDeltaToScene(delta));
|
||||||
|
e->accept();
|
||||||
|
}
|
||||||
|
} else if (type == QEvent::MouseButtonRelease) {
|
||||||
|
const auto mouse = static_cast<QMouseEvent*>(e.get());
|
||||||
|
if (mouse->button() == Qt::MiddleButton && pan->has_value()) {
|
||||||
|
pan->reset();
|
||||||
|
_crop->unsetCursor();
|
||||||
|
e->accept();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, _crop->lifetime());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PhotoEditorContent::applyModifications(
|
void PhotoEditorContent::applyModifications(
|
||||||
@@ -162,6 +209,38 @@ void PhotoEditorContent::applyBrush(const Brush &brush) {
|
|||||||
_paint->applyBrush(brush);
|
_paint->applyBrush(brush);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PhotoEditorContent::createTextItem() {
|
||||||
|
_paint->createTextItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhotoEditorContent::clearSelection() {
|
||||||
|
_paint->clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhotoEditorContent::setTextColor(const QColor &color) {
|
||||||
|
_paint->setTextColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PhotoEditorContent::setSelectedTextColor(const QColor &color) {
|
||||||
|
_paint->setSelectedTextColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<QColor> PhotoEditorContent::textColorRequests() const {
|
||||||
|
return _paint->textColorRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<QColor> PhotoEditorContent::textItemSelections() const {
|
||||||
|
return _paint->textItemSelections();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<> PhotoEditorContent::textItemDeselections() const {
|
||||||
|
return _paint->textItemDeselections();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<bool> PhotoEditorContent::textEditStates() const {
|
||||||
|
return _paint->textEditStates();
|
||||||
|
}
|
||||||
|
|
||||||
bool PhotoEditorContent::handleKeyPress(not_null<QKeyEvent*> e) const {
|
bool PhotoEditorContent::handleKeyPress(not_null<QKeyEvent*> e) const {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ public:
|
|||||||
void applyModifications(PhotoModifications modifications);
|
void applyModifications(PhotoModifications modifications);
|
||||||
void applyMode(const PhotoEditorMode &mode);
|
void applyMode(const PhotoEditorMode &mode);
|
||||||
void applyBrush(const Brush &brush);
|
void applyBrush(const Brush &brush);
|
||||||
|
void createTextItem();
|
||||||
|
void clearSelection();
|
||||||
|
void setTextColor(const QColor &color);
|
||||||
|
void setSelectedTextColor(const QColor &color);
|
||||||
|
|
||||||
|
[[nodiscard]] rpl::producer<QColor> textColorRequests() const;
|
||||||
|
[[nodiscard]] rpl::producer<QColor> textItemSelections() const;
|
||||||
|
[[nodiscard]] rpl::producer<> textItemDeselections() const;
|
||||||
|
[[nodiscard]] rpl::producer<bool> textEditStates() const;
|
||||||
void applyAspectRatio(float64 ratio);
|
void applyAspectRatio(float64 ratio);
|
||||||
void save(PhotoModifications &modifications);
|
void save(PhotoModifications &modifications);
|
||||||
|
|
||||||
@@ -45,6 +54,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
|
|
||||||
const QSize _photoSize;
|
const QSize _photoSize;
|
||||||
|
const bool _fixedCrop = false;
|
||||||
const base::unique_qptr<Paint> _paint;
|
const base::unique_qptr<Paint> _paint;
|
||||||
const base::unique_qptr<Crop> _crop;
|
const base::unique_qptr<Crop> _crop;
|
||||||
const std::shared_ptr<Image> _photo;
|
const std::shared_ptr<Image> _photo;
|
||||||
|
|||||||
@@ -202,6 +202,38 @@ ButtonBar::ButtonBar(
|
|||||||
}, lifetime());
|
}, lifetime());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TextToolButton final : public Ui::AbstractButton {
|
||||||
|
public:
|
||||||
|
TextToolButton(not_null<QWidget*> parent)
|
||||||
|
: AbstractButton(parent) {
|
||||||
|
constexpr auto kSizeShrink = 6;
|
||||||
|
resize(
|
||||||
|
st::photoEditorStickersButton.width - kSizeShrink,
|
||||||
|
st::photoEditorStickersButton.height - kSizeShrink);
|
||||||
|
events(
|
||||||
|
) | rpl::on_next([=](not_null<QEvent*> event) {
|
||||||
|
if (event->type() == QEvent::Enter
|
||||||
|
|| event->type() == QEvent::Leave) {
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}, lifetime());
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void paintEvent(QPaintEvent *) override {
|
||||||
|
auto p = QPainter(this);
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
auto font = st::semiboldFont->f;
|
||||||
|
font.setPixelSize(QWidget::rect().height() / 2);
|
||||||
|
p.setFont(font);
|
||||||
|
p.setPen(isOver()
|
||||||
|
? st::photoEditorButtonIconFgOver
|
||||||
|
: st::photoEditorButtonIconFg);
|
||||||
|
p.translate(0, st::lineWidth * 3);
|
||||||
|
p.drawText(QWidget::rect(), style::al_center, u"A"_q);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
PhotoEditorControls::PhotoEditorControls(
|
PhotoEditorControls::PhotoEditorControls(
|
||||||
not_null<Ui::RpWidget*> parent,
|
not_null<Ui::RpWidget*> parent,
|
||||||
std::shared_ptr<Controllers> controllers,
|
std::shared_ptr<Controllers> controllers,
|
||||||
@@ -272,6 +304,7 @@ PhotoEditorControls::PhotoEditorControls(
|
|||||||
_paintBottomButtons,
|
_paintBottomButtons,
|
||||||
st::photoEditorStickersButton)
|
st::photoEditorStickersButton)
|
||||||
: nullptr)
|
: nullptr)
|
||||||
|
, _textButton(base::make_unique_q<TextToolButton>(_paintBottomButtons))
|
||||||
, _paintDone(base::make_unique_q<EdgeButton>(
|
, _paintDone(base::make_unique_q<EdgeButton>(
|
||||||
_paintBottomButtons,
|
_paintBottomButtons,
|
||||||
tr::lng_box_done(tr::now),
|
tr::lng_box_done(tr::now),
|
||||||
@@ -499,6 +532,10 @@ rpl::producer<> PhotoEditorControls::paintModeRequests() const {
|
|||||||
return _paintModeButton->clicks() | rpl::to_empty;
|
return _paintModeButton->clicks() | rpl::to_empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rpl::producer<> PhotoEditorControls::textRequests() const {
|
||||||
|
return _textButton->clicks() | rpl::to_empty;
|
||||||
|
}
|
||||||
|
|
||||||
rpl::producer<> PhotoEditorControls::doneRequests() const {
|
rpl::producer<> PhotoEditorControls::doneRequests() const {
|
||||||
return rpl::merge(
|
return rpl::merge(
|
||||||
_transformDone->clicks() | rpl::to_empty,
|
_transformDone->clicks() | rpl::to_empty,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "editor/photo_editor_inner_common.h"
|
#include "editor/photo_editor_inner_common.h"
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
|
class AbstractButton;
|
||||||
class IconButton;
|
class IconButton;
|
||||||
class FlatLabel;
|
class FlatLabel;
|
||||||
class PopupMenu;
|
class PopupMenu;
|
||||||
@@ -41,6 +42,7 @@ public:
|
|||||||
[[nodiscard]] rpl::producer<int> rotateRequests() const;
|
[[nodiscard]] rpl::producer<int> rotateRequests() const;
|
||||||
[[nodiscard]] rpl::producer<> flipRequests() const;
|
[[nodiscard]] rpl::producer<> flipRequests() const;
|
||||||
[[nodiscard]] rpl::producer<> paintModeRequests() const;
|
[[nodiscard]] rpl::producer<> paintModeRequests() const;
|
||||||
|
[[nodiscard]] rpl::producer<> textRequests() const;
|
||||||
[[nodiscard]] rpl::producer<> doneRequests() const;
|
[[nodiscard]] rpl::producer<> doneRequests() const;
|
||||||
[[nodiscard]] rpl::producer<> cancelRequests() const;
|
[[nodiscard]] rpl::producer<> cancelRequests() const;
|
||||||
[[nodiscard]] rpl::producer<QPoint> colorLinePositionValue() const;
|
[[nodiscard]] rpl::producer<QPoint> colorLinePositionValue() const;
|
||||||
@@ -82,6 +84,7 @@ private:
|
|||||||
const base::unique_qptr<Ui::IconButton> _redoButton;
|
const base::unique_qptr<Ui::IconButton> _redoButton;
|
||||||
const base::unique_qptr<Ui::IconButton> _paintModeButtonActive;
|
const base::unique_qptr<Ui::IconButton> _paintModeButtonActive;
|
||||||
const base::unique_qptr<Ui::IconButton> _stickersButton;
|
const base::unique_qptr<Ui::IconButton> _stickersButton;
|
||||||
|
const base::unique_qptr<Ui::AbstractButton> _textButton;
|
||||||
const base::unique_qptr<EdgeButton> _paintDone;
|
const base::unique_qptr<EdgeButton> _paintDone;
|
||||||
|
|
||||||
base::unique_qptr<Ui::PopupMenu> _ratioMenu;
|
base::unique_qptr<Ui::PopupMenu> _ratioMenu;
|
||||||
|
|||||||
@@ -10,11 +10,18 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "editor/scene/scene_item_canvas.h"
|
#include "editor/scene/scene_item_canvas.h"
|
||||||
#include "editor/scene/scene_item_line.h"
|
#include "editor/scene/scene_item_line.h"
|
||||||
#include "editor/scene/scene_item_sticker.h"
|
#include "editor/scene/scene_item_sticker.h"
|
||||||
|
#include "editor/scene/scene_item_text.h"
|
||||||
|
#include "editor/scene/scene_emoji_document.h"
|
||||||
#include "ui/image/image_prepare.h"
|
#include "ui/image/image_prepare.h"
|
||||||
#include "ui/rp_widget.h"
|
#include "ui/rp_widget.h"
|
||||||
#include "styles/style_editor.h"
|
#include "styles/style_editor.h"
|
||||||
|
|
||||||
|
#include <QGraphicsSceneContextMenuEvent>
|
||||||
#include <QGraphicsSceneMouseEvent>
|
#include <QGraphicsSceneMouseEvent>
|
||||||
|
#include <QGraphicsTextItem>
|
||||||
|
#include <QGraphicsView>
|
||||||
|
#include <QTextCursor>
|
||||||
|
#include <QTextDocument>
|
||||||
|
|
||||||
namespace Editor {
|
namespace Editor {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -100,6 +107,50 @@ bool SkipMouseEvent(not_null<QGraphicsSceneMouseEvent*> event) {
|
|||||||
return event->isAccepted() || (event->button() == Qt::RightButton);
|
return event->isAccepted() || (event->button() == Qt::RightButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr auto kPaddingFactor = 0.4;
|
||||||
|
constexpr auto kMaxWidthFactor = 0.8;
|
||||||
|
constexpr auto kMinWidthFactor = 0.16;
|
||||||
|
constexpr auto kIdealWidthExtra = 2;
|
||||||
|
constexpr auto kDefaultFontSizeDivisor = 15.;
|
||||||
|
constexpr auto kScaleThreshold = 0.01;
|
||||||
|
|
||||||
|
class TextEditProxy final : public QGraphicsTextItem {
|
||||||
|
public:
|
||||||
|
using QGraphicsTextItem::QGraphicsTextItem;
|
||||||
|
|
||||||
|
Fn<void()> onFinish;
|
||||||
|
Fn<void()> onCancel;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void keyPressEvent(QKeyEvent *event) override {
|
||||||
|
if (event->key() == Qt::Key_Escape) {
|
||||||
|
fire(onCancel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QGraphicsTextItem::keyPressEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void focusOutEvent(QFocusEvent *event) override {
|
||||||
|
QGraphicsTextItem::focusOutEvent(event);
|
||||||
|
fire(onFinish);
|
||||||
|
}
|
||||||
|
|
||||||
|
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override {
|
||||||
|
event->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void fire(Fn<void()> &callback) {
|
||||||
|
if (!callback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto cb = std::exchange(callback, nullptr);
|
||||||
|
onFinish = nullptr;
|
||||||
|
onCancel = nullptr;
|
||||||
|
crl::on_main(cb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Scene::Scene(const QRectF &rect)
|
Scene::Scene(const QRectF &rect)
|
||||||
@@ -237,9 +288,34 @@ Scene::Scene(const QRectF &rect)
|
|||||||
addItem(item);
|
addItem(item);
|
||||||
_canvas->setZValue(++_lastLineZ);
|
_canvas->setZValue(++_lastLineZ);
|
||||||
}, _lifetime);
|
}, _lifetime);
|
||||||
|
|
||||||
|
QObject::connect(
|
||||||
|
this,
|
||||||
|
&QGraphicsScene::selectionChanged,
|
||||||
|
[=] {
|
||||||
|
const auto selected = selectedItems();
|
||||||
|
auto *textItem = (ItemText*)(nullptr);
|
||||||
|
if (selected.size() == 1
|
||||||
|
&& selected.front()->type() == ItemText::Type) {
|
||||||
|
textItem = static_cast<ItemText*>(selected.front());
|
||||||
|
}
|
||||||
|
const auto changed = (textItem != _selectedTextItem);
|
||||||
|
if (!changed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_selectedTextItem = textItem;
|
||||||
|
if (textItem) {
|
||||||
|
_textItemSelections.fire_copy(textItem->color());
|
||||||
|
} else {
|
||||||
|
_textItemDeselections.fire({});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void Scene::cancelDrawing() {
|
void Scene::cancelDrawing() {
|
||||||
|
if (_textEdit.proxy) {
|
||||||
|
finishTextEditing(false);
|
||||||
|
}
|
||||||
_canvas->cancelDrawing();
|
_canvas->cancelDrawing();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +347,16 @@ void Scene::removeItem(const ItemPtr &item) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
|
void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
|
||||||
|
if (_textEdit.proxy) {
|
||||||
|
const auto clickOnProxy = _textEdit.proxy->contains(
|
||||||
|
_textEdit.proxy->mapFromScene(event->scenePos()));
|
||||||
|
if (!clickOnProxy) {
|
||||||
|
finishTextEditing(true);
|
||||||
|
QGraphicsScene::mousePressEvent(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QGraphicsScene::mousePressEvent(event);
|
QGraphicsScene::mousePressEvent(event);
|
||||||
if (SkipMouseEvent(event) || !sceneRect().contains(event->scenePos())) {
|
if (SkipMouseEvent(event) || !sceneRect().contains(event->scenePos())) {
|
||||||
return;
|
return;
|
||||||
@@ -280,7 +366,7 @@ void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
|
|||||||
|
|
||||||
void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
|
void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
|
||||||
QGraphicsScene::mouseReleaseEvent(event);
|
QGraphicsScene::mouseReleaseEvent(event);
|
||||||
if (SkipMouseEvent(event)) {
|
if (SkipMouseEvent(event) || _textEdit.proxy) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_canvas->handleMouseReleaseEvent(event);
|
_canvas->handleMouseReleaseEvent(event);
|
||||||
@@ -288,16 +374,58 @@ void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
|
|||||||
|
|
||||||
void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
|
void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
|
||||||
QGraphicsScene::mouseMoveEvent(event);
|
QGraphicsScene::mouseMoveEvent(event);
|
||||||
if (SkipMouseEvent(event)) {
|
if (SkipMouseEvent(event) || _textEdit.proxy) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_canvas->handleMouseMoveEvent(event);
|
_canvas->handleMouseMoveEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Scene::applyBrush(const QColor &color, float size, Brush::Tool tool) {
|
void Scene::applyBrush(const QColor &color, float64 size, Brush::Tool tool) {
|
||||||
_canvas->applyBrush(color, size, tool);
|
_canvas->applyBrush(color, size, tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Scene::setTextDefaults(
|
||||||
|
const QColor &color,
|
||||||
|
float64 fontSize,
|
||||||
|
int style) {
|
||||||
|
_textColor = color;
|
||||||
|
_textFontSize = fontSize;
|
||||||
|
_textStyle = style;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::setTextColor(const QColor &color) {
|
||||||
|
_textColor = color;
|
||||||
|
if (_textEdit.proxy) {
|
||||||
|
_textEdit.proxy->setDefaultTextColor(EffectiveTextColor(
|
||||||
|
color,
|
||||||
|
static_cast<TextStyle>(_textEditStyle)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::setSelectedTextColor(const QColor &color) {
|
||||||
|
for (auto *item : selectedItems()) {
|
||||||
|
if (item->type() == ItemText::Type) {
|
||||||
|
static_cast<ItemText*>(item)->setColor(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<QColor> Scene::textColorRequests() const {
|
||||||
|
return _textColorRequests.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<QColor> Scene::textItemSelections() const {
|
||||||
|
return _textItemSelections.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<> Scene::textItemDeselections() const {
|
||||||
|
return _textItemDeselections.events();
|
||||||
|
}
|
||||||
|
|
||||||
|
rpl::producer<bool> Scene::textEditStates() const {
|
||||||
|
return _textEditStates.events();
|
||||||
|
}
|
||||||
|
|
||||||
void Scene::setBlurSource(Fn<QImage(QRect)> source) {
|
void Scene::setBlurSource(Fn<QImage(QRect)> source) {
|
||||||
_blurSource = std::move(source);
|
_blurSource = std::move(source);
|
||||||
}
|
}
|
||||||
@@ -328,6 +456,7 @@ std::shared_ptr<float64> Scene::lastZ() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Scene::updateZoom(float64 zoom) {
|
void Scene::updateZoom(float64 zoom) {
|
||||||
|
_currentZoom = zoom;
|
||||||
_canvas->updateZoom(zoom);
|
_canvas->updateZoom(zoom);
|
||||||
for (const auto &item : items()) {
|
for (const auto &item : items()) {
|
||||||
if (item->type() >= ItemBase::Type) {
|
if (item->type() >= ItemBase::Type) {
|
||||||
@@ -396,6 +525,10 @@ void Scene::clearRedoList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Scene::save(SaveState state) {
|
void Scene::save(SaveState state) {
|
||||||
|
if (_textEdit.proxy) {
|
||||||
|
finishTextEditing(true);
|
||||||
|
}
|
||||||
|
|
||||||
removeIf([](const ItemPtr &item) {
|
removeIf([](const ItemPtr &item) {
|
||||||
return item->isRemovedStatus()
|
return item->isRemovedStatus()
|
||||||
&& !item->hasState(SaveState::Keep)
|
&& !item->hasState(SaveState::Keep)
|
||||||
@@ -421,11 +554,283 @@ void Scene::restore(SaveState state) {
|
|||||||
cancelDrawing();
|
cancelDrawing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Scene::setTextEditing(bool editing) {
|
||||||
|
if (_textEditing == editing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_textEditing = editing;
|
||||||
|
_textEditStates.fire_copy(editing);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::setupTextProxy(
|
||||||
|
QGraphicsTextItem *proxy,
|
||||||
|
const QColor &color,
|
||||||
|
float64 fontSize) {
|
||||||
|
proxy->setTextInteractionFlags(Qt::TextEditorInteraction);
|
||||||
|
proxy->setDefaultTextColor(color);
|
||||||
|
|
||||||
|
auto *emojiDoc = new EmojiDocument(proxy);
|
||||||
|
emojiDoc->setDocumentMargin(0);
|
||||||
|
proxy->setDocument(emojiDoc);
|
||||||
|
|
||||||
|
auto font = QFont();
|
||||||
|
font.setPixelSize(int(fontSize));
|
||||||
|
font.setWeight(QFont::DemiBold);
|
||||||
|
proxy->setFont(font);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto option = emojiDoc->defaultTextOption();
|
||||||
|
option.setAlignment(Qt::AlignCenter);
|
||||||
|
emojiDoc->setDefaultTextOption(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::createTextAtCenter() {
|
||||||
|
if (_textEdit.proxy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto generation = ++_textEditGeneration;
|
||||||
|
|
||||||
|
clearSelection();
|
||||||
|
cancelDrawing();
|
||||||
|
setTextEditing(true);
|
||||||
|
_textEditStyle = _textStyle;
|
||||||
|
|
||||||
|
_textEdit.proxy.reset(new TextEditProxy());
|
||||||
|
const auto proxy = _textEdit.proxy.get();
|
||||||
|
setupTextProxy(
|
||||||
|
proxy,
|
||||||
|
EffectiveTextColor(
|
||||||
|
_textColor,
|
||||||
|
static_cast<TextStyle>(_textEditStyle)),
|
||||||
|
_textFontSize);
|
||||||
|
|
||||||
|
const auto emojiDoc = proxy->document();
|
||||||
|
const auto shortSide = std::min(
|
||||||
|
sceneRect().width(),
|
||||||
|
sceneRect().height());
|
||||||
|
const auto padding = int(_textFontSize * kPaddingFactor);
|
||||||
|
const auto maxTextWidth = std::max(
|
||||||
|
int(shortSide * kMaxWidthFactor) - 2 * padding,
|
||||||
|
1);
|
||||||
|
const auto minTextWidth = std::clamp(
|
||||||
|
int(shortSide * kMinWidthFactor) - 2 * padding,
|
||||||
|
1,
|
||||||
|
maxTextWidth);
|
||||||
|
const auto sceneCenter = sceneRect().center();
|
||||||
|
const auto adjustWidth = [=] {
|
||||||
|
emojiDoc->setTextWidth(maxTextWidth);
|
||||||
|
const auto ideal = int(std::ceil(emojiDoc->idealWidth()));
|
||||||
|
const auto width = std::clamp(
|
||||||
|
ideal + kIdealWidthExtra,
|
||||||
|
minTextWidth,
|
||||||
|
maxTextWidth);
|
||||||
|
proxy->setTextWidth(width);
|
||||||
|
proxy->setPos(sceneCenter.x() - width / 2., sceneCenter.y());
|
||||||
|
};
|
||||||
|
adjustWidth();
|
||||||
|
|
||||||
|
QObject::connect(emojiDoc, &QTextDocument::contentsChanged, [=] {
|
||||||
|
ReplaceEmoji(emojiDoc);
|
||||||
|
adjustWidth();
|
||||||
|
});
|
||||||
|
|
||||||
|
QGraphicsScene::addItem(proxy);
|
||||||
|
proxy->setZValue((*_lastZ)++);
|
||||||
|
proxy->setFocus();
|
||||||
|
if (!views().isEmpty()) {
|
||||||
|
views().first()->setFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto raw = static_cast<TextEditProxy*>(proxy);
|
||||||
|
raw->onFinish = crl::guard(this, [=] {
|
||||||
|
if (generation == _textEditGeneration) {
|
||||||
|
finishTextEditing(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
raw->onCancel = crl::guard(this, [=] {
|
||||||
|
if (generation == _textEditGeneration) {
|
||||||
|
finishTextEditing(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_textEdit.item.reset();
|
||||||
|
_textColorRequests.fire_copy(_textColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::startTextEditing(ItemText *item) {
|
||||||
|
if (_textEdit.proxy) {
|
||||||
|
finishTextEditing(true);
|
||||||
|
}
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto generation = ++_textEditGeneration;
|
||||||
|
|
||||||
|
cancelDrawing();
|
||||||
|
setTextEditing(true);
|
||||||
|
_textEditStyle = int(item->textStyle());
|
||||||
|
|
||||||
|
_textEdit.proxy.reset(new TextEditProxy());
|
||||||
|
const auto proxy = _textEdit.proxy.get();
|
||||||
|
setupTextProxy(
|
||||||
|
proxy,
|
||||||
|
EffectiveTextColor(item->color(), item->textStyle()),
|
||||||
|
item->fontSize());
|
||||||
|
|
||||||
|
proxy->setPlainText(item->text());
|
||||||
|
ReplaceEmoji(proxy->document());
|
||||||
|
|
||||||
|
const auto emojiDoc = proxy->document();
|
||||||
|
const auto shortSide = std::min(
|
||||||
|
sceneRect().width(),
|
||||||
|
sceneRect().height());
|
||||||
|
const auto padding = int(item->fontSize() * kPaddingFactor);
|
||||||
|
const auto maxTextWidth = std::max(
|
||||||
|
int(shortSide * kMaxWidthFactor) - 2 * padding,
|
||||||
|
1);
|
||||||
|
const auto minTextWidth = std::clamp(
|
||||||
|
int(shortSide * kMinWidthFactor) - 2 * padding,
|
||||||
|
1,
|
||||||
|
maxTextWidth);
|
||||||
|
const auto anchor = item->scenePos();
|
||||||
|
const auto adjustWidth = [=] {
|
||||||
|
emojiDoc->setTextWidth(maxTextWidth);
|
||||||
|
const auto ideal = int(std::ceil(emojiDoc->idealWidth()));
|
||||||
|
const auto width = std::clamp(
|
||||||
|
ideal + kIdealWidthExtra,
|
||||||
|
minTextWidth,
|
||||||
|
maxTextWidth);
|
||||||
|
proxy->setTextWidth(width);
|
||||||
|
const auto center = proxy->boundingRect().center();
|
||||||
|
proxy->setTransformOriginPoint(center);
|
||||||
|
proxy->setPos(anchor - center);
|
||||||
|
};
|
||||||
|
adjustWidth();
|
||||||
|
|
||||||
|
QObject::connect(emojiDoc, &QTextDocument::contentsChanged, [=] {
|
||||||
|
ReplaceEmoji(emojiDoc);
|
||||||
|
adjustWidth();
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto scale = item->editScale();
|
||||||
|
proxy->setRotation(item->rotation());
|
||||||
|
if (std::abs(scale - 1.) > kScaleThreshold) {
|
||||||
|
proxy->setScale(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
QGraphicsScene::addItem(proxy);
|
||||||
|
proxy->setZValue((*_lastZ)++);
|
||||||
|
proxy->setFocus();
|
||||||
|
|
||||||
|
auto cursor = proxy->textCursor();
|
||||||
|
cursor.select(QTextCursor::Document);
|
||||||
|
proxy->setTextCursor(cursor);
|
||||||
|
|
||||||
|
item->setVisible(false);
|
||||||
|
|
||||||
|
const auto raw = static_cast<TextEditProxy*>(proxy);
|
||||||
|
raw->onFinish = crl::guard(this, [=] {
|
||||||
|
if (generation == _textEditGeneration) {
|
||||||
|
finishTextEditing(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
raw->onCancel = crl::guard(this, [=] {
|
||||||
|
if (generation == _textEditGeneration) {
|
||||||
|
finishTextEditing(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto it = _itemsByPointer.find(item);
|
||||||
|
_textEdit.item = (it != end(_itemsByPointer))
|
||||||
|
? it->second
|
||||||
|
: std::weak_ptr<NumberedItem>();
|
||||||
|
_textColorRequests.fire_copy(item->color());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Scene::finishTextEditing(bool save) {
|
||||||
|
if (!_textEdit.proxy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto text = save
|
||||||
|
? RecoverTextFromDocument(_textEdit.proxy->document()).trimmed()
|
||||||
|
: QString();
|
||||||
|
const auto proxyRect = _textEdit.proxy->boundingRect();
|
||||||
|
const auto proxyCenter = _textEdit.proxy->pos()
|
||||||
|
+ QPointF(proxyRect.width() / 2., proxyRect.height() / 2.);
|
||||||
|
const auto lockedItem = _textEdit.item.lock();
|
||||||
|
auto *existingItem = lockedItem
|
||||||
|
? static_cast<ItemText*>(lockedItem.get())
|
||||||
|
: (ItemText*)(nullptr);
|
||||||
|
|
||||||
|
const auto raw = static_cast<TextEditProxy*>(_textEdit.proxy.get());
|
||||||
|
raw->onFinish = nullptr;
|
||||||
|
raw->onCancel = nullptr;
|
||||||
|
QGraphicsScene::removeItem(_textEdit.proxy.get());
|
||||||
|
_textEdit.proxy = nullptr;
|
||||||
|
_textEdit.item.reset();
|
||||||
|
setTextEditing(false);
|
||||||
|
|
||||||
|
const auto defaultStyle = static_cast<TextStyle>(_textStyle);
|
||||||
|
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
if (existingItem) {
|
||||||
|
existingItem->setText(text);
|
||||||
|
existingItem->setVisible(true);
|
||||||
|
} else {
|
||||||
|
const auto imageSize = sceneRect().size().toSize();
|
||||||
|
const auto contentSize = ItemText::computeContentSize(
|
||||||
|
text,
|
||||||
|
_textFontSize,
|
||||||
|
imageSize,
|
||||||
|
defaultStyle);
|
||||||
|
const auto zoom = (_currentZoom > 0.) ? _currentZoom : 1.;
|
||||||
|
const auto handleInflate = int(
|
||||||
|
std::ceil(st::photoEditorItemHandleSize / zoom));
|
||||||
|
const auto size = std::max(
|
||||||
|
contentSize.width() + handleInflate,
|
||||||
|
1);
|
||||||
|
auto data = ItemBase::Data{
|
||||||
|
.initialZoom = zoom,
|
||||||
|
.zPtr = _lastZ,
|
||||||
|
.size = size,
|
||||||
|
.x = int(proxyCenter.x()),
|
||||||
|
.y = int(proxyCenter.y()),
|
||||||
|
.imageSize = imageSize,
|
||||||
|
};
|
||||||
|
auto item = std::make_shared<ItemText>(
|
||||||
|
text,
|
||||||
|
_textColor,
|
||||||
|
_textFontSize,
|
||||||
|
defaultStyle,
|
||||||
|
imageSize,
|
||||||
|
std::move(data));
|
||||||
|
addItem(item);
|
||||||
|
}
|
||||||
|
} else if (existingItem) {
|
||||||
|
if (save) {
|
||||||
|
removeItem(existingItem);
|
||||||
|
} else {
|
||||||
|
existingItem->setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scene::~Scene() {
|
Scene::~Scene() {
|
||||||
// Prevent destroying by scene of all items.
|
if (_textEdit.proxy) {
|
||||||
|
setTextEditing(false);
|
||||||
|
const auto raw = static_cast<TextEditProxy*>(
|
||||||
|
_textEdit.proxy.get());
|
||||||
|
raw->onFinish = nullptr;
|
||||||
|
raw->onCancel = nullptr;
|
||||||
|
QGraphicsScene::removeItem(_textEdit.proxy.get());
|
||||||
|
_textEdit.proxy = nullptr;
|
||||||
|
}
|
||||||
QGraphicsScene::removeItem(_canvas.get());
|
QGraphicsScene::removeItem(_canvas.get());
|
||||||
for (const auto &item : items()) {
|
for (const auto &item : items()) {
|
||||||
// Scene loses ownership of an item.
|
|
||||||
QGraphicsScene::removeItem(item.get());
|
QGraphicsScene::removeItem(item.get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include <QGraphicsScene>
|
#include <QGraphicsScene>
|
||||||
|
|
||||||
class QGraphicsSceneMouseEvent;
|
class QGraphicsSceneMouseEvent;
|
||||||
|
class QGraphicsTextItem;
|
||||||
|
|
||||||
namespace Ui {
|
namespace Ui {
|
||||||
class RpWidget;
|
class RpWidget;
|
||||||
@@ -21,6 +22,7 @@ class RpWidget;
|
|||||||
namespace Editor {
|
namespace Editor {
|
||||||
|
|
||||||
class ItemCanvas;
|
class ItemCanvas;
|
||||||
|
class ItemText;
|
||||||
class NumberedItem;
|
class NumberedItem;
|
||||||
|
|
||||||
class Scene final : public QGraphicsScene {
|
class Scene final : public QGraphicsScene {
|
||||||
@@ -29,8 +31,9 @@ public:
|
|||||||
|
|
||||||
Scene(const QRectF &rect);
|
Scene(const QRectF &rect);
|
||||||
~Scene();
|
~Scene();
|
||||||
void applyBrush(const QColor &color, float size, Brush::Tool tool);
|
void applyBrush(const QColor &color, float64 size, Brush::Tool tool);
|
||||||
void setBlurSource(Fn<QImage(QRect)> source);
|
void setBlurSource(Fn<QImage(QRect)> source);
|
||||||
|
void setTextDefaults(const QColor &color, float64 fontSize, int style);
|
||||||
|
|
||||||
[[nodiscard]] std::vector<ItemPtr> items(
|
[[nodiscard]] std::vector<ItemPtr> items(
|
||||||
Qt::SortOrder order = Qt::DescendingOrder) const;
|
Qt::SortOrder order = Qt::DescendingOrder) const;
|
||||||
@@ -46,6 +49,16 @@ public:
|
|||||||
|
|
||||||
void cancelDrawing();
|
void cancelDrawing();
|
||||||
|
|
||||||
|
void startTextEditing(ItemText *item);
|
||||||
|
void createTextAtCenter();
|
||||||
|
void setTextColor(const QColor &color);
|
||||||
|
void setSelectedTextColor(const QColor &color);
|
||||||
|
|
||||||
|
[[nodiscard]] rpl::producer<QColor> textColorRequests() const;
|
||||||
|
[[nodiscard]] rpl::producer<QColor> textItemSelections() const;
|
||||||
|
[[nodiscard]] rpl::producer<> textItemDeselections() const;
|
||||||
|
[[nodiscard]] rpl::producer<bool> textEditStates() const;
|
||||||
|
|
||||||
[[nodiscard]] bool hasUndo() const;
|
[[nodiscard]] bool hasUndo() const;
|
||||||
[[nodiscard]] bool hasRedo() const;
|
[[nodiscard]] bool hasRedo() const;
|
||||||
|
|
||||||
@@ -62,6 +75,13 @@ protected:
|
|||||||
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
private:
|
private:
|
||||||
void removeIf(Fn<bool(const ItemPtr &)> proj);
|
void removeIf(Fn<bool(const ItemPtr &)> proj);
|
||||||
|
void finishTextEditing(bool save);
|
||||||
|
void setTextEditing(bool editing);
|
||||||
|
void setupTextProxy(
|
||||||
|
QGraphicsTextItem *proxy,
|
||||||
|
const QColor &color,
|
||||||
|
float64 fontSize);
|
||||||
|
|
||||||
const std::shared_ptr<ItemCanvas> _canvas;
|
const std::shared_ptr<ItemCanvas> _canvas;
|
||||||
const std::shared_ptr<float64> _lastZ;
|
const std::shared_ptr<float64> _lastZ;
|
||||||
Fn<QImage(QRect)> _blurSource;
|
Fn<QImage(QRect)> _blurSource;
|
||||||
@@ -70,9 +90,27 @@ private:
|
|||||||
std::unordered_map<QGraphicsItem*, ItemPtr> _itemsByPointer;
|
std::unordered_map<QGraphicsItem*, ItemPtr> _itemsByPointer;
|
||||||
|
|
||||||
float64 _lastLineZ = 0.;
|
float64 _lastLineZ = 0.;
|
||||||
|
float64 _currentZoom = 1.;
|
||||||
int _itemNumber = 0;
|
int _itemNumber = 0;
|
||||||
|
|
||||||
|
QColor _textColor;
|
||||||
|
float64 _textFontSize = 0.;
|
||||||
|
int _textStyle = 0;
|
||||||
|
int _textEditStyle = 0;
|
||||||
|
|
||||||
|
struct {
|
||||||
|
std::weak_ptr<NumberedItem> item;
|
||||||
|
base::unique_qptr<QGraphicsTextItem> proxy;
|
||||||
|
} _textEdit;
|
||||||
|
|
||||||
rpl::event_stream<> _addsItem, _removesItem;
|
rpl::event_stream<> _addsItem, _removesItem;
|
||||||
|
rpl::event_stream<QColor> _textColorRequests;
|
||||||
|
rpl::event_stream<QColor> _textItemSelections;
|
||||||
|
rpl::event_stream<> _textItemDeselections;
|
||||||
|
rpl::event_stream<bool> _textEditStates;
|
||||||
|
ItemText *_selectedTextItem = nullptr;
|
||||||
|
bool _textEditing = false;
|
||||||
|
int _textEditGeneration = 0;
|
||||||
rpl::lifetime _lifetime;
|
rpl::lifetime _lifetime;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
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 "editor/scene/scene_emoji_document.h"
|
||||||
|
|
||||||
|
#include "ui/emoji_config.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
|
|
||||||
|
#include <QTextBlock>
|
||||||
|
#include <QTextCursor>
|
||||||
|
|
||||||
|
namespace Editor {
|
||||||
|
|
||||||
|
EmojiDocument::EmojiDocument(QObject *parent)
|
||||||
|
: QTextDocument(parent) {
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant EmojiDocument::loadResource(int type, const QUrl &name) {
|
||||||
|
if (type != QTextDocument::ImageResource
|
||||||
|
|| name.scheme() != u"emoji"_q) {
|
||||||
|
return QTextDocument::loadResource(type, name);
|
||||||
|
}
|
||||||
|
const auto i = _cache.find(name);
|
||||||
|
if (i != _cache.end()) {
|
||||||
|
return i->second;
|
||||||
|
}
|
||||||
|
auto result = QVariant();
|
||||||
|
if (const auto emoji = Ui::Emoji::FromUrl(name.toDisplayString())) {
|
||||||
|
const auto factor = style::DevicePixelRatio();
|
||||||
|
const auto logical = QFontMetrics(defaultFont()).height();
|
||||||
|
const auto source = Ui::Emoji::GetSizeLarge();
|
||||||
|
auto image = QImage(
|
||||||
|
QSize(logical, logical) * factor,
|
||||||
|
QImage::Format_ARGB32_Premultiplied);
|
||||||
|
image.setDevicePixelRatio(factor);
|
||||||
|
image.fill(Qt::transparent);
|
||||||
|
{
|
||||||
|
auto p = QPainter(&image);
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
const auto sourceLogical = source / float64(factor);
|
||||||
|
const auto scale = logical / sourceLogical;
|
||||||
|
p.scale(scale, scale);
|
||||||
|
Ui::Emoji::Draw(p, emoji, source, 0, 0);
|
||||||
|
}
|
||||||
|
result = QVariant(QPixmap::fromImage(std::move(image)));
|
||||||
|
}
|
||||||
|
_cache.emplace(name, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReplaceEmoji(QTextDocument *doc) {
|
||||||
|
QSignalBlocker blocker(doc);
|
||||||
|
const auto fontHeight = QFontMetrics(doc->defaultFont()).height();
|
||||||
|
auto cursor = QTextCursor(doc);
|
||||||
|
auto block = doc->begin();
|
||||||
|
while (block.isValid()) {
|
||||||
|
auto text = block.text();
|
||||||
|
auto start = text.constData();
|
||||||
|
auto end = start + text.size();
|
||||||
|
auto ch = start;
|
||||||
|
while (ch < end) {
|
||||||
|
auto emojiLength = 0;
|
||||||
|
const auto emoji = Ui::Emoji::Find(ch, end, &emojiLength);
|
||||||
|
if (!emoji || emojiLength <= 0) {
|
||||||
|
++ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto pos = block.position() + int(ch - start);
|
||||||
|
cursor.setPosition(pos);
|
||||||
|
cursor.setPosition(
|
||||||
|
pos + emojiLength,
|
||||||
|
QTextCursor::KeepAnchor);
|
||||||
|
|
||||||
|
auto format = QTextImageFormat();
|
||||||
|
format.setName(emoji->toUrl());
|
||||||
|
format.setWidth(fontHeight);
|
||||||
|
format.setHeight(fontHeight);
|
||||||
|
format.setVerticalAlignment(
|
||||||
|
QTextCharFormat::AlignBaseline);
|
||||||
|
cursor.insertImage(format);
|
||||||
|
|
||||||
|
block = doc->findBlock(pos);
|
||||||
|
text = block.text();
|
||||||
|
start = text.constData();
|
||||||
|
end = start + text.size();
|
||||||
|
ch = start + (pos - block.position()) + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
block = block.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RecoverTextFromDocument(QTextDocument *doc) {
|
||||||
|
auto result = QString();
|
||||||
|
auto block = doc->begin();
|
||||||
|
while (block.isValid()) {
|
||||||
|
if (block != doc->begin()) {
|
||||||
|
result += '\n';
|
||||||
|
}
|
||||||
|
auto it = block.begin();
|
||||||
|
while (!it.atEnd()) {
|
||||||
|
const auto fragment = it.fragment();
|
||||||
|
if (!fragment.isValid()) {
|
||||||
|
++it;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto text = fragment.text();
|
||||||
|
const auto format = fragment.charFormat();
|
||||||
|
for (const auto &ch : text) {
|
||||||
|
if (ch == QChar::ObjectReplacementCharacter) {
|
||||||
|
if (format.isImageFormat()) {
|
||||||
|
const auto name = format.toImageFormat().name();
|
||||||
|
if (const auto emoji = Ui::Emoji::FromUrl(name)) {
|
||||||
|
result += emoji->text();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result += ch;
|
||||||
|
}
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
block = block.next();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Editor
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
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 <QTextDocument>
|
||||||
|
|
||||||
|
namespace Editor {
|
||||||
|
|
||||||
|
class EmojiDocument final : public QTextDocument {
|
||||||
|
public:
|
||||||
|
explicit EmojiDocument(QObject *parent = nullptr);
|
||||||
|
QVariant loadResource(int type, const QUrl &name) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<QUrl, QVariant> _cache;
|
||||||
|
};
|
||||||
|
|
||||||
|
void ReplaceEmoji(QTextDocument *doc);
|
||||||
|
[[nodiscard]] QString RecoverTextFromDocument(QTextDocument *doc);
|
||||||
|
|
||||||
|
} // namespace Editor
|
||||||
@@ -329,6 +329,7 @@ void ItemBase::updateVerticalSize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ItemBase::setAspectRatio(float64 aspectRatio) {
|
void ItemBase::setAspectRatio(float64 aspectRatio) {
|
||||||
|
prepareGeometryChange();
|
||||||
_aspectRatio = aspectRatio;
|
_aspectRatio = aspectRatio;
|
||||||
updateVerticalSize();
|
updateVerticalSize();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ void ItemCanvas::drawArrowHead() {
|
|||||||
}
|
}
|
||||||
direction /= length;
|
direction /= length;
|
||||||
const auto angle = qDegreesToRadians(
|
const auto angle = qDegreesToRadians(
|
||||||
double(st::photoEditorArrowHeadAngleDegrees));
|
float64(st::photoEditorArrowHeadAngleDegrees));
|
||||||
const auto sinA = std::sin(angle);
|
const auto sinA = std::sin(angle);
|
||||||
const auto cosA = std::cos(angle);
|
const auto cosA = std::cos(angle);
|
||||||
const auto rotate = [&](const QPointF &v, float64 s, float64 c) {
|
const auto rotate = [&](const QPointF &v, float64 s, float64 c) {
|
||||||
|
|||||||
@@ -0,0 +1,660 @@
|
|||||||
|
/*
|
||||||
|
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 "editor/scene/scene_item_text.h"
|
||||||
|
|
||||||
|
#include "editor/scene/scene.h"
|
||||||
|
#include "editor/scene/scene_emoji_document.h"
|
||||||
|
#include "lang/lang_keys.h"
|
||||||
|
#include "ui/emoji_config.h"
|
||||||
|
#include "ui/painter.h"
|
||||||
|
#include "ui/widgets/popup_menu.h"
|
||||||
|
#include "styles/style_editor.h"
|
||||||
|
#include "styles/style_menu_icons.h"
|
||||||
|
|
||||||
|
#include <QGraphicsSceneMouseEvent>
|
||||||
|
#include <QGraphicsSceneContextMenuEvent>
|
||||||
|
#include <QTextBlock>
|
||||||
|
#include <QTextCursor>
|
||||||
|
#include <QTextDocument>
|
||||||
|
#include <QTextLayout>
|
||||||
|
#include <QTextOption>
|
||||||
|
|
||||||
|
namespace Editor {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto kPaddingFactor = 0.4;
|
||||||
|
constexpr auto kMaxWidthFactor = 0.8;
|
||||||
|
constexpr auto kMinContentWidth = 20;
|
||||||
|
constexpr auto kBrightnessFramedThreshold = 0.721;
|
||||||
|
constexpr auto kBrightnessSemiTransparentThreshold = 0.25;
|
||||||
|
constexpr auto kSemiTransparentAlpha = 0x99;
|
||||||
|
constexpr auto kCornerRadiusFactor = 1. / 3.;
|
||||||
|
constexpr auto kLinePadHFactor = 1. / 3.;
|
||||||
|
constexpr auto kLinePadVFactor = 1. / 8.;
|
||||||
|
constexpr auto kMergeRadiusFactor = 1.5;
|
||||||
|
constexpr auto kLineShiftFactor = 1. / 7.;
|
||||||
|
|
||||||
|
struct LayoutMetrics {
|
||||||
|
int contentWidth = 0;
|
||||||
|
int contentHeight = 0;
|
||||||
|
int padding = 0;
|
||||||
|
int textMaxWidth = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
QFont TextFont(float64 fontSize) {
|
||||||
|
auto font = QFont();
|
||||||
|
font.setPixelSize(std::max(int(fontSize), 1));
|
||||||
|
font.setWeight(QFont::DemiBold);
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
|
||||||
|
float64 ComputeBrightness(const QColor &color) {
|
||||||
|
return (color.red() * 0.2126
|
||||||
|
+ color.green() * 0.7152
|
||||||
|
+ color.blue() * 0.0722) / 255.;
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutMetrics ComputeMetrics(
|
||||||
|
const QString &text,
|
||||||
|
float64 fontSize,
|
||||||
|
const QSize &imageSize,
|
||||||
|
TextStyle style) {
|
||||||
|
const auto hasBackground = (style == TextStyle::Framed)
|
||||||
|
|| (style == TextStyle::SemiTransparent);
|
||||||
|
const auto padding = hasBackground ? int(fontSize * kPaddingFactor) : 0;
|
||||||
|
const auto shortSide = std::min(imageSize.width(), imageSize.height());
|
||||||
|
const auto textMaxWidth = std::max(
|
||||||
|
int(shortSide * kMaxWidthFactor) - 2 * padding,
|
||||||
|
kMinContentWidth);
|
||||||
|
|
||||||
|
const auto font = TextFont(fontSize);
|
||||||
|
|
||||||
|
auto processedText = text;
|
||||||
|
processedText.replace('\n', QChar::LineSeparator);
|
||||||
|
|
||||||
|
auto option = QTextOption();
|
||||||
|
option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
|
||||||
|
|
||||||
|
auto layout = QTextLayout(processedText, font);
|
||||||
|
layout.setTextOption(option);
|
||||||
|
layout.beginLayout();
|
||||||
|
|
||||||
|
auto totalHeight = 0.;
|
||||||
|
auto maxWidth = 0.;
|
||||||
|
while (true) {
|
||||||
|
auto line = layout.createLine();
|
||||||
|
if (!line.isValid()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
line.setLineWidth(textMaxWidth);
|
||||||
|
line.setPosition(QPointF(0, totalHeight));
|
||||||
|
totalHeight += line.height();
|
||||||
|
maxWidth = std::max(maxWidth, float64(line.naturalTextWidth()));
|
||||||
|
}
|
||||||
|
layout.endLayout();
|
||||||
|
|
||||||
|
return {
|
||||||
|
.contentWidth = std::max(int(std::ceil(maxWidth)), kMinContentWidth),
|
||||||
|
.contentHeight = int(std::ceil(totalHeight)),
|
||||||
|
.padding = padding,
|
||||||
|
.textMaxWidth = textMaxWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LineRect {
|
||||||
|
float64 left = 0;
|
||||||
|
float64 top = 0;
|
||||||
|
float64 right = 0;
|
||||||
|
float64 bottom = 0;
|
||||||
|
[[nodiscard]] float64 width() const { return right - left; }
|
||||||
|
};
|
||||||
|
|
||||||
|
QPainterPath BuildConnectedBackground(
|
||||||
|
const QTextLayout &layout,
|
||||||
|
int contentWidth,
|
||||||
|
int padding,
|
||||||
|
float64 fontSize) {
|
||||||
|
const auto linePadH = fontSize * kLinePadHFactor;
|
||||||
|
const auto linePadV = fontSize * kLinePadVFactor;
|
||||||
|
const auto cornerRadius = fontSize * kCornerRadiusFactor;
|
||||||
|
const auto mergeRadius = cornerRadius * kMergeRadiusFactor;
|
||||||
|
const auto centerX = padding + contentWidth / 2.;
|
||||||
|
|
||||||
|
auto rects = std::vector<LineRect>();
|
||||||
|
for (auto i = 0; i < layout.lineCount(); ++i) {
|
||||||
|
const auto line = layout.lineAt(i);
|
||||||
|
const auto hw = float64(line.naturalTextWidth()) / 2. + linePadH;
|
||||||
|
rects.push_back({
|
||||||
|
.left = centerX - hw,
|
||||||
|
.top = padding + float64(line.y()) - linePadV,
|
||||||
|
.right = centerX + hw,
|
||||||
|
.bottom = padding + float64(line.y() + line.height()) + linePadV,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rects.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (rects.size() == 1) {
|
||||||
|
auto path = QPainterPath();
|
||||||
|
const auto &r = rects[0];
|
||||||
|
path.addRoundedRect(
|
||||||
|
QRectF(r.left, r.top, r.width(), r.bottom - r.top),
|
||||||
|
cornerRadius,
|
||||||
|
cornerRadius);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto i = 1; i < int(rects.size()); ++i) {
|
||||||
|
rects[i - 1].bottom = rects[i].top;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto i = 1; i < int(rects.size()); ++i) {
|
||||||
|
auto traceback = false;
|
||||||
|
if (std::abs(rects[i - 1].left - rects[i].left) < mergeRadius) {
|
||||||
|
const auto v = std::min(rects[i - 1].left, rects[i].left);
|
||||||
|
rects[i - 1].left = rects[i].left = v;
|
||||||
|
traceback = true;
|
||||||
|
}
|
||||||
|
if (std::abs(rects[i - 1].right - rects[i].right) < mergeRadius) {
|
||||||
|
const auto v = std::max(rects[i - 1].right, rects[i].right);
|
||||||
|
rects[i - 1].right = rects[i].right = v;
|
||||||
|
traceback = true;
|
||||||
|
}
|
||||||
|
if (traceback) {
|
||||||
|
for (auto j = i; j >= 1; --j) {
|
||||||
|
if (std::abs(rects[j - 1].left - rects[j].left)
|
||||||
|
< mergeRadius) {
|
||||||
|
const auto v = std::min(
|
||||||
|
rects[j - 1].left,
|
||||||
|
rects[j].left);
|
||||||
|
rects[j - 1].left = rects[j].left = v;
|
||||||
|
}
|
||||||
|
if (std::abs(rects[j - 1].right - rects[j].right)
|
||||||
|
< mergeRadius) {
|
||||||
|
const auto v = std::max(
|
||||||
|
rects[j - 1].right,
|
||||||
|
rects[j].right);
|
||||||
|
rects[j - 1].right = rects[j].right = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct V { float64 x, y; };
|
||||||
|
auto verts = std::vector<V>();
|
||||||
|
|
||||||
|
verts.push_back({ rects[0].left, rects[0].top });
|
||||||
|
verts.push_back({ rects[0].right, rects[0].top });
|
||||||
|
|
||||||
|
for (auto i = 1; i < int(rects.size()); ++i) {
|
||||||
|
if (std::abs(rects[i].right - rects[i - 1].right) > 0.5) {
|
||||||
|
verts.push_back({ rects[i - 1].right, rects[i].top });
|
||||||
|
verts.push_back({ rects[i].right, rects[i].top });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto last = int(rects.size()) - 1;
|
||||||
|
verts.push_back({ rects[last].right, rects[last].bottom });
|
||||||
|
verts.push_back({ rects[last].left, rects[last].bottom });
|
||||||
|
|
||||||
|
for (auto i = last - 1; i >= 0; --i) {
|
||||||
|
if (std::abs(rects[i].left - rects[i + 1].left) > 0.5) {
|
||||||
|
verts.push_back({ rects[i + 1].left, rects[i + 1].top });
|
||||||
|
verts.push_back({ rects[i].left, rects[i + 1].top });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto path = QPainterPath();
|
||||||
|
const auto n = int(verts.size());
|
||||||
|
for (auto i = 0; i < n; ++i) {
|
||||||
|
const auto &prev = verts[(i + n - 1) % n];
|
||||||
|
const auto &curr = verts[i];
|
||||||
|
const auto &next = verts[(i + 1) % n];
|
||||||
|
|
||||||
|
const auto dx1 = curr.x - prev.x;
|
||||||
|
const auto dy1 = curr.y - prev.y;
|
||||||
|
const auto len1 = std::sqrt(dx1 * dx1 + dy1 * dy1);
|
||||||
|
|
||||||
|
const auto dx2 = next.x - curr.x;
|
||||||
|
const auto dy2 = next.y - curr.y;
|
||||||
|
const auto len2 = std::sqrt(dx2 * dx2 + dy2 * dy2);
|
||||||
|
|
||||||
|
if (len1 < 0.1 || len2 < 0.1) {
|
||||||
|
if (i == 0) {
|
||||||
|
path.moveTo(curr.x, curr.y);
|
||||||
|
} else {
|
||||||
|
path.lineTo(curr.x, curr.y);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto r = std::min({
|
||||||
|
cornerRadius,
|
||||||
|
len1 / 2.,
|
||||||
|
len2 / 2.,
|
||||||
|
});
|
||||||
|
const auto bx = curr.x - dx1 / len1 * r;
|
||||||
|
const auto by = curr.y - dy1 / len1 * r;
|
||||||
|
const auto ax = curr.x + dx2 / len2 * r;
|
||||||
|
const auto ay = curr.y + dy2 / len2 * r;
|
||||||
|
|
||||||
|
if (i == 0) {
|
||||||
|
path.moveTo(bx, by);
|
||||||
|
} else {
|
||||||
|
path.lineTo(bx, by);
|
||||||
|
}
|
||||||
|
path.quadTo(curr.x, curr.y, ax, ay);
|
||||||
|
}
|
||||||
|
path.closeSubpath();
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
QColor EffectiveTextColor(const QColor &color, TextStyle style) {
|
||||||
|
if (style != TextStyle::Framed) {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
return (ComputeBrightness(color) >= kBrightnessFramedThreshold)
|
||||||
|
? QColor(0, 0, 0)
|
||||||
|
: QColor(255, 255, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemText::ItemText(
|
||||||
|
const QString &text,
|
||||||
|
const QColor &color,
|
||||||
|
float64 fontSize,
|
||||||
|
TextStyle style,
|
||||||
|
const QSize &imageSize,
|
||||||
|
ItemBase::Data data)
|
||||||
|
: ItemBase(std::move(data))
|
||||||
|
, _text(text)
|
||||||
|
, _color(color)
|
||||||
|
, _fontSize(fontSize)
|
||||||
|
, _textStyle(style)
|
||||||
|
, _imageSize(imageSize) {
|
||||||
|
renderContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemText::renderContent() {
|
||||||
|
if (_text.isEmpty()) {
|
||||||
|
_pixmap = QPixmap();
|
||||||
|
setAspectRatio(1.);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto m = ComputeMetrics(_text, _fontSize, _imageSize, _textStyle);
|
||||||
|
const auto pixWidth = m.contentWidth + 2 * m.padding;
|
||||||
|
const auto pixHeight = m.contentHeight + 2 * m.padding;
|
||||||
|
|
||||||
|
const auto font = TextFont(_fontSize);
|
||||||
|
|
||||||
|
auto processedText = _text;
|
||||||
|
processedText.replace('\n', QChar::LineSeparator);
|
||||||
|
|
||||||
|
auto option = QTextOption();
|
||||||
|
option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
|
||||||
|
|
||||||
|
auto layout = QTextLayout(processedText, font);
|
||||||
|
layout.setTextOption(option);
|
||||||
|
|
||||||
|
struct EmojiPos {
|
||||||
|
int start = 0;
|
||||||
|
int length = 0;
|
||||||
|
EmojiPtr emoji = nullptr;
|
||||||
|
};
|
||||||
|
auto emojiFormats = QVector<QTextLayout::FormatRange>();
|
||||||
|
auto emojiPositions = std::vector<EmojiPos>();
|
||||||
|
{
|
||||||
|
auto pos = 0;
|
||||||
|
const auto begin = processedText.constData();
|
||||||
|
const auto end = begin + processedText.size();
|
||||||
|
while (pos < processedText.size()) {
|
||||||
|
auto emojiLen = 0;
|
||||||
|
const auto emoji = Ui::Emoji::Find(
|
||||||
|
begin + pos,
|
||||||
|
end,
|
||||||
|
&emojiLen);
|
||||||
|
if (emoji && emojiLen > 0) {
|
||||||
|
auto fmt = QTextCharFormat();
|
||||||
|
fmt.setForeground(QColor(0, 0, 0, 0));
|
||||||
|
emojiFormats.append({ pos, emojiLen, fmt });
|
||||||
|
emojiPositions.push_back({ pos, emojiLen, emoji });
|
||||||
|
pos += emojiLen;
|
||||||
|
} else {
|
||||||
|
++pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layout.setFormats(emojiFormats);
|
||||||
|
|
||||||
|
layout.beginLayout();
|
||||||
|
auto y = 0.;
|
||||||
|
while (true) {
|
||||||
|
auto line = layout.createLine();
|
||||||
|
if (!line.isValid()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
line.setLineWidth(m.textMaxWidth);
|
||||||
|
line.setPosition(QPointF(0, y));
|
||||||
|
y += line.height();
|
||||||
|
}
|
||||||
|
layout.endLayout();
|
||||||
|
|
||||||
|
auto textColor = _color;
|
||||||
|
auto bgColor = QColor(Qt::transparent);
|
||||||
|
const auto brightness = ComputeBrightness(_color);
|
||||||
|
const auto hasBackground =
|
||||||
|
(_textStyle == TextStyle::Framed)
|
||||||
|
|| (_textStyle == TextStyle::SemiTransparent);
|
||||||
|
|
||||||
|
switch (_textStyle) {
|
||||||
|
case TextStyle::Framed:
|
||||||
|
bgColor = _color;
|
||||||
|
textColor = (brightness >= kBrightnessFramedThreshold)
|
||||||
|
? QColor(0, 0, 0)
|
||||||
|
: QColor(255, 255, 255);
|
||||||
|
break;
|
||||||
|
case TextStyle::SemiTransparent:
|
||||||
|
bgColor = (brightness >= kBrightnessSemiTransparentThreshold)
|
||||||
|
? QColor(0, 0, 0, kSemiTransparentAlpha)
|
||||||
|
: QColor(255, 255, 255, kSemiTransparentAlpha);
|
||||||
|
break;
|
||||||
|
case TextStyle::Plain:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto dpr = style::DevicePixelRatio();
|
||||||
|
auto pixmap = QPixmap(QSize(pixWidth, pixHeight) * dpr);
|
||||||
|
pixmap.setDevicePixelRatio(dpr);
|
||||||
|
pixmap.fill(Qt::transparent);
|
||||||
|
|
||||||
|
{
|
||||||
|
auto p = QPainter(&pixmap);
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
|
||||||
|
if (hasBackground) {
|
||||||
|
const auto bgPath = BuildConnectedBackground(
|
||||||
|
layout,
|
||||||
|
m.contentWidth,
|
||||||
|
m.padding,
|
||||||
|
_fontSize);
|
||||||
|
if (_textStyle == TextStyle::SemiTransparent) {
|
||||||
|
auto opaque = bgColor;
|
||||||
|
opaque.setAlpha(255);
|
||||||
|
auto mask = QPixmap(pixmap.size());
|
||||||
|
mask.setDevicePixelRatio(dpr);
|
||||||
|
mask.fill(Qt::transparent);
|
||||||
|
{
|
||||||
|
auto mp = QPainter(&mask);
|
||||||
|
auto mhq = PainterHighQualityEnabler(mp);
|
||||||
|
mp.setPen(Qt::NoPen);
|
||||||
|
mp.setBrush(opaque);
|
||||||
|
mp.drawPath(bgPath);
|
||||||
|
}
|
||||||
|
p.setOpacity(bgColor.alphaF());
|
||||||
|
p.drawPixmap(0, 0, mask);
|
||||||
|
p.setOpacity(1.0);
|
||||||
|
} else {
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(bgColor);
|
||||||
|
p.drawPath(bgPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto lineShift = _fontSize * kLineShiftFactor;
|
||||||
|
const auto lineCount = layout.lineCount();
|
||||||
|
p.setPen(textColor);
|
||||||
|
for (auto i = 0; i < lineCount; ++i) {
|
||||||
|
const auto line = layout.lineAt(i);
|
||||||
|
const auto xOffset =
|
||||||
|
(m.contentWidth - line.naturalTextWidth()) / 2.;
|
||||||
|
const auto yShift = (i < lineCount - 1) ? -lineShift : 0.;
|
||||||
|
line.draw(
|
||||||
|
&p,
|
||||||
|
QPointF(m.padding + xOffset, m.padding + yShift));
|
||||||
|
}
|
||||||
|
|
||||||
|
p.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||||
|
const auto factor = style::DevicePixelRatio();
|
||||||
|
const auto source = Ui::Emoji::GetSizeLarge();
|
||||||
|
const auto sourceLogical = source / float64(factor);
|
||||||
|
const auto emojiSize = float64(QFontMetrics(font).height());
|
||||||
|
const auto emojiScale = emojiSize / sourceLogical;
|
||||||
|
for (const auto &ep : emojiPositions) {
|
||||||
|
auto lineIndex = -1;
|
||||||
|
for (auto i = 0; i < lineCount; ++i) {
|
||||||
|
const auto line = layout.lineAt(i);
|
||||||
|
const auto lineStart = line.textStart();
|
||||||
|
const auto lineEnd = lineStart + line.textLength();
|
||||||
|
if (ep.start >= lineStart && ep.start < lineEnd) {
|
||||||
|
lineIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lineIndex < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto line = layout.lineAt(lineIndex);
|
||||||
|
const auto lineStart = line.textStart();
|
||||||
|
const auto lineEnd = lineStart + line.textLength();
|
||||||
|
const auto drawEnd = std::min(ep.start + ep.length, lineEnd);
|
||||||
|
const auto xOffset =
|
||||||
|
(m.contentWidth - line.naturalTextWidth()) / 2.;
|
||||||
|
const auto yShift = (lineIndex < lineCount - 1)
|
||||||
|
? -lineShift
|
||||||
|
: 0.;
|
||||||
|
const auto x = line.cursorToX(ep.start);
|
||||||
|
const auto nextX = line.cursorToX(drawEnd);
|
||||||
|
const auto glyphWidth = float64(nextX - x);
|
||||||
|
const auto drawX = m.padding
|
||||||
|
+ xOffset
|
||||||
|
+ x
|
||||||
|
+ (glyphWidth - emojiSize) / 2.;
|
||||||
|
const auto drawY = m.padding
|
||||||
|
+ yShift
|
||||||
|
+ line.y()
|
||||||
|
+ (line.height() - emojiSize) / 2.;
|
||||||
|
p.save();
|
||||||
|
p.translate(drawX, drawY);
|
||||||
|
p.scale(emojiScale, emojiScale);
|
||||||
|
Ui::Emoji::Draw(p, ep.emoji, source, 0, 0);
|
||||||
|
p.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_pixmap = std::move(pixmap);
|
||||||
|
const auto handleMargin = std::max(
|
||||||
|
innerRect().width() - contentRect().width(),
|
||||||
|
0.);
|
||||||
|
setAspectRatio(
|
||||||
|
(pixHeight + handleMargin) / float64(pixWidth + handleMargin));
|
||||||
|
}
|
||||||
|
|
||||||
|
QSize ItemText::computeContentSize(
|
||||||
|
const QString &text,
|
||||||
|
float64 fontSize,
|
||||||
|
const QSize &imageSize,
|
||||||
|
TextStyle style) {
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
auto processedText = text;
|
||||||
|
processedText.replace('\n', QChar::LineSeparator);
|
||||||
|
const auto m = ComputeMetrics(processedText, fontSize, imageSize, style);
|
||||||
|
return QSize(
|
||||||
|
m.contentWidth + 2 * m.padding,
|
||||||
|
m.contentHeight + 2 * m.padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemText::paint(
|
||||||
|
QPainter *p,
|
||||||
|
const QStyleOptionGraphicsItem *option,
|
||||||
|
QWidget *w) {
|
||||||
|
if (!_pixmap.isNull()) {
|
||||||
|
const auto rect = contentRect();
|
||||||
|
const auto pixmapSize = QSizeF(
|
||||||
|
_pixmap.size() / style::DevicePixelRatio()
|
||||||
|
).scaled(rect.size(), Qt::KeepAspectRatio);
|
||||||
|
const auto resultRect = QRectF(
|
||||||
|
rect.topLeft(),
|
||||||
|
pixmapSize
|
||||||
|
).translated(
|
||||||
|
(rect.width() - pixmapSize.width()) / 2.,
|
||||||
|
(rect.height() - pixmapSize.height()) / 2.);
|
||||||
|
if (flipped()) {
|
||||||
|
p->save();
|
||||||
|
const auto center = resultRect.center();
|
||||||
|
p->translate(center);
|
||||||
|
p->scale(-1, 1);
|
||||||
|
p->translate(-center);
|
||||||
|
p->drawPixmap(resultRect.toRect(), _pixmap);
|
||||||
|
p->restore();
|
||||||
|
} else {
|
||||||
|
p->drawPixmap(resultRect.toRect(), _pixmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ItemBase::paint(p, option, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ItemText::type() const {
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString &ItemText::text() const {
|
||||||
|
return _text;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemText::setText(const QString &text) {
|
||||||
|
_text = text;
|
||||||
|
renderContent();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QColor &ItemText::color() const {
|
||||||
|
return _color;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemText::setColor(const QColor &color) {
|
||||||
|
_color = color;
|
||||||
|
renderContent();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
float64 ItemText::fontSize() const {
|
||||||
|
return _fontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
float64 ItemText::editScale() const {
|
||||||
|
const auto natural = computeContentSize(
|
||||||
|
_text,
|
||||||
|
_fontSize,
|
||||||
|
_imageSize,
|
||||||
|
_textStyle);
|
||||||
|
if (natural.width() <= 0) {
|
||||||
|
return 1.;
|
||||||
|
}
|
||||||
|
return size() / natural.width();
|
||||||
|
}
|
||||||
|
|
||||||
|
TextStyle ItemText::textStyle() const {
|
||||||
|
return _textStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemText::setTextStyle(TextStyle style) {
|
||||||
|
_textStyle = style;
|
||||||
|
renderContent();
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemText::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) {
|
||||||
|
if (const auto s = static_cast<Scene*>(scene())) {
|
||||||
|
s->startTextEditing(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemText::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {
|
||||||
|
if (scene()) {
|
||||||
|
scene()->clearSelection();
|
||||||
|
setSelected(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_contextMenu = base::make_unique_q<Ui::PopupMenu>(
|
||||||
|
nullptr,
|
||||||
|
st::popupMenuWithIcons);
|
||||||
|
const auto add = [&](const QString &text, TextStyle style) {
|
||||||
|
const auto checked = (_textStyle == style);
|
||||||
|
auto action = _contextMenu->addAction(text, [=] {
|
||||||
|
setTextStyle(style);
|
||||||
|
});
|
||||||
|
if (checked) {
|
||||||
|
action->setChecked(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
add(tr::lng_photo_editor_text_style_plain(tr::now), TextStyle::Plain);
|
||||||
|
add(
|
||||||
|
tr::lng_photo_editor_text_style_framed(tr::now),
|
||||||
|
TextStyle::Framed);
|
||||||
|
add(
|
||||||
|
tr::lng_photo_editor_text_style_semi_transparent(tr::now),
|
||||||
|
TextStyle::SemiTransparent);
|
||||||
|
|
||||||
|
_contextMenu->addSeparator();
|
||||||
|
|
||||||
|
_contextMenu->addAction(
|
||||||
|
tr::lng_photo_editor_menu_delete(tr::now),
|
||||||
|
[=] { actionDelete(); },
|
||||||
|
&st::menuIconDelete);
|
||||||
|
_contextMenu->addAction(
|
||||||
|
tr::lng_photo_editor_menu_duplicate(tr::now),
|
||||||
|
[=] { actionDuplicate(); },
|
||||||
|
&st::menuIconCopy);
|
||||||
|
|
||||||
|
_contextMenu->popup(event->screenPos());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemText::performFlip() {
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<ItemBase> ItemText::duplicate(ItemBase::Data data) const {
|
||||||
|
return std::make_shared<ItemText>(
|
||||||
|
_text,
|
||||||
|
_color,
|
||||||
|
_fontSize,
|
||||||
|
_textStyle,
|
||||||
|
_imageSize,
|
||||||
|
std::move(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemText::save(SaveState state) {
|
||||||
|
ItemBase::save(state);
|
||||||
|
auto &saved = (state == SaveState::Keep) ? _keepedState : _savedState;
|
||||||
|
saved = {
|
||||||
|
.text = _text,
|
||||||
|
.color = _color,
|
||||||
|
.fontSize = _fontSize,
|
||||||
|
.textStyle = _textStyle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ItemText::restore(SaveState state) {
|
||||||
|
if (!hasState(state)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto &saved = (state == SaveState::Keep) ? _keepedState : _savedState;
|
||||||
|
_text = saved.text;
|
||||||
|
_color = saved.color;
|
||||||
|
_fontSize = saved.fontSize;
|
||||||
|
_textStyle = saved.textStyle;
|
||||||
|
renderContent();
|
||||||
|
ItemBase::restore(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Editor
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
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 "base/unique_qptr.h"
|
||||||
|
#include "editor/scene/scene_item_base.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class PopupMenu;
|
||||||
|
} // namespace Ui
|
||||||
|
|
||||||
|
namespace Editor {
|
||||||
|
|
||||||
|
enum class TextStyle : uchar {
|
||||||
|
Framed,
|
||||||
|
SemiTransparent,
|
||||||
|
Plain,
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] QColor EffectiveTextColor(const QColor &color, TextStyle style);
|
||||||
|
|
||||||
|
class ItemText : public ItemBase {
|
||||||
|
public:
|
||||||
|
enum { Type = ItemBase::Type + 2 };
|
||||||
|
|
||||||
|
ItemText(
|
||||||
|
const QString &text,
|
||||||
|
const QColor &color,
|
||||||
|
float64 fontSize,
|
||||||
|
TextStyle style,
|
||||||
|
const QSize &imageSize,
|
||||||
|
ItemBase::Data data);
|
||||||
|
|
||||||
|
void paint(
|
||||||
|
QPainter *p,
|
||||||
|
const QStyleOptionGraphicsItem *option,
|
||||||
|
QWidget *widget) override;
|
||||||
|
int type() const override;
|
||||||
|
|
||||||
|
[[nodiscard]] const QString &text() const;
|
||||||
|
void setText(const QString &text);
|
||||||
|
|
||||||
|
[[nodiscard]] const QColor &color() const;
|
||||||
|
void setColor(const QColor &color);
|
||||||
|
|
||||||
|
[[nodiscard]] float64 fontSize() const;
|
||||||
|
|
||||||
|
[[nodiscard]] TextStyle textStyle() const;
|
||||||
|
void setTextStyle(TextStyle style);
|
||||||
|
|
||||||
|
[[nodiscard]] float64 editScale() const;
|
||||||
|
|
||||||
|
[[nodiscard]] static QSize computeContentSize(
|
||||||
|
const QString &text,
|
||||||
|
float64 fontSize,
|
||||||
|
const QSize &imageSize,
|
||||||
|
TextStyle style);
|
||||||
|
|
||||||
|
void save(SaveState state) override;
|
||||||
|
void restore(SaveState state) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) override;
|
||||||
|
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
|
||||||
|
void performFlip() override;
|
||||||
|
std::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void renderContent();
|
||||||
|
|
||||||
|
QString _text;
|
||||||
|
QColor _color;
|
||||||
|
float64 _fontSize;
|
||||||
|
TextStyle _textStyle = TextStyle::Plain;
|
||||||
|
QSize _imageSize;
|
||||||
|
QPixmap _pixmap;
|
||||||
|
base::unique_qptr<Ui::PopupMenu> _contextMenu;
|
||||||
|
|
||||||
|
struct SavedText {
|
||||||
|
QString text;
|
||||||
|
QColor color;
|
||||||
|
float64 fontSize = 0.;
|
||||||
|
TextStyle textStyle = TextStyle::Plain;
|
||||||
|
};
|
||||||
|
SavedText _savedState, _keepedState;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Editor
|
||||||
@@ -100,6 +100,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "apiwrap.h"
|
#include "apiwrap.h"
|
||||||
#include "api/api_attached_stickers.h"
|
#include "api/api_attached_stickers.h"
|
||||||
#include "api/api_suggest_post.h"
|
#include "api/api_suggest_post.h"
|
||||||
|
#include "api/api_stickers_creator.h"
|
||||||
#include "api/api_toggling_media.h"
|
#include "api/api_toggling_media.h"
|
||||||
#include "api/api_who_reacted.h"
|
#include "api/api_who_reacted.h"
|
||||||
#include "api/api_views.h"
|
#include "api/api_views.h"
|
||||||
@@ -3335,6 +3336,11 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
|||||||
_menu->addAction(document->isStickerSetInstalled() ? tr::lng_context_pack_info(tr::now) : tr::lng_context_pack_add(tr::now), [=] {
|
_menu->addAction(document->isStickerSetInstalled() ? tr::lng_context_pack_info(tr::now) : tr::lng_context_pack_add(tr::now), [=] {
|
||||||
showStickerPackInfo(document);
|
showStickerPackInfo(document);
|
||||||
}, &st::menuIconStickers);
|
}, &st::menuIconStickers);
|
||||||
|
} else {
|
||||||
|
Api::AddAddToStickerSetAction(
|
||||||
|
Ui::Menu::CreateAddActionCallback(_menu),
|
||||||
|
_controller->uiShow(),
|
||||||
|
document);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const auto isFaved = session->data().stickers().isFaved(document);
|
const auto isFaved = session->data().stickers().isFaved(document);
|
||||||
|
|||||||
@@ -1392,9 +1392,14 @@ void HistoryWidget::sendTextAsFile(
|
|||||||
_peer,
|
_peer,
|
||||||
Api::SendType::Normal,
|
Api::SendType::Normal,
|
||||||
sendMenuDetails());
|
sendMenuDetails());
|
||||||
|
box->setReplyTo(replyTo());
|
||||||
box->setConfirmedCallback(crl::guard(this, [=](
|
box->setConfirmedCallback(crl::guard(this, [=](
|
||||||
std::shared_ptr<Ui::PreparedBundle> bundle,
|
std::shared_ptr<Ui::PreparedBundle> bundle,
|
||||||
Api::SendOptions options) {
|
Api::SendOptions options,
|
||||||
|
FullReplyTo currentReplyTo) {
|
||||||
|
if (!currentReplyTo.messageId && replyTo().messageId) {
|
||||||
|
cancelReply();
|
||||||
|
}
|
||||||
sendingFilesConfirmed(std::move(bundle), options);
|
sendingFilesConfirmed(std::move(bundle), options);
|
||||||
}));
|
}));
|
||||||
box->setCancelledCallback(crl::guard(this, [=] {
|
box->setCancelledCallback(crl::guard(this, [=] {
|
||||||
@@ -6820,10 +6825,12 @@ bool HistoryWidget::confirmSendingFiles(
|
|||||||
_peer,
|
_peer,
|
||||||
Api::SendType::Normal,
|
Api::SendType::Normal,
|
||||||
sendMenuDetails());
|
sendMenuDetails());
|
||||||
|
box->setReplyTo(replyTo());
|
||||||
_field->setTextWithTags({});
|
_field->setTextWithTags({});
|
||||||
box->setConfirmedCallback(crl::guard(this, [=](
|
box->setConfirmedCallback(crl::guard(this, [=](
|
||||||
std::shared_ptr<Ui::PreparedBundle> bundle,
|
std::shared_ptr<Ui::PreparedBundle> bundle,
|
||||||
Api::SendOptions options) {
|
Api::SendOptions options,
|
||||||
|
FullReplyTo currentReplyTo) {
|
||||||
if (bundle->way.asVoice && !bundle->groups.empty()
|
if (bundle->way.asVoice && !bundle->groups.empty()
|
||||||
&& !bundle->groups.front().list.files.empty()) {
|
&& !bundle->groups.front().list.files.empty()) {
|
||||||
const auto &front = bundle->groups.front().list.files.front();
|
const auto &front = bundle->groups.front().list.files.front();
|
||||||
@@ -6848,6 +6855,9 @@ bool HistoryWidget::confirmSendingFiles(
|
|||||||
file.close();
|
file.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!currentReplyTo.messageId && replyTo().messageId) {
|
||||||
|
cancelReply();
|
||||||
|
}
|
||||||
sendingFilesConfirmed(std::move(bundle), options);
|
sendingFilesConfirmed(std::move(bundle), options);
|
||||||
}));
|
}));
|
||||||
box->setCancelledCallback(crl::guard(this, [=] {
|
box->setCancelledCallback(crl::guard(this, [=] {
|
||||||
|
|||||||
@@ -3396,6 +3396,16 @@ void ComposeControls::fireSendTextAsFile(
|
|||||||
? Api::SendType::ScheduledToUser
|
? Api::SendType::ScheduledToUser
|
||||||
: Api::SendType::Scheduled)
|
: Api::SendType::Scheduled)
|
||||||
: Api::SendType::Normal;
|
: Api::SendType::Normal;
|
||||||
|
auto confirmed = [=, callback = _sendAsFileConfirmed](
|
||||||
|
std::shared_ptr<Ui::PreparedBundle> bundle,
|
||||||
|
Api::SendOptions options,
|
||||||
|
FullReplyTo replyTo) {
|
||||||
|
if (!replyTo.messageId
|
||||||
|
&& replyingToMessage().messageId) {
|
||||||
|
cancelReplyMessage();
|
||||||
|
}
|
||||||
|
callback(std::move(bundle), options);
|
||||||
|
};
|
||||||
_show->show(Box<SendFilesBox>(SendFilesBoxDescriptor{
|
_show->show(Box<SendFilesBox>(SendFilesBoxDescriptor{
|
||||||
.show = _show,
|
.show = _show,
|
||||||
.list = Ui::PrepareTextAsFile(fileText),
|
.list = Ui::PrepareTextAsFile(fileText),
|
||||||
@@ -3406,8 +3416,9 @@ void ComposeControls::fireSendTextAsFile(
|
|||||||
.sendType = sendType,
|
.sendType = sendType,
|
||||||
.sendMenuDetails = _sendMenuDetails,
|
.sendMenuDetails = _sendMenuDetails,
|
||||||
.stOverride = &_st,
|
.stOverride = &_st,
|
||||||
.confirmed = _sendAsFileConfirmed,
|
.confirmed = std::move(confirmed),
|
||||||
.cancelled = std::move(restoreText),
|
.cancelled = std::move(restoreText),
|
||||||
|
.replyTo = replyingToMessage(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1192,10 +1192,16 @@ bool ChatWidget::confirmSendingFiles(
|
|||||||
_peer,
|
_peer,
|
||||||
Api::SendType::Normal,
|
Api::SendType::Normal,
|
||||||
sendMenuDetails());
|
sendMenuDetails());
|
||||||
|
box->setReplyTo(_composeControls->replyingToMessage());
|
||||||
|
|
||||||
box->setConfirmedCallback(crl::guard(this, [=](
|
box->setConfirmedCallback(crl::guard(this, [=](
|
||||||
std::shared_ptr<Ui::PreparedBundle> bundle,
|
std::shared_ptr<Ui::PreparedBundle> bundle,
|
||||||
Api::SendOptions options) {
|
Api::SendOptions options,
|
||||||
|
FullReplyTo currentReplyTo) {
|
||||||
|
if (!currentReplyTo.messageId
|
||||||
|
&& _composeControls->replyingToMessage().messageId) {
|
||||||
|
_composeControls->cancelReplyMessage();
|
||||||
|
}
|
||||||
sendingFilesConfirmed(std::move(bundle), options);
|
sendingFilesConfirmed(std::move(bundle), options);
|
||||||
}));
|
}));
|
||||||
box->setCancelledCallback(_composeControls->restoreTextCallback(
|
box->setCancelledCallback(_composeControls->restoreTextCallback(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "api/api_ringtones.h"
|
#include "api/api_ringtones.h"
|
||||||
#include "api/api_transcribes.h"
|
#include "api/api_transcribes.h"
|
||||||
#include "api/api_who_reacted.h"
|
#include "api/api_who_reacted.h"
|
||||||
|
#include "api/api_stickers_creator.h"
|
||||||
#include "api/api_toggling_media.h" // Api::ToggleFavedSticker
|
#include "api/api_toggling_media.h" // Api::ToggleFavedSticker
|
||||||
#include "base/qt/qt_key_modifiers.h"
|
#include "base/qt/qt_key_modifiers.h"
|
||||||
#include "base/unixtime.h"
|
#include "base/unixtime.h"
|
||||||
@@ -36,6 +37,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "info/profile/info_profile_widget.h"
|
#include "info/profile/info_profile_widget.h"
|
||||||
#include "ui/widgets/popup_menu.h"
|
#include "ui/widgets/popup_menu.h"
|
||||||
#include "ui/widgets/menu/menu_action.h"
|
#include "ui/widgets/menu/menu_action.h"
|
||||||
|
#include "ui/widgets/menu/menu_add_action_callback_factory.h"
|
||||||
#include "ui/widgets/menu/menu_common.h"
|
#include "ui/widgets/menu/menu_common.h"
|
||||||
#include "ui/widgets/menu/menu_multiline_action.h"
|
#include "ui/widgets/menu/menu_multiline_action.h"
|
||||||
#include "ui/widgets/menu/menu_separator.h"
|
#include "ui/widgets/menu/menu_separator.h"
|
||||||
@@ -295,6 +297,12 @@ void AddDocumentActions(
|
|||||||
[=] { ShowStickerPackInfo(document, list); },
|
[=] { ShowStickerPackInfo(document, list); },
|
||||||
&st::menuIconStickers);
|
&st::menuIconStickers);
|
||||||
}
|
}
|
||||||
|
if (document->sticker() && !document->sticker()->set) {
|
||||||
|
Api::AddAddToStickerSetAction(
|
||||||
|
Ui::Menu::CreateAddActionCallback(menu),
|
||||||
|
controller->uiShow(),
|
||||||
|
document);
|
||||||
|
}
|
||||||
if (document->sticker()) {
|
if (document->sticker()) {
|
||||||
const auto isFaved = document->owner().stickers().isFaved(document);
|
const auto isFaved = document->owner().stickers().isFaved(document);
|
||||||
menu->addAction(
|
menu->addAction(
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
namespace HistoryView {
|
namespace HistoryView {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kSummarizeThreshold = 512;
|
|
||||||
constexpr auto kPlayStatusLimit = 2;
|
constexpr auto kPlayStatusLimit = 2;
|
||||||
constexpr auto kMaxWidth = (1 << 16) - 1;
|
constexpr auto kMaxWidth = (1 << 16) - 1;
|
||||||
constexpr auto kMaxNiceToReadLines = 6;
|
constexpr auto kMaxNiceToReadLines = 6;
|
||||||
@@ -2327,11 +2326,10 @@ void Message::paintText(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto realWidth = textRealWidth();
|
|
||||||
auto highlightRequest = context.computeHighlightCache();
|
auto highlightRequest = context.computeHighlightCache();
|
||||||
text().draw(p, {
|
text().draw(p, {
|
||||||
.position = trect.topLeft(),
|
.position = trect.topLeft(),
|
||||||
.availableWidth = realWidth ? realWidth : trect.width(),
|
.availableWidth = std::max(textRealWidth(), trect.width()),
|
||||||
.palette = &stm->textPalette,
|
.palette = &stm->textPalette,
|
||||||
.pre = stm->preCache.get(),
|
.pre = stm->preCache.get(),
|
||||||
.blockquote = context.quoteCache(
|
.blockquote = context.quoteCache(
|
||||||
@@ -3650,7 +3648,7 @@ bool Message::getStateText(
|
|||||||
if (base::in_range(point.y(), trect.y(), trect.y() + trect.height())) {
|
if (base::in_range(point.y(), trect.y(), trect.y() + trect.height())) {
|
||||||
*outResult = TextState(item, text().getState(
|
*outResult = TextState(item, text().getState(
|
||||||
point - trect.topLeft(),
|
point - trect.topLeft(),
|
||||||
trect.width(),
|
std::max(textRealWidth(), trect.width()),
|
||||||
request.forText()));
|
request.forText()));
|
||||||
if (outResult->link
|
if (outResult->link
|
||||||
&& IsRippleLink(outResult->link)
|
&& IsRippleLink(outResult->link)
|
||||||
@@ -4225,6 +4223,8 @@ int Message::bubbleTextualWidth() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_bubbleTextualWidthCache = right;
|
_bubbleTextualWidthCache = right;
|
||||||
|
[[maybe_unused]] const auto ensureRightCache
|
||||||
|
= textHeightFor(bubbleTextWidth(right));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
namespace HistoryView {
|
namespace HistoryView {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kPremiumToastDuration = 5 * crl::time(1000);
|
|
||||||
|
|
||||||
[[nodiscard]] not_null<Ui::AbstractButton*> MakeUndoButton(
|
[[nodiscard]] not_null<Ui::AbstractButton*> MakeUndoButton(
|
||||||
not_null<QWidget*> parent,
|
not_null<QWidget*> parent,
|
||||||
int width,
|
int width,
|
||||||
|
|||||||
@@ -575,7 +575,8 @@ bool ScheduledWidget::confirmSendingFiles(
|
|||||||
|
|
||||||
box->setConfirmedCallback(crl::guard(this, [=](
|
box->setConfirmedCallback(crl::guard(this, [=](
|
||||||
std::shared_ptr<Ui::PreparedBundle> bundle,
|
std::shared_ptr<Ui::PreparedBundle> bundle,
|
||||||
Api::SendOptions options) {
|
Api::SendOptions options,
|
||||||
|
FullReplyTo) {
|
||||||
sendingFilesConfirmed(std::move(bundle), options);
|
sendingFilesConfirmed(std::move(bundle), options);
|
||||||
}));
|
}));
|
||||||
box->setCancelledCallback(_composeControls->restoreTextCallback(
|
box->setCancelledCallback(_composeControls->restoreTextCallback(
|
||||||
|
|||||||
@@ -605,7 +605,7 @@ QSize Document::countCurrentSize(int newWidth) {
|
|||||||
+ st::mediaUnreadSkip)
|
+ st::mediaUnreadSkip)
|
||||||
+ (thumbedWidth + statusWidth)
|
+ (thumbedWidth + statusWidth)
|
||||||
+ st.thumbSkip
|
+ st.thumbSkip
|
||||||
+ (_realParent->hasUnreadMediaFlag()
|
+ (_realParent->isUnreadMedia()
|
||||||
? st::mediaUnreadSkip + st::mediaUnreadSize
|
? st::mediaUnreadSkip + st::mediaUnreadSize
|
||||||
: 0)
|
: 0)
|
||||||
+ _parent->bottomInfoFirstLineWidth()
|
+ _parent->bottomInfoFirstLineWidth()
|
||||||
@@ -953,7 +953,7 @@ void Document::draw(
|
|||||||
p.setPen(stm->mediaFg);
|
p.setPen(stm->mediaFg);
|
||||||
p.drawTextLeft(nameleft, statustop, width, statusText);
|
p.drawTextLeft(nameleft, statustop, width, statusText);
|
||||||
|
|
||||||
if (_realParent->hasUnreadMediaFlag()) {
|
if (_realParent->isUnreadMedia()) {
|
||||||
auto w = st::normalFont->width(statusText);
|
auto w = st::normalFont->width(statusText);
|
||||||
if (w + st::mediaUnreadSkip + st::mediaUnreadSize <= statuswidth) {
|
if (w + st::mediaUnreadSkip + st::mediaUnreadSize <= statuswidth) {
|
||||||
p.setPen(Qt::NoPen);
|
p.setPen(Qt::NoPen);
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
#include "styles/style_chat.h"
|
#include "styles/style_chat.h"
|
||||||
|
|
||||||
#include <QSvgRenderer>
|
#include <QSvgRenderer>
|
||||||
|
#include <QtWidgets/QApplication>
|
||||||
|
|
||||||
namespace HistoryView {
|
namespace HistoryView {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -68,6 +69,8 @@ namespace {
|
|||||||
constexpr auto kMaxGifForwardedBarLines = 4;
|
constexpr auto kMaxGifForwardedBarLines = 4;
|
||||||
constexpr auto kUseNonBlurredThreshold = 240;
|
constexpr auto kUseNonBlurredThreshold = 240;
|
||||||
constexpr auto kMaxInlineArea = 1920 * 1080;
|
constexpr auto kMaxInlineArea = 1920 * 1080;
|
||||||
|
constexpr auto kSeekAnimationDuration = crl::time(200);
|
||||||
|
constexpr auto kSeekTrackOpacity = 0.2;
|
||||||
|
|
||||||
[[nodiscard]] int GifMaxStatusWidth(not_null<DocumentData*> document) {
|
[[nodiscard]] int GifMaxStatusWidth(not_null<DocumentData*> document) {
|
||||||
auto result = st::normalFont->width(
|
auto result = st::normalFont->width(
|
||||||
@@ -197,6 +200,12 @@ Gif::Gif(
|
|||||||
|
|
||||||
setStatusSize(Ui::FileStatusSizeReady);
|
setStatusSize(Ui::FileStatusSizeReady);
|
||||||
|
|
||||||
|
if (_data->isVideoMessage() && !_parent->data()->media()->ttlSeconds()) {
|
||||||
|
_seekl = std::make_shared<VoiceSeekClickHandler>(
|
||||||
|
_data,
|
||||||
|
[](FullMsgId) {});
|
||||||
|
}
|
||||||
|
|
||||||
if (_spoiler) {
|
if (_spoiler) {
|
||||||
createSpoilerLink(_spoiler.get());
|
createSpoilerLink(_spoiler.get());
|
||||||
}
|
}
|
||||||
@@ -539,6 +548,9 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
|
|||||||
== PaintContext::SkipDrawingParts::Content;
|
== PaintContext::SkipDrawingParts::Content;
|
||||||
const auto drawStreamed = streamed && (shouldBePlaying || !_videoCover);
|
const auto drawStreamed = streamed && (shouldBePlaying || !_videoCover);
|
||||||
if (drawStreamed && !skipDrawingContent && !fullHiddenBySpoiler) {
|
if (drawStreamed && !skipDrawingContent && !fullHiddenBySpoiler) {
|
||||||
|
if (!_seekLastFrame.isNull()) {
|
||||||
|
_seekLastFrame = QImage();
|
||||||
|
}
|
||||||
auto paused = context.paused || !shouldBePlaying;
|
auto paused = context.paused || !shouldBePlaying;
|
||||||
auto request = ::Media::Streaming::FrameRequest{
|
auto request = ::Media::Streaming::FrameRequest{
|
||||||
.outer = QSize(usew, painth) * style::DevicePixelRatio(),
|
.outer = QSize(usew, painth) * style::DevicePixelRatio(),
|
||||||
@@ -574,39 +586,25 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
|
|||||||
|
|
||||||
const auto frame = streamed->frameWithInfo(request);
|
const auto frame = streamed->frameWithInfo(request);
|
||||||
p.drawImage(rthumb, frame.image);
|
p.drawImage(rthumb, frame.image);
|
||||||
|
if (_seeking) {
|
||||||
|
_seekLastFrame = frame.image;
|
||||||
|
}
|
||||||
if (!paused) {
|
if (!paused) {
|
||||||
streamed->markFrameShown();
|
streamed->markFrameShown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (!_seekLastFrame.isNull()
|
||||||
if (const auto playback = videoPlayback()) {
|
&& !skipDrawingContent
|
||||||
const auto value = playback->value();
|
&& !fullHiddenBySpoiler) {
|
||||||
if (value > 0.) {
|
p.drawImage(rthumb, _seekLastFrame);
|
||||||
auto pen = st->historyVideoMessageProgressFg()->p;
|
|
||||||
const auto was = p.pen();
|
|
||||||
pen.setWidth(st::radialLine);
|
|
||||||
pen.setCapStyle(Qt::RoundCap);
|
|
||||||
p.setPen(pen);
|
|
||||||
p.setOpacity(st::historyVideoMessageProgressOpacity);
|
|
||||||
|
|
||||||
const auto from = arc::kQuarterLength;
|
|
||||||
const auto len = std::round(arc::kFullLength
|
|
||||||
* (inTTLViewer ? (1. - value) : -value));
|
|
||||||
const auto stepInside = st::radialLine / 2;
|
|
||||||
{
|
|
||||||
auto hq = PainterHighQualityEnabler(p);
|
|
||||||
p.drawArc(rthumb - Margins(stepInside), from, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
p.setPen(was);
|
|
||||||
p.setOpacity(1.);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (!skipDrawingContent && !fullHiddenBySpoiler) {
|
} else if (!skipDrawingContent && !fullHiddenBySpoiler) {
|
||||||
ensureDataMediaCreated();
|
ensureDataMediaCreated();
|
||||||
validateThumbCache({ usew, painth }, isRound, rounding);
|
validateThumbCache({ usew, painth }, isRound, rounding);
|
||||||
p.drawImage(rthumb, _thumbCache);
|
p.drawImage(rthumb, _thumbCache);
|
||||||
}
|
}
|
||||||
|
if (isRound) {
|
||||||
|
paintRoundPlaybackProgress(p, context, rthumb, inTTLViewer);
|
||||||
|
}
|
||||||
if (!isRound) {
|
if (!isRound) {
|
||||||
paintTimestampMark(p, rthumb, rounding);
|
paintTimestampMark(p, rthumb, rounding);
|
||||||
}
|
}
|
||||||
@@ -736,7 +734,11 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
|
|||||||
} else if (!skipDrawingSurrounding) {
|
} else if (!skipDrawingSurrounding) {
|
||||||
if (isRound) {
|
if (isRound) {
|
||||||
const auto mediaUnread = item->hasUnreadMediaFlag();
|
const auto mediaUnread = item->hasUnreadMediaFlag();
|
||||||
auto statusW = st::normalFont->width(_statusText) + 2 * st::msgDateImgPadding.x();
|
const auto statusText = _seeking
|
||||||
|
? Ui::FormatDurationText(1 + int64(base::SafeRound(
|
||||||
|
(1. - _seekingCurrent) * _data->duration() / 1000.)))
|
||||||
|
: _statusText;
|
||||||
|
auto statusW = st::normalFont->width(statusText) + 2 * st::msgDateImgPadding.x();
|
||||||
auto statusH = st::normalFont->height + 2 * st::msgDateImgPadding.y();
|
auto statusH = st::normalFont->height + 2 * st::msgDateImgPadding.y();
|
||||||
auto statusX = usex + paintx + st::msgDateImgDelta + st::msgDateImgPadding.x();
|
auto statusX = usex + paintx + st::msgDateImgDelta + st::msgDateImgPadding.x();
|
||||||
auto statusY = painty + painth - st::msgDateImgDelta - statusH + st::msgDateImgPadding.y();
|
auto statusY = painty + painth - st::msgDateImgDelta - statusH + st::msgDateImgPadding.y();
|
||||||
@@ -746,7 +748,7 @@ void Gif::draw(Painter &p, const PaintContext &context) const {
|
|||||||
Ui::FillRoundRect(p, style::rtlrect(statusX - st::msgDateImgPadding.x(), statusY - st::msgDateImgPadding.y(), statusW, statusH, width()), sti->msgServiceBg, sti->msgServiceBgCornersSmall);
|
Ui::FillRoundRect(p, style::rtlrect(statusX - st::msgDateImgPadding.x(), statusY - st::msgDateImgPadding.y(), statusW, statusH, width()), sti->msgServiceBg, sti->msgServiceBgCornersSmall);
|
||||||
p.setFont(st::normalFont);
|
p.setFont(st::normalFont);
|
||||||
p.setPen(st->msgServiceFg());
|
p.setPen(st->msgServiceFg());
|
||||||
p.drawTextLeft(statusX, statusY, width(), _statusText, statusW - 2 * st::msgDateImgPadding.x());
|
p.drawTextLeft(statusX, statusY, width(), statusText, statusW - 2 * st::msgDateImgPadding.x());
|
||||||
if (mediaUnread) {
|
if (mediaUnread) {
|
||||||
p.setPen(Qt::NoPen);
|
p.setPen(Qt::NoPen);
|
||||||
p.setBrush(st->msgServiceFg());
|
p.setBrush(st->msgServiceFg());
|
||||||
@@ -959,6 +961,62 @@ void Gif::paintTimestampMark(
|
|||||||
p.restore();
|
p.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Gif::paintRoundPlaybackProgress(
|
||||||
|
Painter &p,
|
||||||
|
const PaintContext &context,
|
||||||
|
QRect rthumb,
|
||||||
|
bool inTTLViewer) const {
|
||||||
|
const auto st = context.st;
|
||||||
|
const auto playback = videoPlayback();
|
||||||
|
const auto seekAmount = _seekAnimation.value(_seeking ? 1. : 0.);
|
||||||
|
const auto value = _seeking
|
||||||
|
? _seekingCurrent
|
||||||
|
: playback
|
||||||
|
? playback->value()
|
||||||
|
: (seekAmount > 0.)
|
||||||
|
? _seekingCurrent
|
||||||
|
: 0.;
|
||||||
|
if (value <= 0. && seekAmount <= 0.) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto pen = st->historyVideoMessageProgressFg()->p;
|
||||||
|
const auto was = p.pen();
|
||||||
|
pen.setWidth(st::radialLine);
|
||||||
|
pen.setCapStyle(Qt::RoundCap);
|
||||||
|
p.setPen(pen);
|
||||||
|
|
||||||
|
const auto from = arc::kQuarterLength;
|
||||||
|
const auto normalInset = 1.5 * st::radialLine;
|
||||||
|
const auto seekInset = st::historyVideoMessageSeekInset;
|
||||||
|
const auto stepInside = normalInset
|
||||||
|
+ (seekInset - normalInset) * seekAmount;
|
||||||
|
const auto arcRect = QRectF(rthumb) - Margins(stepInside);
|
||||||
|
auto hq = PainterHighQualityEnabler(p);
|
||||||
|
if (seekAmount > 0.) {
|
||||||
|
p.setOpacity(kSeekTrackOpacity * seekAmount);
|
||||||
|
p.drawArc(arcRect, 0, arc::kFullLength);
|
||||||
|
}
|
||||||
|
p.setOpacity(st::historyVideoMessageProgressOpacity);
|
||||||
|
const auto len = std::round(arc::kFullLength
|
||||||
|
* (inTTLViewer ? (1. - value) : -value));
|
||||||
|
p.drawArc(arcRect, from, len);
|
||||||
|
if (seekAmount > 0.) {
|
||||||
|
const auto dotSize = float64(st::historyVideoMessageSeekDotSize);
|
||||||
|
const auto angle = M_PI / 2. - value * 2. * M_PI;
|
||||||
|
const auto radius = arcRect.width() / 2.;
|
||||||
|
const auto center = arcRect.center();
|
||||||
|
const auto cx = center.x() + radius * cos(angle);
|
||||||
|
const auto cy = center.y() - radius * sin(angle);
|
||||||
|
p.setOpacity(seekAmount);
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(st->historyVideoMessageProgressFg());
|
||||||
|
p.drawEllipse(QPointF(cx, cy), dotSize / 2., dotSize / 2.);
|
||||||
|
}
|
||||||
|
p.setBrush(Qt::NoBrush);
|
||||||
|
p.setPen(was);
|
||||||
|
p.setOpacity(1.);
|
||||||
|
}
|
||||||
|
|
||||||
void Gif::drawSpoilerTag(
|
void Gif::drawSpoilerTag(
|
||||||
Painter &p,
|
Painter &p,
|
||||||
QRect rthumb,
|
QRect rthumb,
|
||||||
@@ -1289,13 +1347,17 @@ TextState Gif::textState(QPoint point, StateRequest request) const {
|
|||||||
}
|
}
|
||||||
if (QRect(usex + paintx, painty, usew, painth).contains(point)) {
|
if (QRect(usex + paintx, painty, usew, painth).contains(point)) {
|
||||||
ensureDataMediaCreated();
|
ensureDataMediaCreated();
|
||||||
result.link = (_spoiler && !_spoiler->revealed)
|
if (_spoiler && !_spoiler->revealed) {
|
||||||
? (_sensitiveSpoiler
|
result.link = _sensitiveSpoiler
|
||||||
? spoilerTagLink()
|
? spoilerTagLink()
|
||||||
: (isRound && _parent->data()->media()->ttlSeconds())
|
: (isRound && _parent->data()->media()->ttlSeconds())
|
||||||
? _openl // Overriden.
|
? _openl
|
||||||
: _spoiler->link)
|
: _spoiler->link;
|
||||||
: currentVideoLink();
|
} else if (_seekl && isRoundSeekable()) {
|
||||||
|
result.link = _seekl;
|
||||||
|
} else {
|
||||||
|
result.link = currentVideoLink();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const auto checkBottomInfo = !inWebPage
|
const auto checkBottomInfo = !inWebPage
|
||||||
&& (unwrapped || !bubble || isBubbleBottom());
|
&& (unwrapped || !bubble || isBubbleBottom());
|
||||||
@@ -1362,6 +1424,32 @@ TextState Gif::textState(QPoint point, StateRequest request) const {
|
|||||||
void Gif::clickHandlerPressedChanged(
|
void Gif::clickHandlerPressedChanged(
|
||||||
const ClickHandlerPtr &handler,
|
const ClickHandlerPtr &handler,
|
||||||
bool pressed) {
|
bool pressed) {
|
||||||
|
if (_seekl && handler == _seekl) {
|
||||||
|
if (pressed && !_seeking) {
|
||||||
|
_seekPressPoint = QPoint(-1, -1);
|
||||||
|
if (const auto playback = videoPlayback()) {
|
||||||
|
_seekingCurrent = playback->value();
|
||||||
|
}
|
||||||
|
} else if (!pressed) {
|
||||||
|
if (_seeking) {
|
||||||
|
if (isRoundSeekable()) {
|
||||||
|
::Media::Player::instance()->finishSeeking(
|
||||||
|
AudioMsgId::Type::Voice,
|
||||||
|
_seekingCurrent);
|
||||||
|
}
|
||||||
|
_seeking = false;
|
||||||
|
_seekAnimation.start(
|
||||||
|
[=] { repaint(); },
|
||||||
|
1.,
|
||||||
|
0.,
|
||||||
|
kSeekAnimationDuration);
|
||||||
|
} else if (_seekPressPoint != QPoint()) {
|
||||||
|
_seekPressPoint = QPoint();
|
||||||
|
::Media::Player::instance()->playPauseCancelClicked(
|
||||||
|
AudioMsgId::Type::Voice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File::clickHandlerPressedChanged(handler, pressed);
|
File::clickHandlerPressedChanged(handler, pressed);
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
return;
|
return;
|
||||||
@@ -1374,6 +1462,61 @@ void Gif::clickHandlerPressedChanged(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Gif::updatePressed(QPoint point) {
|
||||||
|
if (!_seeking && _seekPressPoint == QPoint()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto item = _parent->data();
|
||||||
|
auto paintx = 0, painty = 0, paintw = width(), painth = height();
|
||||||
|
const auto unwrapped = isUnwrapped();
|
||||||
|
auto usew = paintw, usex = 0;
|
||||||
|
const auto via = unwrapped ? item->Get<HistoryMessageVia>() : nullptr;
|
||||||
|
const auto reply = unwrapped ? _parent->Get<Reply>() : nullptr;
|
||||||
|
const auto forwarded = unwrapped
|
||||||
|
? item->Get<HistoryMessageForwarded>()
|
||||||
|
: nullptr;
|
||||||
|
if (via || reply || forwarded) {
|
||||||
|
usew = maxWidth() - additionalWidth(reply, via, forwarded);
|
||||||
|
if (unwrapped && _parent->hasRightLayout()) {
|
||||||
|
usex = width() - usew;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
accumulate_min(usew, painth);
|
||||||
|
if (rtl()) usex = width() - usex - usew;
|
||||||
|
const auto rthumb = QRect(
|
||||||
|
style::rtlrect(usex + paintx, painty, usew, painth, width()));
|
||||||
|
|
||||||
|
if (!_seeking) {
|
||||||
|
if (_seekPressPoint == QPoint(-1, -1)) {
|
||||||
|
_seekPressPoint = point;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((point - _seekPressPoint).manhattanLength()
|
||||||
|
<= QApplication::startDragDistance()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_seeking = true;
|
||||||
|
_seekPressPoint = QPoint();
|
||||||
|
::Media::Player::instance()->startSeeking(
|
||||||
|
AudioMsgId::Type::Voice);
|
||||||
|
_seekAnimation.start(
|
||||||
|
[=] { repaint(); },
|
||||||
|
0.,
|
||||||
|
1.,
|
||||||
|
kSeekAnimationDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto center = rthumb.center();
|
||||||
|
const auto dx = float64(point.x() - center.x());
|
||||||
|
const auto dy = float64(point.y() - center.y());
|
||||||
|
const auto angle = atan2(-dy, dx);
|
||||||
|
_seekingCurrent = std::clamp(
|
||||||
|
fmod((M_PI / 2. - angle) / (2. * M_PI) + 1., 1.),
|
||||||
|
0.,
|
||||||
|
1.);
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
bool Gif::fullFeaturedGrouped(RectParts sides) const {
|
bool Gif::fullFeaturedGrouped(RectParts sides) const {
|
||||||
return (sides & RectPart::Left) && (sides & RectPart::Right);
|
return (sides & RectPart::Left) && (sides & RectPart::Right);
|
||||||
}
|
}
|
||||||
@@ -1941,6 +2084,7 @@ void Gif::unloadHeavyPart() {
|
|||||||
_spoiler->animation = nullptr;
|
_spoiler->animation = nullptr;
|
||||||
}
|
}
|
||||||
_thumbCache = QImage();
|
_thumbCache = QImage();
|
||||||
|
_seekLastFrame = QImage();
|
||||||
_videoThumbnailFrame = nullptr;
|
_videoThumbnailFrame = nullptr;
|
||||||
togglePollingStory(false);
|
togglePollingStory(false);
|
||||||
}
|
}
|
||||||
@@ -1969,6 +2113,19 @@ int Gif::additionalWidth(
|
|||||||
return ::Media::Player::instance()->roundVideoStreamed(_parent->data());
|
return ::Media::Player::instance()->roundVideoStreamed(_parent->data());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Gif::isRoundSeekable() const {
|
||||||
|
if (!activeRoundStreamed()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto state = ::Media::Player::instance()->getState(
|
||||||
|
AudioMsgId::Type::Voice);
|
||||||
|
return (state.id == AudioMsgId(
|
||||||
|
_data,
|
||||||
|
_realParent->fullId(),
|
||||||
|
state.id.externalPlayId()))
|
||||||
|
&& !::Media::Player::IsStoppedOrStopping(state.state);
|
||||||
|
}
|
||||||
|
|
||||||
Gif::Streamed *Gif::activeOwnStreamed() const {
|
Gif::Streamed *Gif::activeOwnStreamed() const {
|
||||||
return (_streamed
|
return (_streamed
|
||||||
&& _streamed->instance.player().ready()
|
&& _streamed->instance.player().ready()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ struct HistoryMessageReply;
|
|||||||
struct HistoryMessageForwarded;
|
struct HistoryMessageForwarded;
|
||||||
class Painter;
|
class Painter;
|
||||||
class PhotoData;
|
class PhotoData;
|
||||||
|
class VoiceSeekClickHandler;
|
||||||
|
|
||||||
namespace Data {
|
namespace Data {
|
||||||
class DocumentMedia;
|
class DocumentMedia;
|
||||||
@@ -65,6 +66,7 @@ public:
|
|||||||
void clickHandlerPressedChanged(
|
void clickHandlerPressedChanged(
|
||||||
const ClickHandlerPtr &p,
|
const ClickHandlerPtr &p,
|
||||||
bool pressed) override;
|
bool pressed) override;
|
||||||
|
void updatePressed(QPoint point) override;
|
||||||
|
|
||||||
bool uploading() const override;
|
bool uploading() const override;
|
||||||
|
|
||||||
@@ -151,6 +153,7 @@ private:
|
|||||||
Streamed *activeOwnStreamed() const;
|
Streamed *activeOwnStreamed() const;
|
||||||
::Media::Streaming::Instance *activeCurrentStreamed() const;
|
::Media::Streaming::Instance *activeCurrentStreamed() const;
|
||||||
::Media::View::PlaybackProgress *videoPlayback() const;
|
::Media::View::PlaybackProgress *videoPlayback() const;
|
||||||
|
bool isRoundSeekable() const;
|
||||||
|
|
||||||
void createStreamedPlayer();
|
void createStreamedPlayer();
|
||||||
void checkStreamedIsStarted() const;
|
void checkStreamedIsStarted() const;
|
||||||
@@ -172,6 +175,11 @@ private:
|
|||||||
Painter &p,
|
Painter &p,
|
||||||
QRect rthumb,
|
QRect rthumb,
|
||||||
std::optional<Ui::BubbleRounding> rounding) const;
|
std::optional<Ui::BubbleRounding> rounding) const;
|
||||||
|
void paintRoundPlaybackProgress(
|
||||||
|
Painter &p,
|
||||||
|
const PaintContext &context,
|
||||||
|
QRect rthumb,
|
||||||
|
bool inTTLViewer) const;
|
||||||
|
|
||||||
[[nodiscard]] bool needInfoDisplay() const;
|
[[nodiscard]] bool needInfoDisplay() const;
|
||||||
[[nodiscard]] bool needCornerStatusDisplay() const;
|
[[nodiscard]] bool needCornerStatusDisplay() const;
|
||||||
@@ -231,12 +239,18 @@ private:
|
|||||||
mutable QImage _thumbCache;
|
mutable QImage _thumbCache;
|
||||||
mutable QImage _roundingMask;
|
mutable QImage _roundingMask;
|
||||||
mutable crl::time _videoPosition = 0;
|
mutable crl::time _videoPosition = 0;
|
||||||
|
std::shared_ptr<VoiceSeekClickHandler> _seekl;
|
||||||
|
mutable Ui::Animations::Simple _seekAnimation;
|
||||||
|
float64 _seekingCurrent = 0.;
|
||||||
|
QPoint _seekPressPoint;
|
||||||
|
mutable QImage _seekLastFrame;
|
||||||
mutable TimeId _videoTimestamp = 0;
|
mutable TimeId _videoTimestamp = 0;
|
||||||
mutable std::optional<Ui::BubbleRounding> _thumbCacheRounding;
|
mutable std::optional<Ui::BubbleRounding> _thumbCacheRounding;
|
||||||
mutable bool _thumbCacheBlurred : 1 = false;
|
mutable bool _thumbCacheBlurred : 1 = false;
|
||||||
mutable bool _thumbIsEllipse : 1 = false;
|
mutable bool _thumbIsEllipse : 1 = false;
|
||||||
mutable bool _pollingStory : 1 = false;
|
mutable bool _pollingStory : 1 = false;
|
||||||
mutable bool _purchasedPriceTag : 1 = false;
|
mutable bool _purchasedPriceTag : 1 = false;
|
||||||
|
mutable bool _seeking : 1 = false;
|
||||||
mutable bool _smallGroupPart : 1 = false;
|
mutable bool _smallGroupPart : 1 = false;
|
||||||
const bool _sensitiveSpoiler : 1 = false;
|
const bool _sensitiveSpoiler : 1 = false;
|
||||||
const bool _hasVideoCover : 1 = false;
|
const bool _hasVideoCover : 1 = false;
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ QSize Sticker::Size() {
|
|||||||
const auto side = std::min(st::maxStickerSize, kMaxSizeFixed);
|
const auto side = std::min(st::maxStickerSize, kMaxSizeFixed);
|
||||||
if (OptionStickerSize.value() > 0) [[unlikely]] {
|
if (OptionStickerSize.value() > 0) [[unlikely]] {
|
||||||
const auto scaled = std::clamp(
|
const auto scaled = std::clamp(
|
||||||
OptionStickerSize.value(),
|
style::ConvertScale(OptionStickerSize.value()),
|
||||||
style::ConvertScale(50),
|
style::ConvertScale(50),
|
||||||
side);
|
side);
|
||||||
return { scaled, scaled };
|
return { scaled, scaled };
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|||||||
namespace Info::GlobalMedia {
|
namespace Info::GlobalMedia {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kPerPage = 50;
|
|
||||||
constexpr auto kPreloadedScreensCount = 4;
|
constexpr auto kPreloadedScreensCount = 4;
|
||||||
constexpr auto kPreloadedScreensCountFull
|
constexpr auto kPreloadedScreensCountFull
|
||||||
= kPreloadedScreensCount + 1 + kPreloadedScreensCount;
|
= kPreloadedScreensCount + 1 + kPreloadedScreensCount;
|
||||||
|
|||||||
@@ -2232,11 +2232,14 @@ void ListWidget::mouseActionFinish(
|
|||||||
&& (button != Qt::RightButton)
|
&& (button != Qt::RightButton)
|
||||||
&& (_mouseAction == MouseAction::PrepareDrag
|
&& (_mouseAction == MouseAction::PrepareDrag
|
||||||
|| _mouseAction == MouseAction::PrepareSelect);
|
|| _mouseAction == MouseAction::PrepareSelect);
|
||||||
if (_mouseAction == MouseAction::Reordering
|
if (_mouseAction == MouseAction::Reordering) {
|
||||||
|| _mouseAction == MouseAction::PrepareReorder) {
|
|
||||||
finishReorder();
|
finishReorder();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (_mouseAction == MouseAction::PrepareReorder) {
|
||||||
|
_reorderState = {};
|
||||||
|
_mouseAction = MouseAction::PrepareDrag;
|
||||||
|
}
|
||||||
const auto needSelectionToggle = simpleSelectionChange && selectionMode;
|
const auto needSelectionToggle = simpleSelectionChange && selectionMode;
|
||||||
const auto needSelectionClear = simpleSelectionChange
|
const auto needSelectionClear = simpleSelectionChange
|
||||||
&& hasSelectedText();
|
&& hasSelectedText();
|
||||||
|
|||||||
@@ -462,6 +462,7 @@ void Account::startMtp(std::unique_ptr<MTP::Config> config) {
|
|||||||
_mtp->setStateChangedHandler([=](MTP::ShiftedDcId dc, int32 state) {
|
_mtp->setStateChangedHandler([=](MTP::ShiftedDcId dc, int32 state) {
|
||||||
if (dc == _mtp->mainDcId()) {
|
if (dc == _mtp->mainDcId()) {
|
||||||
Core::App().settings().proxy().connectionTypeChangesNotify();
|
Core::App().settings().proxy().connectionTypeChangesNotify();
|
||||||
|
Core::App().checkProxyRotation(this, state);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_mtp->setSessionResetHandler([=](MTP::ShiftedDcId shiftedDcId) {
|
_mtp->setSessionResetHandler([=](MTP::ShiftedDcId shiftedDcId) {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ namespace Media::Audio {
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr auto kMaxDuration = 3 * crl::time(1000);
|
constexpr auto kMaxDuration = 3 * crl::time(1000);
|
||||||
constexpr auto kMaxStreams = 2;
|
|
||||||
constexpr auto kFrameSize = 4096;
|
constexpr auto kFrameSize = 4096;
|
||||||
|
|
||||||
[[nodiscard]] QByteArray ConvertAndCut(const QByteArray &bytes) {
|
[[nodiscard]] QByteArray ConvertAndCut(const QByteArray &bytes) {
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ enum class OrderMode {
|
|||||||
|
|
||||||
struct VideoQuality {
|
struct VideoQuality {
|
||||||
uint32 manual : 1 = 0;
|
uint32 manual : 1 = 0;
|
||||||
uint32 height : 31 = 0;
|
uint32 height : 30 = 0;
|
||||||
|
uint32 original : 1 = 0;
|
||||||
|
|
||||||
friend inline constexpr auto operator<=>(
|
friend inline constexpr auto operator<=>(
|
||||||
VideoQuality,
|
VideoQuality,
|
||||||
|
|||||||
@@ -355,7 +355,7 @@ void SettingsButton::setSpeed(float64 speed) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SettingsButton::setQuality(int quality) {
|
void SettingsButton::setQuality(Media::VideoQuality quality) {
|
||||||
if (_quality != quality) {
|
if (_quality != quality) {
|
||||||
_quality = quality;
|
_quality = quality;
|
||||||
update();
|
update();
|
||||||
@@ -437,13 +437,14 @@ void SettingsButton::prepareFrame() {
|
|||||||
: u"%1X"_q.arg(rounded / 10);
|
: u"%1X"_q.arg(rounded / 10);
|
||||||
paintBadge(p, text, RectPart::TopLeft, color);
|
paintBadge(p, text, RectPart::TopLeft, color);
|
||||||
}
|
}
|
||||||
const auto text = (!_quality)
|
const auto height = _quality.height;
|
||||||
|
const auto text = !height
|
||||||
? QString()
|
? QString()
|
||||||
: (_quality > 2000)
|
: (height > 2000)
|
||||||
? u"4K"_q
|
? u"4K"_q
|
||||||
: (_quality > 1000)
|
: (height > 1000)
|
||||||
? u"FHD"_q
|
? u"FHD"_q
|
||||||
: (_quality > 700)
|
: (height > 700)
|
||||||
? u"HD"_q
|
? u"HD"_q
|
||||||
: u"SD"_q;
|
: u"SD"_q;
|
||||||
if (!text.isEmpty()) {
|
if (!text.isEmpty()) {
|
||||||
|
|||||||
Alguns arquivos não foram exibidos porque demasiados arquivos foram alterados neste diff Mostrar Mais
Referência em uma Nova Issue
Bloquear um usuário