From 46445e0542f0104d8a06938076f7140c6d4a5d84 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Wed, 4 Mar 2020 09:45:44 +0400 Subject: [PATCH] Implement global menu on Linux --- Telegram/Resources/langs/lang.strings | 2 + .../platform/linux/main_window_linux.cpp | 405 ++++++++++++++++++ .../platform/linux/main_window_linux.h | 45 ++ 3 files changed, 452 insertions(+) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 02c23cbb8..849fbcb02 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -2239,6 +2239,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_linux_menu_undo" = "Undo"; "lng_linux_menu_redo" = "Redo"; +"lng_linux_menu_tools" = "Tools"; +"lng_linux_menu_help" = "Help"; "lng_linux_no_audio_prefs" = "You don't have any audio configuration applications installed."; diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp index 1df10783c..af339231c 100644 --- a/Telegram/SourceFiles/platform/linux/main_window_linux.cpp +++ b/Telegram/SourceFiles/platform/linux/main_window_linux.cpp @@ -13,15 +13,23 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "platform/linux/linux_desktop_environment.h" #include "platform/platform_notifications_manager.h" #include "history/history.h" +#include "history/history_widget.h" +#include "history/history_inner_widget.h" +#include "main/main_account.h" // Account::sessionChanges. #include "mainwindow.h" #include "core/application.h" #include "core/sandbox.h" +#include "boxes/peer_list_controllers.h" +#include "boxes/about_box.h" #include "lang/lang_keys.h" #include "storage/localstorage.h" +#include "window/window_session_controller.h" +#include "ui/widgets/input_fields.h" #include "facades.h" #include "app.h" #include +#include #ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION #include @@ -46,6 +54,10 @@ constexpr auto kSNIWatcherService = "org.kde.StatusNotifierWatcher"_cs; constexpr auto kPropertiesInterface = "org.freedesktop.DBus.Properties"_cs; constexpr auto kTrayIconFilename = "tdesktop-trayicon-XXXXXX.png"_cs; +constexpr auto kAppMenuService = "com.canonical.AppMenu.Registrar"_cs; +constexpr auto kAppMenuObjectPath = "/com/canonical/AppMenu/Registrar"_cs; +constexpr auto kAppMenuInterface = kAppMenuService; + bool TrayIconMuted = true; int32 TrayIconCount = 0; base::flat_map TrayIconImageBack; @@ -307,6 +319,71 @@ quint32 djbStringHash(QString string) { return hash; } +#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION +bool AppMenuSupported() { + static const auto Available = QDBusInterface( + kAppMenuService.utf16(), + kAppMenuObjectPath.utf16(), + kAppMenuInterface.utf16()).isValid(); + + return Available; +} + +void RegisterAppMenu(uint winId, const QDBusObjectPath &menuPath) { + auto message = QDBusMessage::createMethodCall( + kAppMenuService.utf16(), + kAppMenuObjectPath.utf16(), + kAppMenuInterface.utf16(), + qsl("RegisterWindow")); + + message.setArguments({ + winId, + QVariant::fromValue(menuPath) + }); + + QDBusConnection::sessionBus().send(message); +} + +void UnregisterAppMenu(uint winId) { + auto message = QDBusMessage::createMethodCall( + kAppMenuService.utf16(), + kAppMenuObjectPath.utf16(), + kAppMenuInterface.utf16(), + qsl("UnregisterWindow")); + + message.setArguments({ + winId + }); + + QDBusConnection::sessionBus().send(message); +} + +void SendKeySequence( + Qt::Key key, + Qt::KeyboardModifiers modifiers = Qt::NoModifier) { + const auto focused = QApplication::focusWidget(); + if (qobject_cast(focused) + || qobject_cast(focused) + || qobject_cast(focused)) { + QApplication::postEvent( + focused, + new QKeyEvent(QEvent::KeyPress, key, modifiers)); + + QApplication::postEvent( + focused, + new QKeyEvent(QEvent::KeyRelease, key, modifiers)); + } +} + +void ForceDisabled(QAction *action, bool disabled) { + if (action->isEnabled()) { + if (disabled) action->setDisabled(true); + } else if (!disabled) { + action->setDisabled(false); + } +} +#endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION + } // namespace MainWindow::MainWindow(not_null controller) @@ -341,6 +418,12 @@ void MainWindow::initHook() { } else { LOG(("Not using Unity launcher counter.")); } + + connect( + windowHandle(), + &QWindow::visibleChanged, + this, + &MainWindow::onVisibleChanged); } bool MainWindow::hasTrayIcon() const { @@ -575,9 +658,331 @@ void MainWindow::initTrayMenuHook() { _trayIconMenuXEmbed->deleteOnHide(false); } +void MainWindow::createGlobalMenu() { +#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION + if (!AppMenuSupported()) return; + + psMainMenu = new QMenu(this); + + auto file = psMainMenu->addMenu(tr::lng_mac_menu_file(tr::now)); + + psLogout = file->addAction( + tr::lng_mac_menu_logout(tr::now), + App::wnd(), + SLOT(onLogout())); + + auto quit = file->addAction( + tr::lng_mac_menu_quit_telegram(tr::now, lt_telegram, qsl("Telegram")), + App::wnd(), + SLOT(quitFromTray()), + QKeySequence::Quit); + + quit->setMenuRole(QAction::QuitRole); + + auto edit = psMainMenu->addMenu(tr::lng_mac_menu_edit(tr::now)); + + psUndo = edit->addAction( + tr::lng_linux_menu_undo(tr::now), + this, + SLOT(psLinuxUndo()), + QKeySequence::Undo); + + psRedo = edit->addAction( + tr::lng_linux_menu_redo(tr::now), + this, + SLOT(psLinuxRedo()), + QKeySequence::Redo); + + edit->addSeparator(); + + psCut = edit->addAction( + tr::lng_mac_menu_cut(tr::now), + this, + SLOT(psLinuxCut()), + QKeySequence::Cut); + psCopy = edit->addAction( + tr::lng_mac_menu_copy(tr::now), + this, + SLOT(psLinuxCopy()), + QKeySequence::Copy); + + psPaste = edit->addAction( + tr::lng_mac_menu_paste(tr::now), + this, + SLOT(psLinuxPaste()), + QKeySequence::Paste); + + psDelete = edit->addAction( + tr::lng_mac_menu_delete(tr::now), + this, + SLOT(psLinuxDelete()), + QKeySequence(Qt::ControlModifier | Qt::Key_Backspace)); + + edit->addSeparator(); + + psBold = edit->addAction( + tr::lng_menu_formatting_bold(tr::now), + this, + SLOT(psLinuxBold()), + QKeySequence::Bold); + + psItalic = edit->addAction( + tr::lng_menu_formatting_italic(tr::now), + this, + SLOT(psLinuxItalic()), + QKeySequence::Italic); + + psUnderline = edit->addAction( + tr::lng_menu_formatting_underline(tr::now), + this, + SLOT(psLinuxUnderline()), + QKeySequence::Underline); + + psStrikeOut = edit->addAction( + tr::lng_menu_formatting_strike_out(tr::now), + this, + SLOT(psLinuxStrikeOut()), + Ui::kStrikeOutSequence); + + psMonospace = edit->addAction( + tr::lng_menu_formatting_monospace(tr::now), + this, + SLOT(psLinuxMonospace()), + Ui::kMonospaceSequence); + + psClearFormat = edit->addAction( + tr::lng_menu_formatting_clear(tr::now), + this, + SLOT(psLinuxClearFormat()), + Ui::kClearFormatSequence); + + edit->addSeparator(); + + psSelectAll = edit->addAction( + tr::lng_mac_menu_select_all(tr::now), + this, SLOT(psLinuxSelectAll()), + QKeySequence::SelectAll); + + edit->addSeparator(); + + auto prefs = edit->addAction( + tr::lng_mac_menu_preferences(tr::now), + App::wnd(), + SLOT(showSettings()), + QKeySequence(Qt::ControlModifier | Qt::Key_Comma)); + + prefs->setMenuRole(QAction::PreferencesRole); + + auto tools = psMainMenu->addMenu(tr::lng_linux_menu_tools(tr::now)); + + psContacts = tools->addAction( + tr::lng_mac_menu_contacts(tr::now), + crl::guard(this, [=] { + if (isHidden()) { + App::wnd()->showFromTray(); + } + + if (!account().sessionExists()) { + return; + } + + Ui::show( + Box(std::make_unique( + sessionController()), + [](not_null box) { + box->addButton(tr::lng_close(), [box] { + box->closeBox(); + }); + + box->addLeftButton(tr::lng_profile_add_contact(), [] { + App::wnd()->onShowAddContact(); + }); + })); + })); + + psAddContact = tools->addAction( + tr::lng_mac_menu_add_contact(tr::now), + App::wnd(), + SLOT(onShowAddContact())); + + tools->addSeparator(); + + psNewGroup = tools->addAction( + tr::lng_mac_menu_new_group(tr::now), + App::wnd(), + SLOT(onShowNewGroup())); + + psNewChannel = tools->addAction( + tr::lng_mac_menu_new_channel(tr::now), + App::wnd(), + SLOT(onShowNewChannel())); + + auto help = psMainMenu->addMenu(tr::lng_linux_menu_help(tr::now)); + + auto about = help->addAction( + tr::lng_mac_menu_about_telegram( + tr::now, + lt_telegram, + qsl("Telegram")), + [] { + if (App::wnd() && App::wnd()->isHidden()) { + App::wnd()->showFromTray(); + } + + Ui::show(Box()); + }); + + about->setMenuRole(QAction::AboutQtRole); + + _mainMenuPath.setPath(qsl("/MenuBar")); + + _mainMenuExporter = new DBusMenuExporter( + _mainMenuPath.path(), + psMainMenu); + + RegisterAppMenu(winId(), _mainMenuPath); + + updateGlobalMenu(); +#endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION +} + +void MainWindow::psLinuxUndo() { + SendKeySequence(Qt::Key_Z, Qt::ControlModifier); +} + +void MainWindow::psLinuxRedo() { + SendKeySequence(Qt::Key_Z, Qt::ControlModifier | Qt::ShiftModifier); +} + +void MainWindow::psLinuxCut() { + SendKeySequence(Qt::Key_X, Qt::ControlModifier); +} + +void MainWindow::psLinuxCopy() { + SendKeySequence(Qt::Key_C, Qt::ControlModifier); +} + +void MainWindow::psLinuxPaste() { + SendKeySequence(Qt::Key_V, Qt::ControlModifier); +} + +void MainWindow::psLinuxDelete() { + SendKeySequence(Qt::Key_Delete); +} + +void MainWindow::psLinuxSelectAll() { + SendKeySequence(Qt::Key_A, Qt::ControlModifier); +} + +void MainWindow::psLinuxBold() { + SendKeySequence(Qt::Key_B, Qt::ControlModifier); +} + +void MainWindow::psLinuxItalic() { + SendKeySequence(Qt::Key_I, Qt::ControlModifier); +} + +void MainWindow::psLinuxUnderline() { + SendKeySequence(Qt::Key_U, Qt::ControlModifier); +} + +void MainWindow::psLinuxStrikeOut() { + SendKeySequence(Qt::Key_X, Qt::ControlModifier | Qt::ShiftModifier); +} + +void MainWindow::psLinuxMonospace() { + SendKeySequence(Qt::Key_M, Qt::ControlModifier | Qt::ShiftModifier); +} + +void MainWindow::psLinuxClearFormat() { + SendKeySequence(Qt::Key_N, Qt::ControlModifier | Qt::ShiftModifier); +} + +void MainWindow::updateGlobalMenuHook() { +#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION + if (!AppMenuSupported() || !App::wnd() || !positionInited()) return; + + const auto focused = QApplication::focusWidget(); + auto canUndo = false; + auto canRedo = false; + auto canCut = false; + auto canCopy = false; + auto canPaste = false; + auto canDelete = false; + auto canSelectAll = false; + const auto clipboardHasText = QGuiApplication::clipboard() + ->ownsClipboard(); + auto markdownEnabled = false; + if (const auto edit = qobject_cast(focused)) { + canCut = canCopy = canDelete = edit->hasSelectedText(); + canSelectAll = !edit->text().isEmpty(); + canUndo = edit->isUndoAvailable(); + canRedo = edit->isRedoAvailable(); + canPaste = clipboardHasText; + } else if (const auto edit = qobject_cast(focused)) { + canCut = canCopy = canDelete = edit->textCursor().hasSelection(); + canSelectAll = !edit->document()->isEmpty(); + canUndo = edit->document()->isUndoAvailable(); + canRedo = edit->document()->isRedoAvailable(); + canPaste = clipboardHasText; + if (canCopy) { + if (const auto inputField = qobject_cast( + focused->parentWidget())) { + markdownEnabled = inputField->isMarkdownEnabled(); + } + } + } else if (const auto list = qobject_cast(focused)) { + canCopy = list->canCopySelected(); + canDelete = list->canDeleteSelected(); + } + App::wnd()->updateIsActive(0); + const auto logged = account().sessionExists(); + const auto locked = Core::App().locked(); + const auto inactive = !logged || locked; + const auto support = logged && account().session().supportMode(); + ForceDisabled(psLogout, !logged && !locked); + ForceDisabled(psUndo, !canUndo); + ForceDisabled(psRedo, !canRedo); + ForceDisabled(psCut, !canCut); + ForceDisabled(psCopy, !canCopy); + ForceDisabled(psPaste, !canPaste); + ForceDisabled(psDelete, !canDelete); + ForceDisabled(psSelectAll, !canSelectAll); + ForceDisabled(psContacts, inactive || support); + ForceDisabled(psAddContact, inactive); + ForceDisabled(psNewGroup, inactive || support); + ForceDisabled(psNewChannel, inactive || support); + + ForceDisabled(psBold, !markdownEnabled); + ForceDisabled(psItalic, !markdownEnabled); + ForceDisabled(psUnderline, !markdownEnabled); + ForceDisabled(psStrikeOut, !markdownEnabled); + ForceDisabled(psMonospace, !markdownEnabled); + ForceDisabled(psClearFormat, !markdownEnabled); +#endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION +} + +void MainWindow::onVisibleChanged(bool visible) { +#ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION + if (AppMenuSupported() && !_mainMenuPath.path().isEmpty()) { + if (visible) { + RegisterAppMenu(winId(), _mainMenuPath); + } else { + UnregisterAppMenu(winId()); + } + } +#endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION +} + MainWindow::~MainWindow() { #ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION delete _sniTrayIcon; + + if (AppMenuSupported()) { + UnregisterAppMenu(winId()); + delete _mainMenuExporter; + delete psMainMenu; + } #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION delete _trayIconMenuXEmbed; diff --git a/Telegram/SourceFiles/platform/linux/main_window_linux.h b/Telegram/SourceFiles/platform/linux/main_window_linux.h index 5a4466645..d870bc74c 100644 --- a/Telegram/SourceFiles/platform/linux/main_window_linux.h +++ b/Telegram/SourceFiles/platform/linux/main_window_linux.h @@ -14,6 +14,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #ifndef TDESKTOP_DISABLE_DBUS_INTEGRATION #include "statusnotifieritem.h" #include +#include +#include #endif namespace Platform { @@ -45,14 +47,33 @@ public slots: const QString &newOwner); #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION + void psLinuxUndo(); + void psLinuxRedo(); + void psLinuxCut(); + void psLinuxCopy(); + void psLinuxPaste(); + void psLinuxDelete(); + void psLinuxSelectAll(); + + void psLinuxBold(); + void psLinuxItalic(); + void psLinuxUnderline(); + void psLinuxStrikeOut(); + void psLinuxMonospace(); + void psLinuxClearFormat(); + + void onVisibleChanged(bool visible); + protected: void initHook() override; void unreadCounterChangedHook() override; + void updateGlobalMenuHook() override; void initTrayMenuHook() override; bool hasTrayIcon() const override; void workmodeUpdated(DBIWorkMode mode) override; + void createGlobalMenu() override; QSystemTrayIcon *trayIcon = nullptr; QMenu *trayIconMenu = nullptr; @@ -77,6 +98,30 @@ private: StatusNotifierItem *_sniTrayIcon = nullptr; std::unique_ptr _trayIconFile = nullptr; + DBusMenuExporter *_mainMenuExporter = nullptr; + QDBusObjectPath _mainMenuPath; + + QMenu *psMainMenu = nullptr; + QAction *psLogout = nullptr; + QAction *psUndo = nullptr; + QAction *psRedo = nullptr; + QAction *psCut = nullptr; + QAction *psCopy = nullptr; + QAction *psPaste = nullptr; + QAction *psDelete = nullptr; + QAction *psSelectAll = nullptr; + QAction *psContacts = nullptr; + QAction *psAddContact = nullptr; + QAction *psNewGroup = nullptr; + QAction *psNewChannel = nullptr; + + QAction *psBold = nullptr; + QAction *psItalic = nullptr; + QAction *psUnderline = nullptr; + QAction *psStrikeOut = nullptr; + QAction *psMonospace = nullptr; + QAction *psClearFormat = nullptr; + void setSNITrayIcon(int counter, bool muted, bool firstShow = false); void attachToSNITrayIcon(); #endif // !TDESKTOP_DISABLE_DBUS_INTEGRATION