/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "core/shortcuts.h" #include "mainwindow.h" #include "mainwidget.h" #include "window/window_controller.h" #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 #include #include #include #include namespace Shortcuts { namespace { constexpr auto kCountLimit = 256; // How many shortcuts can be in json file. rpl::event_stream> RequestsStream; const auto AutoRepeatCommands = base::flat_set{ Command::MediaPrevious, Command::MediaNext, Command::ChatPrevious, Command::ChatNext, Command::ChatFirst, Command::ChatLast, }; const auto MediaCommands = base::flat_set{ Command::MediaPlay, Command::MediaPause, Command::MediaPlayPause, Command::MediaStop, Command::MediaPrevious, Command::MediaNext, }; const auto SupportCommands = base::flat_set{ Command::SupportReloadTemplates, Command::SupportToggleMuted, Command::SupportScrollToCurrent, Command::SupportHistoryBack, Command::SupportHistoryForward, }; const auto CommandByName = base::flat_map{ { u"close_telegram"_q , Command::Close }, { u"lock_telegram"_q , Command::Lock }, { u"minimize_telegram"_q , Command::Minimize }, { u"quit_telegram"_q , Command::Quit }, { u"media_play"_q , Command::MediaPlay }, { u"media_pause"_q , Command::MediaPause }, { u"media_playpause"_q , Command::MediaPlayPause }, { u"media_stop"_q , Command::MediaStop }, { u"media_previous"_q , Command::MediaPrevious }, { u"media_next"_q , Command::MediaNext }, { u"search"_q , Command::Search }, { u"previous_chat"_q , Command::ChatPrevious }, { u"next_chat"_q , Command::ChatNext }, { u"first_chat"_q , Command::ChatFirst }, { u"last_chat"_q , Command::ChatLast }, { u"self_chat"_q , Command::ChatSelf }, { u"previous_folder"_q , Command::FolderPrevious }, { u"next_folder"_q , Command::FolderNext }, { u"all_chats"_q , Command::ShowAllChats }, { u"folder1"_q , Command::ShowFolder1 }, { u"folder2"_q , Command::ShowFolder2 }, { u"folder3"_q , Command::ShowFolder3 }, { u"folder4"_q , Command::ShowFolder4 }, { u"folder5"_q , Command::ShowFolder5 }, { u"folder6"_q , Command::ShowFolder6 }, { u"last_folder"_q , Command::ShowFolderLast }, { u"show_archive"_q , Command::ShowArchive }, { u"show_contacts"_q , Command::ShowContacts }, { u"read_chat"_q , Command::ReadChat }, // Shortcuts that have no default values. { u"message"_q , Command::JustSendMessage }, { u"message_silently"_q , Command::SendSilentMessage }, { u"message_scheduled"_q , Command::ScheduleMessage }, // }; const auto CommandNames = base::flat_map{ { Command::Close , u"close_telegram"_q }, { Command::Lock , u"lock_telegram"_q }, { Command::Minimize , u"minimize_telegram"_q }, { Command::Quit , u"quit_telegram"_q }, { Command::MediaPlay , u"media_play"_q }, { Command::MediaPause , u"media_pause"_q }, { Command::MediaPlayPause , u"media_playpause"_q }, { Command::MediaStop , u"media_stop"_q }, { Command::MediaPrevious , u"media_previous"_q }, { Command::MediaNext , u"media_next"_q }, { Command::Search , u"search"_q }, { Command::ChatPrevious , u"previous_chat"_q }, { Command::ChatNext , u"next_chat"_q }, { Command::ChatFirst , u"first_chat"_q }, { Command::ChatLast , u"last_chat"_q }, { Command::ChatSelf , u"self_chat"_q }, { Command::FolderPrevious , u"previous_folder"_q }, { Command::FolderNext , u"next_folder"_q }, { Command::ShowAllChats , u"all_chats"_q }, { Command::ShowFolder1 , u"folder1"_q }, { Command::ShowFolder2 , u"folder2"_q }, { Command::ShowFolder3 , u"folder3"_q }, { Command::ShowFolder4 , u"folder4"_q }, { Command::ShowFolder5 , u"folder5"_q }, { Command::ShowFolder6 , u"folder6"_q }, { Command::ShowFolderLast , u"last_folder"_q }, { Command::ShowArchive , u"show_archive"_q }, { Command::ShowContacts , u"show_contacts"_q }, { Command::ReadChat , u"read_chat"_q }, }; class Manager { public: void fill(); void clear(); [[nodiscard]] std::vector lookup( not_null object) const; void toggleMedia(bool toggled); void toggleSupport(bool toggled); void listen(not_null widget); [[nodiscard]] const QStringList &errors() const; private: void fillDefaults(); void writeDefaultFile(); bool readCustomFile(); void set(const QString &keys, Command command, bool replace = false); void remove(const QString &keys); void unregister(base::unique_qptr shortcut); QStringList _errors; base::flat_map> _shortcuts; base::flat_multi_map, Command> _commandByObject; base::flat_set _mediaShortcuts; base::flat_set _supportShortcuts; }; QString DefaultFilePath() { return cWorkingDir() + u"tdata/shortcuts-default.json"_q; } QString CustomFilePath() { return cWorkingDir() + u"tdata/shortcuts-custom.json"_q; } bool DefaultFileIsValid() { QFile file(DefaultFilePath()); if (!file.open(QIODevice::ReadOnly)) { return false; } auto error = QJsonParseError{ 0, QJsonParseError::NoError }; const auto document = QJsonDocument::fromJson( base::parse::stripComments(file.readAll()), &error); file.close(); if (error.error != QJsonParseError::NoError || !document.isArray()) { return false; } const auto shortcuts = document.array(); if (shortcuts.isEmpty() || !(*shortcuts.constBegin()).isObject()) { return false; } const auto versionObject = (*shortcuts.constBegin()).toObject(); const auto version = versionObject.constFind(u"version"_q); if (version == versionObject.constEnd() || !(*version).isString() || (*version).toString() != QString::number(AppVersion)) { return false; } return true; } void WriteDefaultCustomFile() { const auto path = CustomFilePath(); auto input = QFile(":/misc/default_shortcuts-custom.json"); auto output = QFile(path); if (input.open(QIODevice::ReadOnly) && output.open(QIODevice::WriteOnly)) { output.write(input.readAll()); } } void Manager::fill() { fillDefaults(); if (!DefaultFileIsValid()) { writeDefaultFile(); } if (!readCustomFile()) { WriteDefaultCustomFile(); } } void Manager::clear() { _errors.clear(); _shortcuts.clear(); _commandByObject.clear(); _mediaShortcuts.clear(); _supportShortcuts.clear(); } const QStringList &Manager::errors() const { return _errors; } std::vector Manager::lookup(not_null object) const { auto result = std::vector(); auto i = _commandByObject.findFirst(object); const auto end = _commandByObject.end(); for (; i != end && (i->first == object); ++i) { result.push_back(i->second); } return result; } void Manager::toggleMedia(bool toggled) { for (const auto shortcut : _mediaShortcuts) { shortcut->setEnabled(toggled); } } void Manager::toggleSupport(bool toggled) { for (const auto shortcut : _supportShortcuts) { shortcut->setEnabled(toggled); } } void Manager::listen(not_null widget) { for (const auto &[keys, shortcut] : _shortcuts) { widget->addAction(shortcut.get()); } } bool Manager::readCustomFile() { // read custom shortcuts from file if it exists or write an empty custom shortcuts file QFile file(CustomFilePath()); if (!file.exists()) { return false; } const auto guard = gsl::finally([&] { if (!_errors.isEmpty()) { _errors.push_front((u"While reading file '%1'..."_q ).arg(file.fileName())); } }); if (!file.open(QIODevice::ReadOnly)) { _errors.push_back(u"Could not read the file!"_q); return true; } auto error = QJsonParseError{ 0, QJsonParseError::NoError }; const auto document = QJsonDocument::fromJson( base::parse::stripComments(file.readAll()), &error); file.close(); if (error.error != QJsonParseError::NoError) { _errors.push_back((u"Failed to parse! Error: %2"_q ).arg(error.errorString())); return true; } else if (!document.isArray()) { _errors.push_back(u"Failed to parse! Error: array expected"_q); return true; } const auto shortcuts = document.array(); auto limit = kCountLimit; for (auto i = shortcuts.constBegin(), e = shortcuts.constEnd(); i != e; ++i) { if (!(*i).isObject()) { _errors.push_back(u"Bad entry! Error: object expected"_q); continue; } const auto entry = (*i).toObject(); const auto keys = entry.constFind(u"keys"_q); const auto command = entry.constFind(u"command"_q); if (keys == entry.constEnd() || command == entry.constEnd() || !(*keys).isString() || (!(*command).isString() && !(*command).isNull())) { _errors.push_back(qsl("Bad entry! " "{\"keys\": \"...\", \"command\": [ \"...\" | null ]} " "expected.")); } else if ((*command).isNull()) { remove((*keys).toString()); } else { const auto name = (*command).toString(); const auto i = CommandByName.find(name); if (i != end(CommandByName)) { set((*keys).toString(), i->second, true); } else { LOG(("Shortcut Warning: " "could not find shortcut command handler '%1'" ).arg(name)); } } if (!--limit) { _errors.push_back(u"Too many entries! Limit is %1"_q.arg( kCountLimit)); break; } } return true; } void Manager::fillDefaults() { const auto ctrl = Platform::IsMac() ? u"meta"_q : u"ctrl"_q; set(u"ctrl+w"_q, Command::Close); set(u"ctrl+f4"_q, Command::Close); set(u"ctrl+l"_q, Command::Lock); set(u"ctrl+m"_q, Command::Minimize); set(u"ctrl+q"_q, Command::Quit); set(u"media play"_q, Command::MediaPlay); set(u"media pause"_q, Command::MediaPause); set(u"toggle media play/pause"_q, Command::MediaPlayPause); set(u"media stop"_q, Command::MediaStop); set(u"media previous"_q, Command::MediaPrevious); set(u"media next"_q, Command::MediaNext); set(u"ctrl+f"_q, Command::Search); set(u"search"_q, Command::Search); set(u"find"_q, Command::Search); set(u"ctrl+pgdown"_q, Command::ChatNext); set(u"alt+down"_q, Command::ChatNext); set(u"ctrl+pgup"_q, Command::ChatPrevious); set(u"alt+up"_q, Command::ChatPrevious); set(u"%1+tab"_q.arg(ctrl), Command::ChatNext); set(u"%1+shift+tab"_q.arg(ctrl), Command::ChatPrevious); set(u"%1+backtab"_q.arg(ctrl), Command::ChatPrevious); set(u"ctrl+alt+home"_q, Command::ChatFirst); set(u"ctrl+alt+end"_q, Command::ChatLast); set(u"f5"_q, Command::SupportReloadTemplates); set(u"ctrl+delete"_q, Command::SupportToggleMuted); set(u"ctrl+insert"_q, Command::SupportScrollToCurrent); set(u"ctrl+shift+x"_q, Command::SupportHistoryBack); set(u"ctrl+shift+c"_q, Command::SupportHistoryForward); set(u"ctrl+1"_q, Command::ChatPinned1); set(u"ctrl+2"_q, Command::ChatPinned2); set(u"ctrl+3"_q, Command::ChatPinned3); set(u"ctrl+4"_q, Command::ChatPinned4); set(u"ctrl+5"_q, Command::ChatPinned5); set(u"ctrl+6"_q, Command::ChatPinned6); set(u"ctrl+7"_q, Command::ChatPinned7); set(u"ctrl+8"_q, Command::ChatPinned8); auto &&folders = ranges::views::zip( kShowFolder, ranges::views::ints(1, ranges::unreachable)); for (const auto [command, index] : folders) { set(u"%1+%2"_q.arg(ctrl).arg(index), command); } set(u"%1+shift+down"_q.arg(ctrl), Command::FolderNext); set(u"%1+shift+up"_q.arg(ctrl), Command::FolderPrevious); set(u"ctrl+0"_q, Command::ChatSelf); set(u"ctrl+9"_q, Command::ShowArchive); set(u"ctrl+j"_q, Command::ShowContacts); set(u"ctrl+r"_q, Command::ReadChat); } void Manager::writeDefaultFile() { auto file = QFile(DefaultFilePath()); if (!file.open(QIODevice::WriteOnly)) { return; } const char *defaultHeader = R"HEADER( // This is a list of default shortcuts for Telegram Desktop // Please don't modify it, its content is not used in any way // You can place your own shortcuts in the 'shortcuts-custom.json' file )HEADER"; file.write(defaultHeader); auto shortcuts = QJsonArray(); auto version = QJsonObject(); version.insert(u"version"_q, QString::number(AppVersion)); shortcuts.push_back(version); for (const auto &[sequence, shortcut] : _shortcuts) { const auto object = shortcut.get(); auto i = _commandByObject.findFirst(object); const auto end = _commandByObject.end(); for (; i != end && i->first == object; ++i) { const auto j = CommandNames.find(i->second); if (j != CommandNames.end()) { QJsonObject entry; entry.insert(u"keys"_q, sequence.toString().toLower()); entry.insert(u"command"_q, j->second); shortcuts.append(entry); } } } auto document = QJsonDocument(); document.setArray(shortcuts); file.write(document.toJson(QJsonDocument::Indented)); } void Manager::set(const QString &keys, Command command, bool replace) { if (keys.isEmpty()) { return; } const auto result = QKeySequence(keys, QKeySequence::PortableText); if (result.isEmpty()) { _errors.push_back(u"Could not derive key sequence '%1'!"_q.arg(keys)); return; } auto shortcut = base::make_unique_q(); shortcut->setShortcut(result); shortcut->setShortcutContext(Qt::ApplicationShortcut); if (!AutoRepeatCommands.contains(command)) { shortcut->setAutoRepeat(false); } const auto isMediaShortcut = MediaCommands.contains(command); const auto isSupportShortcut = SupportCommands.contains(command); if (isMediaShortcut || isSupportShortcut) { shortcut->setEnabled(false); } auto object = shortcut.get(); auto i = _shortcuts.find(result); if (i == end(_shortcuts)) { i = _shortcuts.emplace(result, std::move(shortcut)).first; } else if (replace) { unregister(std::exchange(i->second, std::move(shortcut))); } else { object = i->second.get(); } _commandByObject.emplace(object, command); if (!shortcut && isMediaShortcut) { _mediaShortcuts.emplace(i->second.get()); } if (!shortcut && isSupportShortcut) { _supportShortcuts.emplace(i->second.get()); } } void Manager::remove(const QString &keys) { if (keys.isEmpty()) { return; } const auto result = QKeySequence(keys, QKeySequence::PortableText); if (result.isEmpty()) { _errors.push_back(u"Could not derive key sequence '%1'!"_q.arg(keys)); return; } const auto i = _shortcuts.find(result); if (i != end(_shortcuts)) { unregister(std::move(i->second)); _shortcuts.erase(i); } } void Manager::unregister(base::unique_qptr shortcut) { if (shortcut) { _commandByObject.erase(shortcut.get()); _mediaShortcuts.erase(shortcut.get()); _supportShortcuts.erase(shortcut.get()); } } Manager Data; } // namespace Request::Request(std::vector commands) : _commands(std::move(commands)) { } bool Request::check(Command command, int priority) { if (ranges::contains(_commands, command) && priority > _handlerPriority) { _handlerPriority = priority; return true; } return false; } bool Request::handle(FnMut handler) { _handler = std::move(handler); return true; } FnMut RequestHandler(std::vector commands) { auto request = Request(std::move(commands)); RequestsStream.fire(&request); return std::move(request._handler); } FnMut RequestHandler(Command command) { return RequestHandler(std::vector{ command }); } bool Launch(Command command) { if (auto handler = RequestHandler(command)) { return handler(); } return false; } bool Launch(std::vector commands) { if (auto handler = RequestHandler(std::move(commands))) { return handler(); } return false; } rpl::producer> Requests() { return RequestsStream.events(); } void Start() { Data.fill(); } const QStringList &Errors() { return Data.errors(); } bool HandleEvent( not_null object, not_null event) { return Launch(Data.lookup(object)); } void ToggleMediaShortcuts(bool toggled) { Data.toggleMedia(toggled); } void ToggleSupportShortcuts(bool toggled) { Data.toggleSupport(toggled); } void Finish() { Data.clear(); } void Listen(not_null widget) { Data.listen(widget); } } // namespace Shortcuts