"Fossies" - the Fresh Open Source Software Archive

Member "tdesktop-2.6.0/Telegram/SourceFiles/core/shortcuts.cpp" (23 Feb 2021, 16632 Bytes) of package /linux/misc/tdesktop-2.6.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "shortcuts.cpp" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 2.5.1_vs_2.5.6.

    1 /*
    2 This file is part of Telegram Desktop,
    3 the official desktop application for the Telegram messaging service.
    4 
    5 For license and copyright information please follow this link:
    6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
    7 */
    8 #include "core/shortcuts.h"
    9 
   10 #include "mainwindow.h"
   11 #include "mainwidget.h"
   12 #include "window/window_controller.h"
   13 #include "core/application.h"
   14 #include "media/player/media_player_instance.h"
   15 #include "base/platform/base_platform_info.h"
   16 #include "platform/platform_specific.h"
   17 #include "base/parse_helper.h"
   18 #include "facades.h"
   19 
   20 #include <QtWidgets/QShortcut>
   21 #include <QtCore/QJsonDocument>
   22 #include <QtCore/QJsonObject>
   23 #include <QtCore/QJsonArray>
   24 
   25 namespace Shortcuts {
   26 namespace {
   27 
   28 constexpr auto kCountLimit = 256; // How many shortcuts can be in json file.
   29 
   30 rpl::event_stream<not_null<Request*>> RequestsStream;
   31 
   32 const auto AutoRepeatCommands = base::flat_set<Command>{
   33     Command::MediaPrevious,
   34     Command::MediaNext,
   35     Command::ChatPrevious,
   36     Command::ChatNext,
   37     Command::ChatFirst,
   38     Command::ChatLast,
   39 };
   40 
   41 const auto MediaCommands = base::flat_set<Command>{
   42     Command::MediaPlay,
   43     Command::MediaPause,
   44     Command::MediaPlayPause,
   45     Command::MediaStop,
   46     Command::MediaPrevious,
   47     Command::MediaNext,
   48 };
   49 
   50 const auto SupportCommands = base::flat_set<Command>{
   51     Command::SupportReloadTemplates,
   52     Command::SupportToggleMuted,
   53     Command::SupportScrollToCurrent,
   54     Command::SupportHistoryBack,
   55     Command::SupportHistoryForward,
   56 };
   57 
   58 const auto CommandByName = base::flat_map<QString, Command>{
   59     { qsl("close_telegram")    , Command::Close },
   60     { qsl("lock_telegram")     , Command::Lock },
   61     { qsl("minimize_telegram") , Command::Minimize },
   62     { qsl("quit_telegram")     , Command::Quit },
   63 
   64     { qsl("media_play")        , Command::MediaPlay },
   65     { qsl("media_pause")       , Command::MediaPause },
   66     { qsl("media_playpause")   , Command::MediaPlayPause },
   67     { qsl("media_stop")        , Command::MediaStop },
   68     { qsl("media_previous")    , Command::MediaPrevious },
   69     { qsl("media_next")        , Command::MediaNext },
   70 
   71     { qsl("search")            , Command::Search },
   72 
   73     { qsl("previous_chat")     , Command::ChatPrevious },
   74     { qsl("next_chat")         , Command::ChatNext },
   75     { qsl("first_chat")        , Command::ChatFirst },
   76     { qsl("last_chat")         , Command::ChatLast },
   77     { qsl("self_chat")         , Command::ChatSelf },
   78 
   79     { qsl("previous_folder")   , Command::FolderPrevious },
   80     { qsl("next_folder")       , Command::FolderNext },
   81     { qsl("all_chats")         , Command::ShowAllChats },
   82 
   83     { qsl("folder1")           , Command::ShowFolder1 },
   84     { qsl("folder2")           , Command::ShowFolder2 },
   85     { qsl("folder3")           , Command::ShowFolder3 },
   86     { qsl("folder4")           , Command::ShowFolder4 },
   87     { qsl("folder5")           , Command::ShowFolder5 },
   88     { qsl("folder6")           , Command::ShowFolder6 },
   89     { qsl("last_folder")       , Command::ShowFolderLast },
   90 
   91     { qsl("show_archive")      , Command::ShowArchive },
   92     { qsl("show_contacts")     , Command::ShowContacts },
   93 
   94     { qsl("read_chat")         , Command::ReadChat },
   95 
   96     // Shortcuts that have no default values.
   97     { qsl("message")           , Command::JustSendMessage },
   98     { qsl("message_silently")  , Command::SendSilentMessage },
   99     { qsl("message_scheduled") , Command::ScheduleMessage },
  100     //
  101 };
  102 
  103 const auto CommandNames = base::flat_map<Command, QString>{
  104     { Command::Close          , qsl("close_telegram") },
  105     { Command::Lock           , qsl("lock_telegram") },
  106     { Command::Minimize       , qsl("minimize_telegram") },
  107     { Command::Quit           , qsl("quit_telegram") },
  108 
  109     { Command::MediaPlay      , qsl("media_play") },
  110     { Command::MediaPause     , qsl("media_pause") },
  111     { Command::MediaPlayPause , qsl("media_playpause") },
  112     { Command::MediaStop      , qsl("media_stop") },
  113     { Command::MediaPrevious  , qsl("media_previous") },
  114     { Command::MediaNext      , qsl("media_next") },
  115 
  116     { Command::Search         , qsl("search") },
  117 
  118     { Command::ChatPrevious   , qsl("previous_chat") },
  119     { Command::ChatNext       , qsl("next_chat") },
  120     { Command::ChatFirst      , qsl("first_chat") },
  121     { Command::ChatLast       , qsl("last_chat") },
  122     { Command::ChatSelf       , qsl("self_chat") },
  123 
  124     { Command::FolderPrevious , qsl("previous_folder") },
  125     { Command::FolderNext     , qsl("next_folder") },
  126     { Command::ShowAllChats   , qsl("all_chats") },
  127 
  128     { Command::ShowFolder1    , qsl("folder1") },
  129     { Command::ShowFolder2    , qsl("folder2") },
  130     { Command::ShowFolder3    , qsl("folder3") },
  131     { Command::ShowFolder4    , qsl("folder4") },
  132     { Command::ShowFolder5    , qsl("folder5") },
  133     { Command::ShowFolder6    , qsl("folder6") },
  134     { Command::ShowFolderLast , qsl("last_folder") },
  135 
  136     { Command::ShowArchive    , qsl("show_archive") },
  137     { Command::ShowContacts   , qsl("show_contacts") },
  138 
  139     { Command::ReadChat       , qsl("read_chat") },
  140 };
  141 
  142 class Manager {
  143 public:
  144     void fill();
  145     void clear();
  146 
  147     [[nodiscard]] std::vector<Command> lookup(int shortcutId) const;
  148     void toggleMedia(bool toggled);
  149     void toggleSupport(bool toggled);
  150 
  151     const QStringList &errors() const;
  152 
  153 private:
  154     void fillDefaults();
  155     void writeDefaultFile();
  156     bool readCustomFile();
  157 
  158     void set(const QString &keys, Command command, bool replace = false);
  159     void remove(const QString &keys);
  160     void unregister(base::unique_qptr<QShortcut> shortcut);
  161 
  162     QStringList _errors;
  163 
  164     base::flat_map<QKeySequence, base::unique_qptr<QShortcut>> _shortcuts;
  165     base::flat_multi_map<int, Command> _commandByShortcutId;
  166 
  167     base::flat_set<QShortcut*> _mediaShortcuts;
  168     base::flat_set<QShortcut*> _supportShortcuts;
  169 
  170 };
  171 
  172 QString DefaultFilePath() {
  173     return cWorkingDir() + qsl("tdata/shortcuts-default.json");
  174 }
  175 
  176 QString CustomFilePath() {
  177     return cWorkingDir() + qsl("tdata/shortcuts-custom.json");
  178 }
  179 
  180 bool DefaultFileIsValid() {
  181     QFile file(DefaultFilePath());
  182     if (!file.open(QIODevice::ReadOnly)) {
  183         return false;
  184     }
  185     auto error = QJsonParseError{ 0, QJsonParseError::NoError };
  186     const auto document = QJsonDocument::fromJson(
  187         base::parse::stripComments(file.readAll()),
  188         &error);
  189     file.close();
  190 
  191     if (error.error != QJsonParseError::NoError || !document.isArray()) {
  192         return false;
  193     }
  194     const auto shortcuts = document.array();
  195     if (shortcuts.isEmpty() || !(*shortcuts.constBegin()).isObject()) {
  196         return false;
  197     }
  198     const auto versionObject = (*shortcuts.constBegin()).toObject();
  199     const auto version = versionObject.constFind(qsl("version"));
  200     if (version == versionObject.constEnd()
  201         || !(*version).isString()
  202         || (*version).toString() != QString::number(AppVersion)) {
  203         return false;
  204     }
  205     return true;
  206 }
  207 
  208 void WriteDefaultCustomFile() {
  209     const auto path = CustomFilePath();
  210     auto input = QFile(":/misc/default_shortcuts-custom.json");
  211     auto output = QFile(path);
  212     if (input.open(QIODevice::ReadOnly) && output.open(QIODevice::WriteOnly)) {
  213         output.write(input.readAll());
  214     }
  215 }
  216 
  217 void Manager::fill() {
  218     fillDefaults();
  219 
  220     if (!DefaultFileIsValid()) {
  221         writeDefaultFile();
  222     }
  223     if (!readCustomFile()) {
  224         WriteDefaultCustomFile();
  225     }
  226 }
  227 
  228 void Manager::clear() {
  229     _errors.clear();
  230     _shortcuts.clear();
  231     _commandByShortcutId.clear();
  232     _mediaShortcuts.clear();
  233     _supportShortcuts.clear();
  234 }
  235 
  236 const QStringList &Manager::errors() const {
  237     return _errors;
  238 }
  239 
  240 std::vector<Command> Manager::lookup(int shortcutId) const {
  241     auto result = std::vector<Command>();
  242     auto i = _commandByShortcutId.findFirst(shortcutId);
  243     const auto end = _commandByShortcutId.end();
  244     for (; i != end && (i->first == shortcutId); ++i) {
  245         result.push_back(i->second);
  246     }
  247     return result;
  248 }
  249 
  250 void Manager::toggleMedia(bool toggled) {
  251     for (const auto shortcut : _mediaShortcuts) {
  252         shortcut->setEnabled(toggled);
  253     }
  254 }
  255 
  256 void Manager::toggleSupport(bool toggled) {
  257     for (const auto shortcut : _supportShortcuts) {
  258         shortcut->setEnabled(toggled);
  259     }
  260 }
  261 
  262 bool Manager::readCustomFile() {
  263     // read custom shortcuts from file if it exists or write an empty custom shortcuts file
  264     QFile file(CustomFilePath());
  265     if (!file.exists()) {
  266         return false;
  267     }
  268     const auto guard = gsl::finally([&] {
  269         if (!_errors.isEmpty()) {
  270             _errors.push_front(qsl("While reading file '%1'..."
  271             ).arg(file.fileName()));
  272         }
  273     });
  274     if (!file.open(QIODevice::ReadOnly)) {
  275         _errors.push_back(qsl("Could not read the file!"));
  276         return true;
  277     }
  278     auto error = QJsonParseError{ 0, QJsonParseError::NoError };
  279     const auto document = QJsonDocument::fromJson(
  280         base::parse::stripComments(file.readAll()),
  281         &error);
  282     file.close();
  283 
  284     if (error.error != QJsonParseError::NoError) {
  285         _errors.push_back(qsl("Failed to parse! Error: %2"
  286         ).arg(error.errorString()));
  287         return true;
  288     } else if (!document.isArray()) {
  289         _errors.push_back(qsl("Failed to parse! Error: array expected"));
  290         return true;
  291     }
  292     const auto shortcuts = document.array();
  293     auto limit = kCountLimit;
  294     for (auto i = shortcuts.constBegin(), e = shortcuts.constEnd(); i != e; ++i) {
  295         if (!(*i).isObject()) {
  296             _errors.push_back(qsl("Bad entry! Error: object expected"));
  297             continue;
  298         }
  299         const auto entry = (*i).toObject();
  300         const auto keys = entry.constFind(qsl("keys"));
  301         const auto command = entry.constFind(qsl("command"));
  302         if (keys == entry.constEnd()
  303             || command == entry.constEnd()
  304             || !(*keys).isString()
  305             || (!(*command).isString() && !(*command).isNull())) {
  306             _errors.push_back(qsl("Bad entry! "
  307                 "{\"keys\": \"...\", \"command\": [ \"...\" | null ]} "
  308                 "expected."));
  309         } else if ((*command).isNull()) {
  310             remove((*keys).toString());
  311         } else {
  312             const auto name = (*command).toString();
  313             const auto i = CommandByName.find(name);
  314             if (i != end(CommandByName)) {
  315                 set((*keys).toString(), i->second, true);
  316             } else {
  317                 LOG(("Shortcut Warning: "
  318                     "could not find shortcut command handler '%1'"
  319                     ).arg(name));
  320             }
  321         }
  322         if (!--limit) {
  323             _errors.push_back(qsl("Too many entries! Limit is %1"
  324             ).arg(kCountLimit));
  325             break;
  326         }
  327     }
  328     return true;
  329 }
  330 
  331 void Manager::fillDefaults() {
  332     const auto ctrl = Platform::IsMac() ? qsl("meta") : qsl("ctrl");
  333 
  334     set(qsl("ctrl+w"), Command::Close);
  335     set(qsl("ctrl+f4"), Command::Close);
  336     set(qsl("ctrl+l"), Command::Lock);
  337     set(qsl("ctrl+m"), Command::Minimize);
  338     set(qsl("ctrl+q"), Command::Quit);
  339 
  340     set(qsl("media play"), Command::MediaPlay);
  341     set(qsl("media pause"), Command::MediaPause);
  342     set(qsl("toggle media play/pause"), Command::MediaPlayPause);
  343     set(qsl("media stop"), Command::MediaStop);
  344     set(qsl("media previous"), Command::MediaPrevious);
  345     set(qsl("media next"), Command::MediaNext);
  346 
  347     set(qsl("ctrl+f"), Command::Search);
  348     set(qsl("search"), Command::Search);
  349     set(qsl("find"), Command::Search);
  350 
  351     set(qsl("ctrl+pgdown"), Command::ChatNext);
  352     set(qsl("alt+down"), Command::ChatNext);
  353     set(qsl("ctrl+pgup"), Command::ChatPrevious);
  354     set(qsl("alt+up"), Command::ChatPrevious);
  355 
  356     set(qsl("%1+tab").arg(ctrl), Command::ChatNext);
  357     set(qsl("%1+shift+tab").arg(ctrl), Command::ChatPrevious);
  358     set(qsl("%1+backtab").arg(ctrl), Command::ChatPrevious);
  359 
  360     set(qsl("ctrl+alt+home"), Command::ChatFirst);
  361     set(qsl("ctrl+alt+end"), Command::ChatLast);
  362 
  363     set(qsl("f5"), Command::SupportReloadTemplates);
  364     set(qsl("ctrl+delete"), Command::SupportToggleMuted);
  365     set(qsl("ctrl+insert"), Command::SupportScrollToCurrent);
  366     set(qsl("ctrl+shift+x"), Command::SupportHistoryBack);
  367     set(qsl("ctrl+shift+c"), Command::SupportHistoryForward);
  368 
  369     set(qsl("ctrl+1"), Command::ChatPinned1);
  370     set(qsl("ctrl+2"), Command::ChatPinned2);
  371     set(qsl("ctrl+3"), Command::ChatPinned3);
  372     set(qsl("ctrl+4"), Command::ChatPinned4);
  373     set(qsl("ctrl+5"), Command::ChatPinned5);
  374 
  375     auto &&folders = ranges::view::zip(
  376         kShowFolder,
  377         ranges::view::ints(1, ranges::unreachable));
  378 
  379     for (const auto [command, index] : folders) {
  380         set(qsl("%1+%2").arg(ctrl).arg(index), command);
  381     }
  382 
  383     set(qsl("%1+shift+down").arg(ctrl), Command::FolderNext);
  384     set(qsl("%1+shift+up").arg(ctrl), Command::FolderPrevious);
  385 
  386     set(qsl("ctrl+0"), Command::ChatSelf);
  387 
  388     set(qsl("ctrl+9"), Command::ShowArchive);
  389     set(qsl("ctrl+j"), Command::ShowContacts);
  390 
  391     set(qsl("ctrl+r"), Command::ReadChat);
  392 }
  393 
  394 void Manager::writeDefaultFile() {
  395     auto file = QFile(DefaultFilePath());
  396     if (!file.open(QIODevice::WriteOnly)) {
  397         return;
  398     }
  399     const char *defaultHeader = R"HEADER(
  400 // This is a list of default shortcuts for Telegram Desktop
  401 // Please don't modify it, its content is not used in any way
  402 // You can place your own shortcuts in the 'shortcuts-custom.json' file
  403 
  404 )HEADER";
  405     file.write(defaultHeader);
  406 
  407     auto shortcuts = QJsonArray();
  408     auto version = QJsonObject();
  409     version.insert(qsl("version"), QString::number(AppVersion));
  410     shortcuts.push_back(version);
  411 
  412     for (const auto &[sequence, shortcut] : _shortcuts) {
  413         const auto shortcutId = shortcut->id();
  414         auto i = _commandByShortcutId.findFirst(shortcutId);
  415         const auto end = _commandByShortcutId.end();
  416         for (; i != end && i->first == shortcutId; ++i) {
  417             const auto j = CommandNames.find(i->second);
  418             if (j != CommandNames.end()) {
  419                 QJsonObject entry;
  420                 entry.insert(qsl("keys"), sequence.toString().toLower());
  421                 entry.insert(qsl("command"), j->second);
  422                 shortcuts.append(entry);
  423             }
  424         }
  425     }
  426 
  427     auto document = QJsonDocument();
  428     document.setArray(shortcuts);
  429     file.write(document.toJson(QJsonDocument::Indented));
  430 }
  431 
  432 void Manager::set(const QString &keys, Command command, bool replace) {
  433     if (keys.isEmpty()) {
  434         return;
  435     }
  436 
  437     const auto result = QKeySequence(keys, QKeySequence::PortableText);
  438     if (result.isEmpty()) {
  439         _errors.push_back(qsl("Could not derive key sequence '%1'!"
  440         ).arg(keys));
  441         return;
  442     }
  443     auto shortcut = base::make_unique_q<QShortcut>(
  444         result,
  445         Core::App().activeWindow()->widget().get(),
  446         nullptr,
  447         nullptr,
  448         Qt::ApplicationShortcut);
  449     if (!AutoRepeatCommands.contains(command)) {
  450         shortcut->setAutoRepeat(false);
  451     }
  452     const auto isMediaShortcut = MediaCommands.contains(command);
  453     const auto isSupportShortcut = SupportCommands.contains(command);
  454     if (isMediaShortcut || isSupportShortcut) {
  455         shortcut->setEnabled(false);
  456     }
  457     auto id = shortcut->id();
  458     auto i = _shortcuts.find(result);
  459     if (i == end(_shortcuts)) {
  460         i = _shortcuts.emplace(result, std::move(shortcut)).first;
  461     } else if (replace) {
  462         unregister(std::exchange(i->second, std::move(shortcut)));
  463     } else {
  464         id = i->second->id();
  465     }
  466     if (!id) {
  467         _errors.push_back(qsl("Could not create shortcut '%1'!").arg(keys));
  468         return;
  469     }
  470     _commandByShortcutId.emplace(id, command);
  471     if (!shortcut && isMediaShortcut) {
  472         _mediaShortcuts.emplace(i->second.get());
  473     }
  474     if (!shortcut && isSupportShortcut) {
  475         _supportShortcuts.emplace(i->second.get());
  476     }
  477 }
  478 
  479 void Manager::remove(const QString &keys) {
  480     if (keys.isEmpty()) {
  481         return;
  482     }
  483 
  484     const auto result = QKeySequence(keys, QKeySequence::PortableText);
  485     if (result.isEmpty()) {
  486         _errors.push_back(qsl("Could not derive key sequence '%1'!"
  487         ).arg(keys));
  488         return;
  489     }
  490     const auto i = _shortcuts.find(result);
  491     if (i != end(_shortcuts)) {
  492         unregister(std::move(i->second));
  493         _shortcuts.erase(i);
  494     }
  495 }
  496 
  497 void Manager::unregister(base::unique_qptr<QShortcut> shortcut) {
  498     if (shortcut) {
  499         _commandByShortcutId.erase(shortcut->id());
  500         _mediaShortcuts.erase(shortcut.get());
  501         _supportShortcuts.erase(shortcut.get());
  502     }
  503 }
  504 
  505 Manager Data;
  506 
  507 } // namespace
  508 
  509 Request::Request(std::vector<Command> commands)
  510 : _commands(std::move(commands)) {
  511 }
  512 
  513 bool Request::check(Command command, int priority) {
  514     if (ranges::contains(_commands, command)
  515         && priority > _handlerPriority) {
  516         _handlerPriority = priority;
  517         return true;
  518     }
  519     return false;
  520 }
  521 
  522 bool Request::handle(FnMut<bool()> handler) {
  523     _handler = std::move(handler);
  524     return true;
  525 }
  526 
  527 FnMut<bool()> RequestHandler(std::vector<Command> commands) {
  528     auto request = Request(std::move(commands));
  529     RequestsStream.fire(&request);
  530     return std::move(request._handler);
  531 }
  532 
  533 FnMut<bool()> RequestHandler(Command command) {
  534     return RequestHandler(std::vector<Command>{ command });
  535 }
  536 
  537 bool Launch(Command command) {
  538     if (auto handler = RequestHandler(command)) {
  539         return handler();
  540     }
  541     return false;
  542 }
  543 
  544 bool Launch(std::vector<Command> commands) {
  545     if (auto handler = RequestHandler(std::move(commands))) {
  546         return handler();
  547     }
  548     return false;
  549 }
  550 
  551 rpl::producer<not_null<Request*>> Requests() {
  552     return RequestsStream.events();
  553 }
  554 
  555 void Start() {
  556     Assert(Global::started());
  557 
  558     Data.fill();
  559 }
  560 
  561 const QStringList &Errors() {
  562     return Data.errors();
  563 }
  564 
  565 bool HandleEvent(not_null<QShortcutEvent*> event) {
  566     return Launch(Data.lookup(event->shortcutId()));
  567 }
  568 
  569 void ToggleMediaShortcuts(bool toggled) {
  570     Data.toggleMedia(toggled);
  571     Platform::SetWatchingMediaKeys(toggled);
  572 }
  573 
  574 void ToggleSupportShortcuts(bool toggled) {
  575     Data.toggleSupport(toggled);
  576 }
  577 
  578 void Finish() {
  579     Data.clear();
  580 }
  581 
  582 } // namespace Shortcuts