Compare commits

..

63 Commits

Author SHA1 Message Date
Ilya Fedin
8ed56bb4e4 Don't mess GTK scale factor with other scaling settings
Have this order for scaling settings:
1. devicePixelRatio
2. GTK
3. DPI
2021-01-23 21:55:33 +04:00
John Preston
3793f7c3c9 Update tg_owt commit in snap build. 2021-01-23 20:31:24 +04:00
John Preston
0d1b778612 Beta version 2.5.6: Fix build on macOS. 2021-01-23 16:14:37 +04:00
Ilya Fedin
b919a0627a Ensure GtkIntegration::load() is called only once 2021-01-23 16:14:22 +04:00
Ilya Fedin
6374d4eeda Some cosmetic changes in settigs setters 2021-01-23 16:14:22 +04:00
Ilya Fedin
3967052375 Get scale factor from GTK on Linux 2021-01-23 16:14:22 +04:00
Ilya Fedin
89ccc95023 Fix early return from Platform::ThirdParty::start on Linux 2021-01-23 16:14:22 +04:00
John Preston
24f2ca7443 Beta version 2.5.6.
- Press Up arrow to edit your last sent comment.
- Add more information to date tooltips
in Recent Actions and channel comments.
- Bug and crash fixes.
2021-01-22 19:00:11 +04:00
John Preston
f90e13f8b1 Fix crash after account reset after QR login. 2021-01-22 18:19:27 +04:00
John Preston
606f5377d5 Cherry-pick fix for Pulseaudio OpenAL backend. 2021-01-22 18:08:49 +04:00
John Preston
c698327b24 Update tg_owt commit in Snap. 2021-01-22 17:42:06 +04:00
Ilya Fedin
655731741c Add config for lock bot 2021-01-22 17:23:20 +04:00
Ilya Fedin
d5cdb5582b Add config for no-response bot 2021-01-22 17:23:20 +04:00
Ilya Fedin
5cb081ca9a Fix build without dbus 2021-01-22 17:22:37 +04:00
Ilya Fedin
f1e0b36f61 Use operator-> for tray icon biggest size 2021-01-22 17:22:37 +04:00
Ilya Fedin
ea9813825d Move EscapeShell to specific_linux 2021-01-22 17:22:37 +04:00
Ilya Fedin
36b6f70613 Get rid of unneeded includes in specific_linux 2021-01-22 17:22:37 +04:00
Ilya Fedin
5e60b87cf9 Remove platform_specific.h include from mainwindow.h
In order to avoid mass rebuilds on specific_*.h changing
2021-01-22 17:22:37 +04:00
Ilya Fedin
ada22ee6cc Split GTK integration into a singleton 2021-01-22 17:22:37 +04:00
Ilya Fedin
bb016e1489 Restore frameless hint on showing to workaround a bug in Qt 2021-01-22 17:12:53 +04:00
Ilya Fedin
b115ea74d0 Set config dir for OpenSSL and disable OpenSSL DSO
System-provided engines may crash bundled OpenSSL
2021-01-22 17:12:18 +04:00
Ilya Fedin
1008774aef Update vdpau to latest version 2021-01-22 17:12:18 +04:00
Ilya Fedin
73018ff958 Update libva to latest version 2021-01-22 17:12:18 +04:00
Ilya Fedin
e799fdaa3d Update wayland-protocols to latest version 2021-01-22 17:12:18 +04:00
Ilya Fedin
7656a546b0 Update libxkbcommon to latest version 2021-01-22 17:12:18 +04:00
Ilya Fedin
57f9ae4b2a Fix speed control support check 2021-01-22 17:10:22 +04:00
Ilya Fedin
cbdd86d398 Fix deadlock on OpenAL errors 2021-01-22 17:10:22 +04:00
Ilya Fedin
2fe2105a5f Don't add counter when icon theme has 'panel' icon
These icons should have a dot indicating unread messages, counter is redudant for them
2021-01-22 17:09:50 +04:00
Ilya Fedin
a986d7a3d6 Fix checking cover stream on seeking 2021-01-22 17:05:43 +04:00
Ilya Fedin
690c5df87c Format dbus errors logging 2021-01-22 17:02:50 +04:00
Ilya Fedin
1e2759840d Check _sniDBusProxy for nullptr before connecting to signals 2021-01-22 17:02:50 +04:00
Ilya Fedin
bad888496c Decrease some indentation in linux platform code 2021-01-22 17:02:50 +04:00
Ilya Fedin
4348ddf938 Adjust some constexprs in linux platform code 2021-01-22 17:02:50 +04:00
Ilya Fedin
894d6028bd Don't skip native notification toasts 2021-01-22 17:02:50 +04:00
Ilya Fedin
e8edbb16ae Make notification manager creation async 2021-01-22 17:02:50 +04:00
Ilya Fedin
a0a71687e7 Use QDBusPendingReply in GetServerInformation 2021-01-22 17:02:50 +04:00
Ilya Fedin
d042963a47 Make notification show method async 2021-01-22 17:02:50 +04:00
Ilya Fedin
64b12bde55 Allow qualified notification daemons by default on Linux 2021-01-22 17:02:50 +04:00
Ilya Fedin
49736cd879 Recreate notification manager on notification service owner change 2021-01-22 17:02:50 +04:00
the-vyld
e55581e0c9 fix typos in changelog.txt 2021-01-22 16:55:04 +04:00
John Preston
574d915c23 Fix build and tray icon menu on Windows. 2021-01-22 16:53:59 +04:00
23rd
2616659116 Added missed date detailed info in tooltips to admin log and sections. 2021-01-22 16:53:59 +04:00
23rd
3d1f21bd05 Added sent date info to tooltip of messages in admin log.
Fixed #5706.
2021-01-22 16:53:59 +04:00
23rd
dc631ef631 Added ability to see url of inline button in tooltip.
Fixed #5457.
2021-01-22 16:53:59 +04:00
23rd
5277080115 Fixed adding caption to grouped files.
Fixed #10192.
2021-01-22 16:53:59 +04:00
23rd
1ccfcc824c Updated code to be consistent with lib_ui. 2021-01-22 16:53:59 +04:00
23rd
97e8c0956f Moved all files related to menu to separate namespace. 2021-01-22 16:53:59 +04:00
23rd
03a7131a1a Replaced slots with lambdas to fill context menu in OverlayWidget. 2021-01-22 16:53:59 +04:00
23rd
2d906bddb2 Replaced raw PopupMenu pointer with unique_qptr in OverlayWidget. 2021-01-22 16:53:59 +04:00
23rd
dd7598a701 Replaced singleShot with InvokeQueued in OverlayWidget. 2021-01-22 16:53:59 +04:00
23rd
b6f17e1cea Replaced QTimer with base::Timer in OverlayWidget. 2021-01-22 16:53:59 +04:00
23rd
eb42a77eb7 Changed Up arrow shortcut for albums to edit item with caption.
Fixed #10134.
2021-01-22 16:53:59 +04:00
23rd
ad761011d6 Added ability to fetch song cover from external resource. 2021-01-22 16:53:59 +04:00
23rd
3fadf2ee54 Added Up arrow shortcut to edit comments. 2021-01-11 22:46:56 +03:00
23rd
15254599e2 Unified checking for editable message. 2021-01-11 22:46:56 +03:00
23rd
1607752cf9 Added ability to show song cover in inline results. 2021-01-11 22:46:56 +03:00
23rd
cf0cde6e83 Added ability to show song cover in EditCaptionBox and SendFilesBox. 2021-01-11 22:46:56 +03:00
23rd
8fffe7d128 Added ability to show song cover in HistoryView and Overview::Layout. 2021-01-11 22:46:45 +03:00
John Preston
a483eb98a1 Remove not-needed requests for file parts above real size. 2021-01-11 12:38:47 +04:00
John Preston
838a3b23c7 Beta version 2.5.5.
- Fix recording of audio in voice chats.
- Fix media viewer zoom and crashing.
2021-01-10 12:30:58 +04:00
23rd
a030911ad5 Fixed filling context menu in TabbedPanel between sections.
Fixed #10082.
2021-01-09 14:24:41 +03:00
John Preston
8ae1b10b91 Fix media viewer regression. 2021-01-09 13:55:55 +04:00
John Preston
adc8d6a6d1 Fix audio recording in voice chats. 2021-01-08 19:09:45 +04:00
120 changed files with 3445 additions and 2333 deletions

21
.github/lock.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 45
# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
skipCreatedBefore: false
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels: []
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: false
# Comment to post before locking. Set to `false` to disable
lockComment: >
This thread has been automatically locked since there has not been
any recent activity after it was closed. Please open a new issue for
related bugs.
# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: true

11
.github/no-response.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# Number of days of inactivity before an Issue is closed for lack of response
daysUntilClose: 30
# Label requiring a response
responseRequiredLabel: waiting for answer
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. With only the
information that is currently in the issue, we don't have enough information
to take action. Please reach out if you have or find the answers we need so
that we can investigate further.

View File

@@ -821,8 +821,15 @@ PRIVATE
platform/linux/linux_gdk_helper.h
platform/linux/linux_gsd_media_keys.cpp
platform/linux/linux_gsd_media_keys.h
platform/linux/linux_libs.cpp
platform/linux/linux_libs.h
platform/linux/linux_gtk_file_dialog.cpp
platform/linux/linux_gtk_file_dialog.h
platform/linux/linux_gtk_integration_p.h
platform/linux/linux_gtk_integration.cpp
platform/linux/linux_gtk_integration.h
platform/linux/linux_notification_service_watcher.cpp
platform/linux/linux_notification_service_watcher.h
platform/linux/linux_open_with_dialog.cpp
platform/linux/linux_open_with_dialog.h
platform/linux/linux_wayland_integration.cpp
platform/linux/linux_wayland_integration.h
platform/linux/linux_xlib_helper.cpp
@@ -961,6 +968,8 @@ PRIVATE
storage/storage_account.h
storage/storage_cloud_blob.cpp
storage/storage_cloud_blob.h
storage/storage_cloud_song_cover.cpp
storage/storage_cloud_song_cover.h
storage/storage_domain.cpp
storage/storage_domain.h
storage/storage_facade.cpp
@@ -1114,6 +1123,8 @@ if (LINUX AND DESKTOP_APP_DISABLE_DBUS_INTEGRATION)
remove_target_sources(Telegram ${src_loc}
platform/linux/linux_gsd_media_keys.cpp
platform/linux/linux_gsd_media_keys.h
platform/linux/linux_notification_service_watcher.cpp
platform/linux/linux_notification_service_watcher.h
platform/linux/notifications_manager_linux.cpp
)
@@ -1128,6 +1139,26 @@ if (LINUX AND DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION)
nice_target_sources(Telegram ${src_loc} PRIVATE platform/linux/linux_wayland_integration_dummy.cpp)
endif()
if (LINUX AND TDESKTOP_DISABLE_GTK_INTEGRATION)
remove_target_sources(Telegram ${src_loc}
platform/linux/linux_gdk_helper.cpp
platform/linux/linux_gdk_helper.h
platform/linux/linux_gtk_file_dialog.cpp
platform/linux/linux_gtk_file_dialog.h
platform/linux/linux_gtk_integration_p.h
platform/linux/linux_gtk_integration.cpp
platform/linux/linux_open_with_dialog.cpp
platform/linux/linux_open_with_dialog.h
platform/linux/linux_xlib_helper.cpp
platform/linux/linux_xlib_helper.h
)
nice_target_sources(Telegram ${src_loc}
PRIVATE
platform/linux/linux_gtk_integration_dummy.cpp
)
endif()
if (NOT DESKTOP_APP_USE_PACKAGED)
nice_target_sources(Telegram ${src_loc} PRIVATE platform/mac/mac_iconv_helper.c)
endif()

View File

@@ -1187,6 +1187,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_in_reply_to" = "In reply to";
"lng_edited" = "edited";
"lng_edited_date" = "Edited: {date}";
"lng_sent_date" = "Sent: {date}";
"lng_admin_badge" = "admin";
"lng_owner_badge" = "owner";
"lng_channel_badge" = "channel";

View File

@@ -9,7 +9,7 @@
<Identity Name="TelegramMessengerLLP.TelegramDesktop"
ProcessorArchitecture="ARCHITECTURE"
Publisher="CN=536BC709-8EE1-4478-AF22-F0F0F26FF64A"
Version="2.5.4.0" />
Version="2.5.6.0" />
<Properties>
<DisplayName>Telegram Desktop</DisplayName>
<PublisherDisplayName>Telegram FZ-LLC</PublisherDisplayName>

View File

@@ -44,8 +44,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 2,5,4,0
PRODUCTVERSION 2,5,4,0
FILEVERSION 2,5,6,0
PRODUCTVERSION 2,5,6,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -62,10 +62,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop"
VALUE "FileVersion", "2.5.4.0"
VALUE "FileVersion", "2.5.6.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2021"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "2.5.4.0"
VALUE "ProductVersion", "2.5.6.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -35,8 +35,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
//
VS_VERSION_INFO VERSIONINFO
FILEVERSION 2,5,4,0
PRODUCTVERSION 2,5,4,0
FILEVERSION 2,5,6,0
PRODUCTVERSION 2,5,6,0
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
@@ -53,10 +53,10 @@ BEGIN
BEGIN
VALUE "CompanyName", "Telegram FZ-LLC"
VALUE "FileDescription", "Telegram Desktop Updater"
VALUE "FileVersion", "2.5.4.0"
VALUE "FileVersion", "2.5.6.0"
VALUE "LegalCopyright", "Copyright (C) 2014-2021"
VALUE "ProductName", "Telegram Desktop"
VALUE "ProductVersion", "2.5.4.0"
VALUE "ProductVersion", "2.5.6.0"
END
END
BLOCK "VarFileInfo"

View File

@@ -32,6 +32,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history.h"
#include "history/history_drag_area.h"
#include "history/history_item.h"
#include "history/view/media/history_view_document.h" // DrawThumbnailAsSongCover
#include "platform/platform_specific.h"
#include "lang/lang_keys.h"
#include "media/streaming/media_streaming_instance.h"
@@ -171,7 +172,9 @@ EditCaptionBox::EditCaptionBox(
_thumbw = 0;
_thumbnailImageLoaded = true;
} else {
const auto thumbSize = st::msgFileThumbLayout.thumbSize;
const auto thumbSize = (!media->document()->isSongWithCover()
? st::msgFileThumbLayout
: st::msgFileLayout).thumbSize;
const auto tw = dimensions.width(), th = dimensions.height();
if (tw > th) {
_thumbw = (tw * thumbSize) / th;
@@ -183,19 +186,31 @@ EditCaptionBox::EditCaptionBox(
if (!image) {
return;
}
const auto options = Images::Option::Smooth
| Images::Option::RoundedSmall
| Images::Option::RoundedTopLeft
| Images::Option::RoundedTopRight
| Images::Option::RoundedBottomLeft
| Images::Option::RoundedBottomRight;
_thumb = App::pixmapFromImageInPlace(Images::prepare(
image->original(),
_thumbw * cIntRetinaFactor(),
0,
options,
thumbSize,
thumbSize));
if (media->document()->isSongWithCover()) {
const auto size = QSize(thumbSize, thumbSize);
_thumb = QPixmap(size);
_thumb.fill(Qt::transparent);
Painter p(&_thumb);
HistoryView::DrawThumbnailAsSongCover(
p,
_documentMedia,
QRect(QPoint(), size));
} else {
const auto options = Images::Option::Smooth
| Images::Option::RoundedSmall
| Images::Option::RoundedTopLeft
| Images::Option::RoundedTopRight
| Images::Option::RoundedBottomLeft
| Images::Option::RoundedBottomRight;
_thumb = App::pixmapFromImageInPlace(Images::prepare(
image->original(),
_thumbw * cIntRetinaFactor(),
0,
options,
thumbSize,
thumbSize));
}
_thumbnailImageLoaded = true;
};
_refreshThumbnail();
@@ -539,6 +554,14 @@ void EditCaptionBox::updateEditPreview() {
song->title,
song->performer);
_isAudio = true;
if (auto cover = song->cover; !cover.isNull()) {
_thumb = Ui::PrepareSongCoverForThumbnail(
cover,
st::msgFileLayout.thumbSize);
_thumbw = _thumb.width() / cIntRetinaFactor();
_thumbh = _thumb.height() / cIntRetinaFactor();
}
}
const auto getExt = [&] {
@@ -810,15 +833,21 @@ void EditCaptionBox::setupDragArea() {
areas.photo->setDroppedCallback(droppedCallback(true));
}
bool EditCaptionBox::isThumbedLayout() const {
return (_thumbw && !_isAudio);
}
void EditCaptionBox::updateBoxSize() {
auto newHeight = st::boxPhotoPadding.top() + st::boxPhotoCaptionSkip + _field->height() + errorTopSkip() + st::normalFont->height;
if (_photo) {
newHeight += _wayWrap->height() / 2;
}
const auto &st = _thumbw ? st::msgFileThumbLayout : st::msgFileLayout;
const auto &st = isThumbedLayout()
? st::msgFileThumbLayout
: st::msgFileLayout;
if (_photo || _animated) {
newHeight += std::max(_thumbh, _gifh);
} else if (_thumbw || _doc) {
} else if (isThumbedLayout() || _doc) {
newHeight += 0 + st.thumbSize + 0;
} else {
newHeight += st::boxTitleFont->height;
@@ -902,7 +931,9 @@ void EditCaptionBox::paintEvent(QPaintEvent *e) {
icon->paintInCenter(p, inner);
}
} else if (_doc) {
const auto &st = _thumbw ? st::msgFileThumbLayout : st::msgFileLayout;
const auto &st = isThumbedLayout()
? st::msgFileThumbLayout
: st::msgFileLayout;
const auto w = width() - st::boxPhotoPadding.left() - st::boxPhotoPadding.right();
const auto h = 0 + st.thumbSize + 0;
const auto nameleft = 0 + st.thumbSize + st.padding.right();
@@ -918,18 +949,24 @@ void EditCaptionBox::paintEvent(QPaintEvent *e) {
// Ui::FillRoundCorner(p, x, y, w, h, st::msgInBg, Ui::MessageInCorners, &st::msgInShadow);
const auto rthumb = style::rtlrect(x + 0, y + 0, st.thumbSize, st.thumbSize, width());
if (_thumbw) {
if (isThumbedLayout()) {
p.drawPixmap(rthumb.topLeft(), _thumb);
} else {
p.setPen(Qt::NoPen);
p.setBrush(st::msgFileInBg);
{
if (_isAudio && _thumbw) {
p.drawPixmap(rthumb.topLeft(), _thumb);
} else {
p.setBrush(st::msgFileInBg);
PainterHighQualityEnabler hq(p);
p.drawEllipse(rthumb);
}
const auto icon = &(_isAudio ? st::historyFileInPlay : _isImage ? st::historyFileInImage : st::historyFileInDocument);
const auto icon = &(_isAudio
? st::historyFileInPlay
: _isImage
? st::historyFileInImage
: st::historyFileInDocument);
icon->paintInCenter(p, rthumb);
}
p.setFont(st::semiboldFont);

View File

@@ -96,6 +96,8 @@ private:
void createEditMediaButton();
bool setPreparedList(Ui::PreparedList &&list);
bool isThumbedLayout() const;
inline QString getNewMediaPath() {
return _preparedList.files.empty()
? QString()

View File

@@ -23,7 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "history/history.h"
#include "dialogs/dialogs_main_list.h"
#include "window/window_session_controller.h" // onShowAddContact()
#include "window/window_session_controller.h" // showAddContact()
#include "facades.h"
#include "styles/style_boxes.h"
#include "styles/style_profile.h"
@@ -112,7 +112,7 @@ object_ptr<Ui::BoxContent> PrepareContactsBox(
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
box->addLeftButton(
tr::lng_profile_add_contact(),
[=] { controller->widget()->onShowAddContact(); });
[=] { controller->widget()->showAddContact(); });
};
return Box<PeerListBox>(
std::make_unique<ContactsBoxController>(

View File

@@ -1138,7 +1138,7 @@ void FieldAutocomplete::Inner::contextMenuEvent(QContextMenuEvent *e) {
SendMenu::DefaultSilentCallback(send),
SendMenu::DefaultScheduleCallback(this, type, send));
if (!_menu->actions().empty()) {
if (!_menu->empty()) {
_menu->popup(QCursor::pos());
}
}

View File

@@ -594,7 +594,7 @@ bool TabbedSelector::preventAutoHide() const {
}
bool TabbedSelector::hasMenu() const {
return (_menu && !_menu->actions().empty());
return (_menu && !_menu->empty());
}
QImage TabbedSelector::grabForAnimation() {
@@ -877,18 +877,22 @@ void TabbedSelector::scrollToY(int y) {
}
}
void TabbedSelector::contextMenuEvent(QContextMenuEvent *e) {
void TabbedSelector::showMenuWithType(SendMenu::Type type) {
_menu = base::make_unique_q<Ui::PopupMenu>(this);
const auto type = _sendMenuType
? _sendMenuType()
: SendMenu::Type::Disabled;
currentTab()->widget()->fillContextMenu(_menu, type);
if (!_menu->actions().empty()) {
if (!_menu->empty()) {
_menu->popup(QCursor::pos());
}
}
rpl::producer<> TabbedSelector::contextMenuRequested() const {
return events(
) | rpl::filter([=](not_null<QEvent*> e) {
return e->type() == QEvent::ContextMenu;
}) | rpl::to_empty;
}
TabbedSelector::Inner::Inner(
QWidget *parent,
not_null<Window::SessionController*> controller)

View File

@@ -82,6 +82,7 @@ public:
rpl::producer<> cancelled() const;
rpl::producer<> checkForHide() const;
rpl::producer<> slideFinished() const;
rpl::producer<> contextMenuRequested() const;
void setRoundRadius(int radius);
void refreshStickers();
@@ -109,9 +110,7 @@ public:
_beforeHidingCallback = std::move(callback);
}
void setSendMenuType(Fn<SendMenu::Type()> &&callback) {
_sendMenuType = std::move(callback);
}
void showMenuWithType(SendMenu::Type type);
// Float player interface.
bool floatPlayerHandleWheelEvent(QEvent *e);
@@ -127,7 +126,6 @@ public:
protected:
void paintEvent(QPaintEvent *e) override;
void resizeEvent(QResizeEvent *e) override;
void contextMenuEvent(QContextMenuEvent *e) override;
private:
class Tab {
@@ -228,8 +226,6 @@ private:
Fn<void(SelectorTab)> _afterShownCallback;
Fn<void(SelectorTab)> _beforeHidingCallback;
Fn<SendMenu::Type()> _sendMenuType;
rpl::event_stream<> _showRequests;
rpl::event_stream<> _slideFinished;

View File

@@ -99,6 +99,21 @@ std::map<int, const char*> BetaLogs() {
"- Fix media viewer updating when screen resolution is changed.\n"
},
{
2005005,
"- Fix recording of audio in voice chats.\n"
"- Fix media viewer zoom and crashing.\n"
},
{
2005006,
"- Press Up arrow to edit your last sent comment.\n"
"- Add more information to date tooltips "
"in Recent Actions and channel comments.\n"
"- Bug and crash fixes.\n"
}
};
};

View File

@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/sandbox.h"
#include "base/platform/base_platform_info.h"
#include "platform/platform_specific.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "storage/localstorage.h"

View File

@@ -13,6 +13,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "core/application.h"
#include "media/player/media_player_instance.h"
#include "base/platform/base_platform_info.h"
#include "platform/platform_specific.h"
#include "base/parse_helper.h"
#include "facades.h"

View File

@@ -22,7 +22,7 @@ constexpr auto AppId = "{53F49750-6209-4FBF-9CA8-7A333C87D1ED}"_cs;
constexpr auto AppNameOld = "Telegram Win (Unofficial)"_cs;
constexpr auto AppName = "Telegram Desktop"_cs;
constexpr auto AppFile = "Telegram"_cs;
constexpr auto AppVersion = 2005004;
constexpr auto AppVersionStr = "2.5.4";
constexpr auto AppVersion = 2005006;
constexpr auto AppVersionStr = "2.5.6";
constexpr auto AppBetaVersion = true;
constexpr auto AppAlphaVersion = TDESKTOP_ALPHA_VERSION;

View File

@@ -36,6 +36,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_gif.h"
#include "window/window_session_controller.h"
#include "storage/cache/storage_cache_database.h"
#include "storage/storage_cloud_song_cover.h"
#include "boxes/confirm_box.h"
#include "ui/image/image.h"
#include "ui/text/text_utilities.h"
@@ -540,6 +541,13 @@ void DocumentData::setattributes(
songData->duration = data.vduration().v;
songData->title = qs(data.vtitle().value_or_empty());
songData->performer = qs(data.vperformer().value_or_empty());
if (!hasThumbnail()
&& !songData->title.isEmpty()
&& !songData->performer.isEmpty()) {
Storage::CloudSongCover::LoadThumbnailFromExternal(this);
}
}
}, [&](const MTPDdocumentAttributeFilename &data) {
_filename = qs(data.vfile_name());
@@ -1488,6 +1496,10 @@ bool DocumentData::isSong() const {
return (type == SongDocument);
}
bool DocumentData::isSongWithCover() const {
return isSong() && hasThumbnail();
}
bool DocumentData::isAudioFile() const {
if (isVoiceMessage()) {
return false;

View File

@@ -139,6 +139,7 @@ public:
[[nodiscard]] bool isVoiceMessage() const;
[[nodiscard]] bool isVideoMessage() const;
[[nodiscard]] bool isSong() const;
[[nodiscard]] bool isSongWithCover() const;
[[nodiscard]] bool isAudioFile() const;
[[nodiscard]] bool isVideoFile() const;
[[nodiscard]] bool isAnimation() const;

View File

@@ -145,4 +145,20 @@ void Groups::refreshViews(const HistoryItemsList &items) {
}
}
not_null<HistoryItem*> Groups::findItemToEdit(
not_null<HistoryItem*> item) const {
const auto group = find(item);
if (!group) {
return item;
}
const auto &list = group->items;
const auto it = ranges::find_if(
list,
ranges::not_fn(&HistoryItem::emptyText));
if (it == end(list)) {
return list.front();
}
return (*it);
}
} // namespace Data

View File

@@ -31,6 +31,8 @@ public:
const Group *find(not_null<HistoryItem*> item) const;
not_null<HistoryItem*> findItemToEdit(not_null<HistoryItem*> item) const;
private:
HistoryItemsList::const_iterator findPositionForItem(
const HistoryItemsList &group,

View File

@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/data_replies_list.h"
#include "base/unixtime.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_service.h"
@@ -626,4 +627,22 @@ bool RepliesList::processMessagesIsEmpty(const MTPmessages_Messages &result) {
return (list.size() == skipped);
}
HistoryItem *RepliesList::lastEditableMessage() {
const auto message = [&](MsgId msgId) {
return _history->owner().message(_history->channelId(), msgId);
};
const auto now = base::unixtime::now();
auto proj = [&](MsgId msgId) {
if (const auto item = message(msgId)) {
return item->allowsEdit(now);
}
return false;
};
const auto it = ranges::find_if(_list, std::move(proj));
return (it == end(_list))
? nullptr
: _history->owner().groups().findItemToEdit(message(*it)).get();
}
} // namespace Data

View File

@@ -31,6 +31,8 @@ public:
[[nodiscard]] rpl::producer<int> fullCount() const;
[[nodiscard]] HistoryItem *lastEditableMessage();
private:
struct Viewer;

View File

@@ -543,7 +543,8 @@ int32 ScheduledMessages::countListHash(const List &list) const {
return HashFinalize(hash);
}
HistoryItem *ScheduledMessages::lastSentMessage(not_null<History*> history) {
HistoryItem *ScheduledMessages::lastEditableMessage(
not_null<History*> history) {
const auto i = _data.find(history);
if (i == end(_data)) {
return nullptr;
@@ -552,10 +553,16 @@ HistoryItem *ScheduledMessages::lastSentMessage(not_null<History*> history) {
sort(list);
const auto items = ranges::view::reverse(list.items);
const auto it = ranges::find_if(
items,
&HistoryItem::canBeEditedFromHistory);
return (it == end(items)) ? nullptr : (*it).get();
const auto now = base::unixtime::now();
auto proj = [&](const OwnedItem &item) {
return item->allowsEdit(now);
};
const auto it = ranges::find_if(items, std::move(proj));
return (it == end(items))
? nullptr
: history->owner().groups().findItemToEdit((*it).get()).get();
}
} // namespace Data

View File

@@ -32,7 +32,8 @@ public:
[[nodiscard]] HistoryItem *lookupItem(PeerId peer, MsgId msg) const;
[[nodiscard]] HistoryItem *lookupItem(FullMsgId itemId) const;
[[nodiscard]] int count(not_null<History*> history) const;
[[nodiscard]] HistoryItem *lastSentMessage(not_null<History*> history);
[[nodiscard]] HistoryItem *lastEditableMessage(
not_null<History*> history);
void checkEntitiesAndUpdate(const MTPDmessage &data);
void apply(const MTPDupdateNewScheduledMessage &update);

View File

@@ -1841,7 +1841,7 @@ void InnerWidget::contextMenuEvent(QContextMenuEvent *e) {
selectByMouse(globalPosition);
}
});
if (_menu->actions().empty()) {
if (_menu->empty()) {
_menu = nullptr;
} else {
_menu->popup(e->globalPos());
@@ -2281,7 +2281,7 @@ void InnerWidget::refreshEmptyLabel() {
resizeEmptyLabel();
_empty->setClickHandlerFilter([=](const auto &...) {
if (_emptyState == EmptyState::NoContacts) {
App::wnd()->onShowAddContact();
App::wnd()->showAddContact();
} else if (_emptyState == EmptyState::EmptyFolder) {
editOpenedFilter();
}

View File

@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "chat_helpers/message_field.h"
#include "boxes/sticker_set_box.h"
#include "base/platform/base_platform_info.h"
#include "base/unixtime.h"
#include "mainwindow.h"
#include "mainwidget.h"
#include "core/application.h"
@@ -509,8 +510,17 @@ QString InnerWidget::tooltipText() const {
if (_mouseCursorState == CursorState::Date
&& _mouseAction == MouseAction::None) {
if (const auto view = App::hoveredItem()) {
auto dateText = view->dateTime().toString(
QLocale::system().dateTimeFormat(QLocale::LongFormat));
const auto format = QLocale::system().dateTimeFormat(
QLocale::LongFormat);
auto dateText = HistoryView::DateTooltipText(view);
const auto sentIt = _itemDates.find(view->data());
if (sentIt != end(_itemDates)) {
dateText += '\n' + tr::lng_sent_date(
tr::now,
lt_date,
base::unixtime::parse(sentIt->second).toString(format));
}
return dateText;
}
} else if (_mouseCursorState == CursorState::Forwarded
@@ -722,7 +732,10 @@ void InnerWidget::addEvents(Direction direction, const QVector<MTPChannelAdminLo
}
auto count = 0;
const auto addOne = [&](OwnedItem item) {
const auto addOne = [&](OwnedItem item, TimeId sentDate) {
if (sentDate) {
_itemDates.emplace(item->data(), sentDate);
}
_eventIds.emplace(id);
_itemsByData.emplace(item->data(), item.get());
addToItems.push_back(std::move(item));
@@ -1177,7 +1190,7 @@ void InnerWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
}
}
if (_menu->actions().empty()) {
if (_menu->empty()) {
_menu = nullptr;
} else {
_menu->popup(e->globalPos());

View File

@@ -242,6 +242,7 @@ private:
std::vector<OwnedItem> _items;
std::set<uint64> _eventIds;
std::map<not_null<const HistoryItem*>, not_null<Element*>> _itemsByData;
base::flat_map<not_null<const HistoryItem*>, TimeId> _itemDates;
base::flat_set<FullMsgId> _animatedStickersPlayed;
base::flat_map<
not_null<PeerData*>,

View File

@@ -45,6 +45,16 @@ TextWithEntities PrepareText(const QString &value, const QString &emptyValue) {
return result;
}
TimeId ExtractSentDate(const MTPMessage &message) {
return message.match([&](const MTPDmessageEmpty &) {
return 0;
}, [&](const MTPDmessageService &data) {
return data.vdate().v;
}, [&](const MTPDmessage &data) {
return data.vdate().v;
});
}
MTPMessage PrepareLogMessage(
const MTPMessage &message,
MsgId newId,
@@ -380,7 +390,7 @@ void GenerateItems(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<History*> history,
const MTPDchannelAdminLogEvent &event,
Fn<void(OwnedItem item)> callback) {
Fn<void(OwnedItem item, TimeId sentDate)> callback) {
Expects(history->peer->isChannel());
const auto session = &history->session();
@@ -389,8 +399,10 @@ void GenerateItems(
const auto channel = history->peer->asChannel();
const auto &action = event.vaction();
const auto date = event.vdate().v;
const auto addPart = [&](not_null<HistoryItem*> item) {
return callback(OwnedItem(delegate, item));
const auto addPart = [&](
not_null<HistoryItem*> item,
TimeId sentDate = 0) {
return callback(OwnedItem(delegate, item), sentDate);
};
using Flag = MTPDmessage::Flag;
@@ -545,13 +557,15 @@ void GenerateItems(
addSimpleServiceMessage(text);
auto detachExistingItem = false;
addPart(history->createItem(
PrepareLogMessage(
action.vmessage(),
history->nextNonHistoryEntryId(),
date),
MTPDmessage_ClientFlag::f_admin_log_entry,
detachExistingItem));
addPart(
history->createItem(
PrepareLogMessage(
action.vmessage(),
history->nextNonHistoryEntryId(),
date),
MTPDmessage_ClientFlag::f_admin_log_entry,
detachExistingItem),
ExtractSentDate(action.vmessage()));
}, [&](const auto &) {
auto text = tr::lng_admin_log_unpinned_message(tr::now, lt_from, fromLinkText);
addSimpleServiceMessage(text);
@@ -598,10 +612,15 @@ void GenerateItems(
addSimpleServiceMessage(text);
auto detachExistingItem = false;
addPart(history->createItem(
PrepareLogMessage(action.vmessage(), history->nextNonHistoryEntryId(), date),
MTPDmessage_ClientFlag::f_admin_log_entry,
detachExistingItem));
addPart(
history->createItem(
PrepareLogMessage(
action.vmessage(),
history->nextNonHistoryEntryId(),
date),
MTPDmessage_ClientFlag::f_admin_log_entry,
detachExistingItem),
ExtractSentDate(action.vmessage()));
};
auto createParticipantJoin = [&]() {
@@ -740,10 +759,15 @@ void GenerateItems(
addSimpleServiceMessage(text);
auto detachExistingItem = false;
addPart(history->createItem(
PrepareLogMessage(action.vmessage(), history->nextNonHistoryEntryId(), date),
MTPDmessage_ClientFlag::f_admin_log_entry,
detachExistingItem));
addPart(
history->createItem(
PrepareLogMessage(
action.vmessage(),
history->nextNonHistoryEntryId(),
date),
MTPDmessage_ClientFlag::f_admin_log_entry,
detachExistingItem),
ExtractSentDate(action.vmessage()));
};
auto createChangeLinkedChat = [&](const MTPDchannelAdminLogEventActionChangeLinkedChat &action) {

View File

@@ -22,7 +22,7 @@ void GenerateItems(
not_null<HistoryView::ElementDelegate*> delegate,
not_null<History*> history,
const MTPDchannelAdminLogEvent &event,
Fn<void(OwnedItem item)> callback);
Fn<void(OwnedItem item, TimeId sentDate)> callback);
// Smart pointer wrapper for HistoryItem* that destroys the owned item.
class OwnedItem {

View File

@@ -2704,15 +2704,16 @@ MsgId History::msgIdForRead() const {
: result;
}
HistoryItem *History::lastSentMessage() const {
HistoryItem *History::lastEditableMessage() const {
if (!loadedAtBottom()) {
return nullptr;
}
const auto now = base::unixtime::now();
for (const auto &block : ranges::view::reverse(blocks)) {
for (const auto &message : ranges::view::reverse(block->messages)) {
const auto item = message->data();
if (item->canBeEditedFromHistory()) {
return item;
if (item->allowsEdit(now)) {
return owner().groups().findItemToEdit(item);
}
}
}

View File

@@ -259,7 +259,7 @@ public:
MsgId minMsgId() const;
MsgId maxMsgId() const;
MsgId msgIdForRead() const;
HistoryItem *lastSentMessage() const;
HistoryItem *lastEditableMessage() const;
void resizeToWidth(int newWidth);
void forceFullResize();

View File

@@ -1851,7 +1851,7 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
}
}
if (_menu->actions().empty()) {
if (_menu->empty()) {
_menu = nullptr;
} else {
_menu->popup(e->globalPos());
@@ -3313,40 +3313,7 @@ QString HistoryInner::tooltipText() const {
if (_mouseCursorState == CursorState::Date
&& _mouseAction == MouseAction::None) {
if (const auto view = App::hoveredItem()) {
auto dateText = view->dateTime().toString(
QLocale::system().dateTimeFormat(QLocale::LongFormat));
if (const auto editedDate = view->displayedEditDate()) {
dateText += '\n' + tr::lng_edited_date(
tr::now,
lt_date,
base::unixtime::parse(editedDate).toString(
QLocale::system().dateTimeFormat(
QLocale::LongFormat)));
}
if (const auto forwarded = view->data()->Get<HistoryMessageForwarded>()) {
dateText += '\n' + tr::lng_forwarded_date(
tr::now,
lt_date,
base::unixtime::parse(forwarded->originalDate).toString(
QLocale::system().dateTimeFormat(
QLocale::LongFormat)));
if (const auto media = view->media()) {
if (media->hidesForwardedInfo()) {
dateText += "\n" + tr::lng_forwarded(
tr::now,
lt_user,
(forwarded->originalSender
? forwarded->originalSender->shortName()
: forwarded->hiddenSenderInfo->firstName));
}
}
}
if (const auto msgsigned = view->data()->Get<HistoryMessageSigned>()) {
if (msgsigned->isElided && !msgsigned->isAnonymousRank) {
dateText += '\n' + tr::lng_signed_author(tr::now, lt_user, msgsigned->author);
}
}
return dateText;
return HistoryView::DateTooltipText(view);
}
} else if (_mouseCursorState == CursorState::Forwarded
&& _mouseAction == MouseAction::None) {

View File

@@ -762,27 +762,6 @@ bool HistoryItem::canUpdateDate() const {
return isScheduled();
}
bool HistoryItem::canBeEditedFromHistory() const {
// Skip if message is editing media.
if (isEditingMedia()) {
return false;
}
// Skip if message is video message or sticker.
if (const auto m = media()) {
// Skip only if media is not webpage.
if (!m->webpage() && !m->allowsEditCaption()) {
return false;
}
}
if ((IsServerMsgId(id) || isScheduled())
&& !serviceMsg()
&& (out() || history()->peer->isSelf())
&& !Has<HistoryMessageForwarded>()) {
return true;
}
return false;
}
void HistoryItem::sendFailed() {
Expects(_clientFlags & MTPDmessage_ClientFlag::f_sending);
Expects(!(_clientFlags & MTPDmessage_ClientFlag::f_failed));

View File

@@ -390,8 +390,6 @@ public:
void updateDate(TimeId newDate);
[[nodiscard]] bool canUpdateDate() const;
[[nodiscard]] bool canBeEditedFromHistory() const;
virtual ~HistoryItem();
MsgId id;

View File

@@ -413,23 +413,13 @@ ReplyMarkupClickHandler::ReplyMarkupClickHandler(
// Copy to clipboard support.
QString ReplyMarkupClickHandler::copyToClipboardText() const {
if (const auto button = getButton()) {
using Type = HistoryMessageMarkupButton::Type;
if (button->type == Type::Url || button->type == Type::Auth) {
return QString::fromUtf8(button->data);
}
}
return QString();
const auto button = getUrlButton();
return button ? QString::fromUtf8(button->data) : QString();
}
QString ReplyMarkupClickHandler::copyToClipboardContextItemText() const {
if (const auto button = getButton()) {
using Type = HistoryMessageMarkupButton::Type;
if (button->type == Type::Url || button->type == Type::Auth) {
return tr::lng_context_copy_link(tr::now);
}
}
return QString();
const auto button = getUrlButton();
return button ? tr::lng_context_copy_link(tr::now) : QString();
}
// Finds the corresponding button in the items markup struct.
@@ -440,6 +430,17 @@ const HistoryMessageMarkupButton *ReplyMarkupClickHandler::getButton() const {
return HistoryMessageMarkupButton::Get(_owner, _itemId, _row, _column);
}
auto ReplyMarkupClickHandler::getUrlButton() const
-> const HistoryMessageMarkupButton* {
if (const auto button = getButton()) {
using Type = HistoryMessageMarkupButton::Type;
if (button->type == Type::Url || button->type == Type::Auth) {
return button;
}
}
return nullptr;
}
void ReplyMarkupClickHandler::onClickImpl() const {
if (const auto item = _owner->message(_itemId)) {
App::activateBotCommand(item, _row, _column);
@@ -454,6 +455,19 @@ QString ReplyMarkupClickHandler::buttonText() const {
return QString();
}
QString ReplyMarkupClickHandler::tooltip() const {
const auto button = getUrlButton();
const auto url = button ? QString::fromUtf8(button->data) : QString();
const auto text = _fullDisplayed ? QString() : buttonText();
if (!url.isEmpty() && !text.isEmpty()) {
return QString("%1\n\n%2").arg(text).arg(url);
} else if (url.isEmpty() != text.isEmpty()) {
return text + url;
} else {
return QString();
}
}
ReplyKeyboard::Button::Button() = default;
ReplyKeyboard::Button::Button(Button &&other) = default;
ReplyKeyboard::Button &ReplyKeyboard::Button::operator=(

View File

@@ -251,9 +251,7 @@ public:
int column,
FullMsgId context);
QString tooltip() const override {
return _fullDisplayed ? QString() : buttonText();
}
QString tooltip() const override;
void setFullDisplayed(bool full) {
_fullDisplayed = full;
@@ -269,6 +267,8 @@ public:
// than the one was used when constructing the handler, but not a big deal.
const HistoryMessageMarkupButton *getButton() const;
const HistoryMessageMarkupButton *getUrlButton() const;
// We hold only FullMsgId, not HistoryItem*, because all click handlers
// are activated async and the item may be already destroyed.
void setMessageId(const FullMsgId &msgId) {

View File

@@ -846,6 +846,11 @@ void HistoryWidget::initTabbedSelector() {
return base::EventFilterResult::Continue;
});
auto filter = rpl::filter([=] {
return !isHidden();
});
using Selector = TabbedSelector;
selector->emojiChosen(
) | rpl::filter([=] {
return !isHidden() && !_field->isHidden();
@@ -854,27 +859,24 @@ void HistoryWidget::initTabbedSelector() {
}, lifetime());
selector->fileChosen(
) | rpl::filter([=] {
return !isHidden();
}) | rpl::start_with_next([=](TabbedSelector::FileChosen data) {
) | filter | rpl::start_with_next([=](Selector::FileChosen data) {
sendExistingDocument(data.document, data.options);
}, lifetime());
selector->photoChosen(
) | rpl::filter([=] {
return !isHidden();
}) | rpl::start_with_next([=](TabbedSelector::PhotoChosen data) {
) | filter | rpl::start_with_next([=](Selector::PhotoChosen data) {
sendExistingPhoto(data.photo, data.options);
}, lifetime());
selector->inlineResultChosen(
) | rpl::filter([=] {
return !isHidden();
}) | rpl::start_with_next([=](TabbedSelector::InlineChosen data) {
) | filter | rpl::start_with_next([=](Selector::InlineChosen data) {
sendInlineResult(data);
}, lifetime());
selector->setSendMenuType([=] { return sendMenuType(); });
selector->contextMenuRequested(
) | filter | rpl::start_with_next([=] {
selector->showMenuWithType(sendMenuType());
}, lifetime());
}
void HistoryWidget::supportInitAutocomplete() {
@@ -5020,10 +5022,9 @@ void HistoryWidget::keyPressEvent(QKeyEvent *e) {
_scroll->keyPressEvent(e);
} else if (e->key() == Qt::Key_Up && !commonModifiers) {
const auto item = _history
? _history->lastSentMessage()
? _history->lastEditableMessage()
: nullptr;
if (item
&& item->allowsEdit(base::unixtime::now())
&& _field->empty()
&& !_editMsgId
&& !_replyToId) {

View File

@@ -1410,7 +1410,10 @@ void ComposeControls::initTabbedSelector() {
selector->inlineResultChosen(
) | rpl::start_to_stream(_inlineResultChosen, wrap->lifetime());
selector->setSendMenuType([=] { return sendMenuType(); });
selector->contextMenuRequested(
) | rpl::start_with_next([=] {
selector->showMenuWithType(sendMenuType());
}, wrap->lifetime());
}
void ComposeControls::initSendButton() {

View File

@@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/media/history_view_sticker.h"
#include "history/view/media/history_view_large_emoji.h"
#include "history/history.h"
#include "base/unixtime.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "main/main_session.h"
@@ -157,6 +158,40 @@ TextSelection ShiftItemSelection(
return ShiftItemSelection(selection, byText.length());
}
QString DateTooltipText(not_null<Element*> view) {
const auto format = QLocale::system().dateTimeFormat(QLocale::LongFormat);
auto dateText = view->dateTime().toString(format);
if (const auto editedDate = view->displayedEditDate()) {
dateText += '\n' + tr::lng_edited_date(
tr::now,
lt_date,
base::unixtime::parse(editedDate).toString(format));
}
if (const auto forwarded = view->data()->Get<HistoryMessageForwarded>()) {
dateText += '\n' + tr::lng_forwarded_date(
tr::now,
lt_date,
base::unixtime::parse(forwarded->originalDate).toString(format));
if (const auto media = view->media()) {
if (media->hidesForwardedInfo()) {
dateText += '\n' + tr::lng_forwarded(
tr::now,
lt_user,
(forwarded->originalSender
? forwarded->originalSender->shortName()
: forwarded->hiddenSenderInfo->firstName));
}
}
}
if (const auto msgsigned = view->data()->Get<HistoryMessageSigned>()) {
if (msgsigned->isElided && !msgsigned->isAnonymousRank) {
dateText += '\n'
+ tr::lng_signed_author(tr::now, lt_user, msgsigned->author);
}
}
return dateText;
}
void UnreadBar::init(const QString &string) {
text = string;
width = st::semiboldFont->width(text);

View File

@@ -126,6 +126,8 @@ TextSelection ShiftItemSelection(
TextSelection selection,
const Ui::Text::String &byText);
QString DateTooltipText(not_null<Element*> view);
// Any HistoryView::Element can have this Component for
// displaying the unread messages bar above the message.
struct UnreadBar : public RuntimeComponent<UnreadBar, Element> {

View File

@@ -1216,8 +1216,7 @@ QString ListWidget::tooltipText() const {
? _overElement->data().get()
: nullptr;
if (_mouseCursorState == CursorState::Date && item) {
return _overElement->dateTime().toString(
QLocale::system().dateTimeFormat(QLocale::LongFormat));
return HistoryView::DateTooltipText(_overElement);
} else if (_mouseCursorState == CursorState::Forwarded && item) {
if (const auto forwarded = item->Get<HistoryMessageForwarded>()) {
return forwarded->text.toString();
@@ -1832,7 +1831,7 @@ void ListWidget::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
_overState));
_menu = FillContextMenu(this, request);
if (_menu && !_menu->actions().empty()) {
if (_menu && !_menu->empty()) {
_menu->popup(e->globalPos());
e->accept();
} else if (_menu) {

View File

@@ -492,13 +492,11 @@ void RepliesWidget::setupComposeControls() {
) | rpl::start_with_next([=](not_null<QKeyEvent*> e) {
if (e->key() == Qt::Key_Up) {
if (!_composeControls->isEditingMessage()) {
// #TODO replies edit last sent message
//auto &messages = session().data().scheduledMessages();
//if (const auto item = messages.lastSentMessage(_history)) {
// _inner->editMessageRequestNotify(item->fullId());
//} else {
if (const auto item = _replies->lastEditableMessage()) {
_inner->editMessageRequestNotify(item->fullId());
} else {
_scroll->keyPressEvent(e);
//}
}
} else {
_scroll->keyPressEvent(e);
}

View File

@@ -235,8 +235,9 @@ void ScheduledWidget::setupComposeControls() {
) | rpl::start_with_next([=](not_null<QKeyEvent*> e) {
if (e->key() == Qt::Key_Up) {
if (!_composeControls->isEditingMessage()) {
auto &messages = session().data().scheduledMessages();
if (const auto item = messages.lastSentMessage(_history)) {
const auto item = session().data().scheduledMessages()
.lastEditableMessage(_history);
if (item) {
_inner->editMessageRequestNotify(item->fullId());
} else {
_scroll->keyPressEvent(e);

View File

@@ -256,7 +256,7 @@ void TopBarWidget::showMenu() {
_controller,
_activeChat,
addAction);
if (_menu->actions().empty()) {
if (_menu->empty()) {
_menu.destroy();
} else {
_menu->moveToRight((parentWidget()->width() - width()) + st::topBarMenuPosition.x(), st::topBarMenuPosition.y());

View File

@@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/image/image.h"
#include "ui/text/format_values.h"
#include "ui/cached_round_corners.h"
#include "ui/ui_utility.h"
#include "layout.h" // FullSelection
#include "data/data_session.h"
#include "data/data_document.h"
@@ -442,14 +443,14 @@ void Document::draw(
}
} else {
p.setPen(Qt::NoPen);
if (selected) {
p.setBrush(outbg ? st::msgFileOutBgSelected : st::msgFileInBgSelected);
} else {
p.setBrush(outbg ? st::msgFileOutBg : st::msgFileInBg);
}
{
const auto coverDrawn = _data->isSongWithCover()
&& DrawThumbnailAsSongCover(p, _dataMedia, inner, selected);
if (!coverDrawn) {
PainterHighQualityEnabler hq(p);
p.setBrush(selected
? (outbg ? st::msgFileOutBgSelected : st::msgFileInBgSelected)
: (outbg ? st::msgFileOutBg : st::msgFileInBg));
p.drawEllipse(inner);
}
@@ -581,7 +582,7 @@ void Document::ensureDataMediaCreated() const {
return;
}
_dataMedia = _data->createMediaView();
if (Get<HistoryDocumentThumbed>()) {
if (Get<HistoryDocumentThumbed>() || _data->isSongWithCover()) {
_dataMedia->thumbnailWanted(_realParent->fullId());
}
history()->owner().registerHeavyViewPart(_parent);
@@ -1072,4 +1073,49 @@ Ui::Text::String Document::createCaption() {
timestampLinkBase);
}
bool DrawThumbnailAsSongCover(
Painter &p,
const std::shared_ptr<Data::DocumentMedia> &dataMedia,
const QRect &rect,
const bool selected) {
if (!dataMedia) {
return false;
}
QPixmap cover;
const auto ow = rect.width();
const auto oh = rect.height();
const auto r = ImageRoundRadius::Ellipse;
const auto c = RectPart::AllCorners;
const auto color = &st::songCoverOverlayFg;
const auto aspectRatio = Qt::KeepAspectRatioByExpanding;
const auto scaled = [&](not_null<Image*> image) -> std::pair<int, int> {
const auto size = image->size().scaled(ow, oh, aspectRatio);
return { size.width(), size.height() };
};
if (const auto normal = dataMedia->thumbnail()) {
const auto &[w, h] = scaled(normal);
cover = normal->pixSingle(w, h, ow, oh, r, c, color);
} else if (const auto blurred = dataMedia->thumbnailInline()) {
const auto &[w, h] = scaled(blurred);
cover = blurred->pixBlurredSingle(w, h, ow, oh, r, c, color);
} else {
return false;
}
if (selected) {
auto selectedCover = Images::prepareColored(
p.textPalette().selectOverlay,
cover.toImage());
cover = QPixmap::fromImage(
std::move(selectedCover),
Qt::ColorOnly);
}
p.drawPixmap(rect.topLeft(), cover);
return true;
}
} // namespace HistoryView

View File

@@ -143,4 +143,10 @@ private:
};
bool DrawThumbnailAsSongCover(
Painter &p,
const std::shared_ptr<Data::DocumentMedia> &dataMedia,
const QRect &rect,
const bool selected = false);
} // namespace HistoryView

View File

@@ -23,6 +23,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/player/media_player_instance.h"
#include "history/history_location_manager.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/media/history_view_document.h" // DrawThumbnailAsSongCover
#include "ui/image/image.h"
#include "ui/text/format_values.h"
#include "ui/cached_round_corners.h"
@@ -865,16 +866,21 @@ void File::paint(Painter &p, const QRect &clip, const PaintContext *context) con
auto inner = style::rtlrect(0, st::inlineRowMargin, st::inlineFileSize, st::inlineFileSize, _width);
p.setPen(Qt::NoPen);
if (isThumbAnimation()) {
auto over = _animation->a_thumbOver.value(1.);
p.setBrush(anim::brush(st::msgFileInBg, st::msgFileInBgOver, over));
} else {
bool over = ClickHandler::showAsActive(_document->loading() ? _cancel : _open);
p.setBrush(over ? st::msgFileInBgOver : st::msgFileInBg);
}
{
const auto coverDrawn = _document->isSongWithCover()
&& HistoryView::DrawThumbnailAsSongCover(p, _documentMedia, inner);
if (!coverDrawn) {
PainterHighQualityEnabler hq(p);
if (isThumbAnimation()) {
const auto over = _animation->a_thumbOver.value(1.);
p.setBrush(
anim::brush(st::msgFileInBg, st::msgFileInBgOver, over));
} else {
const auto over = ClickHandler::showAsActive(_document->loading()
? _cancel
: _open);
p.setBrush(over ? st::msgFileInBgOver : st::msgFileInBg);
}
p.drawEllipse(inner);
}

View File

@@ -305,7 +305,7 @@ void Inner::contextMenuEvent(QContextMenuEvent *e) {
SendMenu::DefaultSilentCallback(send),
SendMenu::DefaultScheduleCallback(this, type, send));
if (!_menu->actions().empty()) {
if (!_menu->empty()) {
_menu->popup(QCursor::pos());
}
}

View File

@@ -478,10 +478,17 @@ void Widget::resetAccount() {
_resetRequest = 0;
Ui::hideLayer();
moveToStep(
new SignupWidget(this, _account, getData()),
StackAction::Replace,
Animate::Forward);
if (getData()->phone.isEmpty()) {
moveToStep(
new QrWidget(this, _account, getData()),
StackAction::Replace,
Animate::Back);
} else {
moveToStep(
new SignupWidget(this, _account, getData()),
StackAction::Replace,
Animate::Forward);
}
}).fail([=](const RPCError &error) {
_resetRequest = 0;

View File

@@ -146,11 +146,24 @@ void MainWindow::createTrayIconMenu() {
: tr::lng_enable_notifications_from_tray(tr::now);
if (Platform::IsLinux() && !Platform::IsWayland()) {
trayIconMenu->addAction(tr::lng_open_from_tray(tr::now), this, SLOT(showFromTray()));
trayIconMenu->addAction(tr::lng_open_from_tray(tr::now), [=] {
showFromTray();
});
}
trayIconMenu->addAction(tr::lng_minimize_to_tray(tr::now), this, SLOT(minimizeToTray()));
trayIconMenu->addAction(notificationActionText, this, SLOT(toggleDisplayNotifyFromTray()));
trayIconMenu->addAction(tr::lng_quit_from_tray(tr::now), this, SLOT(quitFromTray()));
const auto showLifetime = std::make_shared<rpl::lifetime>();
trayIconMenu->addAction(tr::lng_minimize_to_tray(tr::now), [=] {
if (_activeForTrayIconAction) {
minimizeToTray();
} else {
showFromTrayMenu();
}
});
trayIconMenu->addAction(notificationActionText, [=] {
toggleDisplayNotifyFromTray();
});
trayIconMenu->addAction(tr::lng_quit_from_tray(tr::now), [=] {
quitFromTray();
});
initTrayMenuHook();
}
@@ -635,18 +648,17 @@ void MainWindow::updateTrayMenu(bool force) {
auto actions = trayIconMenu->actions();
if (Platform::IsLinux() && !Platform::IsWayland()) {
auto minimizeAction = actions.at(1);
const auto minimizeAction = actions.at(1);
minimizeAction->setEnabled(isVisible());
} else {
updateIsActive();
auto active = Platform::IsWayland() ? isVisible() : isActive();
auto toggleAction = actions.at(0);
disconnect(toggleAction, SIGNAL(triggered(bool)), this, SLOT(minimizeToTray()));
disconnect(toggleAction, SIGNAL(triggered(bool)), this, SLOT(showFromTray()));
connect(toggleAction, SIGNAL(triggered(bool)), this, active ? SLOT(minimizeToTray()) : SLOT(showFromTray()));
toggleAction->setText(active
? tr::lng_minimize_to_tray(tr::now)
: tr::lng_open_from_tray(tr::now));
const auto active = isActiveForTrayMenu();
if (_activeForTrayIconAction != active) {
_activeForTrayIconAction = active;
const auto toggleAction = actions.at(0);
toggleAction->setText(_activeForTrayIconAction
? tr::lng_minimize_to_tray(tr::now)
: tr::lng_open_from_tray(tr::now));
}
}
auto notificationAction = actions.at(Platform::IsLinux() && !Platform::IsWayland() ? 2 : 1);
auto notificationActionText = Core::App().settings().desktopNotify()
@@ -657,8 +669,10 @@ void MainWindow::updateTrayMenu(bool force) {
psTrayMenuUpdated();
}
void MainWindow::onShowAddContact() {
if (isHidden()) showFromTray();
void MainWindow::showAddContact() {
if (isHidden()) {
showFromTray();
}
if (const auto controller = sessionController()) {
Ui::show(
@@ -667,8 +681,10 @@ void MainWindow::onShowAddContact() {
}
}
void MainWindow::onShowNewGroup() {
if (isHidden()) showFromTray();
void MainWindow::showNewGroup() {
if (isHidden()) {
showFromTray();
}
if (const auto controller = sessionController()) {
Ui::show(
@@ -677,8 +693,10 @@ void MainWindow::onShowNewGroup() {
}
}
void MainWindow::onShowNewChannel() {
if (isHidden()) showFromTray();
void MainWindow::showNewChannel() {
if (isHidden()) {
showFromTray();
}
if (const auto controller = sessionController()) {
Ui::show(
@@ -720,24 +738,6 @@ void MainWindow::showLogoutConfirmation() {
callback));
}
void MainWindow::quitFromTray() {
App::quit();
}
void MainWindow::activate() {
bool wasHidden = !isVisible();
setWindowState(windowState() & ~Qt::WindowMinimized);
setVisible(true);
psActivateProcess();
activateWindow();
controller().updateIsActiveFocus();
if (wasHidden) {
if (_main) {
_main->windowShown();
}
}
}
bool MainWindow::takeThirdSectionFromLayer() {
return _layer ? _layer->takeToThirdSection() : false;
}
@@ -749,23 +749,12 @@ void MainWindow::fixOrder() {
if (_testingThemeWarning) _testingThemeWarning->raise();
}
void MainWindow::showFromTray(QSystemTrayIcon::ActivationReason reason) {
if (reason != QSystemTrayIcon::Context) {
base::call_delayed(1, this, [this] {
updateTrayMenu();
updateGlobalMenu();
});
activate();
updateUnreadCounter();
}
}
void MainWindow::handleTrayIconActication(
QSystemTrayIcon::ActivationReason reason) {
updateIsActive();
if (Platform::IsMac() && isActive()) {
if (trayIcon && !trayIcon->contextMenu()) {
showFromTray(reason);
showFromTray();
}
return;
}
@@ -775,10 +764,10 @@ void MainWindow::handleTrayIconActication(
psShowTrayMenu();
});
} else if (!skipTrayClick()) {
if (Platform::IsWayland() ? isVisible() : isActive()) {
if (isActiveForTrayMenu()) {
minimizeToTray();
} else {
showFromTray(reason);
showFromTray();
}
_lastTrayClickTime = crl::now();
}

View File

@@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "platform/platform_specific.h"
#include "platform/platform_main_window.h"
#include "base/unique_qptr.h"
#include "ui/layers/layer_widget.h"
@@ -40,8 +39,6 @@ class LayerStackWidget;
class MediaPreviewWidget;
class MainWindow : public Platform::MainWindow {
Q_OBJECT
public:
explicit MainWindow(not_null<Window::Controller*> controller);
~MainWindow();
@@ -55,11 +52,17 @@ public:
void setupIntro(Intro::EnterPoint point);
void setupMain();
void showSettings();
void showAddContact();
void showNewGroup();
void showNewChannel();
void setInnerFocus();
MainWidget *sessionContent() const;
[[nodiscard]] bool doWeMarkAsRead();
void activate();
bool takeThirdSectionFromLayer();
@@ -115,18 +118,6 @@ protected:
void updateIsActiveHook() override;
void clearWidgetsHook() override;
public slots:
void showSettings();
void setInnerFocus();
void quitFromTray();
void showFromTray(QSystemTrayIcon::ActivationReason reason = QSystemTrayIcon::Unknown);
void toggleDisplayNotifyFromTray();
void onShowAddContact();
void onShowNewGroup();
void onShowNewChannel();
private:
[[nodiscard]] bool skipTrayClick() const;
@@ -140,12 +131,15 @@ private:
void themeUpdated(const Window::Theme::BackgroundUpdate &data);
void toggleDisplayNotifyFromTray();
QPixmap grabInner();
QImage icon16, icon32, icon64, iconbig16, iconbig32, iconbig64;
crl::time _lastTrayClickTime = 0;
QPoint _lastMousePosition;
bool _activeForTrayIconAction = true;
object_ptr<Window::PasscodeLockWidget> _passcodeLock = { nullptr };
object_ptr<Intro::Widget> _intro = { nullptr };

View File

@@ -276,7 +276,16 @@ void StopDetachIfNotUsedSafe() {
}
bool SupportsSpeedControl() {
return OpenAL::HasEFXExtension();
return OpenAL::HasEFXExtension()
&& (alGetEnumValue("AL_AUXILIARY_SEND_FILTER") != 0)
&& (alGetEnumValue("AL_DIRECT_FILTER") != 0)
&& (alGetEnumValue("AL_EFFECT_TYPE") != 0)
&& (alGetEnumValue("AL_EFFECT_PITCH_SHIFTER") != 0)
&& (alGetEnumValue("AL_FILTER_TYPE") != 0)
&& (alGetEnumValue("AL_FILTER_LOWPASS") != 0)
&& (alGetEnumValue("AL_LOWPASS_GAIN") != 0)
&& (alGetEnumValue("AL_PITCH_SHIFTER_COARSE_TUNE") != 0)
&& (alGetEnumValue("AL_EFFECTSLOT_EFFECT") != 0);
}
} // namespace Audio
@@ -587,7 +596,7 @@ Mixer::Mixer(not_null<Audio::Instance*> instance)
}, _lifetime);
connect(this, SIGNAL(loaderOnStart(const AudioMsgId&, qint64)), _loader, SLOT(onStart(const AudioMsgId&, qint64)));
connect(this, SIGNAL(loaderOnCancel(const AudioMsgId&)), _loader, SLOT(onCancel(const AudioMsgId&)));
connect(this, SIGNAL(loaderOnCancel(const AudioMsgId&)), _loader, SLOT(onCancel(const AudioMsgId&)), Qt::QueuedConnection);
connect(_loader, SIGNAL(needToCheck()), _fader, SLOT(onTimer()));
connect(_loader, SIGNAL(error(const AudioMsgId&)), this, SLOT(onError(const AudioMsgId&)));
connect(_fader, SIGNAL(needToPreload(const AudioMsgId&)), _loader, SLOT(onLoad(const AudioMsgId&)));

View File

@@ -148,6 +148,10 @@ Stream File::Context::initStream(
const auto info = format->streams[index];
if (type == AVMEDIA_TYPE_VIDEO) {
if (info->disposition & AV_DISPOSITION_ATTACHED_PIC) {
// ignore cover streams
return Stream();
}
result.rotation = FFmpeg::ReadRotationFromMetadata(info);
result.aspect = FFmpeg::ValidateAspectRatio(info->sample_aspect_ratio);
} else if (type == AVMEDIA_TYPE_AUDIO) {
@@ -159,10 +163,6 @@ Stream File::Context::initStream(
result.codec = FFmpeg::MakeCodecPointer(info);
if (!result.codec) {
if (info->codecpar->codec_id == AV_CODEC_ID_MJPEG) {
// mp3 files contain such "video stream", just ignore it.
return Stream();
}
return result;
}

View File

@@ -322,8 +322,7 @@ OverlayWidget::OverlayWidget()
, _radial([=](crl::time now) { return radialAnimationCallback(now); })
, _lastAction(-st::mediaviewDeltaFromLastAction, -st::mediaviewDeltaFromLastAction)
, _stateAnimation([=](crl::time now) { return stateAnimationCallback(now); })
, _dropdown(this, st::mediaviewDropdownMenu)
, _dropdownShowTimer(this) {
, _dropdown(this, st::mediaviewDropdownMenu) {
Lang::Updated(
) | rpl::start_with_next([=] {
refreshLang();
@@ -410,23 +409,19 @@ OverlayWidget::OverlayWidget()
}
}, lifetime());
_saveMsgUpdater.setSingleShot(true);
connect(&_saveMsgUpdater, SIGNAL(timeout()), this, SLOT(updateImage()));
_saveMsgUpdater.setCallback([=] { updateImage(); });
setAttribute(Qt::WA_AcceptTouchEvents);
_touchTimer.setSingleShot(true);
connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer()));
_touchTimer.setCallback([=] { onTouchTimer(); });
_controlsHideTimer.setSingleShot(true);
connect(&_controlsHideTimer, SIGNAL(timeout()), this, SLOT(onHideControls()));
_controlsHideTimer.setCallback([=] { onHideControls(); });
_docDownload->addClickHandler([=] { onDownload(); });
_docSaveAs->addClickHandler([=] { onSaveAs(); });
_docCancel->addClickHandler([=] { onSaveCancel(); });
_dropdown->setHiddenCallback([this] { dropdownHidden(); });
_dropdownShowTimer->setSingleShot(true);
connect(_dropdownShowTimer, SIGNAL(timeout()), this, SLOT(onDropdown()));
_dropdownShowTimer.setCallback([=] { onDropdown(); });
}
void OverlayWidget::refreshLang() {
@@ -454,7 +449,7 @@ void OverlayWidget::moveToScreen() {
}
void OverlayWidget::updateGeometry() {
if (!Platform::IsMac()) {
if (Platform::IsLinux()) {
return;
}
const auto screen = windowHandle() && windowHandle()->screen()
@@ -483,8 +478,8 @@ void OverlayWidget::updateControlsGeometry() {
_saveMsg.moveTo((width() - _saveMsg.width()) / 2, (height() - _saveMsg.height()) / 2);
_photoRadialRect = QRect(QPoint((width() - st::radialSize.width()) / 2, (height() - st::radialSize.height()) / 2), st::radialSize);
resizeContentByScreenSize();
updateControls();
resizeContentByScreenSize();
update();
}
@@ -798,33 +793,37 @@ void OverlayWidget::refreshCaptionGeometry() {
captionHeight);
}
void OverlayWidget::updateActions() {
_actions.clear();
void OverlayWidget::fillContextMenuActions(const MenuCallback &addAction) {
if (_document && _document->loading()) {
_actions.push_back({ tr::lng_cancel(tr::now), SLOT(onSaveCancel()) });
addAction(tr::lng_cancel(tr::now), [=] { onSaveCancel(); });
}
if (IsServerMsgId(_msgid.msg)) {
_actions.push_back({ tr::lng_context_to_msg(tr::now), SLOT(onToMessage()) });
addAction(tr::lng_context_to_msg(tr::now), [=] { onToMessage(); });
}
if (_document && !_document->filepath(true).isEmpty()) {
_actions.push_back({ Platform::IsMac() ? tr::lng_context_show_in_finder(tr::now) : tr::lng_context_show_in_folder(tr::now), SLOT(onShowInFolder()) });
const auto text = Platform::IsMac()
? tr::lng_context_show_in_finder(tr::now)
: tr::lng_context_show_in_folder(tr::now);
addAction(text, [=] { onShowInFolder(); });
}
if ((_document && documentContentShown()) || (_photo && _photoMedia->loaded())) {
_actions.push_back({ tr::lng_mediaview_copy(tr::now), SLOT(onCopy()) });
addAction(tr::lng_mediaview_copy(tr::now), [=] { onCopy(); });
}
if ((_photo && _photo->hasAttachedStickers())
|| (_document && _document->hasAttachedStickers())) {
auto member = _photo
? SLOT(onPhotoAttachedStickers())
: SLOT(onDocumentAttachedStickers());
_actions.push_back({
auto callback = [=] {
if (_photo) {
onPhotoAttachedStickers();
} else if (_document) {
onDocumentAttachedStickers();
}
};
addAction(
tr::lng_context_attached_stickers(tr::now),
std::move(member)
});
std::move(callback));
}
if (_canForwardItem) {
_actions.push_back({ tr::lng_mediaview_forward(tr::now), SLOT(onForward()) });
addAction(tr::lng_mediaview_forward(tr::now), [=] { onForward(); });
}
const auto canDelete = [&] {
if (_canDeleteItem) {
@@ -844,12 +843,15 @@ void OverlayWidget::updateActions() {
return false;
}();
if (canDelete) {
_actions.push_back({ tr::lng_mediaview_delete(tr::now), SLOT(onDelete()) });
addAction(tr::lng_mediaview_delete(tr::now), [=] { onDelete(); });
}
_actions.push_back({ tr::lng_mediaview_save_as(tr::now), SLOT(onSaveAs()) });
addAction(tr::lng_mediaview_save_as(tr::now), [=] { onSaveAs(); });
if (const auto overviewType = computeOverviewType()) {
_actions.push_back({ _document ? tr::lng_mediaview_files_all(tr::now) : tr::lng_mediaview_photos_all(tr::now), SLOT(onOverview()) });
const auto text = _document
? tr::lng_mediaview_files_all(tr::now)
: tr::lng_mediaview_photos_all(tr::now);
addAction(text, [=] { onOverview(); });
}
}
@@ -1167,8 +1169,6 @@ void OverlayWidget::clearSession() {
_animationOpacities.clear();
}
clearStreaming();
delete _menu;
_menu = nullptr;
setContext(v::null);
_from = nullptr;
_fromName = QString();
@@ -1240,7 +1240,7 @@ void OverlayWidget::close() {
void OverlayWidget::activateControls() {
if (!_menu && !_mousePressed) {
_controlsHideTimer.start(int(st::mediaviewWaitHide));
_controlsHideTimer.callOnce(st::mediaviewWaitHide);
}
if (_fullScreenVideo) {
if (_streamed) {
@@ -1625,7 +1625,9 @@ void OverlayWidget::onDelete() {
}
void OverlayWidget::onOverview() {
if (_menu) _menu->hideMenu(true);
if (_menu) {
_menu->hideMenu(true);
}
update();
if (const auto overviewType = computeOverviewType()) {
close();
@@ -3113,7 +3115,7 @@ void OverlayWidget::paintEvent(QPaintEvent *e) {
}
if (!_blurred) {
auto nextFrame = (dt < st::mediaviewSaveMsgShowing || hidingDt >= 0) ? int(AnimationTimerDelta) : (st::mediaviewSaveMsgShowing + st::mediaviewSaveMsgShown + 1 - dt);
_saveMsgUpdater.start(nextFrame);
_saveMsgUpdater.callOnce(nextFrame);
}
} else {
_saveMsgStarted = 0;
@@ -3863,7 +3865,9 @@ void OverlayWidget::preloadData(int delta) {
void OverlayWidget::mousePressEvent(QMouseEvent *e) {
updateOver(e->pos());
if (_menu || !_receiveMouse) return;
if (_menu || !_receiveMouse) {
return;
}
ClickHandler::pressed();
@@ -3968,9 +3972,9 @@ bool OverlayWidget::updateOverState(OverState newState) {
bool result = true;
if (_over != newState) {
if (newState == OverMore && !_ignoringDropdown) {
_dropdownShowTimer->start(0);
_dropdownShowTimer.callOnce(0);
} else {
_dropdownShowTimer->stop();
_dropdownShowTimer.cancel();
}
updateOverRect(_over);
updateOverRect(newState);
@@ -4104,7 +4108,7 @@ void OverlayWidget::mouseReleaseEvent(QMouseEvent *e) {
} else if (_over == OverIcon && _down == OverIcon) {
onDocClick();
} else if (_over == OverMore && _down == OverMore) {
QTimer::singleShot(0, this, SLOT(onDropdown()));
InvokeQueued(this, [=] { onDropdown(); });
} else if (_over == OverClose && _down == OverClose) {
close();
} else if (_over == OverVideo && _down == OverVideo) {
@@ -4143,16 +4147,17 @@ void OverlayWidget::mouseReleaseEvent(QMouseEvent *e) {
void OverlayWidget::contextMenuEvent(QContextMenuEvent *e) {
if (e->reason() != QContextMenuEvent::Mouse || QRect(_x, _y, _w, _h).contains(e->pos())) {
if (_menu) {
_menu->deleteLater();
_menu = nullptr;
}
_menu = new Ui::PopupMenu(this, st::mediaviewPopupMenu);
updateActions();
for_const (auto &action, _actions) {
_menu->addAction(action.text, this, action.member);
}
connect(_menu, SIGNAL(destroyed(QObject*)), this, SLOT(onMenuDestroy(QObject*)));
_menu = base::make_unique_q<Ui::PopupMenu>(
this,
st::mediaviewPopupMenu);
fillContextMenuActions([&] (const QString &text, Fn<void()> handler) {
_menu->addAction(text, std::move(handler));
});
_menu->setDestroyedCallback(crl::guard(this, [=] {
activateControls();
_receiveMouse = false;
InvokeQueued(this, [=] { receiveMouse(); });
}));
_menu->popup(e->globalPos());
e->accept();
activateControls();
@@ -4163,7 +4168,7 @@ void OverlayWidget::touchEvent(QTouchEvent *e) {
switch (e->type()) {
case QEvent::TouchBegin: {
if (_touchPress || e->touchPoints().isEmpty()) return;
_touchTimer.start(QApplication::startDragTime());
_touchTimer.callOnce(QApplication::startDragTime());
_touchPress = true;
_touchMove = _touchRightButton = false;
_touchStart = e->touchPoints().cbegin()->screenPos().toPoint();
@@ -4203,14 +4208,14 @@ void OverlayWidget::touchEvent(QTouchEvent *e) {
}
}
if (weak) {
_touchTimer.stop();
_touchTimer.cancel();
_touchPress = _touchMove = _touchRightButton = false;
}
} break;
case QEvent::TouchCancel: {
_touchPress = false;
_touchTimer.stop();
_touchTimer.cancel();
} break;
}
}
@@ -4314,8 +4319,10 @@ void OverlayWidget::setVisibleHook(bool visible) {
assignMediaPointer(nullptr);
_preloadPhotos.clear();
_preloadDocuments.clear();
if (_menu) _menu->hideMenu(true);
_controlsHideTimer.stop();
if (_menu) {
_menu->hideMenu(true);
}
_controlsHideTimer.cancel();
_controlsState = ControlsShown;
_controlsOpacity = anim::value(1, 1);
_groupThumbs = nullptr;
@@ -4338,25 +4345,15 @@ void OverlayWidget::setVisibleHook(bool visible) {
}
}
void OverlayWidget::onMenuDestroy(QObject *obj) {
if (_menu == obj) {
_menu = nullptr;
activateControls();
}
_receiveMouse = false;
QTimer::singleShot(0, this, SLOT(receiveMouse()));
}
void OverlayWidget::receiveMouse() {
_receiveMouse = true;
}
void OverlayWidget::onDropdown() {
updateActions();
_dropdown->clearActions();
for_const (auto &action, _actions) {
_dropdown->addAction(action.text, this, action.member);
}
fillContextMenuActions([&] (const QString &text, Fn<void()> handler) {
_dropdown->addAction(text, std::move(handler));
});
_dropdown->moveToRight(0, height() - _dropdown->height());
_dropdown->showAnimated(Ui::PanelAnimation::Origin::BottomRight);
_dropdown->setFocus();

View File

@@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/timer.h"
#include "ui/rp_widget.h"
#include "ui/widgets/dropdown_menu.h"
#include "ui/effects/animations.h"
@@ -127,7 +128,6 @@ private slots:
void onDelete();
void onOverview();
void onCopy();
void onMenuDestroy(QObject *obj);
void receiveMouse();
void onPhotoAttachedStickers();
void onDocumentAttachedStickers();
@@ -274,8 +274,11 @@ private:
void dropdownHidden();
void updateDocSize();
void updateControls();
void updateActions();
void updateControlsGeometry();
using MenuCallback = Fn<void(const QString &, Fn<void()>)>;
void fillContextMenuActions(const MenuCallback &addAction);
void resizeCenteredControls();
void resizeContentByScreenSize();
@@ -493,26 +496,20 @@ private:
};
ControlsState _controlsState = ControlsShown;
crl::time _controlsAnimStarted = 0;
QTimer _controlsHideTimer;
base::Timer _controlsHideTimer;
anim::value _controlsOpacity;
bool _mousePressed = false;
Ui::PopupMenu *_menu = nullptr;
base::unique_qptr<Ui::PopupMenu> _menu;
object_ptr<Ui::DropdownMenu> _dropdown;
object_ptr<QTimer> _dropdownShowTimer;
struct ActionData {
QString text;
const char *member;
};
QList<ActionData> _actions;
base::Timer _dropdownShowTimer;
bool _receiveMouse = true;
bool _touchPress = false;
bool _touchMove = false;
bool _touchRightButton = false;
QTimer _touchTimer;
base::Timer _touchTimer;
QPoint _touchStart;
QPoint _accumScroll;
@@ -520,7 +517,7 @@ private:
crl::time _saveMsgStarted = 0;
anim::value _saveMsgOpacity;
QRect _saveMsg;
QTimer _saveMsgUpdater;
base::Timer _saveMsgUpdater;
Ui::Text::String _saveMsgText;
SavePhotoVideo _savePhotoVideoWhenLoaded = SavePhotoVideo::None;

View File

@@ -33,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_item.h"
#include "history/history_item_components.h"
#include "history/view/history_view_cursor_state.h"
#include "history/view/media/history_view_document.h" // DrawThumbnailAsSongCover
#include "base/unixtime.h"
#include "base/qt_adapters.h"
#include "ui/effects/round_checkbox.h"
@@ -1008,21 +1009,33 @@ void Document::paint(Painter &p, const QRect &clip, TextSelection selection, con
auto inner = style::rtlrect(_st.songPadding.left(), _st.songPadding.top(), _st.songThumbSize, _st.songThumbSize, _width);
if (clip.intersects(inner)) {
const auto isLoading = (!cornerDownload
&& (_data->loading() || _data->uploading()));
p.setPen(Qt::NoPen);
if (selected) {
p.setBrush(st::msgFileInBgSelected);
} else {
auto over = ClickHandler::showAsActive((!cornerDownload && (_data->loading() || _data->uploading())) ? _cancell : (loaded || _dataMedia->canBePlayed()) ? _openl : _savel);
p.setBrush(anim::brush(_st.songIconBg, _st.songOverBg, _a_iconOver.value(over ? 1. : 0.)));
}
{
using namespace HistoryView;
const auto coverDrawn = _data->isSongWithCover()
&& DrawThumbnailAsSongCover(p, _dataMedia, inner, selected);
if (!coverDrawn) {
if (selected) {
p.setBrush(st::msgFileInBgSelected);
} else {
const auto over = ClickHandler::showAsActive(isLoading
? _cancell
: (loaded || _dataMedia->canBePlayed())
? _openl
: _savel);
p.setBrush(anim::brush(
_st.songIconBg,
_st.songOverBg,
_a_iconOver.value(over ? 1. : 0.)));
}
PainterHighQualityEnabler hq(p);
p.drawEllipse(inner);
}
const auto icon = [&] {
if (!cornerDownload && (_data->loading() || _data->uploading())) {
if (isLoading) {
return &(selected ? _st.songCancelSelected : _st.songCancel);
} else if (showPause) {
return &(selected ? _st.songPauseSelected : _st.songPause);

View File

@@ -7,14 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "platform/linux/file_utilities_linux.h"
#include "platform/linux/linux_libs.h"
#include "platform/linux/linux_gdk_helper.h"
#include "platform/linux/linux_desktop_environment.h"
#include "platform/linux/linux_gtk_integration.h"
#include "platform/linux/specific_linux.h"
#include "storage/localstorage.h"
#include "base/qt_adapters.h"
#include "window/window_controller.h"
#include "core/application.h"
#include <QtGui/QDesktopServices>
@@ -24,150 +18,10 @@ extern "C" {
#define signals public
} // extern "C"
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
#include <private/qguiapplication_p.h>
extern "C" {
#undef signals
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#define signals public
} // extern "C"
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
using Platform::internal::GtkIntegration;
namespace Platform {
namespace File {
namespace {
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
bool ShowOpenWithSupported() {
return Platform::internal::GdkHelperLoaded()
&& (Libs::gtk_app_chooser_dialog_new != nullptr)
&& (Libs::gtk_app_chooser_get_app_info != nullptr)
&& (Libs::gtk_app_chooser_get_type != nullptr)
&& (Libs::gtk_widget_get_window != nullptr)
&& (Libs::gtk_widget_realize != nullptr)
&& (Libs::gtk_widget_show != nullptr)
&& (Libs::gtk_widget_destroy != nullptr);
}
class OpenWithDialog : public QWindow {
public:
OpenWithDialog(const QString &filepath);
~OpenWithDialog();
bool exec();
private:
static void handleResponse(OpenWithDialog *dialog, int responseId);
GFile *_gfileInstance = nullptr;
GtkWidget *_gtkWidget = nullptr;
QEventLoop _loop;
std::optional<bool> _result = std::nullopt;
};
OpenWithDialog::OpenWithDialog(const QString &filepath)
: _gfileInstance(g_file_new_for_path(filepath.toUtf8()))
, _gtkWidget(Libs::gtk_app_chooser_dialog_new(
nullptr,
GTK_DIALOG_MODAL,
_gfileInstance)) {
g_signal_connect_swapped(
_gtkWidget,
"response",
G_CALLBACK(handleResponse),
this);
}
OpenWithDialog::~OpenWithDialog() {
Libs::gtk_widget_destroy(_gtkWidget);
g_object_unref(_gfileInstance);
}
bool OpenWithDialog::exec() {
Libs::gtk_widget_realize(_gtkWidget);
if (const auto activeWindow = Core::App().activeWindow()) {
Platform::internal::XSetTransientForHint(
Libs::gtk_widget_get_window(_gtkWidget),
activeWindow->widget().get()->windowHandle()->winId());
}
QGuiApplicationPrivate::showModalWindow(this);
Libs::gtk_widget_show(_gtkWidget);
if (!_result.has_value()) {
_loop.exec();
}
QGuiApplicationPrivate::hideModalWindow(this);
return *_result;
}
void OpenWithDialog::handleResponse(OpenWithDialog *dialog, int responseId) {
GAppInfo *chosenAppInfo = nullptr;
dialog->_result = true;
switch (responseId) {
case GTK_RESPONSE_OK:
chosenAppInfo = Libs::gtk_app_chooser_get_app_info(
Libs::gtk_app_chooser_cast(dialog->_gtkWidget));
if (chosenAppInfo) {
GList *uris = nullptr;
uris = g_list_prepend(uris, g_file_get_uri(dialog->_gfileInstance));
g_app_info_launch_uris(chosenAppInfo, uris, nullptr, nullptr);
g_list_free(uris);
g_object_unref(chosenAppInfo);
}
break;
case GTK_RESPONSE_CANCEL:
case GTK_RESPONSE_DELETE_EVENT:
break;
default:
dialog->_result = false;
break;
}
dialog->_loop.quit();
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
} // namespace
namespace internal {
QByteArray EscapeShell(const QByteArray &content) {
auto result = QByteArray();
auto b = content.constData(), e = content.constEnd();
for (auto ch = b; ch != e; ++ch) {
if (*ch == ' ' || *ch == '"' || *ch == '\'' || *ch == '\\') {
if (result.isEmpty()) {
result.reserve(content.size() * 2);
}
if (ch > b) {
result.append(b, ch - b);
}
result.append('\\');
b = ch;
}
}
if (result.isEmpty()) {
return content;
}
if (e > b) {
result.append(b, e - b);
}
return result;
}
} // namespace internal
void UnsafeOpenUrl(const QString &url) {
if (!g_app_info_launch_default_for_uri(
@@ -183,18 +37,16 @@ void UnsafeOpenEmailLink(const QString &email) {
}
bool UnsafeShowOpenWith(const QString &filepath) {
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
if (InFlatpak()
|| InSnap()
|| !ShowOpenWithSupported()) {
if (InFlatpak() || InSnap()) {
return false;
}
const auto absolutePath = QFileInfo(filepath).absoluteFilePath();
return OpenWithDialog(absolutePath).exec();
#else // !TDESKTOP_DISABLE_GTK_INTEGRATION
if (const auto integration = GtkIntegration::Instance()) {
const auto absolutePath = QFileInfo(filepath).absoluteFilePath();
return integration->showOpenWithDialog(absolutePath);
}
return false;
#endif // TDESKTOP_DISABLE_GTK_INTEGRATION
}
void UnsafeLaunch(const QString &filepath) {
@@ -213,136 +65,6 @@ void UnsafeLaunch(const QString &filepath) {
} // namespace File
namespace FileDialog {
namespace {
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
// GTK file chooser image preview: thanks to Chromium
// The size of the preview we display for selected image files. We set height
// larger than width because generally there is more free space vertically
// than horiztonally (setting the preview image will alway expand the width of
// the dialog, but usually not the height). The image's aspect ratio will always
// be preserved.
constexpr auto kPreviewWidth = 256;
constexpr auto kPreviewHeight = 512;
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
using Type = ::FileDialog::internal::Type;
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
bool UseNative(Type type = Type::ReadFile) {
// use gtk file dialog on gtk-based desktop environments
// or if QT_QPA_PLATFORMTHEME=(gtk2|gtk3)
// or if portals are used and operation is to open folder
// and portal doesn't support folder choosing
const auto sandboxedOrCustomPortal = InFlatpak()
|| InSnap()
|| UseXDGDesktopPortal();
const auto neededForPortal = (type == Type::ReadFolder)
&& !CanOpenDirectoryWithPortal();
const auto neededNonForced = DesktopEnvironment::IsGtkBased()
|| (sandboxedOrCustomPortal && neededForPortal);
const auto excludeNonForced = sandboxedOrCustomPortal && !neededForPortal;
return IsGtkIntegrationForced()
|| (neededNonForced && !excludeNonForced);
}
bool NativeSupported() {
return Platform::internal::GdkHelperLoaded()
&& (Libs::gtk_widget_hide_on_delete != nullptr)
&& (Libs::gtk_clipboard_store != nullptr)
&& (Libs::gtk_clipboard_get != nullptr)
&& (Libs::gtk_widget_destroy != nullptr)
&& (Libs::gtk_dialog_get_type != nullptr)
&& (Libs::gtk_dialog_run != nullptr)
&& (Libs::gtk_widget_realize != nullptr)
&& (Libs::gdk_window_set_modal_hint != nullptr)
&& (Libs::gtk_widget_show != nullptr)
&& (Libs::gdk_window_focus != nullptr)
&& (Libs::gtk_widget_hide != nullptr)
&& (Libs::gtk_widget_hide_on_delete != nullptr)
&& (Libs::gtk_file_chooser_dialog_new != nullptr)
&& (Libs::gtk_file_chooser_get_type != nullptr)
&& (Libs::gtk_file_chooser_set_current_folder != nullptr)
&& (Libs::gtk_file_chooser_get_current_folder != nullptr)
&& (Libs::gtk_file_chooser_set_current_name != nullptr)
&& (Libs::gtk_file_chooser_select_filename != nullptr)
&& (Libs::gtk_file_chooser_get_filenames != nullptr)
&& (Libs::gtk_file_chooser_set_filter != nullptr)
&& (Libs::gtk_file_chooser_get_filter != nullptr)
&& (Libs::gtk_window_get_type != nullptr)
&& (Libs::gtk_window_set_title != nullptr)
&& (Libs::gtk_file_chooser_set_local_only != nullptr)
&& (Libs::gtk_file_chooser_set_action != nullptr)
&& (Libs::gtk_file_chooser_set_select_multiple != nullptr)
&& (Libs::gtk_file_chooser_set_do_overwrite_confirmation != nullptr)
&& (Libs::gtk_file_chooser_remove_filter != nullptr)
&& (Libs::gtk_file_filter_set_name != nullptr)
&& (Libs::gtk_file_filter_add_pattern != nullptr)
&& (Libs::gtk_file_chooser_add_filter != nullptr)
&& (Libs::gtk_file_filter_new != nullptr);
}
bool PreviewSupported() {
return NativeSupported()
&& (Libs::gdk_pixbuf_new_from_file_at_size != nullptr);
}
bool GetNative(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
Type type,
QString startFile) {
internal::GtkFileDialog dialog(parent, caption, QString(), filter);
dialog.setModal(true);
if (type == Type::ReadFile || type == Type::ReadFiles) {
dialog.setFileMode((type == Type::ReadFiles) ? QFileDialog::ExistingFiles : QFileDialog::ExistingFile);
dialog.setAcceptMode(QFileDialog::AcceptOpen);
} else if (type == Type::ReadFolder) {
dialog.setAcceptMode(QFileDialog::AcceptOpen);
dialog.setFileMode(QFileDialog::Directory);
dialog.setOption(QFileDialog::ShowDirsOnly);
} else {
dialog.setFileMode(QFileDialog::AnyFile);
dialog.setAcceptMode(QFileDialog::AcceptSave);
}
if (startFile.isEmpty() || startFile.at(0) != '/') {
startFile = cDialogLastPath() + '/' + startFile;
}
dialog.selectFile(startFile);
int res = dialog.exec();
QString path = dialog.directory().absolutePath();
if (path != cDialogLastPath()) {
cSetDialogLastPath(path);
Local::writeSettings();
}
if (res == QDialog::Accepted) {
if (type == Type::ReadFiles) {
files = dialog.selectedFiles();
} else {
files = dialog.selectedFiles().mid(0, 1);
}
return true;
}
files = QStringList();
remoteContent = QByteArray();
return false;
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
} // namespace
bool Get(
QPointer<QWidget> parent,
@@ -350,23 +72,24 @@ bool Get(
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
Type type,
::FileDialog::internal::Type type,
QString startFile) {
if (parent) {
parent = parent->window();
}
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
if (UseNative(type) && NativeSupported()) {
return GetNative(
parent,
files,
remoteContent,
caption,
filter,
type,
startFile);
if (const auto integration = GtkIntegration::Instance()) {
if (integration->fileDialogSupported()
&& integration->useFileDialog(type)) {
return integration->getFileDialog(
parent,
files,
remoteContent,
caption,
filter,
type,
startFile);
}
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
return ::FileDialog::internal::GetDefault(
parent,
files,
@@ -377,448 +100,5 @@ bool Get(
startFile);
}
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
namespace internal {
QGtkDialog::QGtkDialog(GtkWidget *gtkWidget) : gtkWidget(gtkWidget) {
g_signal_connect_swapped(G_OBJECT(gtkWidget), "response", G_CALLBACK(onResponse), this);
g_signal_connect(G_OBJECT(gtkWidget), "delete-event", G_CALLBACK(Libs::gtk_widget_hide_on_delete), nullptr);
if (PreviewSupported()) {
_preview = Libs::gtk_image_new();
g_signal_connect_swapped(G_OBJECT(gtkWidget), "update-preview", G_CALLBACK(onUpdatePreview), this);
Libs::gtk_file_chooser_set_preview_widget(Libs::gtk_file_chooser_cast(gtkWidget), _preview);
}
}
QGtkDialog::~QGtkDialog() {
Libs::gtk_clipboard_store(Libs::gtk_clipboard_get(GDK_SELECTION_CLIPBOARD));
Libs::gtk_widget_destroy(gtkWidget);
}
GtkDialog *QGtkDialog::gtkDialog() const {
return Libs::gtk_dialog_cast(gtkWidget);
}
void QGtkDialog::exec() {
if (modality() == Qt::ApplicationModal) {
// block input to the whole app, including other GTK dialogs
Libs::gtk_dialog_run(gtkDialog());
} else {
// block input to the window, allow input to other GTK dialogs
QEventLoop loop;
connect(this, SIGNAL(accept()), &loop, SLOT(quit()));
connect(this, SIGNAL(reject()), &loop, SLOT(quit()));
loop.exec();
}
}
void QGtkDialog::show(Qt::WindowFlags flags, Qt::WindowModality modality, QWindow *parent) {
connect(parent, &QWindow::destroyed, this, &QGtkDialog::onParentWindowDestroyed,
Qt::UniqueConnection);
setParent(parent);
setFlags(flags);
setModality(modality);
Libs::gtk_widget_realize(gtkWidget); // creates X window
if (parent) {
Platform::internal::XSetTransientForHint(Libs::gtk_widget_get_window(gtkWidget), parent->winId());
}
if (modality != Qt::NonModal) {
Libs::gdk_window_set_modal_hint(Libs::gtk_widget_get_window(gtkWidget), true);
QGuiApplicationPrivate::showModalWindow(this);
}
Libs::gtk_widget_show(gtkWidget);
Libs::gdk_window_focus(Libs::gtk_widget_get_window(gtkWidget), 0);
}
void QGtkDialog::hide() {
QGuiApplicationPrivate::hideModalWindow(this);
Libs::gtk_widget_hide(gtkWidget);
}
void QGtkDialog::onResponse(QGtkDialog *dialog, int response) {
if (response == GTK_RESPONSE_OK)
emit dialog->accept();
else
emit dialog->reject();
}
void QGtkDialog::onUpdatePreview(QGtkDialog* dialog) {
auto filename = Libs::gtk_file_chooser_get_preview_filename(Libs::gtk_file_chooser_cast(dialog->gtkWidget));
if (!filename) {
Libs::gtk_file_chooser_set_preview_widget_active(Libs::gtk_file_chooser_cast(dialog->gtkWidget), false);
return;
}
// Don't attempt to open anything which isn't a regular file. If a named pipe,
// this may hang. See https://crbug.com/534754.
struct stat stat_buf;
if (stat(filename, &stat_buf) != 0 || !S_ISREG(stat_buf.st_mode)) {
g_free(filename);
Libs::gtk_file_chooser_set_preview_widget_active(Libs::gtk_file_chooser_cast(dialog->gtkWidget), false);
return;
}
// This will preserve the image's aspect ratio.
auto pixbuf = Libs::gdk_pixbuf_new_from_file_at_size(filename, kPreviewWidth, kPreviewHeight, nullptr);
g_free(filename);
if (pixbuf) {
Libs::gtk_image_set_from_pixbuf(Libs::gtk_image_cast(dialog->_preview), pixbuf);
g_object_unref(pixbuf);
}
Libs::gtk_file_chooser_set_preview_widget_active(Libs::gtk_file_chooser_cast(dialog->gtkWidget), pixbuf ? true : false);
}
void QGtkDialog::onParentWindowDestroyed() {
// The Gtk*DialogHelper classes own this object. Make sure the parent doesn't delete it.
setParent(nullptr);
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
namespace {
const char *filterRegExp =
"^(.*)\\(([a-zA-Z0-9_.,*? +;#\\-\\[\\]@\\{\\}/!<>\\$%&=^~:\\|]*)\\)$";
QStringList makeFilterList(const QString &filter) {
QString f(filter);
if (f.isEmpty())
return QStringList();
QString sep(QLatin1String(";;"));
int i = f.indexOf(sep, 0);
if (i == -1) {
if (f.indexOf(QLatin1Char('\n'), 0) != -1) {
sep = QLatin1Char('\n');
i = f.indexOf(sep, 0);
}
}
return f.split(sep);
}
// Makes a list of filters from a normal filter string "Image Files (*.png *.jpg)"
QStringList cleanFilterList(const QString &filter) {
QRegExp regexp(QString::fromLatin1(filterRegExp));
Q_ASSERT(regexp.isValid());
QString f = filter;
int i = regexp.indexIn(f);
if (i >= 0)
f = regexp.cap(2);
return f.split(QLatin1Char(' '), base::QStringSkipEmptyParts);
}
} // namespace
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
GtkFileDialog::GtkFileDialog(QWidget *parent, const QString &caption, const QString &directory, const QString &filter) : QDialog(parent)
, _windowTitle(caption)
, _initialDirectory(directory) {
auto filters = makeFilterList(filter);
const int numFilters = filters.count();
_nameFilters.reserve(numFilters);
for (int i = 0; i < numFilters; ++i) {
_nameFilters << filters[i].simplified();
}
d.reset(new QGtkDialog(Libs::gtk_file_chooser_dialog_new("", nullptr,
GTK_FILE_CHOOSER_ACTION_OPEN,
// https://developer.gnome.org/gtk3/stable/GtkFileChooserDialog.html#gtk-file-chooser-dialog-new
// first_button_text doesn't need explicit conversion to char*, while all others are vardict
tr::lng_cancel(tr::now).toUtf8(), GTK_RESPONSE_CANCEL,
tr::lng_box_ok(tr::now).toUtf8().constData(), GTK_RESPONSE_OK, nullptr)));
connect(d.data(), SIGNAL(accept()), this, SLOT(onAccepted()));
connect(d.data(), SIGNAL(reject()), this, SLOT(onRejected()));
g_signal_connect(Libs::gtk_file_chooser_cast(d->gtkDialog()), "selection-changed", G_CALLBACK(onSelectionChanged), this);
g_signal_connect_swapped(Libs::gtk_file_chooser_cast(d->gtkDialog()), "current-folder-changed", G_CALLBACK(onCurrentFolderChanged), this);
}
GtkFileDialog::~GtkFileDialog() {
}
void GtkFileDialog::showHelper(Qt::WindowFlags flags, Qt::WindowModality modality, QWindow *parent) {
_dir.clear();
_selection.clear();
applyOptions();
return d->show(flags, modality, parent);
}
void GtkFileDialog::setVisible(bool visible) {
if (visible) {
if (testAttribute(Qt::WA_WState_ExplicitShowHide) && !testAttribute(Qt::WA_WState_Hidden)) {
return;
}
} else if (testAttribute(Qt::WA_WState_ExplicitShowHide) && testAttribute(Qt::WA_WState_Hidden)) {
return;
}
if (visible) {
showHelper(windowFlags(), windowModality(), parentWidget() ? parentWidget()->windowHandle() : nullptr);
} else {
hideHelper();
}
// Set WA_DontShowOnScreen so that QDialog::setVisible(visible) below
// updates the state correctly, but skips showing the non-native version:
setAttribute(Qt::WA_DontShowOnScreen);
QDialog::setVisible(visible);
}
int GtkFileDialog::exec() {
d->setModality(windowModality());
bool deleteOnClose = testAttribute(Qt::WA_DeleteOnClose);
setAttribute(Qt::WA_DeleteOnClose, false);
bool wasShowModal = testAttribute(Qt::WA_ShowModal);
setAttribute(Qt::WA_ShowModal, true);
setResult(0);
show();
QPointer<QDialog> guard = this;
d->exec();
if (guard.isNull())
return QDialog::Rejected;
setAttribute(Qt::WA_ShowModal, wasShowModal);
return result();
}
void GtkFileDialog::hideHelper() {
// After GtkFileChooserDialog has been hidden, gtk_file_chooser_get_current_folder()
// & gtk_file_chooser_get_filenames() will return bogus values -> cache the actual
// values before hiding the dialog
_dir = directory().absolutePath();
_selection = selectedFiles();
d->hide();
}
bool GtkFileDialog::defaultNameFilterDisables() const {
return false;
}
void GtkFileDialog::setDirectory(const QString &directory) {
GtkDialog *gtkDialog = d->gtkDialog();
Libs::gtk_file_chooser_set_current_folder(Libs::gtk_file_chooser_cast(gtkDialog), directory.toUtf8());
}
QDir GtkFileDialog::directory() const {
// While GtkFileChooserDialog is hidden, gtk_file_chooser_get_current_folder()
// returns a bogus value -> return the cached value before hiding
if (!_dir.isEmpty())
return _dir;
QString ret;
GtkDialog *gtkDialog = d->gtkDialog();
gchar *folder = Libs::gtk_file_chooser_get_current_folder(Libs::gtk_file_chooser_cast(gtkDialog));
if (folder) {
ret = QString::fromUtf8(folder);
g_free(folder);
}
return QDir(ret);
}
void GtkFileDialog::selectFile(const QString &filename) {
_initialFiles.clear();
_initialFiles.append(filename);
}
QStringList GtkFileDialog::selectedFiles() const {
// While GtkFileChooserDialog is hidden, gtk_file_chooser_get_filenames()
// returns a bogus value -> return the cached value before hiding
if (!_selection.isEmpty())
return _selection;
QStringList selection;
GtkDialog *gtkDialog = d->gtkDialog();
GSList *filenames = Libs::gtk_file_chooser_get_filenames(Libs::gtk_file_chooser_cast(gtkDialog));
for (GSList *it = filenames; it; it = it->next)
selection += QString::fromUtf8((const char*)it->data);
g_slist_free(filenames);
return selection;
}
void GtkFileDialog::setFilter() {
applyOptions();
}
void GtkFileDialog::selectNameFilter(const QString &filter) {
GtkFileFilter *gtkFilter = _filters.value(filter);
if (gtkFilter) {
GtkDialog *gtkDialog = d->gtkDialog();
Libs::gtk_file_chooser_set_filter(Libs::gtk_file_chooser_cast(gtkDialog), gtkFilter);
}
}
QString GtkFileDialog::selectedNameFilter() const {
GtkDialog *gtkDialog = d->gtkDialog();
GtkFileFilter *gtkFilter = Libs::gtk_file_chooser_get_filter(Libs::gtk_file_chooser_cast(gtkDialog));
return _filterNames.value(gtkFilter);
}
void GtkFileDialog::onAccepted() {
emit accept();
// QString filter = selectedNameFilter();
// if (filter.isEmpty())
// emit filterSelected(filter);
// QList<QUrl> files = selectedFiles();
// emit filesSelected(files);
// if (files.count() == 1)
// emit fileSelected(files.first());
}
void GtkFileDialog::onRejected() {
emit reject();
//
}
void GtkFileDialog::onSelectionChanged(GtkDialog *gtkDialog, GtkFileDialog *helper) {
// QString selection;
// gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(gtkDialog));
// if (filename) {
// selection = QString::fromUtf8(filename);
// g_free(filename);
// }
// emit helper->currentChanged(QUrl::fromLocalFile(selection));
}
void GtkFileDialog::onCurrentFolderChanged(GtkFileDialog *dialog) {
// emit dialog->directoryEntered(dialog->directory());
}
GtkFileChooserAction gtkFileChooserAction(QFileDialog::FileMode fileMode, QFileDialog::AcceptMode acceptMode) {
switch (fileMode) {
case QFileDialog::AnyFile:
case QFileDialog::ExistingFile:
case QFileDialog::ExistingFiles:
if (acceptMode == QFileDialog::AcceptOpen)
return GTK_FILE_CHOOSER_ACTION_OPEN;
else
return GTK_FILE_CHOOSER_ACTION_SAVE;
case QFileDialog::Directory:
default:
if (acceptMode == QFileDialog::AcceptOpen)
return GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
else
return GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER;
}
}
bool CustomButtonsSupported() {
return (Libs::gtk_dialog_get_widget_for_response != nullptr)
&& (Libs::gtk_button_set_label != nullptr)
&& (Libs::gtk_button_get_type != nullptr);
}
void GtkFileDialog::applyOptions() {
GtkDialog *gtkDialog = d->gtkDialog();
Libs::gtk_window_set_title(Libs::gtk_window_cast(gtkDialog), _windowTitle.toUtf8());
Libs::gtk_file_chooser_set_local_only(Libs::gtk_file_chooser_cast(gtkDialog), true);
const GtkFileChooserAction action = gtkFileChooserAction(_fileMode, _acceptMode);
Libs::gtk_file_chooser_set_action(Libs::gtk_file_chooser_cast(gtkDialog), action);
const bool selectMultiple = (_fileMode == QFileDialog::ExistingFiles);
Libs::gtk_file_chooser_set_select_multiple(Libs::gtk_file_chooser_cast(gtkDialog), selectMultiple);
const bool confirmOverwrite = !_options.testFlag(QFileDialog::DontConfirmOverwrite);
Libs::gtk_file_chooser_set_do_overwrite_confirmation(Libs::gtk_file_chooser_cast(gtkDialog), confirmOverwrite);
if (!_nameFilters.isEmpty())
setNameFilters(_nameFilters);
if (!_initialDirectory.isEmpty())
setDirectory(_initialDirectory);
for_const (const auto &filename, _initialFiles) {
if (_acceptMode == QFileDialog::AcceptSave) {
QFileInfo fi(filename);
Libs::gtk_file_chooser_set_current_folder(Libs::gtk_file_chooser_cast(gtkDialog), fi.path().toUtf8());
Libs::gtk_file_chooser_set_current_name(Libs::gtk_file_chooser_cast(gtkDialog), fi.fileName().toUtf8());
} else if (filename.endsWith('/')) {
Libs::gtk_file_chooser_set_current_folder(Libs::gtk_file_chooser_cast(gtkDialog), filename.toUtf8());
} else {
Libs::gtk_file_chooser_select_filename(Libs::gtk_file_chooser_cast(gtkDialog), filename.toUtf8());
}
}
const QString initialNameFilter = _nameFilters.isEmpty() ? QString() : _nameFilters.front();
if (!initialNameFilter.isEmpty())
selectNameFilter(initialNameFilter);
if (CustomButtonsSupported()) {
GtkWidget *acceptButton = Libs::gtk_dialog_get_widget_for_response(gtkDialog, GTK_RESPONSE_OK);
if (acceptButton) {
/*if (opts->isLabelExplicitlySet(QFileDialogOptions::Accept))
Libs::gtk_button_set_label(Libs::gtk_button_cast(acceptButton), opts->labelText(QFileDialogOptions::Accept).toUtf8());
else*/ if (_acceptMode == QFileDialog::AcceptOpen)
Libs::gtk_button_set_label(Libs::gtk_button_cast(acceptButton), tr::lng_open_link(tr::now).toUtf8());
else
Libs::gtk_button_set_label(Libs::gtk_button_cast(acceptButton), tr::lng_settings_save(tr::now).toUtf8());
}
GtkWidget *rejectButton = Libs::gtk_dialog_get_widget_for_response(gtkDialog, GTK_RESPONSE_CANCEL);
if (rejectButton) {
/*if (opts->isLabelExplicitlySet(QFileDialogOptions::Reject))
Libs::gtk_button_set_label(Libs::gtk_button_cast(rejectButton), opts->labelText(QFileDialogOptions::Reject).toUtf8());
else*/
Libs::gtk_button_set_label(Libs::gtk_button_cast(rejectButton), tr::lng_cancel(tr::now).toUtf8());
}
}
}
void GtkFileDialog::setNameFilters(const QStringList &filters) {
GtkDialog *gtkDialog = d->gtkDialog();
foreach (GtkFileFilter *filter, _filters)
Libs::gtk_file_chooser_remove_filter(Libs::gtk_file_chooser_cast(gtkDialog), filter);
_filters.clear();
_filterNames.clear();
for_const (auto &filter, filters) {
GtkFileFilter *gtkFilter = Libs::gtk_file_filter_new();
auto name = filter;//.left(filter.indexOf(QLatin1Char('(')));
auto extensions = cleanFilterList(filter);
Libs::gtk_file_filter_set_name(gtkFilter, name.isEmpty() ? extensions.join(QStringLiteral(", ")).toUtf8() : name.toUtf8());
for_const (auto &ext, extensions) {
auto caseInsensitiveExt = QString();
caseInsensitiveExt.reserve(4 * ext.size());
for_const (auto ch, ext) {
auto chLower = ch.toLower();
auto chUpper = ch.toUpper();
if (chLower != chUpper) {
caseInsensitiveExt.append('[').append(chLower).append(chUpper).append(']');
} else {
caseInsensitiveExt.append(ch);
}
}
Libs::gtk_file_filter_add_pattern(gtkFilter, caseInsensitiveExt.toUtf8());
}
Libs::gtk_file_chooser_add_filter(Libs::gtk_file_chooser_cast(gtkDialog), gtkFilter);
_filters.insert(filter, gtkFilter);
_filterNames.insert(gtkFilter, filter);
}
}
} // namespace internal
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
} // namespace FileDialog
} // namespace Platform

View File

@@ -9,22 +9,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "platform/platform_file_utilities.h"
#include <QtGui/QWindow>
#include <QtWidgets/QFileDialog>
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
typedef struct _GtkWidget GtkWidget;
typedef struct _GtkDialog GtkDialog;
typedef struct _GtkFileFilter GtkFileFilter;
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
namespace Platform {
namespace File {
namespace internal {
QByteArray EscapeShell(const QByteArray &content);
} // namespace internal
inline QString UrlToLocal(const QUrl &url) {
return ::File::internal::UrlToLocalDefault(url);
@@ -45,114 +31,5 @@ inline void InitLastPath() {
::FileDialog::internal::InitLastPathDefault();
}
namespace internal {
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
// This is a patched copy of qgtk2 theme plugin.
// We need to use our own gtk file dialog instead of
// styling Qt file dialog, because Qt only works with gtk2.
// We need to be able to work with gtk2 and gtk3, because
// we use gtk3 to work with appindicator3.
class QGtkDialog : public QWindow {
Q_OBJECT
public:
QGtkDialog(GtkWidget *gtkWidget);
~QGtkDialog();
GtkDialog *gtkDialog() const;
void exec();
void show(Qt::WindowFlags flags, Qt::WindowModality modality, QWindow *parent);
void hide();
signals:
void accept();
void reject();
protected:
static void onResponse(QGtkDialog *dialog, int response);
static void onUpdatePreview(QGtkDialog *dialog);
private slots:
void onParentWindowDestroyed();
private:
GtkWidget *gtkWidget;
GtkWidget *_preview = nullptr;
};
class GtkFileDialog : public QDialog {
Q_OBJECT
public:
GtkFileDialog(QWidget *parent = Q_NULLPTR,
const QString &caption = QString(),
const QString &directory = QString(),
const QString &filter = QString());
~GtkFileDialog();
void setVisible(bool visible) override;
void setWindowTitle(const QString &windowTitle) {
_windowTitle = windowTitle;
}
void setAcceptMode(QFileDialog::AcceptMode acceptMode) {
_acceptMode = acceptMode;
}
void setFileMode(QFileDialog::FileMode fileMode) {
_fileMode = fileMode;
}
void setOption(QFileDialog::Option option, bool on = true) {
if (on) {
_options |= option;
} else {
_options &= ~option;
}
}
int exec() override;
bool defaultNameFilterDisables() const;
void setDirectory(const QString &directory);
QDir directory() const;
void selectFile(const QString &filename);
QStringList selectedFiles() const;
void setFilter();
void selectNameFilter(const QString &filter);
QString selectedNameFilter() const;
private slots:
void onAccepted();
void onRejected();
private:
static void onSelectionChanged(GtkDialog *dialog, GtkFileDialog *helper);
static void onCurrentFolderChanged(GtkFileDialog *helper);
void applyOptions();
void setNameFilters(const QStringList &filters);
void showHelper(Qt::WindowFlags flags, Qt::WindowModality modality, QWindow *parent);
void hideHelper();
// Options
QFileDialog::Options _options;
QString _windowTitle = "Choose file";
QString _initialDirectory;
QStringList _initialFiles;
QStringList _nameFilters;
QFileDialog::AcceptMode _acceptMode = QFileDialog::AcceptOpen;
QFileDialog::FileMode _fileMode = QFileDialog::ExistingFile;
QString _dir;
QStringList _selection;
QHash<QString, GtkFileFilter*> _filters;
QHash<GtkFileFilter*, QString> _filterNames;
QScopedPointer<QGtkDialog> d;
};
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
} // namespace internal
} // namespace FileDialog
} // namespace Platform

View File

@@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "platform/linux/linux_desktop_environment.h"
#include "platform/linux/specific_linux.h"
#include "base/qt_adapters.h"
namespace Platform {

View File

@@ -5,10 +5,9 @@ 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
*/
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
#include "platform/linux/linux_gdk_helper.h"
#include "platform/linux/linux_libs.h"
#include "platform/linux/linux_gtk_integration_p.h"
extern "C" {
#undef signals
@@ -19,6 +18,8 @@ extern "C" {
namespace Platform {
namespace internal {
using namespace Platform::Gtk;
enum class GtkLoaded {
GtkNone,
Gtk2,
@@ -44,7 +45,7 @@ f_gdk_x11_window_get_type gdk_x11_window_get_type = nullptr;
// To be able to compile with gtk-2.0 headers as well
template <typename Object>
inline bool gdk_is_x11_window_check(Object *obj) {
return Libs::g_type_cit_helper(obj, gdk_x11_window_get_type());
return g_type_cit_helper(obj, gdk_x11_window_get_type());
}
using f_gdk_window_get_display = GdkDisplay*(*)(GdkWindow *window);
@@ -57,20 +58,20 @@ using f_gdk_x11_window_get_xid = Window(*)(GdkWindow *window);
f_gdk_x11_window_get_xid gdk_x11_window_get_xid = nullptr;
bool GdkHelperLoadGtk2(QLibrary &lib) {
#if defined DESKTOP_APP_USE_PACKAGED && !defined DESKTOP_APP_USE_PACKAGED_LAZY
#ifdef LINK_TO_GTK
return false;
#else // DESKTOP_APP_USE_PACKAGED && !DESKTOP_APP_USE_PACKAGED_LAZY
if (!LOAD_SYMBOL(lib, "gdk_x11_drawable_get_xdisplay", gdk_x11_drawable_get_xdisplay)) return false;
if (!LOAD_SYMBOL(lib, "gdk_x11_drawable_get_xid", gdk_x11_drawable_get_xid)) return false;
#else // LINK_TO_GTK
if (!LOAD_GTK_SYMBOL(lib, "gdk_x11_drawable_get_xdisplay", gdk_x11_drawable_get_xdisplay)) return false;
if (!LOAD_GTK_SYMBOL(lib, "gdk_x11_drawable_get_xid", gdk_x11_drawable_get_xid)) return false;
return true;
#endif // !DESKTOP_APP_USE_PACKAGED || DESKTOP_APP_USE_PACKAGED_LAZY
#endif // !LINK_TO_GTK
}
bool GdkHelperLoadGtk3(QLibrary &lib) {
if (!LOAD_SYMBOL(lib, "gdk_x11_window_get_type", gdk_x11_window_get_type)) return false;
if (!LOAD_SYMBOL(lib, "gdk_window_get_display", gdk_window_get_display)) return false;
if (!LOAD_SYMBOL(lib, "gdk_x11_display_get_xdisplay", gdk_x11_display_get_xdisplay)) return false;
if (!LOAD_SYMBOL(lib, "gdk_x11_window_get_xid", gdk_x11_window_get_xid)) return false;
if (!LOAD_GTK_SYMBOL(lib, "gdk_x11_window_get_type", gdk_x11_window_get_type)) return false;
if (!LOAD_GTK_SYMBOL(lib, "gdk_window_get_display", gdk_window_get_display)) return false;
if (!LOAD_GTK_SYMBOL(lib, "gdk_x11_display_get_xdisplay", gdk_x11_display_get_xdisplay)) return false;
if (!LOAD_GTK_SYMBOL(lib, "gdk_x11_window_get_xid", gdk_x11_window_get_xid)) return false;
return true;
}
@@ -103,4 +104,3 @@ void XSetTransientForHint(GdkWindow *window, quintptr winId) {
} // namespace internal
} // namespace Platform
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION

View File

@@ -7,11 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include <QtCore/QObject>
class QLibrary;
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
extern "C" {
#undef signals
#include <gtk/gtk.h>
@@ -28,4 +25,3 @@ void XSetTransientForHint(GdkWindow *window, quintptr winId);
} // namespace internal
} // namespace Platform
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION

View File

@@ -23,7 +23,6 @@ namespace Platform {
namespace internal {
namespace {
constexpr auto kDBusTimeout = 30000;
constexpr auto kService = "org.gnome.SettingsDaemon.MediaKeys"_cs;
constexpr auto kOldService = "org.gnome.SettingsDaemon"_cs;
constexpr auto kMATEService = "org.mate.SettingsDaemon"_cs;
@@ -114,7 +113,7 @@ GSDMediaKeys::GSDMediaKeys() {
0),
nullptr,
G_DBUS_CALL_FLAGS_NONE,
kDBusTimeout,
-1,
nullptr,
&error);
@@ -160,7 +159,7 @@ GSDMediaKeys::~GSDMediaKeys() {
QCoreApplication::applicationName().toUtf8().constData()),
nullptr,
G_DBUS_CALL_FLAGS_NONE,
kDBusTimeout,
-1,
nullptr,
&error);

View File

@@ -0,0 +1,714 @@
/*
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 "platform/linux/linux_gtk_file_dialog.h"
#include "platform/linux/linux_gtk_integration_p.h"
#include "platform/linux/linux_gdk_helper.h"
#include "platform/linux/linux_desktop_environment.h"
#include "platform/linux/specific_linux.h"
#include "lang/lang_keys.h"
#include "storage/localstorage.h"
#include "base/qt_adapters.h"
#include <QtGui/QWindow>
#include <QtWidgets/QFileDialog>
#include <private/qguiapplication_p.h>
namespace Platform {
namespace FileDialog {
namespace Gtk {
using namespace Platform::Gtk;
namespace {
// GTK file chooser image preview: thanks to Chromium
// The size of the preview we display for selected image files. We set height
// larger than width because generally there is more free space vertically
// than horiztonally (setting the preview image will alway expand the width of
// the dialog, but usually not the height). The image's aspect ratio will always
// be preserved.
constexpr auto kPreviewWidth = 256;
constexpr auto kPreviewHeight = 512;
const char *filterRegExp =
"^(.*)\\(([a-zA-Z0-9_.,*? +;#\\-\\[\\]@\\{\\}/!<>\\$%&=^~:\\|]*)\\)$";
QStringList makeFilterList(const QString &filter) {
QString f(filter);
if (f.isEmpty())
return QStringList();
QString sep(QLatin1String(";;"));
int i = f.indexOf(sep, 0);
if (i == -1) {
if (f.indexOf(QLatin1Char('\n'), 0) != -1) {
sep = QLatin1Char('\n');
i = f.indexOf(sep, 0);
}
}
return f.split(sep);
}
// Makes a list of filters from a normal filter string "Image Files (*.png *.jpg)"
QStringList cleanFilterList(const QString &filter) {
QRegExp regexp(QString::fromLatin1(filterRegExp));
Assert(regexp.isValid());
QString f = filter;
int i = regexp.indexIn(f);
if (i >= 0)
f = regexp.cap(2);
return f.split(QLatin1Char(' '), base::QStringSkipEmptyParts);
}
bool PreviewSupported() {
return (gdk_pixbuf_new_from_file_at_size != nullptr);
}
bool CustomButtonsSupported() {
return (gtk_dialog_get_widget_for_response != nullptr)
&& (gtk_button_set_label != nullptr)
&& (gtk_button_get_type != nullptr);
}
// This is a patched copy of qgtk2 theme plugin.
// We need to use our own gtk file dialog instead of
// styling Qt file dialog, because Qt only works with gtk2.
// We need to be able to work with gtk2 and gtk3, because
// we use gtk3 to work with appindicator3.
class QGtkDialog : public QWindow {
public:
QGtkDialog(GtkWidget *gtkWidget);
~QGtkDialog();
GtkDialog *gtkDialog() const;
void exec();
void show(Qt::WindowFlags flags, Qt::WindowModality modality, QWindow *parent);
void hide();
rpl::producer<> accept();
rpl::producer<> reject();
protected:
static void onResponse(QGtkDialog *dialog, int response);
static void onUpdatePreview(QGtkDialog *dialog);
private:
void onParentWindowDestroyed();
GtkWidget *gtkWidget = nullptr;
GtkWidget *_preview = nullptr;
rpl::event_stream<> _accept;
rpl::event_stream<> _reject;
rpl::lifetime _lifetime;
};
class GtkFileDialog : public QDialog {
public:
GtkFileDialog(QWidget *parent = nullptr,
const QString &caption = QString(),
const QString &directory = QString(),
const QString &filter = QString());
~GtkFileDialog();
void setVisible(bool visible) override;
void setWindowTitle(const QString &windowTitle) {
_windowTitle = windowTitle;
}
void setAcceptMode(QFileDialog::AcceptMode acceptMode) {
_acceptMode = acceptMode;
}
void setFileMode(QFileDialog::FileMode fileMode) {
_fileMode = fileMode;
}
void setOption(QFileDialog::Option option, bool on = true) {
if (on) {
_options |= option;
} else {
_options &= ~option;
}
}
int exec() override;
bool defaultNameFilterDisables() const;
void setDirectory(const QString &directory);
QDir directory() const;
void selectFile(const QString &filename);
QStringList selectedFiles() const;
void setFilter();
void selectNameFilter(const QString &filter);
QString selectedNameFilter() const;
private:
static void onSelectionChanged(GtkDialog *dialog, GtkFileDialog *helper);
static void onCurrentFolderChanged(GtkFileDialog *helper);
void applyOptions();
void setNameFilters(const QStringList &filters);
void showHelper(Qt::WindowFlags flags, Qt::WindowModality modality, QWindow *parent);
void hideHelper();
void onAccepted();
void onRejected();
// Options
QFileDialog::Options _options;
QString _windowTitle = "Choose file";
QString _initialDirectory;
QStringList _initialFiles;
QStringList _nameFilters;
QFileDialog::AcceptMode _acceptMode = QFileDialog::AcceptOpen;
QFileDialog::FileMode _fileMode = QFileDialog::ExistingFile;
QString _dir;
QStringList _selection;
QHash<QString, GtkFileFilter*> _filters;
QHash<GtkFileFilter*, QString> _filterNames;
QScopedPointer<QGtkDialog> d;
rpl::lifetime _lifetime;
};
QGtkDialog::QGtkDialog(GtkWidget *gtkWidget) : gtkWidget(gtkWidget) {
g_signal_connect_swapped(G_OBJECT(gtkWidget), "response", G_CALLBACK(onResponse), this);
g_signal_connect(G_OBJECT(gtkWidget), "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), nullptr);
if (PreviewSupported()) {
_preview = gtk_image_new();
g_signal_connect_swapped(G_OBJECT(gtkWidget), "update-preview", G_CALLBACK(onUpdatePreview), this);
gtk_file_chooser_set_preview_widget(gtk_file_chooser_cast(gtkWidget), _preview);
}
}
QGtkDialog::~QGtkDialog() {
gtk_clipboard_store(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD));
gtk_widget_destroy(gtkWidget);
}
GtkDialog *QGtkDialog::gtkDialog() const {
return gtk_dialog_cast(gtkWidget);
}
void QGtkDialog::exec() {
if (modality() == Qt::ApplicationModal) {
// block input to the whole app, including other GTK dialogs
gtk_dialog_run(gtkDialog());
} else {
// block input to the window, allow input to other GTK dialogs
QEventLoop loop;
accept(
) | rpl::start_with_next([=, &loop] {
loop.quit();
}, _lifetime);
reject(
) | rpl::start_with_next([=, &loop] {
loop.quit();
}, _lifetime);
loop.exec();
}
}
void QGtkDialog::show(Qt::WindowFlags flags, Qt::WindowModality modality, QWindow *parent) {
connect(parent, &QWindow::destroyed, this, [=] { onParentWindowDestroyed(); },
Qt::UniqueConnection);
setParent(parent);
setFlags(flags);
setModality(modality);
gtk_widget_realize(gtkWidget); // creates X window
if (parent) {
internal::XSetTransientForHint(gtk_widget_get_window(gtkWidget), parent->winId());
}
if (modality != Qt::NonModal) {
gdk_window_set_modal_hint(gtk_widget_get_window(gtkWidget), true);
QGuiApplicationPrivate::showModalWindow(this);
}
gtk_widget_show(gtkWidget);
gdk_window_focus(gtk_widget_get_window(gtkWidget), 0);
}
void QGtkDialog::hide() {
QGuiApplicationPrivate::hideModalWindow(this);
gtk_widget_hide(gtkWidget);
}
rpl::producer<> QGtkDialog::accept() {
return _accept.events();
}
rpl::producer<> QGtkDialog::reject() {
return _reject.events();
}
void QGtkDialog::onResponse(QGtkDialog *dialog, int response) {
if (response == GTK_RESPONSE_OK)
dialog->_accept.fire({});
else
dialog->_reject.fire({});
}
void QGtkDialog::onUpdatePreview(QGtkDialog* dialog) {
auto filename = gtk_file_chooser_get_preview_filename(gtk_file_chooser_cast(dialog->gtkWidget));
if (!filename) {
gtk_file_chooser_set_preview_widget_active(gtk_file_chooser_cast(dialog->gtkWidget), false);
return;
}
// Don't attempt to open anything which isn't a regular file. If a named pipe,
// this may hang. See https://crbug.com/534754.
struct stat stat_buf;
if (stat(filename, &stat_buf) != 0 || !S_ISREG(stat_buf.st_mode)) {
g_free(filename);
gtk_file_chooser_set_preview_widget_active(gtk_file_chooser_cast(dialog->gtkWidget), false);
return;
}
// This will preserve the image's aspect ratio.
auto pixbuf = gdk_pixbuf_new_from_file_at_size(filename, kPreviewWidth, kPreviewHeight, nullptr);
g_free(filename);
if (pixbuf) {
gtk_image_set_from_pixbuf(gtk_image_cast(dialog->_preview), pixbuf);
g_object_unref(pixbuf);
}
gtk_file_chooser_set_preview_widget_active(gtk_file_chooser_cast(dialog->gtkWidget), pixbuf ? true : false);
}
void QGtkDialog::onParentWindowDestroyed() {
// The Gtk*DialogHelper classes own this object. Make sure the parent doesn't delete it.
setParent(nullptr);
}
GtkFileDialog::GtkFileDialog(QWidget *parent, const QString &caption, const QString &directory, const QString &filter) : QDialog(parent)
, _windowTitle(caption)
, _initialDirectory(directory) {
auto filters = makeFilterList(filter);
const int numFilters = filters.count();
_nameFilters.reserve(numFilters);
for (int i = 0; i < numFilters; ++i) {
_nameFilters << filters[i].simplified();
}
d.reset(new QGtkDialog(gtk_file_chooser_dialog_new("", nullptr,
GTK_FILE_CHOOSER_ACTION_OPEN,
// https://developer.gnome.org/gtk3/stable/GtkFileChooserDialog.html#gtk-file-chooser-dialog-new
// first_button_text doesn't need explicit conversion to char*, while all others are vardict
tr::lng_cancel(tr::now).toUtf8(), GTK_RESPONSE_CANCEL,
tr::lng_box_ok(tr::now).toUtf8().constData(), GTK_RESPONSE_OK, nullptr)));
d.data()->accept(
) | rpl::start_with_next([=] {
onAccepted();
}, _lifetime);
d.data()->reject(
) | rpl::start_with_next([=] {
onRejected();
}, _lifetime);
g_signal_connect(gtk_file_chooser_cast(d->gtkDialog()), "selection-changed", G_CALLBACK(onSelectionChanged), this);
g_signal_connect_swapped(gtk_file_chooser_cast(d->gtkDialog()), "current-folder-changed", G_CALLBACK(onCurrentFolderChanged), this);
}
GtkFileDialog::~GtkFileDialog() {
}
void GtkFileDialog::showHelper(Qt::WindowFlags flags, Qt::WindowModality modality, QWindow *parent) {
_dir.clear();
_selection.clear();
applyOptions();
return d->show(flags, modality, parent);
}
void GtkFileDialog::setVisible(bool visible) {
if (visible) {
if (testAttribute(Qt::WA_WState_ExplicitShowHide) && !testAttribute(Qt::WA_WState_Hidden)) {
return;
}
} else if (testAttribute(Qt::WA_WState_ExplicitShowHide) && testAttribute(Qt::WA_WState_Hidden)) {
return;
}
if (visible) {
showHelper(windowFlags(), windowModality(), parentWidget() ? parentWidget()->windowHandle() : nullptr);
} else {
hideHelper();
}
// Set WA_DontShowOnScreen so that QDialog::setVisible(visible) below
// updates the state correctly, but skips showing the non-native version:
setAttribute(Qt::WA_DontShowOnScreen);
QDialog::setVisible(visible);
}
int GtkFileDialog::exec() {
d->setModality(windowModality());
bool deleteOnClose = testAttribute(Qt::WA_DeleteOnClose);
setAttribute(Qt::WA_DeleteOnClose, false);
bool wasShowModal = testAttribute(Qt::WA_ShowModal);
setAttribute(Qt::WA_ShowModal, true);
setResult(0);
show();
QPointer<QDialog> guard = this;
d->exec();
if (guard.isNull())
return QDialog::Rejected;
setAttribute(Qt::WA_ShowModal, wasShowModal);
return result();
}
void GtkFileDialog::hideHelper() {
// After GtkFileChooserDialog has been hidden, gtk_file_chooser_get_current_folder()
// & gtk_file_chooser_get_filenames() will return bogus values -> cache the actual
// values before hiding the dialog
_dir = directory().absolutePath();
_selection = selectedFiles();
d->hide();
}
bool GtkFileDialog::defaultNameFilterDisables() const {
return false;
}
void GtkFileDialog::setDirectory(const QString &directory) {
GtkDialog *gtkDialog = d->gtkDialog();
gtk_file_chooser_set_current_folder(gtk_file_chooser_cast(gtkDialog), directory.toUtf8());
}
QDir GtkFileDialog::directory() const {
// While GtkFileChooserDialog is hidden, gtk_file_chooser_get_current_folder()
// returns a bogus value -> return the cached value before hiding
if (!_dir.isEmpty())
return _dir;
QString ret;
GtkDialog *gtkDialog = d->gtkDialog();
gchar *folder = gtk_file_chooser_get_current_folder(gtk_file_chooser_cast(gtkDialog));
if (folder) {
ret = QString::fromUtf8(folder);
g_free(folder);
}
return QDir(ret);
}
void GtkFileDialog::selectFile(const QString &filename) {
_initialFiles.clear();
_initialFiles.append(filename);
}
QStringList GtkFileDialog::selectedFiles() const {
// While GtkFileChooserDialog is hidden, gtk_file_chooser_get_filenames()
// returns a bogus value -> return the cached value before hiding
if (!_selection.isEmpty())
return _selection;
QStringList selection;
GtkDialog *gtkDialog = d->gtkDialog();
GSList *filenames = gtk_file_chooser_get_filenames(gtk_file_chooser_cast(gtkDialog));
for (GSList *it = filenames; it; it = it->next)
selection += QString::fromUtf8((const char*)it->data);
g_slist_free(filenames);
return selection;
}
void GtkFileDialog::setFilter() {
applyOptions();
}
void GtkFileDialog::selectNameFilter(const QString &filter) {
GtkFileFilter *gtkFilter = _filters.value(filter);
if (gtkFilter) {
GtkDialog *gtkDialog = d->gtkDialog();
gtk_file_chooser_set_filter(gtk_file_chooser_cast(gtkDialog), gtkFilter);
}
}
QString GtkFileDialog::selectedNameFilter() const {
GtkDialog *gtkDialog = d->gtkDialog();
GtkFileFilter *gtkFilter = gtk_file_chooser_get_filter(gtk_file_chooser_cast(gtkDialog));
return _filterNames.value(gtkFilter);
}
void GtkFileDialog::onAccepted() {
emit accept();
// QString filter = selectedNameFilter();
// if (filter.isEmpty())
// emit filterSelected(filter);
// QList<QUrl> files = selectedFiles();
// emit filesSelected(files);
// if (files.count() == 1)
// emit fileSelected(files.first());
}
void GtkFileDialog::onRejected() {
emit reject();
//
}
void GtkFileDialog::onSelectionChanged(GtkDialog *gtkDialog, GtkFileDialog *helper) {
// QString selection;
// gchar *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(gtkDialog));
// if (filename) {
// selection = QString::fromUtf8(filename);
// g_free(filename);
// }
// emit helper->currentChanged(QUrl::fromLocalFile(selection));
}
void GtkFileDialog::onCurrentFolderChanged(GtkFileDialog *dialog) {
// emit dialog->directoryEntered(dialog->directory());
}
GtkFileChooserAction gtkFileChooserAction(QFileDialog::FileMode fileMode, QFileDialog::AcceptMode acceptMode) {
switch (fileMode) {
case QFileDialog::AnyFile:
case QFileDialog::ExistingFile:
case QFileDialog::ExistingFiles:
if (acceptMode == QFileDialog::AcceptOpen)
return GTK_FILE_CHOOSER_ACTION_OPEN;
else
return GTK_FILE_CHOOSER_ACTION_SAVE;
case QFileDialog::Directory:
default:
if (acceptMode == QFileDialog::AcceptOpen)
return GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
else
return GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER;
}
}
void GtkFileDialog::applyOptions() {
GtkDialog *gtkDialog = d->gtkDialog();
gtk_window_set_title(gtk_window_cast(gtkDialog), _windowTitle.toUtf8());
gtk_file_chooser_set_local_only(gtk_file_chooser_cast(gtkDialog), true);
const GtkFileChooserAction action = gtkFileChooserAction(_fileMode, _acceptMode);
gtk_file_chooser_set_action(gtk_file_chooser_cast(gtkDialog), action);
const bool selectMultiple = (_fileMode == QFileDialog::ExistingFiles);
gtk_file_chooser_set_select_multiple(gtk_file_chooser_cast(gtkDialog), selectMultiple);
const bool confirmOverwrite = !_options.testFlag(QFileDialog::DontConfirmOverwrite);
gtk_file_chooser_set_do_overwrite_confirmation(gtk_file_chooser_cast(gtkDialog), confirmOverwrite);
if (!_nameFilters.isEmpty())
setNameFilters(_nameFilters);
if (!_initialDirectory.isEmpty())
setDirectory(_initialDirectory);
for_const (const auto &filename, _initialFiles) {
if (_acceptMode == QFileDialog::AcceptSave) {
QFileInfo fi(filename);
gtk_file_chooser_set_current_folder(gtk_file_chooser_cast(gtkDialog), fi.path().toUtf8());
gtk_file_chooser_set_current_name(gtk_file_chooser_cast(gtkDialog), fi.fileName().toUtf8());
} else if (filename.endsWith('/')) {
gtk_file_chooser_set_current_folder(gtk_file_chooser_cast(gtkDialog), filename.toUtf8());
} else {
gtk_file_chooser_select_filename(gtk_file_chooser_cast(gtkDialog), filename.toUtf8());
}
}
const QString initialNameFilter = _nameFilters.isEmpty() ? QString() : _nameFilters.front();
if (!initialNameFilter.isEmpty())
selectNameFilter(initialNameFilter);
if (CustomButtonsSupported()) {
GtkWidget *acceptButton = gtk_dialog_get_widget_for_response(gtkDialog, GTK_RESPONSE_OK);
if (acceptButton) {
/*if (opts->isLabelExplicitlySet(QFileDialogOptions::Accept))
gtk_button_set_label(gtk_button_cast(acceptButton), opts->labelText(QFileDialogOptions::Accept).toUtf8());
else*/ if (_acceptMode == QFileDialog::AcceptOpen)
gtk_button_set_label(gtk_button_cast(acceptButton), tr::lng_open_link(tr::now).toUtf8());
else
gtk_button_set_label(gtk_button_cast(acceptButton), tr::lng_settings_save(tr::now).toUtf8());
}
GtkWidget *rejectButton = gtk_dialog_get_widget_for_response(gtkDialog, GTK_RESPONSE_CANCEL);
if (rejectButton) {
/*if (opts->isLabelExplicitlySet(QFileDialogOptions::Reject))
gtk_button_set_label(gtk_button_cast(rejectButton), opts->labelText(QFileDialogOptions::Reject).toUtf8());
else*/
gtk_button_set_label(gtk_button_cast(rejectButton), tr::lng_cancel(tr::now).toUtf8());
}
}
}
void GtkFileDialog::setNameFilters(const QStringList &filters) {
GtkDialog *gtkDialog = d->gtkDialog();
foreach (GtkFileFilter *filter, _filters)
gtk_file_chooser_remove_filter(gtk_file_chooser_cast(gtkDialog), filter);
_filters.clear();
_filterNames.clear();
for_const (auto &filter, filters) {
GtkFileFilter *gtkFilter = gtk_file_filter_new();
auto name = filter;//.left(filter.indexOf(QLatin1Char('(')));
auto extensions = cleanFilterList(filter);
gtk_file_filter_set_name(gtkFilter, name.isEmpty() ? extensions.join(QStringLiteral(", ")).toUtf8() : name.toUtf8());
for_const (auto &ext, extensions) {
auto caseInsensitiveExt = QString();
caseInsensitiveExt.reserve(4 * ext.size());
for_const (auto ch, ext) {
auto chLower = ch.toLower();
auto chUpper = ch.toUpper();
if (chLower != chUpper) {
caseInsensitiveExt.append('[').append(chLower).append(chUpper).append(']');
} else {
caseInsensitiveExt.append(ch);
}
}
gtk_file_filter_add_pattern(gtkFilter, caseInsensitiveExt.toUtf8());
}
gtk_file_chooser_add_filter(gtk_file_chooser_cast(gtkDialog), gtkFilter);
_filters.insert(filter, gtkFilter);
_filterNames.insert(gtkFilter, filter);
}
}
} // namespace
bool Supported() {
return internal::GdkHelperLoaded()
&& (gtk_widget_hide_on_delete != nullptr)
&& (gtk_clipboard_store != nullptr)
&& (gtk_clipboard_get != nullptr)
&& (gtk_widget_destroy != nullptr)
&& (gtk_dialog_get_type != nullptr)
&& (gtk_dialog_run != nullptr)
&& (gtk_widget_realize != nullptr)
&& (gdk_window_set_modal_hint != nullptr)
&& (gtk_widget_show != nullptr)
&& (gdk_window_focus != nullptr)
&& (gtk_widget_hide != nullptr)
&& (gtk_widget_hide_on_delete != nullptr)
&& (gtk_file_chooser_dialog_new != nullptr)
&& (gtk_file_chooser_get_type != nullptr)
&& (gtk_file_chooser_set_current_folder != nullptr)
&& (gtk_file_chooser_get_current_folder != nullptr)
&& (gtk_file_chooser_set_current_name != nullptr)
&& (gtk_file_chooser_select_filename != nullptr)
&& (gtk_file_chooser_get_filenames != nullptr)
&& (gtk_file_chooser_set_filter != nullptr)
&& (gtk_file_chooser_get_filter != nullptr)
&& (gtk_window_get_type != nullptr)
&& (gtk_window_set_title != nullptr)
&& (gtk_file_chooser_set_local_only != nullptr)
&& (gtk_file_chooser_set_action != nullptr)
&& (gtk_file_chooser_set_select_multiple != nullptr)
&& (gtk_file_chooser_set_do_overwrite_confirmation != nullptr)
&& (gtk_file_chooser_remove_filter != nullptr)
&& (gtk_file_filter_set_name != nullptr)
&& (gtk_file_filter_add_pattern != nullptr)
&& (gtk_file_chooser_add_filter != nullptr)
&& (gtk_file_filter_new != nullptr);
}
bool Use(Type type) {
// use gtk file dialog on gtk-based desktop environments
// or if QT_QPA_PLATFORMTHEME=(gtk2|gtk3)
// or if portals are used and operation is to open folder
// and portal doesn't support folder choosing
const auto sandboxedOrCustomPortal = InFlatpak()
|| InSnap()
|| UseXDGDesktopPortal();
const auto neededForPortal = (type == Type::ReadFolder)
&& !CanOpenDirectoryWithPortal();
const auto neededNonForced = DesktopEnvironment::IsGtkBased()
|| (sandboxedOrCustomPortal && neededForPortal);
const auto excludeNonForced = sandboxedOrCustomPortal && !neededForPortal;
return IsGtkIntegrationForced()
|| (neededNonForced && !excludeNonForced);
}
bool Get(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
Type type,
QString startFile) {
GtkFileDialog dialog(parent, caption, QString(), filter);
dialog.setModal(true);
if (type == Type::ReadFile || type == Type::ReadFiles) {
dialog.setFileMode((type == Type::ReadFiles) ? QFileDialog::ExistingFiles : QFileDialog::ExistingFile);
dialog.setAcceptMode(QFileDialog::AcceptOpen);
} else if (type == Type::ReadFolder) {
dialog.setAcceptMode(QFileDialog::AcceptOpen);
dialog.setFileMode(QFileDialog::Directory);
dialog.setOption(QFileDialog::ShowDirsOnly);
} else {
dialog.setFileMode(QFileDialog::AnyFile);
dialog.setAcceptMode(QFileDialog::AcceptSave);
}
if (startFile.isEmpty() || startFile.at(0) != '/') {
startFile = cDialogLastPath() + '/' + startFile;
}
dialog.selectFile(startFile);
int res = dialog.exec();
QString path = dialog.directory().absolutePath();
if (path != cDialogLastPath()) {
cSetDialogLastPath(path);
Local::writeSettings();
}
if (res == QDialog::Accepted) {
if (type == Type::ReadFiles) {
files = dialog.selectedFiles();
} else {
files = dialog.selectedFiles().mid(0, 1);
}
return true;
}
files = QStringList();
remoteContent = QByteArray();
return false;
}
} // namespace Gtk
} // namespace FileDialog
} // namespace Platform

View File

@@ -0,0 +1,31 @@
/*
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 "core/file_utilities.h"
namespace Platform {
namespace FileDialog {
namespace Gtk {
using Type = ::FileDialog::internal::Type;
bool Supported();
bool Use(Type type = Type::ReadFile);
bool Get(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
Type type,
QString startFile);
} // namespace Gtk
} // namespace FileDialog
} // namespace Platform

View File

@@ -0,0 +1,523 @@
/*
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 "platform/linux/linux_gtk_integration.h"
#include "platform/linux/linux_gtk_integration_p.h"
#include "base/platform/base_platform_info.h"
#include "platform/linux/linux_xlib_helper.h"
#include "platform/linux/linux_gdk_helper.h"
#include "platform/linux/linux_gtk_file_dialog.h"
#include "platform/linux/linux_open_with_dialog.h"
#include "platform/linux/specific_linux.h"
#include "core/sandbox.h"
#include "core/core_settings.h"
#include "core/application.h"
#include "main/main_domain.h"
#include "mainwindow.h"
namespace Platform {
namespace internal {
using namespace Platform::Gtk;
namespace {
bool GtkTriedToInit = false;
bool GtkLoaded = false;
bool LoadLibrary(QLibrary &lib, const char *name, int version) {
#ifdef LINK_TO_GTK
return true;
#else // LINK_TO_GTK
DEBUG_LOG(("Loading '%1' with version %2...").arg(
QLatin1String(name)).arg(version));
lib.setFileNameAndVersion(QLatin1String(name), version);
if (lib.load()) {
DEBUG_LOG(("Loaded '%1' with version %2!").arg(
QLatin1String(name)).arg(version));
return true;
}
lib.setFileNameAndVersion(QLatin1String(name), QString());
if (lib.load()) {
DEBUG_LOG(("Loaded '%1' without version!").arg(QLatin1String(name)));
return true;
}
LOG(("Could not load '%1' with version %2 :(").arg(
QLatin1String(name)).arg(version));
return false;
#endif // !LINK_TO_GTK
}
void GtkMessageHandler(
const gchar *log_domain,
GLogLevelFlags log_level,
const gchar *message,
gpointer unused_data) {
// Silence false-positive Gtk warnings (we are using Xlib to set
// the WM_TRANSIENT_FOR hint).
if (message != qstr("GtkDialog mapped without a transient parent. "
"This is discouraged.")) {
// For other messages, call the default handler.
g_log_default_handler(log_domain, log_level, message, unused_data);
}
}
bool SetupGtkBase(QLibrary &lib_gtk) {
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_init_check", gtk_init_check)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_check_version", gtk_check_version)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_settings_get_default", gtk_settings_get_default)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_widget_show", gtk_widget_show)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_widget_hide", gtk_widget_hide)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_widget_get_window", gtk_widget_get_window)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_widget_realize", gtk_widget_realize)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_widget_hide_on_delete", gtk_widget_hide_on_delete)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_widget_destroy", gtk_widget_destroy)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_clipboard_get", gtk_clipboard_get)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_clipboard_store", gtk_clipboard_store)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_clipboard_wait_for_contents", gtk_clipboard_wait_for_contents)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_clipboard_wait_for_image", gtk_clipboard_wait_for_image)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_selection_data_targets_include_image", gtk_selection_data_targets_include_image)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_selection_data_free", gtk_selection_data_free)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_dialog_new", gtk_file_chooser_dialog_new)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_get_type", gtk_file_chooser_get_type)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_image_get_type", gtk_image_get_type)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_set_current_folder", gtk_file_chooser_set_current_folder)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_get_current_folder", gtk_file_chooser_get_current_folder)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_set_current_name", gtk_file_chooser_set_current_name)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_select_filename", gtk_file_chooser_select_filename)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_get_filenames", gtk_file_chooser_get_filenames)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_set_filter", gtk_file_chooser_set_filter)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_get_filter", gtk_file_chooser_get_filter)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_window_get_type", gtk_window_get_type)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_window_set_title", gtk_window_set_title)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_set_local_only", gtk_file_chooser_set_local_only)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_set_action", gtk_file_chooser_set_action)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_set_select_multiple", gtk_file_chooser_set_select_multiple)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_set_do_overwrite_confirmation", gtk_file_chooser_set_do_overwrite_confirmation)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_remove_filter", gtk_file_chooser_remove_filter)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_filter_set_name", gtk_file_filter_set_name)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_filter_add_pattern", gtk_file_filter_add_pattern)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_add_filter", gtk_file_chooser_add_filter)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_set_preview_widget", gtk_file_chooser_set_preview_widget)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_get_preview_filename", gtk_file_chooser_get_preview_filename)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_chooser_set_preview_widget_active", gtk_file_chooser_set_preview_widget_active)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_file_filter_new", gtk_file_filter_new)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_image_new", gtk_image_new)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_image_set_from_pixbuf", gtk_image_set_from_pixbuf)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gdk_window_set_modal_hint", gdk_window_set_modal_hint)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gdk_window_focus", gdk_window_focus)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_dialog_get_type", gtk_dialog_get_type)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gtk_dialog_run", gtk_dialog_run)) return false;
if (!LOAD_GTK_SYMBOL(lib_gtk, "gdk_atom_intern", gdk_atom_intern)) return false;
if (LOAD_GTK_SYMBOL(lib_gtk, "gdk_set_allowed_backends", gdk_set_allowed_backends)) {
// We work only with Wayland and X11 GDK backends.
// Otherwise we get segfault in Ubuntu 17.04 in gtk_init_check() call.
// See https://github.com/telegramdesktop/tdesktop/issues/3176
// See https://github.com/telegramdesktop/tdesktop/issues/3162
if(IsWayland()) {
DEBUG_LOG(("Limit allowed GDK backends to wayland,x11"));
gdk_set_allowed_backends("wayland,x11");
} else {
DEBUG_LOG(("Limit allowed GDK backends to x11,wayland"));
gdk_set_allowed_backends("x11,wayland");
}
}
// gtk_init will reset the Xlib error handler,
// and that causes Qt applications to quit on X errors.
// Therefore, we need to manually restore it.
XErrorHandlerRestorer handlerRestorer;
DEBUG_LOG(("Library gtk functions loaded!"));
GtkTriedToInit = true;
if (!gtk_init_check(0, 0)) {
gtk_init_check = nullptr;
DEBUG_LOG(("Failed to gtk_init_check(0, 0)!"));
return false;
}
DEBUG_LOG(("Checked gtk with gtk_init_check!"));
// Use our custom log handler.
g_log_set_handler("Gtk", G_LOG_LEVEL_MESSAGE, GtkMessageHandler, nullptr);
return true;
}
bool GetImageFromClipboardSupported() {
return (gtk_clipboard_get != nullptr)
&& (gtk_clipboard_wait_for_contents != nullptr)
&& (gtk_clipboard_wait_for_image != nullptr)
&& (gtk_selection_data_targets_include_image != nullptr)
&& (gtk_selection_data_free != nullptr)
&& (gdk_pixbuf_get_pixels != nullptr)
&& (gdk_pixbuf_get_width != nullptr)
&& (gdk_pixbuf_get_height != nullptr)
&& (gdk_pixbuf_get_rowstride != nullptr)
&& (gdk_pixbuf_get_has_alpha != nullptr)
&& (gdk_atom_intern != nullptr);
}
template <typename T>
std::optional<T> GtkSetting(const QString &propertyName) {
const auto integration = GtkIntegration::Instance();
if (!integration
|| !integration->loaded()
|| gtk_settings_get_default == nullptr) {
return std::nullopt;
}
auto settings = gtk_settings_get_default();
T value;
g_object_get(settings, propertyName.toUtf8(), &value, nullptr);
return value;
}
bool IconThemeShouldBeSet() {
// change the icon theme only if
// it isn't already set by a platformtheme plugin
// if QT_QPA_PLATFORMTHEME=(gtk2|gtk3), then force-apply the icon theme
static const auto Result =
// QGenericUnixTheme
(QIcon::themeName() == qstr("hicolor")
&& QIcon::fallbackThemeName() == qstr("hicolor"))
// QGnomeTheme
|| (QIcon::themeName() == qstr("Adwaita")
&& QIcon::fallbackThemeName() == qstr("gnome"))
// qt5ct
|| (QIcon::themeName().isEmpty()
&& QIcon::fallbackThemeName().isEmpty())
|| IsGtkIntegrationForced();
return Result;
}
bool CursorSizeShouldBeSet() {
// change the cursor size only on Wayland and if it wasn't already set
static const auto Result = IsWayland()
&& qEnvironmentVariableIsEmpty("XCURSOR_SIZE");
return Result;
}
void SetScaleFactor() {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
const auto integration = GtkIntegration::Instance();
const auto ratio = Core::Sandbox::Instance().devicePixelRatio();
if (!integration || ratio > 1.) {
return;
}
const auto scaleFactor = integration->scaleFactor().value_or(1);
if (scaleFactor == 1) {
return;
}
LOG(("GTK scale factor: %1").arg(scaleFactor));
cSetScreenScale(std::clamp(scaleFactor * 100, 100, 300));
});
}
void SetIconTheme() {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
const auto integration = GtkIntegration::Instance();
if (!integration || !IconThemeShouldBeSet()) {
return;
}
const auto themeName = integration->getStringSetting(
qsl("gtk-icon-theme-name"));
const auto fallbackThemeName = integration->getStringSetting(
qsl("gtk-fallback-icon-theme"));
if (!themeName.has_value() || !fallbackThemeName.has_value()) {
return;
}
DEBUG_LOG(("Setting GTK icon theme"));
QIcon::setThemeName(*themeName);
QIcon::setFallbackThemeName(*fallbackThemeName);
DEBUG_LOG(("New icon theme: %1").arg(QIcon::themeName()));
DEBUG_LOG(("New fallback icon theme: %1").arg(
QIcon::fallbackThemeName()));
SetApplicationIcon(Window::CreateIcon());
if (App::wnd()) {
App::wnd()->setWindowIcon(Window::CreateIcon());
}
Core::App().domain().notifyUnreadBadgeChanged();
});
}
void SetCursorSize() {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
const auto integration = GtkIntegration::Instance();
if (!integration || !CursorSizeShouldBeSet()) {
return;
}
const auto newCursorSize = integration->getIntSetting(
qsl("gtk-cursor-theme-size"));
if (!newCursorSize.has_value()) {
return;
}
DEBUG_LOG(("Setting GTK cursor size"));
qputenv("XCURSOR_SIZE", QByteArray::number(*newCursorSize));
DEBUG_LOG(("New cursor size: %1").arg(*newCursorSize));
});
}
void DarkModeChanged() {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
Core::App().settings().setSystemDarkMode(IsDarkMode());
});
}
void DecorationLayoutChanged() {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
Core::App().settings().setWindowControlsLayout(
WindowControlsLayout());
});
}
} // namespace
GtkIntegration::GtkIntegration() {
}
GtkIntegration *GtkIntegration::Instance() {
static const auto useGtkIntegration = !qEnvironmentVariableIsSet(
kDisableGtkIntegration.utf8());
if (!useGtkIntegration) {
return nullptr;
}
static GtkIntegration instance;
return &instance;
}
void GtkIntegration::load() {
Expects(!GtkLoaded);
DEBUG_LOG(("Loading GTK"));
QLibrary lib_gtk;
lib_gtk.setLoadHints(QLibrary::DeepBindHint);
if (LoadLibrary(lib_gtk, "gtk-3", 0)) {
GtkLoaded = SetupGtkBase(lib_gtk);
}
if (!GtkLoaded
&& !GtkTriedToInit
&& LoadLibrary(lib_gtk, "gtk-x11-2.0", 0)) {
GtkLoaded = SetupGtkBase(lib_gtk);
}
if (GtkLoaded) {
LOAD_GTK_SYMBOL(lib_gtk, "gdk_display_get_default", gdk_display_get_default);
LOAD_GTK_SYMBOL(lib_gtk, "gdk_display_get_primary_monitor", gdk_display_get_primary_monitor);
LOAD_GTK_SYMBOL(lib_gtk, "gdk_monitor_get_scale_factor", gdk_monitor_get_scale_factor);
LOAD_GTK_SYMBOL(lib_gtk, "gdk_pixbuf_new_from_file_at_size", gdk_pixbuf_new_from_file_at_size);
LOAD_GTK_SYMBOL(lib_gtk, "gdk_pixbuf_get_has_alpha", gdk_pixbuf_get_has_alpha);
LOAD_GTK_SYMBOL(lib_gtk, "gdk_pixbuf_get_pixels", gdk_pixbuf_get_pixels);
LOAD_GTK_SYMBOL(lib_gtk, "gdk_pixbuf_get_width", gdk_pixbuf_get_width);
LOAD_GTK_SYMBOL(lib_gtk, "gdk_pixbuf_get_height", gdk_pixbuf_get_height);
LOAD_GTK_SYMBOL(lib_gtk, "gdk_pixbuf_get_rowstride", gdk_pixbuf_get_rowstride);
GdkHelperLoad(lib_gtk);
LOAD_GTK_SYMBOL(lib_gtk, "gtk_dialog_get_widget_for_response", gtk_dialog_get_widget_for_response);
LOAD_GTK_SYMBOL(lib_gtk, "gtk_button_set_label", gtk_button_set_label);
LOAD_GTK_SYMBOL(lib_gtk, "gtk_button_get_type", gtk_button_get_type);
LOAD_GTK_SYMBOL(lib_gtk, "gtk_app_chooser_dialog_new", gtk_app_chooser_dialog_new);
LOAD_GTK_SYMBOL(lib_gtk, "gtk_app_chooser_get_app_info", gtk_app_chooser_get_app_info);
LOAD_GTK_SYMBOL(lib_gtk, "gtk_app_chooser_get_type", gtk_app_chooser_get_type);
SetScaleFactor();
SetIconTheme();
SetCursorSize();
const auto settings = gtk_settings_get_default();
g_signal_connect(
settings,
"notify::gtk-icon-theme-name",
G_CALLBACK(SetIconTheme),
nullptr);
g_signal_connect(
settings,
"notify::gtk-theme-name",
G_CALLBACK(DarkModeChanged),
nullptr);
g_signal_connect(
settings,
"notify::gtk-cursor-theme-size",
G_CALLBACK(SetCursorSize),
nullptr);
if (checkVersion(3, 0, 0)) {
g_signal_connect(
settings,
"notify::gtk-application-prefer-dark-theme",
G_CALLBACK(DarkModeChanged),
nullptr);
}
if (checkVersion(3, 12, 0)) {
g_signal_connect(
settings,
"notify::gtk-decoration-layout",
G_CALLBACK(DecorationLayoutChanged),
nullptr);
}
} else {
LOG(("Could not load gtk-3 or gtk-x11-2.0!"));
}
}
bool GtkIntegration::loaded() const {
return GtkLoaded;
}
bool GtkIntegration::checkVersion(uint major, uint minor, uint micro) const {
return (loaded() && gtk_check_version != nullptr)
? !gtk_check_version(major, minor, micro)
: false;
}
std::optional<bool> GtkIntegration::getBoolSetting(
const QString &propertyName) const {
const auto value = GtkSetting<gboolean>(propertyName);
if (!value.has_value()) {
return std::nullopt;
}
DEBUG_LOG(("Getting GTK setting, %1: %2")
.arg(propertyName)
.arg(Logs::b(*value)));
return *value;
}
std::optional<int> GtkIntegration::getIntSetting(
const QString &propertyName) const {
const auto value = GtkSetting<gint>(propertyName);
if (value.has_value()) {
DEBUG_LOG(("Getting GTK setting, %1: %2")
.arg(propertyName)
.arg(*value));
}
return value;
}
std::optional<QString> GtkIntegration::getStringSetting(
const QString &propertyName) const {
auto value = GtkSetting<gchararray>(propertyName);
if (!value.has_value()) {
return std::nullopt;
}
const auto str = QString::fromUtf8(*value);
g_free(*value);
DEBUG_LOG(("Getting GTK setting, %1: '%2'").arg(propertyName).arg(str));
return str;
}
std::optional<int> GtkIntegration::scaleFactor() const {
if (!loaded()
|| (gdk_display_get_default == nullptr)
|| (gdk_display_get_primary_monitor == nullptr)
|| (gdk_monitor_get_scale_factor == nullptr)) {
return std::nullopt;
}
return gdk_monitor_get_scale_factor(
gdk_display_get_primary_monitor(gdk_display_get_default()));
}
bool GtkIntegration::fileDialogSupported() const {
return FileDialog::Gtk::Supported();
}
bool GtkIntegration::useFileDialog(FileDialogType type) const {
return FileDialog::Gtk::Use(type);
}
bool GtkIntegration::getFileDialog(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
FileDialogType type,
QString startFile) const {
return FileDialog::Gtk::Get(
parent,
files,
remoteContent,
caption,
filter,
type,
startFile);
}
bool GtkIntegration::showOpenWithDialog(const QString &filepath) const {
return File::internal::ShowOpenWithDialog(filepath);
}
QImage GtkIntegration::getImageFromClipboard() const {
QImage data;
if (!GetImageFromClipboardSupported()) {
return data;
}
const auto clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
if (!clipboard) {
return data;
}
auto gsel = gtk_clipboard_wait_for_contents(
clipboard,
gdk_atom_intern("TARGETS", true));
if (gsel) {
if (gtk_selection_data_targets_include_image(gsel, false)) {
auto img = gtk_clipboard_wait_for_image(clipboard);
if (img) {
data = QImage(
gdk_pixbuf_get_pixels(img),
gdk_pixbuf_get_width(img),
gdk_pixbuf_get_height(img),
gdk_pixbuf_get_rowstride(img),
gdk_pixbuf_get_has_alpha(img)
? QImage::Format_RGBA8888
: QImage::Format_RGB888).copy();
g_object_unref(img);
}
}
gtk_selection_data_free(gsel);
}
return data;
}
} // namespace internal
} // namespace Platform

View File

@@ -0,0 +1,61 @@
/*
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 "core/file_utilities.h"
namespace Platform {
namespace internal {
inline constexpr auto kDisableGtkIntegration = "TDESKTOP_DISABLE_GTK_INTEGRATION"_cs;
class GtkIntegration {
public:
static GtkIntegration *Instance();
void load();
[[nodiscard]] bool loaded() const;
[[nodiscard]] bool checkVersion(
uint major,
uint minor,
uint micro) const;
[[nodiscard]] std::optional<bool> getBoolSetting(
const QString &propertyName) const;
[[nodiscard]] std::optional<int> getIntSetting(
const QString &propertyName) const;
[[nodiscard]] std::optional<QString> getStringSetting(
const QString &propertyName) const;
[[nodiscard]] std::optional<int> scaleFactor() const;
using FileDialogType = ::FileDialog::internal::Type;
[[nodiscard]] bool fileDialogSupported() const;
[[nodiscard]] bool useFileDialog(
FileDialogType type = FileDialogType::ReadFile) const;
[[nodiscard]] bool getFileDialog(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
FileDialogType type,
QString startFile) const;
[[nodiscard]] bool showOpenWithDialog(const QString &filepath) const;
[[nodiscard]] QImage getImageFromClipboard() const;
private:
GtkIntegration();
};
} // namespace internal
} // namespace Platform

View File

@@ -0,0 +1,78 @@
/*
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 "platform/linux/linux_gtk_integration.h"
namespace Platform {
namespace internal {
GtkIntegration::GtkIntegration() {
}
GtkIntegration *GtkIntegration::Instance() {
return nullptr;
}
void GtkIntegration::load() {
}
bool GtkIntegration::loaded() const {
return false;
}
bool GtkIntegration::checkVersion(uint major, uint minor, uint micro) const {
return false;
}
std::optional<bool> GtkIntegration::getBoolSetting(
const QString &propertyName) const {
return std::nullopt;
}
std::optional<int> GtkIntegration::getIntSetting(
const QString &propertyName) const {
return std::nullopt;
}
std::optional<QString> GtkIntegration::getStringSetting(
const QString &propertyName) const {
return std::nullopt;
}
std::optional<int> GtkIntegration::scaleFactor() const {
return std::nullopt;
}
bool GtkIntegration::fileDialogSupported() const {
return false;
}
bool GtkIntegration::useFileDialog(FileDialogType type) const {
return false;
}
bool GtkIntegration::getFileDialog(
QPointer<QWidget> parent,
QStringList &files,
QByteArray &remoteContent,
const QString &caption,
const QString &filter,
FileDialogType type,
QString startFile) const {
return false;
}
bool GtkIntegration::showOpenWithDialog(const QString &filepath) const {
return false;
}
QImage GtkIntegration::getImageFromClipboard() const {
return {};
}
} // namespace internal
} // namespace Platform

View File

@@ -0,0 +1,162 @@
/*
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 <QtCore/QLibrary>
extern "C" {
#undef signals
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#define signals public
} // extern "C"
#if defined DESKTOP_APP_USE_PACKAGED && !defined DESKTOP_APP_USE_PACKAGED_LAZY
#define LINK_TO_GTK
#endif // DESKTOP_APP_USE_PACKAGED && !DESKTOP_APP_USE_PACKAGED_LAZY
#ifdef LINK_TO_GTK
#define LOAD_GTK_SYMBOL(lib, name, func) (func = ::func)
#else // LINK_TO_GTK
#define LOAD_GTK_SYMBOL Platform::Gtk::LoadSymbol
#endif // !LINK_TO_GTK
// To be able to compile with gtk-2.0 headers as well
#define GdkMonitor GdkScreen
typedef struct _GtkAppChooser GtkAppChooser;
namespace Platform {
namespace Gtk {
template <typename Function>
bool LoadSymbol(QLibrary &lib, const char *name, Function &func) {
func = nullptr;
if (!lib.isLoaded()) {
return false;
}
func = reinterpret_cast<Function>(lib.resolve(name));
if (func) {
return true;
}
LOG(("Error: failed to load '%1' function!").arg(name));
return false;
}
inline gboolean (*gtk_init_check)(int *argc, char ***argv) = nullptr;
inline const gchar* (*gtk_check_version)(guint required_major, guint required_minor, guint required_micro) = nullptr;
inline GtkSettings* (*gtk_settings_get_default)(void) = nullptr;
inline void (*gtk_widget_show)(GtkWidget *widget) = nullptr;
inline void (*gtk_widget_hide)(GtkWidget *widget) = nullptr;
inline GdkWindow* (*gtk_widget_get_window)(GtkWidget *widget) = nullptr;
inline void (*gtk_widget_realize)(GtkWidget *widget) = nullptr;
inline gboolean (*gtk_widget_hide_on_delete)(GtkWidget *widget) = nullptr;
inline void (*gtk_widget_destroy)(GtkWidget *widget) = nullptr;
inline GtkClipboard* (*gtk_clipboard_get)(GdkAtom selection) = nullptr;
inline void (*gtk_clipboard_store)(GtkClipboard *clipboard) = nullptr;
inline GtkSelectionData* (*gtk_clipboard_wait_for_contents)(GtkClipboard *clipboard, GdkAtom target) = nullptr;
inline GdkPixbuf* (*gtk_clipboard_wait_for_image)(GtkClipboard *clipboard) = nullptr;
inline gboolean (*gtk_selection_data_targets_include_image)(const GtkSelectionData *selection_data, gboolean writable) = nullptr;
inline void (*gtk_selection_data_free)(GtkSelectionData *data) = nullptr;
inline GtkWidget* (*gtk_file_chooser_dialog_new)(const gchar *title, GtkWindow *parent, GtkFileChooserAction action, const gchar *first_button_text, ...) G_GNUC_NULL_TERMINATED = nullptr;
inline gboolean (*gtk_file_chooser_set_current_folder)(GtkFileChooser *chooser, const gchar *filename) = nullptr;
inline gchar* (*gtk_file_chooser_get_current_folder)(GtkFileChooser *chooser) = nullptr;
inline void (*gtk_file_chooser_set_current_name)(GtkFileChooser *chooser, const gchar *name) = nullptr;
inline gboolean (*gtk_file_chooser_select_filename)(GtkFileChooser *chooser, const gchar *filename) = nullptr;
inline GSList* (*gtk_file_chooser_get_filenames)(GtkFileChooser *chooser) = nullptr;
inline void (*gtk_file_chooser_set_filter)(GtkFileChooser *chooser, GtkFileFilter *filter) = nullptr;
inline GtkFileFilter* (*gtk_file_chooser_get_filter)(GtkFileChooser *chooser) = nullptr;
inline void (*gtk_window_set_title)(GtkWindow *window, const gchar *title) = nullptr;
inline void (*gtk_file_chooser_set_local_only)(GtkFileChooser *chooser, gboolean local_only) = nullptr;
inline void (*gtk_file_chooser_set_action)(GtkFileChooser *chooser, GtkFileChooserAction action) = nullptr;
inline void (*gtk_file_chooser_set_select_multiple)(GtkFileChooser *chooser, gboolean select_multiple) = nullptr;
inline void (*gtk_file_chooser_set_do_overwrite_confirmation)(GtkFileChooser *chooser, gboolean do_overwrite_confirmation) = nullptr;
inline GtkWidget* (*gtk_dialog_get_widget_for_response)(GtkDialog *dialog, gint response_id) = nullptr;
inline void (*gtk_button_set_label)(GtkButton *button, const gchar *label) = nullptr;
inline void (*gtk_file_chooser_remove_filter)(GtkFileChooser *chooser, GtkFileFilter *filter) = nullptr;
inline void (*gtk_file_filter_set_name)(GtkFileFilter *filter, const gchar *name) = nullptr;
inline void (*gtk_file_filter_add_pattern)(GtkFileFilter *filter, const gchar *pattern) = nullptr;
inline void (*gtk_file_chooser_add_filter)(GtkFileChooser *chooser, GtkFileFilter *filter) = nullptr;
inline void (*gtk_file_chooser_set_preview_widget)(GtkFileChooser *chooser, GtkWidget *preview_widget) = nullptr;
inline gchar* (*gtk_file_chooser_get_preview_filename)(GtkFileChooser *chooser) = nullptr;
inline void (*gtk_file_chooser_set_preview_widget_active)(GtkFileChooser *chooser, gboolean active) = nullptr;
inline GtkFileFilter* (*gtk_file_filter_new)(void) = nullptr;
inline GtkWidget* (*gtk_image_new)(void) = nullptr;
inline void (*gtk_image_set_from_pixbuf)(GtkImage *image, GdkPixbuf *pixbuf) = nullptr;
inline GtkWidget* (*gtk_app_chooser_dialog_new)(GtkWindow *parent, GtkDialogFlags flags, GFile *file) = nullptr;
inline GAppInfo* (*gtk_app_chooser_get_app_info)(GtkAppChooser *self) = nullptr;
inline void (*gdk_set_allowed_backends)(const gchar *backends) = nullptr;
inline void (*gdk_window_set_modal_hint)(GdkWindow *window, gboolean modal) = nullptr;
inline void (*gdk_window_focus)(GdkWindow *window, guint32 timestamp) = nullptr;
template <typename Result, typename Object>
inline Result *g_type_cic_helper(Object *instance, GType iface_type) {
return reinterpret_cast<Result*>(g_type_check_instance_cast(reinterpret_cast<GTypeInstance*>(instance), iface_type));
}
inline GType (*gtk_dialog_get_type)(void) G_GNUC_CONST = nullptr;
template <typename Object>
inline GtkDialog *gtk_dialog_cast(Object *obj) {
return g_type_cic_helper<GtkDialog, Object>(obj, gtk_dialog_get_type());
}
inline GType (*gtk_file_chooser_get_type)(void) G_GNUC_CONST = nullptr;
template <typename Object>
inline GtkFileChooser *gtk_file_chooser_cast(Object *obj) {
return g_type_cic_helper<GtkFileChooser, Object>(obj, gtk_file_chooser_get_type());
}
inline GType (*gtk_image_get_type)(void) G_GNUC_CONST = nullptr;
template <typename Object>
inline GtkImage *gtk_image_cast(Object *obj) {
return g_type_cic_helper<GtkImage, Object>(obj, gtk_image_get_type());
}
inline GType (*gtk_button_get_type)(void) G_GNUC_CONST = nullptr;
template <typename Object>
inline GtkButton *gtk_button_cast(Object *obj) {
return g_type_cic_helper<GtkButton, Object>(obj, gtk_button_get_type());
}
inline GType (*gtk_window_get_type)(void) G_GNUC_CONST = nullptr;
template <typename Object>
inline GtkWindow *gtk_window_cast(Object *obj) {
return g_type_cic_helper<GtkWindow, Object>(obj, gtk_window_get_type());
}
inline GType (*gtk_app_chooser_get_type)(void) G_GNUC_CONST = nullptr;
template <typename Object>
inline GtkAppChooser *gtk_app_chooser_cast(Object *obj) {
return g_type_cic_helper<GtkAppChooser, Object>(obj, gtk_app_chooser_get_type());
}
template <typename Object>
inline bool g_type_cit_helper(Object *instance, GType iface_type) {
if (!instance) return false;
auto ginstance = reinterpret_cast<GTypeInstance*>(instance);
if (ginstance->g_class && ginstance->g_class->g_type == iface_type) {
return true;
}
return g_type_check_instance_is_a(ginstance, iface_type);
}
inline gint (*gtk_dialog_run)(GtkDialog *dialog) = nullptr;
inline GdkAtom (*gdk_atom_intern)(const gchar *atom_name, gboolean only_if_exists) = nullptr;
inline GdkDisplay* (*gdk_display_get_default)(void) = nullptr;
inline GdkMonitor* (*gdk_display_get_primary_monitor)(GdkDisplay *display) = nullptr;
inline int (*gdk_monitor_get_scale_factor)(GdkMonitor *monitor) = nullptr;
inline GdkPixbuf* (*gdk_pixbuf_new_from_file_at_size)(const gchar *filename, int width, int height, GError **error) = nullptr;
inline gboolean (*gdk_pixbuf_get_has_alpha)(const GdkPixbuf *pixbuf) = nullptr;
inline guchar* (*gdk_pixbuf_get_pixels)(const GdkPixbuf *pixbuf) = nullptr;
inline int (*gdk_pixbuf_get_width)(const GdkPixbuf *pixbuf) = nullptr;
inline int (*gdk_pixbuf_get_height)(const GdkPixbuf *pixbuf) = nullptr;
inline int (*gdk_pixbuf_get_rowstride)(const GdkPixbuf *pixbuf) = nullptr;
} // namespace Gtk
} // namespace Platform

View File

@@ -1,360 +0,0 @@
/*
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 "platform/linux/linux_libs.h"
#include "base/platform/base_platform_info.h"
#include "platform/linux/linux_xlib_helper.h"
#include "platform/linux/linux_gdk_helper.h"
#include "platform/linux/specific_linux.h"
#include "core/sandbox.h"
#include "core/core_settings.h"
#include "core/application.h"
#include "main/main_domain.h"
#include "mainwindow.h"
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
using Platform::internal::XErrorHandlerRestorer;
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
namespace Platform {
namespace Libs {
namespace {
bool gtkTriedToInit = false;
bool gtkLoaded = false;
bool loadLibrary(QLibrary &lib, const char *name, int version) {
#if defined DESKTOP_APP_USE_PACKAGED && !defined DESKTOP_APP_USE_PACKAGED_LAZY
return true;
#else // DESKTOP_APP_USE_PACKAGED && !DESKTOP_APP_USE_PACKAGED_LAZY
DEBUG_LOG(("Loading '%1' with version %2...").arg(QLatin1String(name)).arg(version));
lib.setFileNameAndVersion(QLatin1String(name), version);
if (lib.load()) {
DEBUG_LOG(("Loaded '%1' with version %2!").arg(QLatin1String(name)).arg(version));
return true;
}
lib.setFileNameAndVersion(QLatin1String(name), QString());
if (lib.load()) {
DEBUG_LOG(("Loaded '%1' without version!").arg(QLatin1String(name)));
return true;
}
LOG(("Could not load '%1' with version %2 :(").arg(QLatin1String(name)).arg(version));
return false;
#endif // !DESKTOP_APP_USE_PACKAGED || DESKTOP_APP_USE_PACKAGED_LAZY
}
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
void gtkMessageHandler(
const gchar *log_domain,
GLogLevelFlags log_level,
const gchar *message,
gpointer unused_data) {
// Silence false-positive Gtk warnings (we are using Xlib to set
// the WM_TRANSIENT_FOR hint).
if (message != qstr("GtkDialog mapped without a transient parent. "
"This is discouraged.")) {
// For other messages, call the default handler.
g_log_default_handler(log_domain, log_level, message, unused_data);
}
}
bool setupGtkBase(QLibrary &lib_gtk) {
if (!LOAD_SYMBOL(lib_gtk, "gtk_init_check", gtk_init_check)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_check_version", gtk_check_version)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_settings_get_default", gtk_settings_get_default)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_widget_show", gtk_widget_show)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_widget_hide", gtk_widget_hide)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_widget_get_window", gtk_widget_get_window)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_widget_realize", gtk_widget_realize)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_widget_hide_on_delete", gtk_widget_hide_on_delete)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_widget_destroy", gtk_widget_destroy)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_clipboard_get", gtk_clipboard_get)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_clipboard_store", gtk_clipboard_store)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_clipboard_wait_for_contents", gtk_clipboard_wait_for_contents)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_clipboard_wait_for_image", gtk_clipboard_wait_for_image)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_selection_data_targets_include_image", gtk_selection_data_targets_include_image)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_selection_data_free", gtk_selection_data_free)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_dialog_new", gtk_file_chooser_dialog_new)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_get_type", gtk_file_chooser_get_type)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_image_get_type", gtk_image_get_type)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_set_current_folder", gtk_file_chooser_set_current_folder)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_get_current_folder", gtk_file_chooser_get_current_folder)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_set_current_name", gtk_file_chooser_set_current_name)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_select_filename", gtk_file_chooser_select_filename)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_get_filenames", gtk_file_chooser_get_filenames)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_set_filter", gtk_file_chooser_set_filter)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_get_filter", gtk_file_chooser_get_filter)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_window_get_type", gtk_window_get_type)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_window_set_title", gtk_window_set_title)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_set_local_only", gtk_file_chooser_set_local_only)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_set_action", gtk_file_chooser_set_action)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_set_select_multiple", gtk_file_chooser_set_select_multiple)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_set_do_overwrite_confirmation", gtk_file_chooser_set_do_overwrite_confirmation)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_remove_filter", gtk_file_chooser_remove_filter)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_filter_set_name", gtk_file_filter_set_name)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_filter_add_pattern", gtk_file_filter_add_pattern)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_add_filter", gtk_file_chooser_add_filter)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_set_preview_widget", gtk_file_chooser_set_preview_widget)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_get_preview_filename", gtk_file_chooser_get_preview_filename)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_chooser_set_preview_widget_active", gtk_file_chooser_set_preview_widget_active)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_file_filter_new", gtk_file_filter_new)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_image_new", gtk_image_new)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_image_set_from_pixbuf", gtk_image_set_from_pixbuf)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gdk_window_set_modal_hint", gdk_window_set_modal_hint)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gdk_window_focus", gdk_window_focus)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_dialog_get_type", gtk_dialog_get_type)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gtk_dialog_run", gtk_dialog_run)) return false;
if (!LOAD_SYMBOL(lib_gtk, "gdk_atom_intern", gdk_atom_intern)) return false;
if (LOAD_SYMBOL(lib_gtk, "gdk_set_allowed_backends", gdk_set_allowed_backends)) {
// We work only with Wayland and X11 GDK backends.
// Otherwise we get segfault in Ubuntu 17.04 in gtk_init_check() call.
// See https://github.com/telegramdesktop/tdesktop/issues/3176
// See https://github.com/telegramdesktop/tdesktop/issues/3162
if(IsWayland()) {
DEBUG_LOG(("Limit allowed GDK backends to wayland,x11"));
gdk_set_allowed_backends("wayland,x11");
} else {
DEBUG_LOG(("Limit allowed GDK backends to x11,wayland"));
gdk_set_allowed_backends("x11,wayland");
}
}
// gtk_init will reset the Xlib error handler, and that causes
// Qt applications to quit on X errors. Therefore, we need to manually restore it.
XErrorHandlerRestorer handlerRestorer;
DEBUG_LOG(("Library gtk functions loaded!"));
gtkTriedToInit = true;
if (!gtk_init_check(0, 0)) {
gtk_init_check = nullptr;
DEBUG_LOG(("Failed to gtk_init_check(0, 0)!"));
return false;
}
DEBUG_LOG(("Checked gtk with gtk_init_check!"));
// Use our custom log handler.
g_log_set_handler("Gtk", G_LOG_LEVEL_MESSAGE, gtkMessageHandler, nullptr);
return true;
}
bool IconThemeShouldBeSet() {
// change the icon theme only if it isn't already set by a platformtheme plugin
// if QT_QPA_PLATFORMTHEME=(gtk2|gtk3), then force-apply the icon theme
static const auto Result =
// QGenericUnixTheme
(QIcon::themeName() == qstr("hicolor")
&& QIcon::fallbackThemeName() == qstr("hicolor"))
// QGnomeTheme
|| (QIcon::themeName() == qstr("Adwaita")
&& QIcon::fallbackThemeName() == qstr("gnome"))
// qt5ct
|| (QIcon::themeName().isEmpty()
&& QIcon::fallbackThemeName().isEmpty())
|| IsGtkIntegrationForced();
return Result;
}
bool CursorSizeShouldBeSet() {
// change the cursor size only on Wayland and if it wasn't already set
static const auto Result = IsWayland()
&& qEnvironmentVariableIsEmpty("XCURSOR_SIZE");
return Result;
}
void SetIconTheme() {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
if (GtkSettingSupported()
&& GtkLoaded()
&& IconThemeShouldBeSet()) {
DEBUG_LOG(("Setting GTK icon theme"));
QIcon::setThemeName(GtkSetting("gtk-icon-theme-name"));
QIcon::setFallbackThemeName(GtkSetting("gtk-fallback-icon-theme"));
DEBUG_LOG(("New icon theme: %1").arg(QIcon::themeName()));
DEBUG_LOG(("New fallback icon theme: %1").arg(QIcon::fallbackThemeName()));
SetApplicationIcon(Window::CreateIcon());
if (App::wnd()) {
App::wnd()->setWindowIcon(Window::CreateIcon());
}
Core::App().domain().notifyUnreadBadgeChanged();
}
});
}
void SetCursorSize() {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
if (GtkSettingSupported()
&& GtkLoaded()
&& CursorSizeShouldBeSet()) {
DEBUG_LOG(("Setting GTK cursor size"));
const auto newCursorSize = GtkSetting<gint>("gtk-cursor-theme-size");
qputenv("XCURSOR_SIZE", QByteArray::number(newCursorSize));
DEBUG_LOG(("New cursor size: %1").arg(newCursorSize));
}
});
}
void DarkModeChanged() {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
Core::App().settings().setSystemDarkMode(IsDarkMode());
});
}
void DecorationLayoutChanged() {
Core::Sandbox::Instance().customEnterFromEventLoop([] {
Core::App().settings().setWindowControlsLayout(WindowControlsLayout());
});
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
} // namespace
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
f_gtk_init_check gtk_init_check = nullptr;
f_gtk_check_version gtk_check_version = nullptr;
f_gtk_settings_get_default gtk_settings_get_default = nullptr;
f_gtk_widget_show gtk_widget_show = nullptr;
f_gtk_widget_hide gtk_widget_hide = nullptr;
f_gtk_widget_get_window gtk_widget_get_window = nullptr;
f_gtk_widget_realize gtk_widget_realize = nullptr;
f_gtk_widget_hide_on_delete gtk_widget_hide_on_delete = nullptr;
f_gtk_widget_destroy gtk_widget_destroy = nullptr;
f_gtk_clipboard_get gtk_clipboard_get = nullptr;
f_gtk_clipboard_store gtk_clipboard_store = nullptr;
f_gtk_clipboard_wait_for_contents gtk_clipboard_wait_for_contents = nullptr;
f_gtk_clipboard_wait_for_image gtk_clipboard_wait_for_image = nullptr;
f_gtk_selection_data_targets_include_image gtk_selection_data_targets_include_image = nullptr;
f_gtk_selection_data_free gtk_selection_data_free = nullptr;
f_gtk_file_chooser_dialog_new gtk_file_chooser_dialog_new = nullptr;
f_gtk_file_chooser_get_type gtk_file_chooser_get_type = nullptr;
f_gtk_image_get_type gtk_image_get_type = nullptr;
f_gtk_file_chooser_set_current_folder gtk_file_chooser_set_current_folder = nullptr;
f_gtk_file_chooser_get_current_folder gtk_file_chooser_get_current_folder = nullptr;
f_gtk_file_chooser_set_current_name gtk_file_chooser_set_current_name = nullptr;
f_gtk_file_chooser_select_filename gtk_file_chooser_select_filename = nullptr;
f_gtk_file_chooser_get_filenames gtk_file_chooser_get_filenames = nullptr;
f_gtk_file_chooser_set_filter gtk_file_chooser_set_filter = nullptr;
f_gtk_file_chooser_get_filter gtk_file_chooser_get_filter = nullptr;
f_gtk_window_get_type gtk_window_get_type = nullptr;
f_gtk_window_set_title gtk_window_set_title = nullptr;
f_gtk_file_chooser_set_local_only gtk_file_chooser_set_local_only = nullptr;
f_gtk_file_chooser_set_action gtk_file_chooser_set_action = nullptr;
f_gtk_file_chooser_set_select_multiple gtk_file_chooser_set_select_multiple = nullptr;
f_gtk_file_chooser_set_do_overwrite_confirmation gtk_file_chooser_set_do_overwrite_confirmation = nullptr;
f_gtk_file_chooser_remove_filter gtk_file_chooser_remove_filter = nullptr;
f_gtk_file_filter_set_name gtk_file_filter_set_name = nullptr;
f_gtk_file_filter_add_pattern gtk_file_filter_add_pattern = nullptr;
f_gtk_file_chooser_add_filter gtk_file_chooser_add_filter = nullptr;
f_gtk_file_chooser_set_preview_widget gtk_file_chooser_set_preview_widget = nullptr;
f_gtk_file_chooser_get_preview_filename gtk_file_chooser_get_preview_filename = nullptr;
f_gtk_file_chooser_set_preview_widget_active gtk_file_chooser_set_preview_widget_active = nullptr;
f_gtk_file_filter_new gtk_file_filter_new = nullptr;
f_gtk_image_new gtk_image_new = nullptr;
f_gtk_image_set_from_pixbuf gtk_image_set_from_pixbuf = nullptr;
f_gtk_dialog_get_widget_for_response gtk_dialog_get_widget_for_response = nullptr;
f_gtk_button_set_label gtk_button_set_label = nullptr;
f_gtk_button_get_type gtk_button_get_type = nullptr;
f_gtk_app_chooser_dialog_new gtk_app_chooser_dialog_new = nullptr;
f_gtk_app_chooser_get_app_info gtk_app_chooser_get_app_info = nullptr;
f_gtk_app_chooser_get_type gtk_app_chooser_get_type = nullptr;
f_gdk_set_allowed_backends gdk_set_allowed_backends = nullptr;
f_gdk_window_set_modal_hint gdk_window_set_modal_hint = nullptr;
f_gdk_window_focus gdk_window_focus = nullptr;
f_gtk_dialog_get_type gtk_dialog_get_type = nullptr;
f_gtk_dialog_run gtk_dialog_run = nullptr;
f_gdk_atom_intern gdk_atom_intern = nullptr;
f_gdk_pixbuf_new_from_file_at_size gdk_pixbuf_new_from_file_at_size = nullptr;
f_gdk_pixbuf_get_has_alpha gdk_pixbuf_get_has_alpha = nullptr;
f_gdk_pixbuf_get_pixels gdk_pixbuf_get_pixels = nullptr;
f_gdk_pixbuf_get_width gdk_pixbuf_get_width = nullptr;
f_gdk_pixbuf_get_height gdk_pixbuf_get_height = nullptr;
f_gdk_pixbuf_get_rowstride gdk_pixbuf_get_rowstride = nullptr;
bool GtkLoaded() {
return gtkLoaded;
}
::GtkClipboard *GtkClipboard() {
if (gtk_clipboard_get != nullptr) {
return gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
}
return nullptr;
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
void start() {
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
if (!UseGtkIntegration()) {
return;
}
DEBUG_LOG(("Loading libraries"));
QLibrary lib_gtk;
lib_gtk.setLoadHints(QLibrary::DeepBindHint);
if (loadLibrary(lib_gtk, "gtk-3", 0)) {
gtkLoaded = setupGtkBase(lib_gtk);
}
if (!gtkLoaded && !gtkTriedToInit && loadLibrary(lib_gtk, "gtk-x11-2.0", 0)) {
gtkLoaded = setupGtkBase(lib_gtk);
}
if (gtkLoaded) {
LOAD_SYMBOL(lib_gtk, "gdk_pixbuf_new_from_file_at_size", gdk_pixbuf_new_from_file_at_size);
LOAD_SYMBOL(lib_gtk, "gdk_pixbuf_get_has_alpha", gdk_pixbuf_get_has_alpha);
LOAD_SYMBOL(lib_gtk, "gdk_pixbuf_get_pixels", gdk_pixbuf_get_pixels);
LOAD_SYMBOL(lib_gtk, "gdk_pixbuf_get_width", gdk_pixbuf_get_width);
LOAD_SYMBOL(lib_gtk, "gdk_pixbuf_get_height", gdk_pixbuf_get_height);
LOAD_SYMBOL(lib_gtk, "gdk_pixbuf_get_rowstride", gdk_pixbuf_get_rowstride);
internal::GdkHelperLoad(lib_gtk);
LOAD_SYMBOL(lib_gtk, "gtk_dialog_get_widget_for_response", gtk_dialog_get_widget_for_response);
LOAD_SYMBOL(lib_gtk, "gtk_button_set_label", gtk_button_set_label);
LOAD_SYMBOL(lib_gtk, "gtk_button_get_type", gtk_button_get_type);
LOAD_SYMBOL(lib_gtk, "gtk_app_chooser_dialog_new", gtk_app_chooser_dialog_new);
LOAD_SYMBOL(lib_gtk, "gtk_app_chooser_get_app_info", gtk_app_chooser_get_app_info);
LOAD_SYMBOL(lib_gtk, "gtk_app_chooser_get_type", gtk_app_chooser_get_type);
SetIconTheme();
SetCursorSize();
const auto settings = gtk_settings_get_default();
g_signal_connect(settings, "notify::gtk-icon-theme-name", G_CALLBACK(SetIconTheme), nullptr);
g_signal_connect(settings, "notify::gtk-theme-name", G_CALLBACK(DarkModeChanged), nullptr);
g_signal_connect(settings, "notify::gtk-cursor-theme-size", G_CALLBACK(SetCursorSize), nullptr);
if (!gtk_check_version(3, 0, 0)) {
g_signal_connect(settings, "notify::gtk-application-prefer-dark-theme", G_CALLBACK(DarkModeChanged), nullptr);
}
if (!gtk_check_version(3, 12, 0)) {
g_signal_connect(settings, "notify::gtk-decoration-layout", G_CALLBACK(DecorationLayoutChanged), nullptr);
}
} else {
LOG(("Could not load gtk-3 or gtk-x11-2.0!"));
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
}
} // namespace Libs
} // namespace Platform

View File

@@ -1,303 +0,0 @@
/*
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 <QtCore/QLibrary>
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
extern "C" {
#undef signals
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#define signals public
} // extern "C"
// present starting with gtk 3.0, we can build with gtk2 headers
typedef struct _GtkAppChooser GtkAppChooser;
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
#if defined DESKTOP_APP_USE_PACKAGED && !defined DESKTOP_APP_USE_PACKAGED_LAZY
#define LOAD_SYMBOL(lib, name, func) (func = ::func)
#else // DESKTOP_APP_USE_PACKAGED && !DESKTOP_APP_USE_PACKAGED_LAZY
#define LOAD_SYMBOL Platform::Libs::load
#endif // !DESKTOP_APP_USE_PACKAGED || DESKTOP_APP_USE_PACKAGED_LAZY
namespace Platform {
namespace Libs {
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
bool GtkLoaded();
::GtkClipboard *GtkClipboard();
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
void start();
template <typename Function>
bool load(QLibrary &lib, const char *name, Function &func) {
func = nullptr;
if (!lib.isLoaded()) {
return false;
}
func = reinterpret_cast<Function>(lib.resolve(name));
if (func) {
return true;
}
LOG(("Error: failed to load '%1' function!").arg(name));
return false;
}
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
typedef gboolean (*f_gtk_init_check)(int *argc, char ***argv);
extern f_gtk_init_check gtk_init_check;
typedef const gchar* (*f_gtk_check_version)(guint required_major, guint required_minor, guint required_micro);
extern f_gtk_check_version gtk_check_version;
typedef GtkSettings* (*f_gtk_settings_get_default)(void);
extern f_gtk_settings_get_default gtk_settings_get_default;
typedef void (*f_gtk_widget_show)(GtkWidget *widget);
extern f_gtk_widget_show gtk_widget_show;
typedef void (*f_gtk_widget_hide)(GtkWidget *widget);
extern f_gtk_widget_hide gtk_widget_hide;
typedef GdkWindow* (*f_gtk_widget_get_window)(GtkWidget *widget);
extern f_gtk_widget_get_window gtk_widget_get_window;
typedef void (*f_gtk_widget_realize)(GtkWidget *widget);
extern f_gtk_widget_realize gtk_widget_realize;
typedef gboolean (*f_gtk_widget_hide_on_delete)(GtkWidget *widget);
extern f_gtk_widget_hide_on_delete gtk_widget_hide_on_delete;
typedef void (*f_gtk_widget_destroy)(GtkWidget *widget);
extern f_gtk_widget_destroy gtk_widget_destroy;
typedef ::GtkClipboard* (*f_gtk_clipboard_get)(GdkAtom selection);
extern f_gtk_clipboard_get gtk_clipboard_get;
typedef void (*f_gtk_clipboard_store)(::GtkClipboard *clipboard);
extern f_gtk_clipboard_store gtk_clipboard_store;
typedef GtkSelectionData* (*f_gtk_clipboard_wait_for_contents)(::GtkClipboard *clipboard, GdkAtom target);
extern f_gtk_clipboard_wait_for_contents gtk_clipboard_wait_for_contents;
typedef GdkPixbuf* (*f_gtk_clipboard_wait_for_image)(::GtkClipboard *clipboard);
extern f_gtk_clipboard_wait_for_image gtk_clipboard_wait_for_image;
typedef gboolean (*f_gtk_selection_data_targets_include_image)(const GtkSelectionData *selection_data, gboolean writable);
extern f_gtk_selection_data_targets_include_image gtk_selection_data_targets_include_image;
typedef void (*f_gtk_selection_data_free)(GtkSelectionData *data);
extern f_gtk_selection_data_free gtk_selection_data_free;
typedef GtkWidget* (*f_gtk_file_chooser_dialog_new)(const gchar *title, GtkWindow *parent, GtkFileChooserAction action, const gchar *first_button_text, ...) G_GNUC_NULL_TERMINATED;
extern f_gtk_file_chooser_dialog_new gtk_file_chooser_dialog_new;
typedef gboolean (*f_gtk_file_chooser_set_current_folder)(GtkFileChooser *chooser, const gchar *filename);
extern f_gtk_file_chooser_set_current_folder gtk_file_chooser_set_current_folder;
typedef gchar* (*f_gtk_file_chooser_get_current_folder)(GtkFileChooser *chooser);
extern f_gtk_file_chooser_get_current_folder gtk_file_chooser_get_current_folder;
typedef void (*f_gtk_file_chooser_set_current_name)(GtkFileChooser *chooser, const gchar *name);
extern f_gtk_file_chooser_set_current_name gtk_file_chooser_set_current_name;
typedef gboolean (*f_gtk_file_chooser_select_filename)(GtkFileChooser *chooser, const gchar *filename);
extern f_gtk_file_chooser_select_filename gtk_file_chooser_select_filename;
typedef GSList* (*f_gtk_file_chooser_get_filenames)(GtkFileChooser *chooser);
extern f_gtk_file_chooser_get_filenames gtk_file_chooser_get_filenames;
typedef void (*f_gtk_file_chooser_set_filter)(GtkFileChooser *chooser, GtkFileFilter *filter);
extern f_gtk_file_chooser_set_filter gtk_file_chooser_set_filter;
typedef GtkFileFilter* (*f_gtk_file_chooser_get_filter)(GtkFileChooser *chooser);
extern f_gtk_file_chooser_get_filter gtk_file_chooser_get_filter;
typedef void (*f_gtk_window_set_title)(GtkWindow *window, const gchar *title);
extern f_gtk_window_set_title gtk_window_set_title;
typedef void (*f_gtk_file_chooser_set_local_only)(GtkFileChooser *chooser, gboolean local_only);
extern f_gtk_file_chooser_set_local_only gtk_file_chooser_set_local_only;
typedef void (*f_gtk_file_chooser_set_action)(GtkFileChooser *chooser, GtkFileChooserAction action);
extern f_gtk_file_chooser_set_action gtk_file_chooser_set_action;
typedef void (*f_gtk_file_chooser_set_select_multiple)(GtkFileChooser *chooser, gboolean select_multiple);
extern f_gtk_file_chooser_set_select_multiple gtk_file_chooser_set_select_multiple;
typedef void (*f_gtk_file_chooser_set_do_overwrite_confirmation)(GtkFileChooser *chooser, gboolean do_overwrite_confirmation);
extern f_gtk_file_chooser_set_do_overwrite_confirmation gtk_file_chooser_set_do_overwrite_confirmation;
typedef GtkWidget* (*f_gtk_dialog_get_widget_for_response)(GtkDialog *dialog, gint response_id);
extern f_gtk_dialog_get_widget_for_response gtk_dialog_get_widget_for_response;
typedef void (*f_gtk_button_set_label)(GtkButton *button, const gchar *label);
extern f_gtk_button_set_label gtk_button_set_label;
typedef void (*f_gtk_file_chooser_remove_filter)(GtkFileChooser *chooser, GtkFileFilter *filter);
extern f_gtk_file_chooser_remove_filter gtk_file_chooser_remove_filter;
typedef void (*f_gtk_file_filter_set_name)(GtkFileFilter *filter, const gchar *name);
extern f_gtk_file_filter_set_name gtk_file_filter_set_name;
typedef void (*f_gtk_file_filter_add_pattern)(GtkFileFilter *filter, const gchar *pattern);
extern f_gtk_file_filter_add_pattern gtk_file_filter_add_pattern;
typedef void (*f_gtk_file_chooser_add_filter)(GtkFileChooser *chooser, GtkFileFilter *filter);
extern f_gtk_file_chooser_add_filter gtk_file_chooser_add_filter;
typedef void (*f_gtk_file_chooser_set_preview_widget)(GtkFileChooser *chooser, GtkWidget *preview_widget);
extern f_gtk_file_chooser_set_preview_widget gtk_file_chooser_set_preview_widget;
typedef gchar* (*f_gtk_file_chooser_get_preview_filename)(GtkFileChooser *chooser);
extern f_gtk_file_chooser_get_preview_filename gtk_file_chooser_get_preview_filename;
typedef void (*f_gtk_file_chooser_set_preview_widget_active)(GtkFileChooser *chooser, gboolean active);
extern f_gtk_file_chooser_set_preview_widget_active gtk_file_chooser_set_preview_widget_active;
typedef GtkFileFilter* (*f_gtk_file_filter_new)(void);
extern f_gtk_file_filter_new gtk_file_filter_new;
typedef GtkWidget* (*f_gtk_image_new)(void);
extern f_gtk_image_new gtk_image_new;
typedef void (*f_gtk_image_set_from_pixbuf)(GtkImage *image, GdkPixbuf *pixbuf);
extern f_gtk_image_set_from_pixbuf gtk_image_set_from_pixbuf;
typedef GtkWidget* (*f_gtk_app_chooser_dialog_new)(GtkWindow *parent, GtkDialogFlags flags, GFile *file);
extern f_gtk_app_chooser_dialog_new gtk_app_chooser_dialog_new;
typedef GAppInfo* (*f_gtk_app_chooser_get_app_info)(GtkAppChooser *self);
extern f_gtk_app_chooser_get_app_info gtk_app_chooser_get_app_info;
typedef void (*f_gdk_set_allowed_backends)(const gchar *backends);
extern f_gdk_set_allowed_backends gdk_set_allowed_backends;
typedef void (*f_gdk_window_set_modal_hint)(GdkWindow *window, gboolean modal);
extern f_gdk_window_set_modal_hint gdk_window_set_modal_hint;
typedef void (*f_gdk_window_focus)(GdkWindow *window, guint32 timestamp);
extern f_gdk_window_focus gdk_window_focus;
template <typename Result, typename Object>
inline Result *g_type_cic_helper(Object *instance, GType iface_type) {
return reinterpret_cast<Result*>(g_type_check_instance_cast(reinterpret_cast<GTypeInstance*>(instance), iface_type));
}
typedef GType (*f_gtk_dialog_get_type)(void) G_GNUC_CONST;
extern f_gtk_dialog_get_type gtk_dialog_get_type;
template <typename Object>
inline GtkDialog *gtk_dialog_cast(Object *obj) {
return g_type_cic_helper<GtkDialog, Object>(obj, gtk_dialog_get_type());
}
typedef GType (*f_gtk_file_chooser_get_type)(void) G_GNUC_CONST;
extern f_gtk_file_chooser_get_type gtk_file_chooser_get_type;
template <typename Object>
inline GtkFileChooser *gtk_file_chooser_cast(Object *obj) {
return g_type_cic_helper<GtkFileChooser, Object>(obj, gtk_file_chooser_get_type());
}
typedef GType (*f_gtk_image_get_type)(void) G_GNUC_CONST;
extern f_gtk_image_get_type gtk_image_get_type;
template <typename Object>
inline GtkImage *gtk_image_cast(Object *obj) {
return g_type_cic_helper<GtkImage, Object>(obj, gtk_image_get_type());
}
typedef GType (*f_gtk_button_get_type)(void) G_GNUC_CONST;
extern f_gtk_button_get_type gtk_button_get_type;
template <typename Object>
inline GtkButton *gtk_button_cast(Object *obj) {
return g_type_cic_helper<GtkButton, Object>(obj, gtk_button_get_type());
}
typedef GType (*f_gtk_window_get_type)(void) G_GNUC_CONST;
extern f_gtk_window_get_type gtk_window_get_type;
template <typename Object>
inline GtkWindow *gtk_window_cast(Object *obj) {
return g_type_cic_helper<GtkWindow, Object>(obj, gtk_window_get_type());
}
typedef GType (*f_gtk_app_chooser_get_type)(void) G_GNUC_CONST;
extern f_gtk_app_chooser_get_type gtk_app_chooser_get_type;
template <typename Object>
inline GtkAppChooser *gtk_app_chooser_cast(Object *obj) {
return g_type_cic_helper<GtkAppChooser, Object>(obj, gtk_app_chooser_get_type());
}
template <typename Object>
inline bool g_type_cit_helper(Object *instance, GType iface_type) {
if (!instance) return false;
auto ginstance = reinterpret_cast<GTypeInstance*>(instance);
if (ginstance->g_class && ginstance->g_class->g_type == iface_type) {
return true;
}
return g_type_check_instance_is_a(ginstance, iface_type);
}
typedef gint (*f_gtk_dialog_run)(GtkDialog *dialog);
extern f_gtk_dialog_run gtk_dialog_run;
typedef GdkAtom (*f_gdk_atom_intern)(const gchar *atom_name, gboolean only_if_exists);
extern f_gdk_atom_intern gdk_atom_intern;
typedef GdkPixbuf* (*f_gdk_pixbuf_new_from_file_at_size)(const gchar *filename, int width, int height, GError **error);
extern f_gdk_pixbuf_new_from_file_at_size gdk_pixbuf_new_from_file_at_size;
typedef gboolean (*f_gdk_pixbuf_get_has_alpha)(const GdkPixbuf *pixbuf);
extern f_gdk_pixbuf_get_has_alpha gdk_pixbuf_get_has_alpha;
typedef guchar* (*f_gdk_pixbuf_get_pixels)(const GdkPixbuf *pixbuf);
extern f_gdk_pixbuf_get_pixels gdk_pixbuf_get_pixels;
typedef int (*f_gdk_pixbuf_get_width)(const GdkPixbuf *pixbuf);
extern f_gdk_pixbuf_get_width gdk_pixbuf_get_width;
typedef int (*f_gdk_pixbuf_get_height)(const GdkPixbuf *pixbuf);
extern f_gdk_pixbuf_get_height gdk_pixbuf_get_height;
typedef int (*f_gdk_pixbuf_get_rowstride)(const GdkPixbuf *pixbuf);
extern f_gdk_pixbuf_get_rowstride gdk_pixbuf_get_rowstride;
inline bool GtkSettingSupported() {
return gtk_settings_get_default != nullptr;
}
template <typename T>
inline T GtkSetting(const gchar *propertyName) {
GtkSettings *settings = gtk_settings_get_default();
T value;
g_object_get(settings, propertyName, &value, nullptr);
return value;
}
inline QString GtkSetting(const gchar *propertyName) {
gchararray value = GtkSetting<gchararray>(propertyName);
QString str = QString::fromUtf8(value);
g_free(value);
DEBUG_LOG(("Getting GTK setting, %1: '%2'").arg(propertyName).arg(str));
return str;
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
} // namespace Libs
} // namespace Platform

View File

@@ -0,0 +1,44 @@
/*
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 "platform/linux/linux_notification_service_watcher.h"
#include "core/application.h"
#include "main/main_domain.h"
#include "window/notifications_manager.h"
#include "platform/linux/specific_linux.h"
#include <QtDBus/QDBusConnection>
namespace Platform {
namespace internal {
NotificationServiceWatcher::NotificationServiceWatcher()
: _dbusWatcher(
qsl("org.freedesktop.Notifications"),
QDBusConnection::sessionBus(),
QDBusServiceWatcher::WatchForOwnerChange) {
const auto signal = &QDBusServiceWatcher::serviceOwnerChanged;
QObject::connect(&_dbusWatcher, signal, [=](
const QString &service,
const QString &oldOwner,
const QString &newOwner) {
crl::on_main([=] {
if (!Core::App().domain().started()) {
return;
} else if (IsNotificationServiceActivatable()
&& newOwner.isEmpty()) {
return;
}
Core::App().notifications().createManager();
});
});
}
} // namespace internal
} // namespace Platform

View File

@@ -0,0 +1,24 @@
/*
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 <QtDBus/QDBusServiceWatcher>
namespace Platform {
namespace internal {
class NotificationServiceWatcher {
public:
NotificationServiceWatcher();
private:
QDBusServiceWatcher _dbusWatcher;
};
} // namespace internal
} // namespace Platform

View File

@@ -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 "platform/linux/linux_open_with_dialog.h"
#include "platform/linux/linux_gtk_integration_p.h"
#include "platform/linux/linux_gdk_helper.h"
#include "window/window_controller.h"
#include "core/application.h"
#include <private/qguiapplication_p.h>
namespace Platform {
namespace File {
namespace internal {
namespace {
using namespace Platform::Gtk;
bool OpenWithDialogSupported() {
return Platform::internal::GdkHelperLoaded()
&& (gtk_app_chooser_dialog_new != nullptr)
&& (gtk_app_chooser_get_app_info != nullptr)
&& (gtk_app_chooser_get_type != nullptr)
&& (gtk_widget_get_window != nullptr)
&& (gtk_widget_realize != nullptr)
&& (gtk_widget_show != nullptr)
&& (gtk_widget_destroy != nullptr);
}
class OpenWithDialog : public QWindow {
public:
OpenWithDialog(const QString &filepath);
~OpenWithDialog();
bool exec();
private:
static void handleResponse(OpenWithDialog *dialog, int responseId);
GFile *_gfileInstance = nullptr;
GtkWidget *_gtkWidget = nullptr;
QEventLoop _loop;
std::optional<bool> _result;
};
OpenWithDialog::OpenWithDialog(const QString &filepath)
: _gfileInstance(g_file_new_for_path(filepath.toUtf8()))
, _gtkWidget(gtk_app_chooser_dialog_new(
nullptr,
GTK_DIALOG_MODAL,
_gfileInstance)) {
g_signal_connect_swapped(
_gtkWidget,
"response",
G_CALLBACK(handleResponse),
this);
}
OpenWithDialog::~OpenWithDialog() {
gtk_widget_destroy(_gtkWidget);
g_object_unref(_gfileInstance);
}
bool OpenWithDialog::exec() {
gtk_widget_realize(_gtkWidget);
if (const auto activeWindow = Core::App().activeWindow()) {
Platform::internal::XSetTransientForHint(
gtk_widget_get_window(_gtkWidget),
activeWindow->widget().get()->windowHandle()->winId());
}
QGuiApplicationPrivate::showModalWindow(this);
gtk_widget_show(_gtkWidget);
if (!_result.has_value()) {
_loop.exec();
}
QGuiApplicationPrivate::hideModalWindow(this);
return *_result;
}
void OpenWithDialog::handleResponse(OpenWithDialog *dialog, int responseId) {
GAppInfo *chosenAppInfo = nullptr;
dialog->_result = true;
switch (responseId) {
case GTK_RESPONSE_OK:
chosenAppInfo = gtk_app_chooser_get_app_info(
gtk_app_chooser_cast(dialog->_gtkWidget));
if (chosenAppInfo) {
GList *uris = nullptr;
uris = g_list_prepend(uris, g_file_get_uri(dialog->_gfileInstance));
g_app_info_launch_uris(chosenAppInfo, uris, nullptr, nullptr);
g_list_free(uris);
g_object_unref(chosenAppInfo);
}
break;
case GTK_RESPONSE_CANCEL:
case GTK_RESPONSE_DELETE_EVENT:
break;
default:
dialog->_result = false;
break;
}
dialog->_loop.quit();
}
} // namespace
bool ShowOpenWithDialog(const QString &filepath) {
if (!OpenWithDialogSupported()) {
return false;
}
return OpenWithDialog(filepath).exec();
}
} // namespace internal
} // namespace File
} // namespace Platform

View File

@@ -0,0 +1,18 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Platform {
namespace File {
namespace internal {
bool ShowOpenWithDialog(const QString &filepath);
} // namespace internal
} // namespace File
} // namespace Platform

View File

@@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "platform/linux/linux_xlib_helper.h"
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
extern "C" {
#include <X11/Xlib.h>
}
@@ -37,4 +36,3 @@ XErrorHandlerRestorer::~XErrorHandlerRestorer() = default;
} // namespace internal
} // namespace Platform
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION

View File

@@ -7,7 +7,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
namespace Platform {
namespace internal {
@@ -23,4 +22,3 @@ private:
} // namespace internal
} // namespace Platform
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION

View File

@@ -204,6 +204,13 @@ QIcon TrayIconGen(int counter, bool muted) {
}
const auto iconName = GetTrayIconName(counter, muted);
const auto panelIconName = GetPanelIconName(counter, muted);
if (iconName == panelIconName) {
const auto result = QIcon::fromTheme(iconName);
UpdateIconRegenerationNeeded(result, counter, muted, iconThemeName);
return result;
}
QIcon result;
QIcon systemIcon;
@@ -250,7 +257,7 @@ QIcon TrayIconGen(int counter, bool muted) {
std::less<>(),
&QSize::width);
if ((*biggestSize).width() > firstAttemptSize.width()) {
if (biggestSize->width() > firstAttemptSize.width()) {
currentImageBack = systemIcon
.pixmap(*biggestSize)
.toImage();
@@ -381,7 +388,7 @@ std::unique_ptr<QTemporaryFile> TrayIconFile(
std::less<>(),
&QSize::width);
if ((*biggestSize).width() > firstAttemptSize.width()) {
if (biggestSize->width() > firstAttemptSize.width()) {
scalePixmap(icon.pixmap(*biggestSize)).save(ret.get());
} else {
scalePixmap(firstAttempt).save(ret.get());
@@ -420,10 +427,20 @@ bool IsSNIAvailable() {
if (reply.isValid()) {
return reply.value().toBool();
} else if (reply.error().type() != QDBusError::ServiceUnknown) {
LOG(("SNI Error: %1").arg(reply.error().message()));
}
switch (reply.error().type()) {
case QDBusError::Disconnected:
case QDBusError::ServiceUnknown:
return false;
default:
break;
}
LOG(("SNI Error: %1: %2")
.arg(reply.error().name())
.arg(reply.error().message()));
return false;
}
@@ -505,6 +522,11 @@ void ForceDisabled(QAction *action, bool disabled) {
MainWindow::MainWindow(not_null<Window::Controller*> controller)
: Window::MainWindow(controller) {
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
qDBusRegisterMetaType<ToolTip>();
qDBusRegisterMetaType<IconPixmap>();
qDBusRegisterMetaType<IconPixmapList>();
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
}
void MainWindow::initHook() {
@@ -521,11 +543,13 @@ void MainWindow::initHook() {
nullptr,
nullptr);
g_signal_connect(
_sniDBusProxy,
"g-signal",
G_CALLBACK(sniSignalEmitted),
nullptr);
if (_sniDBusProxy) {
g_signal_connect(
_sniDBusProxy,
"g-signal",
G_CALLBACK(sniSignalEmitted),
nullptr);
}
auto sniWatcher = new QDBusServiceWatcher(
kSNIWatcherService.utf16(),
@@ -593,6 +617,11 @@ bool MainWindow::hasTrayIcon() const {
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
}
bool MainWindow::isActiveForTrayMenu() {
updateIsActive();
return Platform::IsWayland() ? isVisible() : isActive();
}
void MainWindow::psShowTrayMenu() {
_trayIconMenuXEmbed->popup(QCursor::pos());
}
@@ -607,7 +636,17 @@ void MainWindow::psTrayMenuUpdated() {
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
void MainWindow::setSNITrayIcon(int counter, bool muted) {
if (IsIndicatorApplication()) {
const auto iconName = GetTrayIconName(counter, muted);
const auto panelIconName = GetPanelIconName(counter, muted);
if (iconName == panelIconName) {
if (_sniTrayIcon->iconName() == iconName) {
return;
}
_sniTrayIcon->setIconByName(iconName);
_sniTrayIcon->setToolTipIconByName(iconName);
} else if (IsIndicatorApplication()) {
if (!IsIconRegenerationNeeded(counter, muted)
&& _trayIconFile
&& _sniTrayIcon->iconName() == _trayIconFile->fileName()) {
@@ -661,7 +700,7 @@ void MainWindow::sniSignalEmitted(
gchar *signal_name,
GVariant *parameters,
gpointer user_data) {
if(signal_name == qstr("StatusNotifierHostRegistered")) {
if (signal_name == qstr("StatusNotifierHostRegistered")) {
crl::on_main([] {
if (const auto window = App::wnd()) {
window->handleSNIHostRegistered();
@@ -684,7 +723,7 @@ void MainWindow::handleSNIHostRegistered() {
LOG(("Switching to SNI tray icon..."));
if (trayIcon) {
trayIcon->setContextMenu(0);
trayIcon->setContextMenu(nullptr);
trayIcon->deleteLater();
}
trayIcon = nullptr;
@@ -877,16 +916,8 @@ void MainWindow::updateWaylandDecorationColors() {
windowHandle()->resize(windowHandle()->size());
}
void MainWindow::LibsLoaded() {
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
qDBusRegisterMetaType<ToolTip>();
qDBusRegisterMetaType<IconPixmap>();
qDBusRegisterMetaType<IconPixmapList>();
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
}
void MainWindow::initTrayMenuHook() {
_trayIconMenuXEmbed = new Ui::PopupMenu(nullptr, trayIconMenu);
_trayIconMenuXEmbed.emplace(nullptr, trayIconMenu);
_trayIconMenuXEmbed->deleteOnHide(false);
}
@@ -1031,19 +1062,19 @@ void MainWindow::createGlobalMenu() {
psAddContact = tools->addAction(
tr::lng_mac_menu_add_contact(tr::now),
App::wnd(),
[=] { App::wnd()->onShowAddContact(); });
[=] { App::wnd()->showAddContact(); });
tools->addSeparator();
psNewGroup = tools->addAction(
tr::lng_mac_menu_new_group(tr::now),
App::wnd(),
[=] { App::wnd()->onShowNewGroup(); });
[=] { App::wnd()->showNewGroup(); });
psNewChannel = tools->addAction(
tr::lng_mac_menu_new_channel(tr::now),
App::wnd(),
[=] { App::wnd()->onShowNewChannel(); });
[=] { App::wnd()->showNewChannel(); });
auto help = psMainMenu->addMenu(tr::lng_linux_menu_help(tr::now));
@@ -1220,10 +1251,10 @@ MainWindow::~MainWindow() {
delete _mainMenuExporter;
delete psMainMenu;
g_object_unref(_sniDBusProxy);
if (_sniDBusProxy) {
g_object_unref(_sniDBusProxy);
}
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
delete _trayIconMenuXEmbed;
}
} // namespace Platform

View File

@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#pragma once
#include "platform/platform_main_window.h"
#include "base/unique_qptr.h"
namespace Ui {
class PopupMenu;
@@ -43,7 +44,7 @@ public:
return _sniAvailable || QSystemTrayIcon::isSystemTrayAvailable();
}
static void LibsLoaded();
bool isActiveForTrayMenu() override;
~MainWindow();
@@ -75,7 +76,7 @@ protected:
private:
bool _sniAvailable = false;
Ui::PopupMenu *_trayIconMenuXEmbed = nullptr;
base::unique_qptr<Ui::PopupMenu> _trayIconMenuXEmbed;
void updateIconCounters();
void updateWaylandDecorationColors();

View File

@@ -19,7 +19,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include <QtCore/QVersionNumber>
#include <QtDBus/QDBusConnection>
#include <QtDBus/QDBusConnectionInterface>
#include <QtDBus/QDBusMessage>
#include <QtDBus/QDBusPendingCall>
#include <QtDBus/QDBusPendingCallWatcher>
#include <QtDBus/QDBusPendingReply>
#include <QtDBus/QDBusReply>
#include <QtDBus/QDBusError>
@@ -33,120 +37,104 @@ namespace Platform {
namespace Notifications {
namespace {
constexpr auto kDBusTimeout = 30000;
constexpr auto kService = "org.freedesktop.Notifications"_cs;
constexpr auto kObjectPath = "/org/freedesktop/Notifications"_cs;
constexpr auto kInterface = kService;
constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"_cs;
constexpr auto kImageDataType = "(iiibii@ay)"_cs;
constexpr auto kNotifyArgsType = "(susssasa{sv}i)"_cs;
bool NotificationsSupported = false;
bool InhibitedNotSupported = false;
struct ServerInformation {
QString name;
QString vendor;
QVersionNumber version;
QVersionNumber specVersion;
};
bool ServiceRegistered = false;
bool InhibitionSupported = false;
std::optional<ServerInformation> CurrentServerInformation;
QStringList CurrentCapabilities;
bool GetServiceRegistered() {
const auto interface = QDBusConnection::sessionBus().interface();
const auto activatable = IsNotificationServiceActivatable();
return interface
? interface->isServiceRegistered(kService.utf16()) || activatable
: activatable;
}
void GetServerInformation(Fn<void(std::optional<ServerInformation>)> callback) {
using ServerInformationReply = QDBusPendingReply<
QString,
QString,
QString,
QString>;
void ComputeSupported(bool wait = false) {
const auto message = QDBusMessage::createMethodCall(
kService.utf16(),
kObjectPath.utf16(),
kInterface.utf16(),
qsl("GetServerInformation"));
auto async = QDBusConnection::sessionBus().asyncCall(message);
const auto async = QDBusConnection::sessionBus().asyncCall(message);
auto watcher = new QDBusPendingCallWatcher(async);
QObject::connect(
watcher,
&QDBusPendingCallWatcher::finished,
[=](QDBusPendingCallWatcher *call) {
QDBusPendingReply<
QString,
QString,
QString,
QString> reply = *call;
const auto finished = [=](QDBusPendingCallWatcher *call) {
const ServerInformationReply reply = *call;
if (reply.isValid()) {
NotificationsSupported = true;
}
if (reply.isValid()) {
crl::on_main([=] {
callback(ServerInformation{
reply.argumentAt<0>(),
reply.argumentAt<1>(),
QVersionNumber::fromString(reply.argumentAt<2>()),
QVersionNumber::fromString(reply.argumentAt<3>()),
});
});
} else {
LOG(("Native Notification Error: %1: %2")
.arg(reply.error().name())
.arg(reply.error().message()));
call->deleteLater();
});
crl::on_main([=] { callback(std::nullopt); });
}
if (wait) {
watcher->waitForFinished();
}
call->deleteLater();
};
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, finished);
}
void GetSupported() {
static auto Checked = false;
if (Checked) {
return;
}
Checked = true;
if (Core::App().settings().nativeNotifications() && !IsWayland()) {
ComputeSupported(true);
} else {
ComputeSupported();
}
}
std::vector<QString> ComputeServerInformation() {
std::vector<QString> serverInformation;
const auto message = QDBusMessage::createMethodCall(
kService.utf16(),
kObjectPath.utf16(),
kInterface.utf16(),
qsl("GetServerInformation"));
const auto reply = QDBusConnection::sessionBus().call(message);
if (reply.type() == QDBusMessage::ReplyMessage) {
ranges::transform(
reply.arguments(),
ranges::back_inserter(serverInformation),
&QVariant::toString
);
} else if (reply.type() == QDBusMessage::ErrorMessage) {
LOG(("Native notification error: %1").arg(reply.errorMessage()));
} else {
LOG(("Native notification error: "
"invalid reply from GetServerInformation"));
}
return serverInformation;
}
std::vector<QString> GetServerInformation() {
static const auto Result = ComputeServerInformation();
return Result;
}
QStringList ComputeCapabilities() {
void GetCapabilities(Fn<void(QStringList)> callback) {
const auto message = QDBusMessage::createMethodCall(
kService.utf16(),
kObjectPath.utf16(),
kInterface.utf16(),
qsl("GetCapabilities"));
const QDBusReply<QStringList> reply = QDBusConnection::sessionBus().call(
message);
const auto async = QDBusConnection::sessionBus().asyncCall(message);
auto watcher = new QDBusPendingCallWatcher(async);
if (reply.isValid()) {
return reply.value();
} else {
LOG(("Native notification error: %1").arg(reply.error().message()));
}
const auto finished = [=](QDBusPendingCallWatcher *call) {
const QDBusPendingReply<QStringList> reply = *call;
return {};
if (reply.isValid()) {
crl::on_main([=] { callback(reply.value()); });
} else {
LOG(("Native Notification Error: %1: %2")
.arg(reply.error().name())
.arg(reply.error().message()));
crl::on_main([=] { callback({}); });
}
call->deleteLater();
};
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, finished);
}
QStringList GetCapabilities() {
static const auto Result = ComputeCapabilities();
return Result;
}
bool Inhibited() {
void GetInhibitionSupported(Fn<void(bool)> callback) {
auto message = QDBusMessage::createMethodCall(
kService.utf16(),
kObjectPath.utf16(),
@@ -154,61 +142,102 @@ bool Inhibited() {
qsl("Get"));
message.setArguments({
qsl("org.freedesktop.Notifications"),
kInterface.utf16(),
qsl("Inhibited")
});
const auto async = QDBusConnection::sessionBus().asyncCall(message);
auto watcher = new QDBusPendingCallWatcher(async);
const auto finished = [=](QDBusPendingCallWatcher *call) {
const auto error = QDBusPendingReply<QVariant>(*call).error();
if (error.isValid() && error.type() != QDBusError::InvalidArgs) {
LOG(("Native Notification Error: %1: %2")
.arg(error.name())
.arg(error.message()));
}
crl::on_main([=] { callback(!error.isValid()); });
call->deleteLater();
};
QObject::connect(watcher, &QDBusPendingCallWatcher::finished, finished);
}
bool Inhibited() {
if (!Supported()
|| !CurrentCapabilities.contains(qsl("inhibitions"))
|| !InhibitionSupported) {
return false;
}
auto message = QDBusMessage::createMethodCall(
kService.utf16(),
kObjectPath.utf16(),
kPropertiesInterface.utf16(),
qsl("Get"));
message.setArguments({
kInterface.utf16(),
qsl("Inhibited")
});
const QDBusReply<QVariant> reply = QDBusConnection::sessionBus().call(
message);
static const auto NotSupportedErrors = {
QDBusError::ServiceUnknown,
QDBusError::InvalidArgs,
};
if (reply.isValid()) {
return reply.value().toBool();
} else if (ranges::contains(NotSupportedErrors, reply.error().type())) {
InhibitedNotSupported = true;
} else {
if (reply.error().type() == QDBusError::AccessDenied) {
InhibitedNotSupported = true;
}
LOG(("Native notification error: %1").arg(reply.error().message()));
}
LOG(("Native Notification Error: %1: %2")
.arg(reply.error().name())
.arg(reply.error().message()));
return false;
}
QVersionNumber ParseSpecificationVersion(
const std::vector<QString> &serverInformation) {
if (serverInformation.size() >= 4) {
return QVersionNumber::fromString(serverInformation[3]);
} else {
LOG(("Native notification error: "
"server information should have 4 elements"));
}
bool IsQualifiedDaemon() {
// A list of capabilities that offer feature parity
// with custom notifications
static const auto NeededCapabilities = {
// To show message content
qsl("body"),
// To make the sender name bold
qsl("body-markup"),
// To have buttons on notifications
qsl("actions"),
// To have quick reply
qsl("inline-reply"),
// To not to play sound with Don't Disturb activated
// (no, using sound capability is not a way)
qsl("inhibitions"),
};
return QVersionNumber();
return ranges::all_of(NeededCapabilities, [&](const auto &capability) {
return CurrentCapabilities.contains(capability);
}) && InhibitionSupported;
}
ServerInformation CurrentServerInformationValue() {
return CurrentServerInformation.value_or(ServerInformation{});
}
QString GetImageKey(const QVersionNumber &specificationVersion) {
if (!specificationVersion.isNull()) {
if (specificationVersion >= QVersionNumber(1, 2)) {
return qsl("image-data");
} else if (specificationVersion == QVersionNumber(1, 1)) {
return qsl("image_data");
} else if (specificationVersion < QVersionNumber(1, 1)) {
return qsl("icon_data");
} else {
LOG(("Native notification error: unknown specification version"));
}
} else {
LOG(("Native notification error: specification version is null"));
const auto normalizedVersion = specificationVersion.normalized();
if (normalizedVersion.isNull()) {
LOG(("Native Notification Error: specification version is null"));
return QString();
}
return QString();
if (normalizedVersion >= QVersionNumber(1, 2)) {
return qsl("image-data");
} else if (normalizedVersion == QVersionNumber(1, 1)) {
return qsl("image_data");
}
return qsl("icon_data");
}
class NotificationData {
@@ -230,7 +259,7 @@ public:
~NotificationData();
bool show();
void show();
void close();
void setImage(const QString &imagePath);
@@ -255,6 +284,11 @@ private:
void actionInvoked(uint id, const QString &actionName);
void notificationReplied(uint id, const QString &text);
static void notificationShown(
GObject *source_object,
GAsyncResult *res,
gpointer user_data);
static void signalEmitted(
GDBusConnection *connection,
const gchar *sender_name,
@@ -277,8 +311,7 @@ NotificationData::NotificationData(
bool hideReplyButton)
: _manager(manager)
, _title(title)
, _imageKey(GetImageKey(ParseSpecificationVersion(
GetServerInformation())))
, _imageKey(GetImageKey(CurrentServerInformationValue().specVersion))
, _id(id) {
GError *error = nullptr;
@@ -288,12 +321,12 @@ NotificationData::NotificationData(
&error);
if (error) {
LOG(("Native notification error: %1").arg(error->message));
LOG(("Native Notification Error: %1").arg(error->message));
g_error_free(error);
return;
}
const auto capabilities = GetCapabilities();
const auto capabilities = CurrentCapabilities;
if (capabilities.contains(qsl("body-markup"))) {
_body = subtitle.isEmpty()
@@ -424,7 +457,7 @@ NotificationData::~NotificationData() {
}
}
bool NotificationData::show() {
void NotificationData::show() {
GVariantBuilder actionsBuilder, hintsBuilder;
GError *error = nullptr;
@@ -451,14 +484,14 @@ bool NotificationData::show() {
? GetIconName()
: QString();
auto reply = g_dbus_connection_call_sync(
g_dbus_connection_call(
_dbusConnection,
kService.utf8(),
kObjectPath.utf8(),
kInterface.utf8(),
"Notify",
g_variant_new(
kNotifyArgsType.utf8(),
"(susssasa{sv}i)",
AppName.utf8().constData(),
0,
iconName.toUtf8().constData(),
@@ -469,21 +502,42 @@ bool NotificationData::show() {
-1),
nullptr,
G_DBUS_CALL_FLAGS_NONE,
kDBusTimeout,
-1,
nullptr,
&error);
notificationShown,
this);
}
const auto replyValid = !error;
void NotificationData::notificationShown(
GObject *source_object,
GAsyncResult *res,
gpointer user_data) {
const auto notificationData = reinterpret_cast<NotificationData*>(
user_data);
if (replyValid) {
g_variant_get(reply, "(u)", &_notificationId);
g_variant_unref(reply);
} else {
LOG(("Native notification error: %1").arg(error->message));
g_error_free(error);
if (!notificationData) {
return;
}
return replyValid;
GError *error = nullptr;
auto reply = g_dbus_connection_call_finish(
notificationData->_dbusConnection,
res,
&error);
if (!error) {
g_variant_get(reply, "(u)", &notificationData->_notificationId);
g_variant_unref(reply);
} else {
const auto manager = notificationData->_manager;
const auto my = notificationData->_id;
crl::on_main(manager, [=] {
manager->clearNotification(my);
});
LOG(("Native Notification Error: %1").arg(error->message));
g_error_free(error);
}
}
void NotificationData::close() {
@@ -510,7 +564,7 @@ void NotificationData::setImage(const QString &imagePath) {
_image = QImage(imagePath).convertToFormat(QImage::Format_RGBA8888);
_hints.emplace(_imageKey, g_variant_new(
kImageDataType.utf8(),
"(iiibii@ay)",
_image.width(),
_image.height(),
_image.bytesPerLine(),
@@ -613,37 +667,84 @@ void NotificationData::notificationReplied(uint id, const QString &text) {
} // namespace
bool SkipAudio() {
if (Supported()
&& GetCapabilities().contains(qsl("inhibitions"))
&& !InhibitedNotSupported) {
return Inhibited();
}
return false;
return Inhibited();
}
bool SkipToast() {
return SkipAudio();
// Do not skip native notifications because of Do not disturb.
// They respect this setting anyway.
if ((Core::App().settings().nativeNotifications() && Supported())
|| Enforced()) {
return false;
}
return Inhibited();
}
bool SkipFlashBounce() {
return SkipAudio();
return Inhibited();
}
bool Supported() {
return NotificationsSupported;
return ServiceRegistered;
}
std::unique_ptr<Window::Notifications::Manager> Create(
Window::Notifications::System *system) {
GetSupported();
bool Enforced() {
// Wayland doesn't support positioning
// and custom notifications don't work here
return IsQualifiedDaemon() || IsWayland();
}
if ((Core::App().settings().nativeNotifications() && Supported())
|| IsWayland()) {
return std::make_unique<Manager>(system);
void Create(Window::Notifications::System *system) {
ServiceRegistered = GetServiceRegistered();
const auto managerSetter = [=] {
using ManagerType = Window::Notifications::ManagerType;
if ((Core::App().settings().nativeNotifications() && Supported())
|| Enforced()) {
if (*system->managerType() != ManagerType::Native) {
system->setManager(std::make_unique<Manager>(system));
}
} else {
if (*system->managerType() != ManagerType::Default) {
system->setManager(nullptr);
}
}
};
if (!system->managerType().has_value()) {
using DummyManager = Window::Notifications::DummyManager;
system->setManager(std::make_unique<DummyManager>(system));
}
return nullptr;
if (ServiceRegistered) {
const auto counter = std::make_shared<int>(3);
const auto oneReady = [=] {
if (!--*counter) {
managerSetter();
}
};
GetServerInformation([=](std::optional<ServerInformation> result) {
CurrentServerInformation = result;
oneReady();
});
GetCapabilities([=](QStringList result) {
CurrentCapabilities = result;
oneReady();
});
GetInhibitionSupported([=](bool result) {
InhibitionSupported = result;
oneReady();
});
} else {
CurrentServerInformation = std::nullopt;
CurrentCapabilities = QStringList{};
InhibitionSupported = false;
managerSetter();
}
}
class Manager::Private {
@@ -683,21 +784,21 @@ Manager::Private::Private(not_null<Manager*> manager, Type type)
return;
}
const auto serverInformation = GetServerInformation();
const auto capabilities = GetCapabilities();
const auto serverInformation = CurrentServerInformation;
const auto capabilities = CurrentCapabilities;
if (!serverInformation.empty()) {
if (serverInformation.has_value()) {
LOG(("Notification daemon product name: %1")
.arg(serverInformation[0]));
.arg(serverInformation->name));
LOG(("Notification daemon vendor name: %1")
.arg(serverInformation[1]));
.arg(serverInformation->vendor));
LOG(("Notification daemon version: %1")
.arg(serverInformation[2]));
.arg(serverInformation->version.toString()));
LOG(("Notification daemon specification version: %1")
.arg(serverInformation[3]));
.arg(serverInformation->specVersion.toString()));
}
if (!capabilities.isEmpty()) {
@@ -753,15 +854,7 @@ void Manager::Private::showNotification(
base::flat_map<MsgId, Notification>()).first;
}
i->second.emplace(msgId, notification);
if (!notification->show()) {
i = _notifications.find(key);
if (i != _notifications.cend()) {
i->second.remove(msgId);
if (i->second.empty()) {
_notifications.erase(i);
}
}
}
notification->show();
}
void Manager::Private::clearAll() {

View File

@@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "platform/linux/notifications_manager_linux.h"
#include "base/platform/base_platform_info.h"
namespace Platform {
namespace Notifications {
@@ -27,13 +29,19 @@ bool Supported() {
return false;
}
std::unique_ptr<Window::Notifications::Manager> Create(
Window::Notifications::System *system) {
if (IsWayland()) {
return std::make_unique<Window::Notifications::DummyManager>(system);
}
bool Enforced() {
// Wayland doesn't support positioning
// and custom notifications don't work here
return IsWayland();
}
return nullptr;
void Create(Window::Notifications::System *system) {
if (Enforced()) {
using DummyManager = Window::Notifications::DummyManager;
system->setManager(std::make_unique<DummyManager>(system));
} else {
system->setManager(nullptr);
}
}
} // namespace Notifications

View File

@@ -7,24 +7,21 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "platform/linux/specific_linux.h"
#include "platform/linux/linux_libs.h"
#include "base/platform/base_platform_info.h"
#include "base/platform/linux/base_xcb_utilities_linux.h"
#include "platform/linux/linux_desktop_environment.h"
#include "platform/linux/linux_gtk_integration.h"
#include "platform/linux/linux_wayland_integration.h"
#include "base/qt_adapters.h"
#include "lang/lang_keys.h"
#include "mainwidget.h"
#include "mainwindow.h"
#include "platform/linux/linux_desktop_environment.h"
#include "platform/linux/file_utilities_linux.h"
#include "platform/linux/linux_wayland_integration.h"
#include "platform/platform_notifications_manager.h"
#include "storage/localstorage.h"
#include "core/crash_reports.h"
#include "core/update_checker.h"
#include "window/window_controller.h"
#include "core/application.h"
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
#include "platform/linux/linux_notification_service_watcher.h"
#include "platform/linux/linux_gsd_media_keys.h"
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
@@ -65,13 +62,12 @@ extern "C" {
#include <iostream>
using namespace Platform;
using Platform::File::internal::EscapeShell;
using Platform::internal::WaylandIntegration;
using Platform::internal::GtkIntegration;
namespace Platform {
namespace {
constexpr auto kDisableGtkIntegration = "TDESKTOP_DISABLE_GTK_INTEGRATION"_cs;
constexpr auto kIgnoreGtkIncompatibility = "TDESKTOP_I_KNOW_ABOUT_GTK_INCOMPATIBILITY"_cs;
constexpr auto kDesktopFile = ":/misc/telegramdesktop.desktop"_cs;
@@ -87,6 +83,8 @@ constexpr auto kXCBFrameExtentsAtomName = "_GTK_FRAME_EXTENTS"_cs;
QStringList PlatformThemes;
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
std::unique_ptr<internal::NotificationServiceWatcher> NSWInstance;
QStringList ListDBusActivatableNames() {
static const auto Result = [&] {
const auto message = QDBusMessage::createMethodCall(
@@ -100,8 +98,8 @@ QStringList ListDBusActivatableNames() {
if (reply.isValid()) {
return reply.value();
} else {
LOG(("App Error: %1: %2")
} else if (reply.error().type() != QDBusError::Disconnected) {
LOG(("ListActivatableNames Error: %1: %2")
.arg(reply.error().name())
.arg(reply.error().message()));
}
@@ -152,13 +150,14 @@ void PortalAutostart(bool autostart, bool silent = false) {
if (silent) {
QDBusConnection::sessionBus().send(message);
} else {
const QDBusReply<void> reply = QDBusConnection::sessionBus().call(
message);
return;
}
if (!reply.isValid()) {
LOG(("Flatpak autostart error: %1").arg(reply.error().message()));
}
const QDBusError error = QDBusConnection::sessionBus().call(message);
if (error.isValid()) {
LOG(("Flatpak Autostart Error: %1: %2")
.arg(error.name())
.arg(error.message()));
}
}
@@ -214,11 +213,12 @@ uint FileChooserPortalVersion() {
if (reply.isValid()) {
return reply.value().toUInt();
} else {
LOG(("Error getting FileChooser portal version: %1")
.arg(reply.error().message()));
}
LOG(("Error getting FileChooser portal version: %1: %2")
.arg(reply.error().name())
.arg(reply.error().message()));
return 0;
}();
@@ -226,6 +226,32 @@ uint FileChooserPortalVersion() {
}
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
QByteArray EscapeShell(const QByteArray &content) {
auto result = QByteArray();
auto b = content.constData(), e = content.constEnd();
for (auto ch = b; ch != e; ++ch) {
if (*ch == ' ' || *ch == '"' || *ch == '\'' || *ch == '\\') {
if (result.isEmpty()) {
result.reserve(content.size() * 2);
}
if (ch > b) {
result.append(b, ch - b);
}
result.append('\\');
b = ch;
}
}
if (result.isEmpty()) {
return content;
}
if (e > b) {
result.append(b, e - b);
}
return result;
}
QString EscapeShellInLauncher(const QString &content) {
return EscapeShell(content.toUtf8()).replace('\\', "\\\\");
}
@@ -327,21 +353,6 @@ bool GenerateDesktopFile(
}
}
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
bool GetImageFromClipboardSupported() {
return (Libs::gtk_clipboard_wait_for_contents != nullptr)
&& (Libs::gtk_clipboard_wait_for_image != nullptr)
&& (Libs::gtk_selection_data_targets_include_image != nullptr)
&& (Libs::gtk_selection_data_free != nullptr)
&& (Libs::gdk_pixbuf_get_pixels != nullptr)
&& (Libs::gdk_pixbuf_get_width != nullptr)
&& (Libs::gdk_pixbuf_get_height != nullptr)
&& (Libs::gdk_pixbuf_get_rowstride != nullptr)
&& (Libs::gdk_pixbuf_get_has_alpha != nullptr)
&& (Libs::gdk_atom_intern != nullptr);
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
uint XCBMoveResizeFromEdges(Qt::Edges edges) {
if (edges == (Qt::TopEdge | Qt::LeftEdge))
return 0;
@@ -559,28 +570,17 @@ bool IsStaticBinary() {
#endif // !DESKTOP_APP_USE_PACKAGED
}
bool UseGtkIntegration() {
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
static const auto Result = !qEnvironmentVariableIsSet(
kDisableGtkIntegration.utf8());
return Result;
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
return false;
}
bool IsGtkIntegrationForced() {
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
static const auto Result = [&] {
if (!GtkIntegration::Instance()) {
return false;
}
return PlatformThemes.contains(qstr("gtk3"), Qt::CaseInsensitive)
|| PlatformThemes.contains(qstr("gtk2"), Qt::CaseInsensitive);
}();
return Result;
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
return false;
}
bool AreQtPluginsBundled() {
@@ -632,6 +632,17 @@ bool CanOpenDirectoryWithPortal() {
return false;
}
bool IsNotificationServiceActivatable() {
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
static const auto Result = ListDBusActivatableNames().contains(
qsl("org.freedesktop.Notifications"));
return Result;
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
return false;
}
QString AppRuntimeDirectory() {
static const auto Result = [&] {
auto runtimeDir = QStandardPaths::writableLocation(
@@ -707,64 +718,38 @@ QString GetIconName() {
}
QImage GetImageFromClipboard() {
QImage data;
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
if (!GetImageFromClipboardSupported() || !Libs::GtkClipboard()) {
return data;
if (const auto integration = GtkIntegration::Instance()) {
return integration->getImageFromClipboard();
}
auto gsel = Libs::gtk_clipboard_wait_for_contents(
Libs::GtkClipboard(),
Libs::gdk_atom_intern("TARGETS", true));
if (gsel) {
if (Libs::gtk_selection_data_targets_include_image(gsel, false)) {
auto img = Libs::gtk_clipboard_wait_for_image(
Libs::GtkClipboard());
if (img) {
data = QImage(
Libs::gdk_pixbuf_get_pixels(img),
Libs::gdk_pixbuf_get_width(img),
Libs::gdk_pixbuf_get_height(img),
Libs::gdk_pixbuf_get_rowstride(img),
Libs::gdk_pixbuf_get_has_alpha(img)
? QImage::Format_RGBA8888
: QImage::Format_RGB888).copy();
g_object_unref(img);
}
}
Libs::gtk_selection_data_free(gsel);
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
return data;
return {};
}
std::optional<bool> IsDarkMode() {
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
if (Libs::GtkSettingSupported() && Libs::GtkLoaded()) {
if (Libs::gtk_check_version != nullptr
&& !Libs::gtk_check_version(3, 0, 0)
&& Libs::GtkSetting<gboolean>(
"gtk-application-prefer-dark-theme")) {
return true;
}
const auto themeName = Libs::GtkSetting("gtk-theme-name").toLower();
if (themeName.contains(qsl("-dark"))) {
return true;
}
return false;
const auto integration = GtkIntegration::Instance();
if (!integration) {
return std::nullopt;
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
return std::nullopt;
if (integration->checkVersion(3, 0, 0)) {
const auto preferDarkTheme = integration->getBoolSetting(
qsl("gtk-application-prefer-dark-theme"));
if (!preferDarkTheme.has_value()) {
return std::nullopt;
} else if (*preferDarkTheme) {
return true;
}
}
const auto themeName = integration->getStringSetting(qsl("gtk-theme-name"));
if (!themeName.has_value()) {
return std::nullopt;
} else if (themeName->toLower().contains(qsl("-dark"))) {
return true;
}
return false;
}
bool AutostartSupported() {
@@ -832,38 +817,44 @@ bool WindowsNeedShadow() {
}
Window::ControlsLayout WindowControlsLayout() {
#ifndef TDESKTOP_DISABLE_GTK_INTEGRATION
if (Libs::GtkSettingSupported()
&& Libs::GtkLoaded()
&& Libs::gtk_check_version != nullptr
&& !Libs::gtk_check_version(3, 12, 0)) {
const auto decorationLayout = Libs::GtkSetting(
"gtk-decoration-layout").split(':');
const auto gtkResult = []() -> std::optional<Window::ControlsLayout> {
const auto integration = GtkIntegration::Instance();
if (!integration || !integration->checkVersion(3, 12, 0)) {
return std::nullopt;
}
const auto decorationLayoutSetting = integration->getStringSetting(
qsl("gtk-decoration-layout"));
if (!decorationLayoutSetting.has_value()) {
return std::nullopt;
}
const auto decorationLayout = decorationLayoutSetting->split(':');
std::vector<Window::Control> controlsLeft;
ranges::transform(
decorationLayout[0].split(','),
ranges::back_inserter(controlsLeft),
GtkKeywordToWindowControl
);
GtkKeywordToWindowControl);
std::vector<Window::Control> controlsRight;
if (decorationLayout.size() > 1) {
ranges::transform(
decorationLayout[1].split(','),
ranges::back_inserter(controlsRight),
GtkKeywordToWindowControl
);
GtkKeywordToWindowControl);
}
return Window::ControlsLayout{
.left = controlsLeft,
.right = controlsRight
};
}
#endif // !TDESKTOP_DISABLE_GTK_INTEGRATION
}();
if (DesktopEnvironment::IsUnity()) {
if (gtkResult.has_value()) {
return *gtkResult;
} else if (DesktopEnvironment::IsUnity()) {
return Window::ControlsLayout{
.left = {
Window::Control::Close,
@@ -967,7 +958,7 @@ void start() {
// if gtk integration and qgtk3/qgtk2 platformtheme (or qgtk2 style)
// is used at the same time, the app will crash
if (UseGtkIntegration()
if (GtkIntegration::Instance()
&& !IsStaticBinary()
&& !qEnvironmentVariableIsSet(
kIgnoreGtkIncompatibility.utf8())) {
@@ -988,7 +979,7 @@ void start() {
"Keep in mind that this will lead to clipboard issues "
"and tdesktop will be unable to get settings from GTK "
"(such as decoration layout, dark mode & more).",
kDisableGtkIntegration.utf8().constData());
internal::kDisableGtkIntegration.utf8().constData());
qunsetenv("QT_QPA_PLATFORMTHEME");
qunsetenv("QT_STYLE_OVERRIDE");
@@ -999,7 +990,7 @@ void start() {
}
}
if (!UseGtkIntegration()) {
if (!GtkIntegration::Instance()) {
g_warning(
"GTK integration was disabled on build or in runtime. "
"This will lead to clipboard issues and a lack of some features "
@@ -1224,16 +1215,24 @@ void start() {
DEBUG_LOG(("Icon theme: %1").arg(QIcon::themeName()));
DEBUG_LOG(("Fallback icon theme: %1").arg(QIcon::fallbackThemeName()));
Libs::start();
MainWindow::LibsLoaded();
if (const auto integration = GtkIntegration::Instance()) {
integration->load();
}
// wait for interface announce to know if native window frame is supported
if (const auto waylandIntegration = WaylandIntegration::Instance()) {
waylandIntegration->waitForInterfaceAnnounce();
}
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
NSWInstance = std::make_unique<internal::NotificationServiceWatcher>();
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
}
void finish() {
#ifndef DESKTOP_APP_DISABLE_DBUS_INTEGRATION
NSWInstance = nullptr;
#endif // !DESKTOP_APP_DISABLE_DBUS_INTEGRATION
}
} // namespace ThirdParty

View File

@@ -9,8 +9,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "platform/platform_specific.h"
#include <signal.h>
namespace Data {
class LocationPoint;
} // namespace Data
@@ -21,10 +19,10 @@ bool InFlatpak();
bool InSnap();
bool IsStaticBinary();
bool AreQtPluginsBundled();
bool UseGtkIntegration();
bool IsGtkIntegrationForced();
bool UseXDGDesktopPortal();
bool CanOpenDirectoryWithPortal();
bool IsNotificationServiceActivatable();
QString AppRuntimeDirectory();
QString GetLauncherBasename();

View File

@@ -27,6 +27,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "window/window_controller.h"
#include "window/themes/window_theme.h"
#include "platform/mac/touchbar/mac_touchbar_manager.h"
#include "platform/platform_specific.h"
#include "platform/platform_notifications_manager.h"
#include "base/platform/base_platform_info.h"
#include "boxes/peer_list_controllers.h"
@@ -692,7 +693,9 @@ void MainWindow::createGlobalMenu() {
about->setMenuRole(QAction::AboutQtRole);
main->addSeparator();
QAction *prefs = main->addAction(tr::lng_mac_menu_preferences(tr::now), App::wnd(), SLOT(showSettings()), QKeySequence(Qt::ControlModifier | Qt::Key_Comma));
QAction *prefs = main->addAction(tr::lng_mac_menu_preferences(tr::now), App::wnd(), [=] {
App::wnd()->showSettings();
}, QKeySequence(Qt::ControlModifier | Qt::Key_Comma));
prefs->setMenuRole(QAction::PreferencesRole);
QMenu *file = psMainMenu.addMenu(tr::lng_mac_menu_file(tr::now));
@@ -728,19 +731,27 @@ void MainWindow::createGlobalMenu() {
psContacts = window->addAction(tr::lng_mac_menu_contacts(tr::now));
connect(psContacts, &QAction::triggered, psContacts, crl::guard(this, [=] {
if (isHidden()) {
App::wnd()->showFromTray();
showFromTray();
}
if (!sessionController()) {
return;
}
Ui::show(PrepareContactsBox(sessionController()));
}));
psAddContact = window->addAction(tr::lng_mac_menu_add_contact(tr::now), App::wnd(), SLOT(onShowAddContact()));
psAddContact = window->addAction(tr::lng_mac_menu_add_contact(tr::now), App::wnd(), [=] {
App::wnd()->showAddContact();
});
window->addSeparator();
psNewGroup = window->addAction(tr::lng_mac_menu_new_group(tr::now), App::wnd(), SLOT(onShowNewGroup()));
psNewChannel = window->addAction(tr::lng_mac_menu_new_channel(tr::now), App::wnd(), SLOT(onShowNewChannel()));
psNewGroup = window->addAction(tr::lng_mac_menu_new_group(tr::now), App::wnd(), [=] {
App::wnd()->showNewGroup();
});
psNewChannel = window->addAction(tr::lng_mac_menu_new_channel(tr::now), App::wnd(), [=] {
App::wnd()->showNewChannel();
});
window->addSeparator();
psShowTelegram = window->addAction(tr::lng_mac_menu_show(tr::now), App::wnd(), SLOT(showFromTray()));
psShowTelegram = window->addAction(tr::lng_mac_menu_show(tr::now), App::wnd(), [=] {
showFromTray();
});
updateGlobalMenu();
}

View File

@@ -162,11 +162,16 @@ bool Supported() {
return Platform::IsMac10_8OrGreater();
}
std::unique_ptr<Window::Notifications::Manager> Create(Window::Notifications::System *system) {
bool Enforced() {
return Supported();
}
void Create(Window::Notifications::System *system) {
if (Supported()) {
return std::make_unique<Manager>(system);
system->setManager(std::make_unique<Manager>(system));
} else {
system->setManager(nullptr);
}
return nullptr;
}
class Manager::Private : public QObject, private base::Subscriber {

View File

@@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "base/timer.h"
#include "styles/style_window.h"
#include "platform/platform_specific.h"
#include <QtGui/QWindow>
#include <QtWidgets/QApplication>

View File

@@ -17,8 +17,8 @@ namespace Notifications {
[[nodiscard]] bool SkipFlashBounce();
[[nodiscard]] bool Supported();
[[nodiscard]] std::unique_ptr<Window::Notifications::Manager> Create(
Window::Notifications::System *system);
[[nodiscard]] bool Enforced();
void Create(Window::Notifications::System *system);
} // namespace Notifications
} // namespace Platform

View File

@@ -8,6 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "platform/win/main_window_win.h"
#include "styles/style_window.h"
#include "platform/platform_specific.h"
#include "platform/platform_notifications_manager.h"
#include "platform/win/windows_dlls.h"
#include "platform/win/windows_event_filter.h"
@@ -47,6 +48,13 @@ Q_DECLARE_METATYPE(QMargins);
namespace Platform {
namespace {
// Mouse down on tray icon deactivates the application.
// So there is no way to know for sure if the tray icon was clicked from
// active application or from inactive application. So we assume that
// if the application was deactivated less than 0.5s ago, then the tray
// icon click (both left or right button) was made from the active app.
constexpr auto kKeepActiveForTrayIcon = crl::time(500);
HICON createHIconFromQIcon(const QIcon &icon, int xSize, int ySize) {
if (!icon.isNull()) {
const QPixmap pm = icon.pixmap(icon.actualSize(QSize(xSize, ySize)));
@@ -113,6 +121,13 @@ MainWindow::MainWindow(not_null<Window::Controller*> controller)
}
});
setupNativeWindowFrame();
using namespace rpl::mappers;
Core::App().appDeactivatedValue(
) | rpl::distinct_until_changed(
) | rpl::filter(_1) | rpl::start_with_next([=] {
_lastDeactivateTime = crl::now();
}, lifetime());
}
void MainWindow::setupNativeWindowFrame() {
@@ -280,6 +295,11 @@ void MainWindow::updateWindowIcon() {
updateIconCounters();
}
bool MainWindow::isActiveForTrayMenu() {
return !_lastDeactivateTime
|| (_lastDeactivateTime + kKeepActiveForTrayIcon >= crl::now());
}
void MainWindow::unreadCounterChangedHook() {
setWindowTitle(titleText());
updateIconCounters();
@@ -604,6 +624,17 @@ void MainWindow::fixMaximizedWindow() {
}
}
void MainWindow::showFromTrayMenu() {
// If we try to activate() window before the trayIconMenu is hidden,
// then the window will be shown in semi-active state (Qt bug).
// It will receive input events, but it will be rendered as inactive.
using namespace rpl::mappers;
_showFromTrayLifetime = trayIconMenu->shownValue(
) | rpl::filter(_1) | rpl::take(1) | rpl::start_with_next([=] {
showFromTray();
});
}
HWND MainWindow::psHwnd() const {
return ps_hWnd;
}

View File

@@ -24,6 +24,8 @@ class MainWindow : public Window::MainWindow {
public:
explicit MainWindow(not_null<Window::Controller*> controller);
void showFromTrayMenu() override;
HWND psHwnd() const;
HMENU psMenu() const;
@@ -32,6 +34,7 @@ public:
void updateCustomMargins();
void updateWindowIcon() override;
bool isActiveForTrayMenu() override;
void psRefreshTaskbarIcon();
@@ -103,6 +106,10 @@ private:
bool _wasNativeFrame = false;
bool _hasActiveFrame = false;
// Workarounds for activation from tray icon.
crl::time _lastDeactivateTime = 0;
rpl::lifetime _showFromTrayLifetime;
HWND ps_hWnd = nullptr;
HWND ps_tbHider_hWnd = nullptr;
HMENU ps_menu = nullptr;

View File

@@ -325,16 +325,21 @@ bool Supported() {
return false;
}
std::unique_ptr<Window::Notifications::Manager> Create(Window::Notifications::System *system) {
bool Enforced() {
return false;
}
void Create(Window::Notifications::System *system) {
#ifndef __MINGW32__
if (Core::App().settings().nativeNotifications() && Supported()) {
auto result = std::make_unique<Manager>(system);
if (result->init()) {
return std::move(result);
system->setManager(std::move(result));
return;
}
}
#endif // !__MINGW32__
return nullptr;
system->setManager(nullptr);
}
#ifndef __MINGW32__

View File

@@ -44,6 +44,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_file_origin.h"
#include "chat_helpers/emoji_sets_manager.h"
#include "base/platform/base_platform_info.h"
#include "platform/platform_specific.h"
#include "base/call_delayed.h"
#include "support/support_common.h"
#include "support/support_templates.h"

View File

@@ -17,6 +17,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "lang/lang_keys.h"
#include "window/notifications_manager.h"
#include "window/window_session_controller.h"
#include "platform/platform_specific.h"
#include "platform/platform_notifications_manager.h"
#include "base/platform/base_platform_info.h"
#include "mainwindow.h"
@@ -676,14 +677,13 @@ void SetupNotificationsContent(
}, joined->lifetime());
const auto nativeText = [&] {
if (!Platform::Notifications::Supported()) {
if (!Platform::Notifications::Supported()
|| Platform::Notifications::Enforced()) {
return QString();
} else if (Platform::IsWindows()) {
return tr::lng_settings_use_windows(tr::now);
} else if (Platform::IsLinux() && !Platform::IsWayland()) {
return tr::lng_settings_use_native_notifications(tr::now);
}
return QString();
return tr::lng_settings_use_native_notifications(tr::now);
}();
const auto native = [&]() -> Ui::Checkbox* {
if (nativeText.isEmpty()) {
@@ -697,8 +697,7 @@ void SetupNotificationsContent(
return addCheckbox(nativeText, settings.nativeNotifications());
}();
const auto advancedSlide = !Platform::IsMac10_8OrGreater()
&& !Platform::IsWayland()
const auto advancedSlide = !Platform::Notifications::Enforced()
? container->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
container,

View File

@@ -171,6 +171,10 @@ void DownloadManagerMtproto::checkSendNext(MTP::DcId dcId, Queue &queue) {
}
}
void DownloadManagerMtproto::checkSendNextAfterSuccess(MTP::DcId dcId) {
checkSendNext(dcId, _queues[dcId]);
}
bool DownloadManagerMtproto::trySendNextPart(MTP::DcId dcId, Queue &queue) {
auto &balanceData = _balanceData[dcId];
const auto &sessions = balanceData.sessions;
@@ -227,10 +231,6 @@ void DownloadManagerMtproto::requestSucceeded(
crl::time timeAtRequestStart) {
using namespace rpl::mappers;
const auto guard = gsl::finally([&] {
checkSendNext(dcId, _queues[dcId]);
});
const auto i = _balanceData.find(dcId);
Assert(i != end(_balanceData));
auto &dc = i->second;
@@ -606,24 +606,34 @@ void DownloadMtprotoTask::normalPartLoaded(
const auto requestData = finishSentRequest(
requestId,
FinishRequestReason::Success);
const auto owner = _owner;
const auto dcId = this->dcId();
result.match([&](const MTPDupload_fileCdnRedirect &data) {
switchToCDN(requestData, data);
}, [&](const MTPDupload_file &data) {
partLoaded(requestData.offset, data.vbytes().v);
});
// 'this' may be deleted at this point.
owner->checkSendNextAfterSuccess(dcId);
}
void DownloadMtprotoTask::webPartLoaded(
const MTPupload_WebFile &result,
mtpRequestId requestId) {
const auto requestData = finishSentRequest(
requestId,
FinishRequestReason::Success);
const auto owner = _owner;
const auto dcId = this->dcId();
result.match([&](const MTPDupload_webFile &data) {
const auto requestData = finishSentRequest(
requestId,
FinishRequestReason::Success);
if (setWebFileSizeHook(data.vsize().v)) {
partLoaded(requestData.offset, data.vbytes().v);
}
});
// 'this' may be deleted at this point.
owner->checkSendNextAfterSuccess(dcId);
}
void DownloadMtprotoTask::cdnPartLoaded(const MTPupload_CdnFile &result, mtpRequestId requestId) {
@@ -647,6 +657,13 @@ void DownloadMtprotoTask::cdnPartLoaded(const MTPupload_CdnFile &result, mtpRequ
const auto requestData = finishSentRequest(
requestId,
FinishRequestReason::Success);
const auto owner = _owner;
const auto dcId = this->dcId();
const auto guard = gsl::finally([=] {
// 'this' may be deleted at this point.
owner->checkSendNextAfterSuccess(dcId);
});
auto key = bytes::make_span(_cdnEncryptionKey);
auto iv = bytes::make_span(_cdnEncryptionIV);
Expects(key.size() == MTP::CTRState::KeySize);

View File

@@ -51,6 +51,7 @@ public:
int index,
int amountAtRequestStart,
crl::time timeAtRequestStart);
void checkSendNextAfterSuccess(MTP::DcId dcId);
[[nodiscard]] int chooseSessionIndex(MTP::DcId dcId) const;
private:

View File

@@ -0,0 +1,151 @@
/*
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 "storage/storage_cloud_song_cover.h"
#include "data/data_cloud_file.h"
#include "data/data_document.h"
#include "data/data_file_origin.h"
#include "data/data_session.h"
#include "main/main_session.h"
#include "storage/file_download.h"
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QJsonValue>
namespace Storage::CloudSongCover {
namespace {
constexpr auto kMaxResponseSize = 1024 * 1024;
constexpr auto kDefaultCoverSize = 100;
struct Responce {
const QString artworkUrl;
const int size;
};
auto Location(const QString &url) {
return DownloadLocation{ PlainUrlLocation{ url } };
}
auto JsonUrl(not_null<SongData*> song) {
return QString("https://itunes.apple.com/search?term=" \
"%1 %2&entity=song&limit=4").arg(song->performer).arg(song->title);
}
// Dummy JSON responce.
// {
// "resultCount": 2,
// "results": [
// {
// "artworkUrl100": ""
// },
// {
// "artworkUrl100": ""
// }
// ]
// }
std::optional<Responce> ParseResponce(const QByteArray &response) {
if (response.size() >= kMaxResponseSize) {
return std::nullopt;
}
auto error = QJsonParseError{ 0, QJsonParseError::NoError };
const auto document = QJsonDocument::fromJson(response, &error);
const auto log = [](const QString &message) {
DEBUG_LOG(("Parse Artwork JSON Error: %1.").arg(message));
};
if (error.error != QJsonParseError::NoError) {
log(error.errorString());
return std::nullopt;
} else if (!document.isObject()) {
log("not an object received in JSON");
return std::nullopt;
}
const auto results = document.object().value("results");
if (!results.isArray()) {
log("'results' field not found");
return std::nullopt;
}
const auto resultsArray = results.toArray();
if (resultsArray.empty()) {
return std::nullopt;
}
const auto artworkUrl = resultsArray.first().toObject()
.value("artworkUrl100").toString();
if (artworkUrl.isEmpty()) {
log("'artworkUrl100' field is empty");
return std::nullopt;
}
return Responce{ artworkUrl, kDefaultCoverSize };
}
void LoadAndApplyThumbnail(
not_null<DocumentData*> document,
const Responce &responce) {
const auto size = responce.size;
const auto imageWithLocation = ImageWithLocation{
.location = ImageLocation(Location(responce.artworkUrl), size, size)
};
document->updateThumbnails(
QByteArray(),
imageWithLocation,
ImageWithLocation{ .location = ImageLocation() });
document->loadThumbnail(Data::FileOrigin());
}
}
void LoadThumbnailFromExternal(not_null<DocumentData*> document) {
const auto songData = document->song();
if (!songData
|| songData->performer.isEmpty()
|| songData->title.isEmpty()) {
return;
}
const auto &size = kDefaultCoverSize;
const auto jsonLocation = ImageWithLocation{
.location = ImageLocation(Location(JsonUrl(songData)), size, size)
};
const auto jsonCloudFile = std::make_shared<Data::CloudFile>();
Data::UpdateCloudFile(
*jsonCloudFile,
jsonLocation,
document->owner().cache(),
0, // Cache tag.
nullptr,
nullptr);
auto done = [=](const QByteArray &result) {
if (!jsonCloudFile) {
return;
}
if (const auto responce = ParseResponce(result)) {
LoadAndApplyThumbnail(document, *responce);
}
};
Data::LoadCloudFile(
&document->session(),
*jsonCloudFile,
Data::FileOrigin(),
LoadFromCloudOrLocal,
true,
0,
[] { return true; },
std::move(done));
}
} // namespace Storage::CloudSongCover

View File

@@ -0,0 +1,16 @@
/*
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
class DocumentData;
namespace Storage::CloudSongCover {
void LoadThumbnailFromExternal(not_null<DocumentData*> document);
} // namespace Storage::CloudSongCover

View File

@@ -8,6 +8,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/chat/attach/attach_prepare.h"
#include "ui/chat/attach/attach_send_files_way.h"
#include "ui/image/image_prepare.h"
#include "ui/ui_utility.h"
#include "core/mime_type.h"
namespace Ui {
@@ -150,6 +152,25 @@ bool PreparedList::canAddCaption(bool sendingAlbum) const {
} else if (!sendingAlbum) {
return false;
}
// All music.
{
auto pred = [](const PreparedFile &file) {
return file.type == PreparedFile::Type::Music;
};
if (ranges::all_of(files, std::move(pred))) {
return true;
}
}
// All files.
{
auto pred = [](const PreparedFile &file) {
return file.type == PreparedFile::Type::File;
};
if (ranges::all_of(files, std::move(pred))) {
return true;
}
}
const auto hasFiles = ranges::contains(
files,
PreparedFile::Type::File,
@@ -255,4 +276,20 @@ std::vector<PreparedGroup> DivideByGroups(
return result;
}
QPixmap PrepareSongCoverForThumbnail(QImage image, int size) {
const auto scaledSize = image.size().scaled(
size,
size,
Qt::KeepAspectRatioByExpanding);
using Option = Images::Option;
return PixmapFromImage(Images::prepare(
std::move(image),
scaledSize.width() * style::DevicePixelRatio(),
scaledSize.height() * style::DevicePixelRatio(),
Option::Circled | Option::Colored | Option::Smooth,
size,
size,
&st::songCoverOverlayFg));
}
} // namespace Ui

View File

@@ -120,7 +120,7 @@ struct PreparedGroup {
[[nodiscard]] bool sentWithCaption() const {
return (list.files.size() == 1)
|| (type == AlbumType::PhotoVideo);
|| (type != AlbumType::None);
}
};
@@ -132,4 +132,6 @@ struct PreparedGroup {
[[nodiscard]] int MaxAlbumItems();
[[nodiscard]] bool ValidateThumbDimensions(int width, int height);
[[nodiscard]] QPixmap PrepareSongCoverForThumbnail(QImage image, int size);
} // namespace Ui

View File

@@ -32,7 +32,7 @@ SingleFilePreview::SingleFilePreview(
_editMedia->setIconOverride(&st::sendBoxAlbumGroupEditButtonIconFile);
_deleteMedia->setIconOverride(&st::sendBoxAlbumGroupDeleteButtonIconFile);
const auto &st = _fileThumb.isNull()
const auto &st = !isThumbedLayout()
? st::attachPreviewLayout
: st::attachPreviewThumbLayout;
resize(width(), st.thumbSize);
@@ -106,13 +106,19 @@ void SingleFilePreview::preparePreview(const PreparedFile &file) {
songTitle = song->title;
songPerformer = song->performer;
_fileIsAudio = true;
if (auto cover = song->cover; !cover.isNull()) {
_fileThumb = Ui::PrepareSongCoverForThumbnail(
cover,
st::attachPreviewLayout.thumbSize);
}
}
}
_name = ComposeNameString(filename, songTitle, songPerformer);
_statusText = FormatSizeText(fileinfo.size());
}
const auto &st = _fileThumb.isNull()
const auto &st = !isThumbedLayout()
? st::attachPreviewLayout
: st::attachPreviewThumbLayout;
const auto nameleft = st.thumbSize + st.padding.right();
@@ -141,7 +147,7 @@ void SingleFilePreview::paintEvent(QPaintEvent *e) {
auto w = width() - st::boxPhotoPadding.left() - st::boxPhotoPadding.right();
auto h = height();
const auto &st = _fileThumb.isNull()
const auto &st = !isThumbedLayout()
? st::attachPreviewLayout
: st::attachPreviewThumbLayout;
const auto nameleft = st.thumbSize + st.padding.right();
@@ -149,12 +155,14 @@ void SingleFilePreview::paintEvent(QPaintEvent *e) {
const auto statustop = st.statusTop;
const auto x = (width() - w) / 2, y = 0;
if (_fileThumb.isNull()) {
if (!isThumbedLayout()) {
QRect inner(style::rtlrect(x, y, st.thumbSize, st.thumbSize, width()));
p.setPen(Qt::NoPen);
p.setBrush(st::msgFileInBg);
{
if (_fileIsAudio && !_fileThumb.isNull()) {
p.drawPixmap(inner.topLeft(), _fileThumb);
} else {
p.setBrush(st::msgFileInBg);
PainterHighQualityEnabler hq(p);
p.drawEllipse(inner);
}
@@ -188,4 +196,8 @@ void SingleFilePreview::resizeEvent(QResizeEvent *e) {
_editMedia->moveToRight(right, top);
}
bool SingleFilePreview::isThumbedLayout() const {
return (!_fileThumb.isNull() && !_fileIsAudio);
}
} // namespace Ui

View File

@@ -32,6 +32,8 @@ private:
void preparePreview(const PreparedFile &file);
void prepareThumb(const QImage &preview);
bool isThumbedLayout() const;
QPixmap _fileThumb;
QString _name;
QString _statusText;

Some files were not shown because too many files have changed in this diff Show More