Files
AyuGramDesktop/Telegram/SourceFiles/platform/mac/global_menu_mac.mm
T

675 lines
18 KiB
Plaintext

/*
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/mac/global_menu_mac.h"
#include "core/application.h"
#include "core/sandbox.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "window/main_window.h"
#include "main/main_session.h"
#include "history/history_inner_widget.h"
#include "boxes/peer_list_controllers.h"
#include "boxes/about_box.h"
#include "lang/lang_keys.h"
#include "base/platform/base_platform_info.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/fields/input_field.h"
#include "base/invoke_queued.h"
#include <QtWidgets/QMenuBar>
#include <QtWidgets/QMenu>
#include <QtWidgets/QLabel>
#include <QtWidgets/QLineEdit>
#include <QtWidgets/QTextEdit>
#include <QtGui/QAction>
#include <QtGui/QGuiApplication>
#include <QtGui/QClipboard>
#include <Cocoa/Cocoa.h>
namespace Platform {
namespace {
struct ComputedState {
bool logoutDisabled = false;
bool undoDisabled = false;
bool redoDisabled = false;
bool cutDisabled = false;
bool copyDisabled = false;
bool pasteDisabled = false;
bool deleteDisabled = false;
bool selectAllDisabled = false;
bool contactsDisabled = false;
bool addContactDisabled = false;
bool newGroupDisabled = false;
bool newChannelDisabled = false;
bool showTelegramDisabled = false;
Ui::MarkdownEnabledState markdown;
friend inline bool operator==(
const ComputedState &,
const ComputedState &) = default;
};
class Manager final {
public:
void create();
void destroy();
void requestUpdate();
[[nodiscard]] auto markdownStateChanges() const
-> rpl::producer<Ui::MarkdownEnabledState>;
private:
void buildMenu();
void buildAppleMenu(QMenu *main);
void buildFileMenu(QMenu *file);
void buildEditMenu(QMenu *edit);
void buildWindowMenu(QMenu *window);
void retranslate();
void ensureLanguageBound();
void recomputeState();
[[nodiscard]] bool clipboardHasText();
[[nodiscard]] Window::Controller *resolveActiveWindow() const;
template <typename Callback>
void withActiveWindow(Callback callback) {
const auto window = resolveActiveWindow();
if (!window) {
return;
}
if (window->widget()->isHidden()) {
window->widget()->showFromTray();
} else {
window->activate();
}
callback(window);
}
std::unique_ptr<QMenuBar> _menuBar;
// Coalesces requestUpdate() bursts into one recomputeState() per Qt
// event-loop turn. Posted events drain on every CFRunLoop
// BeforeWaiting before the next NSEvent dispatches, so menu state
// is fresh by the time Cmd+C reaches AppKit's performKeyEquivalent.
std::unique_ptr<SingleQueuedInvokation> _scheduledUpdate;
QAction *_logout = nullptr;
QAction *_undo = nullptr;
QAction *_redo = nullptr;
QAction *_cut = nullptr;
QAction *_copy = nullptr;
QAction *_paste = nullptr;
QAction *_delete = nullptr;
QAction *_selectAll = nullptr;
QAction *_contacts = nullptr;
QAction *_addContact = nullptr;
QAction *_newGroup = nullptr;
QAction *_newChannel = nullptr;
QAction *_showTelegram = nullptr;
QAction *_fullScreen = nullptr;
QAction *_emoji = nullptr;
QAction *_bold = nullptr;
QAction *_italic = nullptr;
QAction *_underline = nullptr;
QAction *_strikeOut = nullptr;
QAction *_blockquote = nullptr;
QAction *_monospace = nullptr;
QAction *_clearFormat = nullptr;
NSPasteboard *_pasteboard = nullptr;
int _pasteboardChangeCount = -1;
bool _pasteboardHasText = false;
std::optional<ComputedState> _lastState;
rpl::event_stream<Ui::MarkdownEnabledState> _markdownChanges;
rpl::lifetime _lifetime;
bool _languageBound = false;
};
void SendKeySequence(
Qt::Key key,
Qt::KeyboardModifiers modifiers = Qt::NoModifier) {
const auto focused = QApplication::focusWidget();
if (qobject_cast<QLineEdit*>(focused)
|| qobject_cast<QTextEdit*>(focused)
|| qobject_cast<QLabel*>(focused)
|| dynamic_cast<HistoryInner*>(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);
}
}
Window::Controller *Manager::resolveActiveWindow() const {
if (!Core::IsAppLaunched()) {
return nullptr;
}
const auto active = Core::App().activeWindow();
return active ? active : Core::App().activePrimaryWindow();
}
bool Manager::clipboardHasText() {
const auto current = static_cast<int>([_pasteboard changeCount]);
if (_pasteboardChangeCount != current) {
_pasteboardChangeCount = current;
_pasteboardHasText = !QGuiApplication::clipboard()->text().isEmpty();
}
return _pasteboardHasText;
}
void Manager::retranslate() {
if (_logout) {
_logout->setText(tr::lng_mac_menu_logout(tr::now));
}
if (_delete) {
_delete->setText(tr::lng_mac_menu_delete(tr::now));
}
if (_contacts) {
_contacts->setText(tr::lng_mac_menu_contacts(tr::now));
}
if (_addContact) {
_addContact->setText(tr::lng_mac_menu_add_contact(tr::now));
}
if (_newGroup) {
_newGroup->setText(tr::lng_mac_menu_new_group(tr::now));
}
if (_newChannel) {
_newChannel->setText(tr::lng_mac_menu_new_channel(tr::now));
}
if (_showTelegram) {
_showTelegram->setText(tr::lng_mac_menu_show(tr::now));
}
if (_fullScreen) {
_fullScreen->setText(tr::lng_mac_menu_fullscreen(tr::now));
}
if (_emoji) {
_emoji->setText(tr::lng_mac_menu_emoji_and_symbols(
tr::now,
Ui::Text::FixAmpersandInAction));
}
if (_bold) {
_bold->setText(tr::lng_menu_formatting_bold(tr::now));
}
if (_italic) {
_italic->setText(tr::lng_menu_formatting_italic(tr::now));
}
if (_underline) {
_underline->setText(tr::lng_menu_formatting_underline(tr::now));
}
if (_strikeOut) {
_strikeOut->setText(tr::lng_menu_formatting_strike_out(tr::now));
}
if (_blockquote) {
_blockquote->setText(tr::lng_menu_formatting_blockquote(tr::now));
}
if (_monospace) {
_monospace->setText(tr::lng_menu_formatting_monospace(tr::now));
}
if (_clearFormat) {
_clearFormat->setText(tr::lng_menu_formatting_clear(tr::now));
}
}
void Manager::ensureLanguageBound() {
if (_languageBound || !Core::IsAppLaunched()) {
return;
}
_languageBound = true;
retranslate();
Lang::Updated() | rpl::on_next([this] {
retranslate();
}, _lifetime);
}
void Manager::recomputeState() {
const auto window = resolveActiveWindow();
if (!window) {
return;
}
const auto widget = window->widget();
if (!widget->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 = this->clipboardHasText();
auto markdownState = Ui::MarkdownEnabledState();
if (const auto edit = qobject_cast<QLineEdit*>(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<QTextEdit*>(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 = dynamic_cast<Ui::InputField*>(
focused->parentWidget())) {
markdownState = inputField->markdownEnabledState();
}
}
} else if (const auto label = qobject_cast<QLabel*>(focused)) {
const auto flags = label->textInteractionFlags();
const auto selectable = flags
& (Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard);
if (selectable) {
canCopy = label->hasSelectedText();
canSelectAll = !label->text().isEmpty();
}
} else if (const auto list = dynamic_cast<HistoryInner*>(focused)) {
canCopy = list->canCopySelected();
canDelete = list->canDeleteSelected();
}
widget->updateIsActive();
const auto controller = window->sessionController();
const auto logged = (controller != nullptr);
const auto inactive = !logged || window->locked();
const auto support = logged && controller->session().supportMode();
auto next = ComputedState{
.logoutDisabled = !logged && !Core::App().passcodeLocked(),
.undoDisabled = !canUndo,
.redoDisabled = !canRedo,
.cutDisabled = !canCut,
.copyDisabled = !canCopy,
.pasteDisabled = !canPaste,
.deleteDisabled = !canDelete,
.selectAllDisabled = !canSelectAll,
.contactsDisabled = inactive || support,
.addContactDisabled = inactive,
.newGroupDisabled = inactive || support,
.newChannelDisabled = inactive || support,
.showTelegramDisabled = widget->isActive(),
.markdown = markdownState,
};
if (_lastState && *_lastState == next) {
return;
}
_lastState = next;
_markdownChanges.fire_copy(markdownState);
ForceDisabled(_logout, next.logoutDisabled);
ForceDisabled(_undo, next.undoDisabled);
ForceDisabled(_redo, next.redoDisabled);
ForceDisabled(_cut, next.cutDisabled);
ForceDisabled(_copy, next.copyDisabled);
ForceDisabled(_paste, next.pasteDisabled);
ForceDisabled(_delete, next.deleteDisabled);
ForceDisabled(_selectAll, next.selectAllDisabled);
ForceDisabled(_contacts, next.contactsDisabled);
ForceDisabled(_addContact, next.addContactDisabled);
ForceDisabled(_newGroup, next.newGroupDisabled);
ForceDisabled(_newChannel, next.newChannelDisabled);
ForceDisabled(_showTelegram, next.showTelegramDisabled);
const auto disabled = [&](const QString &tag) {
return !markdownState.enabledForTag(tag);
};
using Field = Ui::InputField;
ForceDisabled(_bold, disabled(Field::kTagBold));
ForceDisabled(_italic, disabled(Field::kTagItalic));
ForceDisabled(_underline, disabled(Field::kTagUnderline));
ForceDisabled(_strikeOut, disabled(Field::kTagStrikeOut));
ForceDisabled(_blockquote, disabled(Field::kTagBlockquote));
ForceDisabled(
_monospace,
disabled(Field::kTagPre) || disabled(Field::kTagCode));
ForceDisabled(_clearFormat, markdownState.disabled());
}
void Manager::buildAppleMenu(QMenu *main) {
{
auto callback = [this] {
withActiveWindow([](not_null<Window::Controller*> window) {
window->show(Box(AboutBox));
});
};
const auto about = main->addAction(
u"About Telegram"_q,
std::move(callback));
about->setMenuRole(QAction::AboutQtRole);
}
main->addSeparator();
{
auto callback = [this] {
withActiveWindow([](not_null<Window::Controller*> window) {
window->showSettings();
});
};
const auto preferences = main->addAction(
u"Preferences..."_q,
_menuBar.get(),
std::move(callback),
QKeySequence(Qt::ControlModifier | Qt::Key_Comma));
preferences->setMenuRole(QAction::PreferencesRole);
preferences->setShortcutContext(Qt::WidgetShortcut);
}
}
void Manager::buildFileMenu(QMenu *file) {
auto callback = [this] {
withActiveWindow([](not_null<Window::Controller*> window) {
window->showLogoutConfirmation();
});
};
_logout = file->addAction(
u"Log Out"_q,
_menuBar.get(),
std::move(callback));
}
void Manager::buildEditMenu(QMenu *edit) {
const auto receiver = _menuBar.get();
_undo = edit->addAction(
u"Undo"_q,
receiver,
[] { SendKeySequence(Qt::Key_Z, Qt::ControlModifier); },
QKeySequence::Undo);
_undo->setShortcutContext(Qt::WidgetShortcut);
_redo = edit->addAction(
u"Redo"_q,
receiver,
[] {
SendKeySequence(
Qt::Key_Z,
Qt::ControlModifier | Qt::ShiftModifier);
},
QKeySequence::Redo);
_redo->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
_cut = edit->addAction(
u"Cut"_q,
receiver,
[] { SendKeySequence(Qt::Key_X, Qt::ControlModifier); },
QKeySequence::Cut);
_cut->setShortcutContext(Qt::WidgetShortcut);
_copy = edit->addAction(
u"Copy"_q,
receiver,
[] { SendKeySequence(Qt::Key_C, Qt::ControlModifier); },
QKeySequence::Copy);
_copy->setShortcutContext(Qt::WidgetShortcut);
_paste = edit->addAction(
u"Paste"_q,
receiver,
[] { SendKeySequence(Qt::Key_V, Qt::ControlModifier); },
QKeySequence::Paste);
_paste->setShortcutContext(Qt::WidgetShortcut);
_delete = edit->addAction(
u"Delete"_q,
receiver,
[] { SendKeySequence(Qt::Key_Delete); },
QKeySequence(Qt::ControlModifier | Qt::Key_Backspace));
_delete->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
_bold = edit->addAction(
u"Bold"_q,
receiver,
[] { SendKeySequence(Qt::Key_B, Qt::ControlModifier); },
QKeySequence::Bold);
_bold->setShortcutContext(Qt::WidgetShortcut);
_italic = edit->addAction(
u"Italic"_q,
receiver,
[] { SendKeySequence(Qt::Key_I, Qt::ControlModifier); },
QKeySequence::Italic);
_italic->setShortcutContext(Qt::WidgetShortcut);
_underline = edit->addAction(
u"Underline"_q,
receiver,
[] { SendKeySequence(Qt::Key_U, Qt::ControlModifier); },
QKeySequence::Underline);
_underline->setShortcutContext(Qt::WidgetShortcut);
_strikeOut = edit->addAction(
u"Strikethrough"_q,
receiver,
[] {
SendKeySequence(
Qt::Key_X,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kStrikeOutSequence);
_strikeOut->setShortcutContext(Qt::WidgetShortcut);
_blockquote = edit->addAction(
u"Quote"_q,
receiver,
[] {
SendKeySequence(
Qt::Key_Period,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kBlockquoteSequence);
_blockquote->setShortcutContext(Qt::WidgetShortcut);
_monospace = edit->addAction(
u"Monospace"_q,
receiver,
[] {
SendKeySequence(
Qt::Key_M,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kMonospaceSequence);
_monospace->setShortcutContext(Qt::WidgetShortcut);
_clearFormat = edit->addAction(
u"Clear formatting"_q,
receiver,
[] {
SendKeySequence(
Qt::Key_N,
Qt::ControlModifier | Qt::ShiftModifier);
},
Ui::kClearFormatSequence);
_clearFormat->setShortcutContext(Qt::WidgetShortcut);
edit->addSeparator();
_selectAll = edit->addAction(
u"Select All"_q,
receiver,
[] { SendKeySequence(Qt::Key_A, Qt::ControlModifier); },
QKeySequence::SelectAll);
_selectAll->setShortcutContext(Qt::WidgetShortcut);
if (!Platform::IsMac26_0OrGreater()) {
edit->addSeparator();
_emoji = edit->addAction(
u"Emoji & Symbols"_q,
receiver,
[] { [NSApp orderFrontCharacterPalette:nil]; },
QKeySequence(Qt::MetaModifier
| Qt::ControlModifier
| Qt::Key_Space));
_emoji->setShortcutContext(Qt::WidgetShortcut);
}
}
void Manager::buildWindowMenu(QMenu *window) {
const auto receiver = _menuBar.get();
_fullScreen = window->addAction(
u"Toggle Full Screen"_q,
receiver,
[this] {
withActiveWindow([](not_null<Window::Controller*> w) {
const auto view = reinterpret_cast<NSView*>(
w->widget()->winId());
NSWindow *nsWindow = [view window];
[nsWindow toggleFullScreen:nsWindow];
});
},
QKeySequence(Qt::MetaModifier | Qt::ControlModifier | Qt::Key_F));
_fullScreen->setShortcutContext(Qt::WidgetShortcut);
window->addSeparator();
_contacts = window->addAction(u"Contacts"_q);
QObject::connect(_contacts, &QAction::triggered, _contacts, [this] {
withActiveWindow([](not_null<Window::Controller*> w) {
const auto sc = w->sessionController();
if (!sc || w->locked()) {
return;
}
sc->show(PrepareContactsBox(sc));
});
});
{
auto callback = [this] {
withActiveWindow([](not_null<Window::Controller*> w) {
const auto sc = w->sessionController();
if (!sc || w->locked()) {
return;
}
sc->showAddContact();
});
};
_addContact = window->addAction(
u"Add Contact"_q,
receiver,
std::move(callback));
}
window->addSeparator();
{
auto callback = [this] {
withActiveWindow([](not_null<Window::Controller*> w) {
const auto sc = w->sessionController();
if (!sc || w->locked()) {
return;
}
sc->showNewGroup();
});
};
_newGroup = window->addAction(
u"New Group"_q,
receiver,
std::move(callback));
}
{
auto callback = [this] {
withActiveWindow([](not_null<Window::Controller*> w) {
const auto sc = w->sessionController();
if (!sc || w->locked()) {
return;
}
sc->showNewChannel();
});
};
_newChannel = window->addAction(
u"New Channel"_q,
receiver,
std::move(callback));
}
window->addSeparator();
_showTelegram = window->addAction(
u"Show Telegram"_q,
receiver,
[this] {
if (const auto w = resolveActiveWindow()) {
w->widget()->showFromTray();
}
});
}
void Manager::buildMenu() {
buildAppleMenu(_menuBar->addMenu(u"Telegram"_q));
buildFileMenu(_menuBar->addMenu(u"File"_q));
buildEditMenu(_menuBar->addMenu(u"Edit"_q));
buildWindowMenu(_menuBar->addMenu(u"Window"_q));
}
void Manager::create() {
Expects(_menuBar == nullptr);
_pasteboard = [NSPasteboard generalPasteboard];
_menuBar = std::make_unique<QMenuBar>();
buildMenu();
_scheduledUpdate = std::make_unique<SingleQueuedInvokation>([this] {
if (!_menuBar) {
return;
}
recomputeState();
});
}
void Manager::destroy() {
_scheduledUpdate.reset();
_lifetime.destroy();
_menuBar.reset();
_languageBound = false;
_logout = _undo = _redo = _cut = _copy = _paste = _delete
= _selectAll = _contacts = _addContact = _newGroup
= _newChannel = _showTelegram = _fullScreen = _emoji
= _bold = _italic = _underline
= _strikeOut = _blockquote = _monospace = _clearFormat
= nullptr;
_pasteboard = nullptr;
_pasteboardChangeCount = -1;
_pasteboardHasText = false;
_lastState.reset();
}
void Manager::requestUpdate() {
if (!_menuBar) {
return;
}
ensureLanguageBound();
_scheduledUpdate->call();
}
auto Manager::markdownStateChanges() const
-> rpl::producer<Ui::MarkdownEnabledState> {
return _markdownChanges.events();
}
Manager GlobalMenu;
} // namespace
void CreateGlobalMenu() {
GlobalMenu.create();
}
void DestroyGlobalMenu() {
GlobalMenu.destroy();
}
void RequestUpdateGlobalMenu() {
GlobalMenu.requestUpdate();
}
rpl::producer<Ui::MarkdownEnabledState> GlobalMenuMarkdownState() {
return GlobalMenu.markdownStateChanges();
}
} // namespace Platform