shortcuts.cpp 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "core/shortcuts.h"
  8. #include "mainwindow.h"
  9. #include "mainwidget.h"
  10. #include "window/window_controller.h"
  11. #include "core/application.h"
  12. #include "media/player/media_player_instance.h"
  13. #include "base/platform/base_platform_info.h"
  14. #include "platform/platform_specific.h"
  15. #include "base/parse_helper.h"
  16. #include <QAction>
  17. #include <QShortcut>
  18. #include <QtCore/QJsonDocument>
  19. #include <QtCore/QJsonObject>
  20. #include <QtCore/QJsonArray>
  21. namespace Shortcuts {
  22. namespace {
  23. constexpr auto kCountLimit = 256; // How many shortcuts can be in json file.
  24. rpl::event_stream<not_null<Request*>> RequestsStream;
  25. bool Paused/* = false*/;
  26. const auto AutoRepeatCommands = base::flat_set<Command>{
  27. Command::MediaPrevious,
  28. Command::MediaNext,
  29. Command::ChatPrevious,
  30. Command::ChatNext,
  31. Command::ChatFirst,
  32. Command::ChatLast,
  33. };
  34. const auto MediaCommands = base::flat_set<Command>{
  35. Command::MediaPlay,
  36. Command::MediaPause,
  37. Command::MediaPlayPause,
  38. Command::MediaStop,
  39. Command::MediaPrevious,
  40. Command::MediaNext,
  41. };
  42. const auto SupportCommands = base::flat_set<Command>{
  43. Command::SupportReloadTemplates,
  44. Command::SupportToggleMuted,
  45. Command::SupportScrollToCurrent,
  46. Command::SupportHistoryBack,
  47. Command::SupportHistoryForward,
  48. };
  49. const auto CommandByName = base::flat_map<QString, Command>{
  50. { u"close_telegram"_q , Command::Close },
  51. { u"lock_telegram"_q , Command::Lock },
  52. { u"minimize_telegram"_q , Command::Minimize },
  53. { u"quit_telegram"_q , Command::Quit },
  54. { u"media_play"_q , Command::MediaPlay },
  55. { u"media_pause"_q , Command::MediaPause },
  56. { u"media_playpause"_q , Command::MediaPlayPause },
  57. { u"media_stop"_q , Command::MediaStop },
  58. { u"media_previous"_q , Command::MediaPrevious },
  59. { u"media_next"_q , Command::MediaNext },
  60. { u"search"_q , Command::Search },
  61. { u"previous_chat"_q , Command::ChatPrevious },
  62. { u"next_chat"_q , Command::ChatNext },
  63. { u"first_chat"_q , Command::ChatFirst },
  64. { u"last_chat"_q , Command::ChatLast },
  65. { u"self_chat"_q , Command::ChatSelf },
  66. { u"pinned_chat1"_q , Command::ChatPinned1 },
  67. { u"pinned_chat2"_q , Command::ChatPinned2 },
  68. { u"pinned_chat3"_q , Command::ChatPinned3 },
  69. { u"pinned_chat4"_q , Command::ChatPinned4 },
  70. { u"pinned_chat5"_q , Command::ChatPinned5 },
  71. { u"pinned_chat6"_q , Command::ChatPinned6 },
  72. { u"pinned_chat7"_q , Command::ChatPinned7 },
  73. { u"pinned_chat8"_q , Command::ChatPinned8 },
  74. { u"previous_folder"_q , Command::FolderPrevious },
  75. { u"next_folder"_q , Command::FolderNext },
  76. { u"all_chats"_q , Command::ShowAllChats },
  77. { u"account1"_q , Command::ShowAccount1 },
  78. { u"account2"_q , Command::ShowAccount2 },
  79. { u"account3"_q , Command::ShowAccount3 },
  80. { u"account4"_q , Command::ShowAccount4 },
  81. { u"account5"_q , Command::ShowAccount5 },
  82. { u"account6"_q , Command::ShowAccount6 },
  83. { u"folder1"_q , Command::ShowFolder1 },
  84. { u"folder2"_q , Command::ShowFolder2 },
  85. { u"folder3"_q , Command::ShowFolder3 },
  86. { u"folder4"_q , Command::ShowFolder4 },
  87. { u"folder5"_q , Command::ShowFolder5 },
  88. { u"folder6"_q , Command::ShowFolder6 },
  89. { u"last_folder"_q , Command::ShowFolderLast },
  90. { u"show_archive"_q , Command::ShowArchive },
  91. { u"show_contacts"_q , Command::ShowContacts },
  92. { u"read_chat"_q , Command::ReadChat },
  93. { u"show_chat_menu"_q , Command::ShowChatMenu },
  94. { u"show_chat_preview"_q , Command::ShowChatPreview },
  95. // Shortcuts that have no default values.
  96. { u"message"_q , Command::JustSendMessage },
  97. { u"message_silently"_q , Command::SendSilentMessage },
  98. { u"message_scheduled"_q , Command::ScheduleMessage },
  99. { u"media_viewer_video_fullscreen"_q , Command::MediaViewerFullscreen },
  100. { u"show_scheduled"_q , Command::ShowScheduled },
  101. { u"archive_chat"_q , Command::ArchiveChat },
  102. //
  103. };
  104. const base::flat_map<Command, QString> &CommandNames() {
  105. static const auto result = [&] {
  106. auto result = base::flat_map<Command, QString>();
  107. for (const auto &[name, command] : CommandByName) {
  108. result.emplace(command, name);
  109. }
  110. return result;
  111. }();
  112. return result;
  113. };
  114. [[maybe_unused]] constexpr auto kNoValue = {
  115. Command::JustSendMessage,
  116. Command::SendSilentMessage,
  117. Command::ScheduleMessage,
  118. Command::MediaViewerFullscreen,
  119. Command::ShowScheduled,
  120. Command::ArchiveChat,
  121. };
  122. class Manager {
  123. public:
  124. void fill();
  125. void clear();
  126. [[nodiscard]] std::vector<Command> lookup(
  127. not_null<QObject*> object) const;
  128. void toggleMedia(bool toggled);
  129. void toggleSupport(bool toggled);
  130. void listen(not_null<QWidget*> widget);
  131. [[nodiscard]] const QStringList &errors() const;
  132. [[nodiscard]] auto keysDefaults() const
  133. -> base::flat_map<QKeySequence, base::flat_set<Command>>;
  134. [[nodiscard]] auto keysCurrents() const
  135. -> base::flat_map<QKeySequence, base::flat_set<Command>>;
  136. void change(
  137. QKeySequence was,
  138. QKeySequence now,
  139. Command command,
  140. std::optional<Command> restore);
  141. void resetToDefaults();
  142. private:
  143. void fillDefaults();
  144. void writeDefaultFile();
  145. void writeCustomFile();
  146. bool readCustomFile();
  147. void set(const QString &keys, Command command, bool replace = false);
  148. void set(const QKeySequence &result, Command command, bool replace);
  149. void remove(const QString &keys);
  150. void remove(const QKeySequence &keys);
  151. void remove(const QKeySequence &keys, Command command);
  152. void unregister(base::unique_qptr<QAction> shortcut);
  153. void pruneListened();
  154. QStringList _errors;
  155. base::flat_map<QKeySequence, base::unique_qptr<QAction>> _shortcuts;
  156. base::flat_multi_map<not_null<QObject*>, Command> _commandByObject;
  157. std::vector<QPointer<QWidget>> _listened;
  158. base::flat_map<QKeySequence, base::flat_set<Command>> _defaults;
  159. base::flat_set<QAction*> _mediaShortcuts;
  160. base::flat_set<QAction*> _supportShortcuts;
  161. };
  162. QString DefaultFilePath() {
  163. return cWorkingDir() + u"tdata/shortcuts-default.json"_q;
  164. }
  165. QString CustomFilePath() {
  166. return cWorkingDir() + u"tdata/shortcuts-custom.json"_q;
  167. }
  168. bool DefaultFileIsValid() {
  169. QFile file(DefaultFilePath());
  170. if (!file.open(QIODevice::ReadOnly)) {
  171. return false;
  172. }
  173. auto error = QJsonParseError{ 0, QJsonParseError::NoError };
  174. const auto document = QJsonDocument::fromJson(
  175. base::parse::stripComments(file.readAll()),
  176. &error);
  177. file.close();
  178. if (error.error != QJsonParseError::NoError || !document.isArray()) {
  179. return false;
  180. }
  181. const auto shortcuts = document.array();
  182. if (shortcuts.isEmpty() || !(*shortcuts.constBegin()).isObject()) {
  183. return false;
  184. }
  185. const auto versionObject = (*shortcuts.constBegin()).toObject();
  186. const auto version = versionObject.constFind(u"version"_q);
  187. if (version == versionObject.constEnd()
  188. || !(*version).isString()
  189. || (*version).toString() != QString::number(AppVersion)) {
  190. return false;
  191. }
  192. return true;
  193. }
  194. void WriteDefaultCustomFile() {
  195. const auto path = CustomFilePath();
  196. auto input = QFile(":/misc/default_shortcuts-custom.json");
  197. auto output = QFile(path);
  198. if (input.open(QIODevice::ReadOnly)
  199. && output.open(QIODevice::WriteOnly)) {
  200. #ifdef Q_OS_MAC
  201. auto text = qs(input.readAll());
  202. const auto note = R"(
  203. // Note:
  204. // On Apple platforms, reference to "ctrl" corresponds to the Command keys )"
  205. + QByteArray()
  206. + R"(on the Macintosh keyboard.
  207. // On Apple platforms, reference to "meta" corresponds to the Control keys.
  208. [
  209. )";
  210. text.replace(u"\n\n["_q, QString(note));
  211. output.write(text.toUtf8());
  212. #else
  213. output.write(input.readAll());
  214. #endif // !Q_OS_MAC
  215. }
  216. }
  217. void Manager::fill() {
  218. fillDefaults();
  219. if (!DefaultFileIsValid()) {
  220. writeDefaultFile();
  221. }
  222. if (!readCustomFile()) {
  223. WriteDefaultCustomFile();
  224. }
  225. }
  226. void Manager::clear() {
  227. _errors.clear();
  228. _shortcuts.clear();
  229. _commandByObject.clear();
  230. _mediaShortcuts.clear();
  231. _supportShortcuts.clear();
  232. }
  233. const QStringList &Manager::errors() const {
  234. return _errors;
  235. }
  236. auto Manager::keysDefaults() const
  237. -> base::flat_map<QKeySequence, base::flat_set<Command>> {
  238. return _defaults;
  239. }
  240. auto Manager::keysCurrents() const
  241. -> base::flat_map<QKeySequence, base::flat_set<Command>> {
  242. auto result = base::flat_map<QKeySequence, base::flat_set<Command>>();
  243. for (const auto &[keys, command] : _shortcuts) {
  244. auto i = _commandByObject.findFirst(command);
  245. const auto end = _commandByObject.end();
  246. for (; i != end && (i->first == command); ++i) {
  247. result[keys].emplace(i->second);
  248. }
  249. }
  250. return result;
  251. }
  252. void Manager::change(
  253. QKeySequence was,
  254. QKeySequence now,
  255. Command command,
  256. std::optional<Command> restore) {
  257. if (!was.isEmpty()) {
  258. remove(was, command);
  259. }
  260. if (!now.isEmpty()) {
  261. set(now, command, true);
  262. }
  263. if (restore) {
  264. Assert(!was.isEmpty());
  265. set(was, *restore, true);
  266. }
  267. writeCustomFile();
  268. }
  269. void Manager::resetToDefaults() {
  270. while (!_shortcuts.empty()) {
  271. remove(_shortcuts.begin()->first);
  272. }
  273. for (const auto &[sequence, commands] : _defaults) {
  274. for (const auto command : commands) {
  275. set(sequence, command, false);
  276. }
  277. }
  278. writeCustomFile();
  279. }
  280. std::vector<Command> Manager::lookup(not_null<QObject*> object) const {
  281. auto result = std::vector<Command>();
  282. auto i = _commandByObject.findFirst(object);
  283. const auto end = _commandByObject.end();
  284. for (; i != end && (i->first == object); ++i) {
  285. result.push_back(i->second);
  286. }
  287. return result;
  288. }
  289. void Manager::toggleMedia(bool toggled) {
  290. for (const auto shortcut : _mediaShortcuts) {
  291. shortcut->setEnabled(toggled);
  292. }
  293. }
  294. void Manager::toggleSupport(bool toggled) {
  295. for (const auto shortcut : _supportShortcuts) {
  296. shortcut->setEnabled(toggled);
  297. }
  298. }
  299. void Manager::listen(not_null<QWidget*> widget) {
  300. pruneListened();
  301. _listened.push_back(widget.get());
  302. for (const auto &[keys, shortcut] : _shortcuts) {
  303. widget->addAction(shortcut.get());
  304. }
  305. }
  306. void Manager::pruneListened() {
  307. for (auto i = begin(_listened); i != end(_listened);) {
  308. if (i->data()) {
  309. ++i;
  310. } else {
  311. i = _listened.erase(i);
  312. }
  313. }
  314. }
  315. bool Manager::readCustomFile() {
  316. // read custom shortcuts from file if it exists or write an empty custom shortcuts file
  317. QFile file(CustomFilePath());
  318. if (!file.exists()) {
  319. return false;
  320. }
  321. const auto guard = gsl::finally([&] {
  322. if (!_errors.isEmpty()) {
  323. _errors.push_front((u"While reading file '%1'..."_q
  324. ).arg(file.fileName()));
  325. }
  326. });
  327. if (!file.open(QIODevice::ReadOnly)) {
  328. _errors.push_back(u"Could not read the file!"_q);
  329. return true;
  330. }
  331. auto error = QJsonParseError{ 0, QJsonParseError::NoError };
  332. const auto document = QJsonDocument::fromJson(
  333. base::parse::stripComments(file.readAll()),
  334. &error);
  335. file.close();
  336. if (error.error != QJsonParseError::NoError) {
  337. _errors.push_back((u"Failed to parse! Error: %2"_q
  338. ).arg(error.errorString()));
  339. return true;
  340. } else if (!document.isArray()) {
  341. _errors.push_back(u"Failed to parse! Error: array expected"_q);
  342. return true;
  343. }
  344. const auto shortcuts = document.array();
  345. auto limit = kCountLimit;
  346. for (auto i = shortcuts.constBegin(), e = shortcuts.constEnd(); i != e; ++i) {
  347. if (!(*i).isObject()) {
  348. _errors.push_back(u"Bad entry! Error: object expected"_q);
  349. continue;
  350. }
  351. const auto entry = (*i).toObject();
  352. const auto keys = entry.constFind(u"keys"_q);
  353. const auto command = entry.constFind(u"command"_q);
  354. const auto removed = entry.constFind(u"removed"_q);
  355. if (keys == entry.constEnd()
  356. || command == entry.constEnd()
  357. || !(*keys).isString()
  358. || (!(*command).isString() && !(*command).isNull())) {
  359. _errors.push_back(qsl("Bad entry! "
  360. "{\"keys\": \"...\", \"command\": [ \"...\" | null ]} "
  361. "expected."));
  362. } else if ((*command).isNull()) {
  363. remove((*keys).toString());
  364. } else {
  365. const auto name = (*command).toString();
  366. const auto i = CommandByName.find(name);
  367. if (i != end(CommandByName)) {
  368. if (removed != entry.constEnd() && removed->toBool()) {
  369. remove((*keys).toString(), i->second);
  370. } else {
  371. set((*keys).toString(), i->second, true);
  372. }
  373. } else {
  374. LOG(("Shortcut Warning: "
  375. "could not find shortcut command handler '%1'"
  376. ).arg(name));
  377. }
  378. }
  379. if (!--limit) {
  380. _errors.push_back(u"Too many entries! Limit is %1"_q.arg(
  381. kCountLimit));
  382. break;
  383. }
  384. }
  385. return true;
  386. }
  387. void Manager::fillDefaults() {
  388. const auto ctrl = Platform::IsMac() ? u"meta"_q : u"ctrl"_q;
  389. set(u"ctrl+w"_q, Command::Close);
  390. set(u"ctrl+f4"_q, Command::Close);
  391. set(u"ctrl+l"_q, Command::Lock);
  392. set(u"ctrl+m"_q, Command::Minimize);
  393. set(u"ctrl+q"_q, Command::Quit);
  394. set(u"media play"_q, Command::MediaPlay);
  395. set(u"media pause"_q, Command::MediaPause);
  396. set(u"toggle media play/pause"_q, Command::MediaPlayPause);
  397. set(u"media stop"_q, Command::MediaStop);
  398. set(u"media previous"_q, Command::MediaPrevious);
  399. set(u"media next"_q, Command::MediaNext);
  400. set(u"ctrl+f"_q, Command::Search);
  401. set(u"search"_q, Command::Search);
  402. set(u"find"_q, Command::Search);
  403. set(u"ctrl+pgdown"_q, Command::ChatNext);
  404. set(u"alt+down"_q, Command::ChatNext);
  405. set(u"ctrl+pgup"_q, Command::ChatPrevious);
  406. set(u"alt+up"_q, Command::ChatPrevious);
  407. set(u"%1+tab"_q.arg(ctrl), Command::ChatNext);
  408. set(u"%1+shift+tab"_q.arg(ctrl), Command::ChatPrevious);
  409. set(u"%1+backtab"_q.arg(ctrl), Command::ChatPrevious);
  410. set(u"ctrl+alt+home"_q, Command::ChatFirst);
  411. set(u"ctrl+alt+end"_q, Command::ChatLast);
  412. set(u"f5"_q, Command::SupportReloadTemplates);
  413. set(u"ctrl+delete"_q, Command::SupportToggleMuted);
  414. set(u"ctrl+insert"_q, Command::SupportScrollToCurrent);
  415. set(u"ctrl+shift+x"_q, Command::SupportHistoryBack);
  416. set(u"ctrl+shift+c"_q, Command::SupportHistoryForward);
  417. set(u"ctrl+1"_q, Command::ChatPinned1);
  418. set(u"ctrl+2"_q, Command::ChatPinned2);
  419. set(u"ctrl+3"_q, Command::ChatPinned3);
  420. set(u"ctrl+4"_q, Command::ChatPinned4);
  421. set(u"ctrl+5"_q, Command::ChatPinned5);
  422. set(u"ctrl+6"_q, Command::ChatPinned6);
  423. set(u"ctrl+7"_q, Command::ChatPinned7);
  424. set(u"ctrl+8"_q, Command::ChatPinned8);
  425. auto &&folders = ranges::views::zip(
  426. kShowFolder,
  427. ranges::views::ints(1, ranges::unreachable));
  428. for (const auto &[command, index] : folders) {
  429. set(u"%1+%2"_q.arg(ctrl).arg(index), command);
  430. }
  431. set(u"%1+shift+down"_q.arg(ctrl), Command::FolderNext);
  432. set(u"%1+shift+up"_q.arg(ctrl), Command::FolderPrevious);
  433. set(u"ctrl+0"_q, Command::ChatSelf);
  434. set(u"ctrl+9"_q, Command::ShowArchive);
  435. set(u"ctrl+j"_q, Command::ShowContacts);
  436. set(u"ctrl+r"_q, Command::ReadChat);
  437. set(u"ctrl+\\"_q, Command::ShowChatMenu);
  438. set(u"ctrl+]"_q, Command::ShowChatPreview);
  439. _defaults = keysCurrents();
  440. }
  441. void Manager::writeDefaultFile() {
  442. auto file = QFile(DefaultFilePath());
  443. if (!file.open(QIODevice::WriteOnly)) {
  444. return;
  445. }
  446. const char *defaultHeader = R"HEADER(
  447. // This is a list of default shortcuts for Telegram Desktop
  448. // Please don't modify it, its content is not used in any way
  449. // You can place your own shortcuts in the 'shortcuts-custom.json' file
  450. )HEADER";
  451. file.write(defaultHeader);
  452. auto shortcuts = QJsonArray();
  453. auto version = QJsonObject();
  454. version.insert(u"version"_q, QString::number(AppVersion));
  455. shortcuts.push_back(version);
  456. for (const auto &[sequence, shortcut] : _shortcuts) {
  457. const auto object = shortcut.get();
  458. auto i = _commandByObject.findFirst(object);
  459. const auto end = _commandByObject.end();
  460. for (; i != end && i->first == object; ++i) {
  461. const auto j = CommandNames().find(i->second);
  462. if (j != CommandNames().end()) {
  463. QJsonObject entry;
  464. entry.insert(u"keys"_q, sequence.toString().toLower());
  465. entry.insert(u"command"_q, j->second);
  466. shortcuts.append(entry);
  467. }
  468. }
  469. }
  470. // Commands without a default value.
  471. for (const auto c : ranges::views::concat(kShowAccount, kNoValue)) {
  472. for (const auto &[name, command] : CommandByName) {
  473. if (c == command) {
  474. auto entry = QJsonObject();
  475. entry.insert(u"keys"_q, QJsonValue());
  476. entry.insert(u"command"_q, name);
  477. shortcuts.append(entry);
  478. }
  479. }
  480. }
  481. auto document = QJsonDocument();
  482. document.setArray(shortcuts);
  483. file.write(document.toJson(QJsonDocument::Indented));
  484. }
  485. void Manager::writeCustomFile() {
  486. auto shortcuts = QJsonArray();
  487. for (const auto &[sequence, shortcut] : _shortcuts) {
  488. const auto object = shortcut.get();
  489. auto i = _commandByObject.findFirst(object);
  490. const auto end = _commandByObject.end();
  491. for (; i != end && i->first == object; ++i) {
  492. const auto d = _defaults.find(sequence);
  493. if (d == _defaults.end() || !d->second.contains(i->second)) {
  494. const auto j = CommandNames().find(i->second);
  495. if (j != CommandNames().end()) {
  496. QJsonObject entry;
  497. entry.insert(u"keys"_q, sequence.toString().toLower());
  498. entry.insert(u"command"_q, j->second);
  499. shortcuts.append(entry);
  500. }
  501. }
  502. }
  503. }
  504. const auto has = [&](not_null<QObject*> shortcut, Command command) {
  505. for (auto i = _commandByObject.findFirst(shortcut)
  506. ; i != end(_commandByObject) && i->first == shortcut
  507. ; ++i) {
  508. if (i->second == command) {
  509. return true;
  510. }
  511. }
  512. return false;
  513. };
  514. for (const auto &[sequence, commands] : _defaults) {
  515. const auto i = _shortcuts.find(sequence);
  516. if (i == end(_shortcuts)) {
  517. QJsonObject entry;
  518. entry.insert(u"keys"_q, sequence.toString().toLower());
  519. entry.insert(u"command"_q, QJsonValue());
  520. shortcuts.append(entry);
  521. continue;
  522. }
  523. for (const auto command : commands) {
  524. if (!has(i->second.get(), command)) {
  525. const auto j = CommandNames().find(command);
  526. if (j != CommandNames().end()) {
  527. QJsonObject entry;
  528. entry.insert(u"keys"_q, sequence.toString().toLower());
  529. entry.insert(u"command"_q, j->second);
  530. entry.insert(u"removed"_q, true);
  531. shortcuts.append(entry);
  532. }
  533. }
  534. }
  535. }
  536. if (shortcuts.isEmpty()) {
  537. WriteDefaultCustomFile();
  538. return;
  539. }
  540. auto file = QFile(CustomFilePath());
  541. if (!file.open(QIODevice::WriteOnly)) {
  542. LOG(("Shortcut Warning: could not write custom shortcuts file."));
  543. return;
  544. }
  545. const char *customHeader = R"HEADER(
  546. // This is a list of changed shortcuts for Telegram Desktop
  547. // You can edit them in Settings > Chat Settings > Keyboard Shortcuts.
  548. )HEADER";
  549. file.write(customHeader);
  550. auto document = QJsonDocument();
  551. document.setArray(shortcuts);
  552. file.write(document.toJson(QJsonDocument::Indented));
  553. }
  554. void Manager::set(const QString &keys, Command command, bool replace) {
  555. if (keys.isEmpty()) {
  556. return;
  557. }
  558. const auto result = QKeySequence(keys, QKeySequence::PortableText);
  559. if (result.isEmpty()) {
  560. _errors.push_back(u"Could not derive key sequence '%1'!"_q.arg(keys));
  561. return;
  562. }
  563. set(result, command, replace);
  564. }
  565. void Manager::set(
  566. const QKeySequence &keys,
  567. Command command,
  568. bool replace) {
  569. auto shortcut = base::make_unique_q<QAction>();
  570. shortcut->setShortcut(keys);
  571. shortcut->setShortcutContext(Qt::ApplicationShortcut);
  572. if (!AutoRepeatCommands.contains(command)) {
  573. shortcut->setAutoRepeat(false);
  574. }
  575. const auto isMediaShortcut = MediaCommands.contains(command);
  576. const auto isSupportShortcut = SupportCommands.contains(command);
  577. if (isMediaShortcut || isSupportShortcut) {
  578. shortcut->setEnabled(false);
  579. }
  580. auto object = shortcut.get();
  581. auto i = _shortcuts.find(keys);
  582. if (i == end(_shortcuts)) {
  583. i = _shortcuts.emplace(keys, std::move(shortcut)).first;
  584. } else if (replace) {
  585. unregister(std::exchange(i->second, std::move(shortcut)));
  586. } else {
  587. object = i->second.get();
  588. }
  589. _commandByObject.emplace(object, command);
  590. if (!shortcut) { // Added the new one.
  591. if (isMediaShortcut) {
  592. _mediaShortcuts.emplace(i->second.get());
  593. }
  594. if (isSupportShortcut) {
  595. _supportShortcuts.emplace(i->second.get());
  596. }
  597. pruneListened();
  598. for (const auto &widget : _listened) {
  599. widget->addAction(i->second.get());
  600. }
  601. }
  602. }
  603. void Manager::remove(const QString &keys) {
  604. if (keys.isEmpty()) {
  605. return;
  606. }
  607. const auto result = QKeySequence(keys, QKeySequence::PortableText);
  608. if (result.isEmpty()) {
  609. _errors.push_back(u"Could not derive key sequence '%1'!"_q.arg(keys));
  610. return;
  611. }
  612. remove(result);
  613. }
  614. void Manager::remove(const QKeySequence &keys) {
  615. const auto i = _shortcuts.find(keys);
  616. if (i != end(_shortcuts)) {
  617. unregister(std::move(i->second));
  618. _shortcuts.erase(i);
  619. }
  620. }
  621. void Manager::remove(const QKeySequence &keys, Command command) {
  622. const auto i = _shortcuts.find(keys);
  623. if (i != end(_shortcuts)) {
  624. _commandByObject.remove(i->second.get(), command);
  625. if (!_commandByObject.contains(i->second.get())) {
  626. unregister(std::move(i->second));
  627. _shortcuts.erase(i);
  628. }
  629. }
  630. }
  631. void Manager::unregister(base::unique_qptr<QAction> shortcut) {
  632. if (shortcut) {
  633. _commandByObject.removeAll(shortcut.get());
  634. _mediaShortcuts.erase(shortcut.get());
  635. _supportShortcuts.erase(shortcut.get());
  636. }
  637. }
  638. Manager Data;
  639. } // namespace
  640. Request::Request(std::vector<Command> commands)
  641. : _commands(std::move(commands)) {
  642. }
  643. bool Request::check(Command command, int priority) {
  644. if (ranges::contains(_commands, command)
  645. && priority > _handlerPriority) {
  646. _handlerPriority = priority;
  647. return true;
  648. }
  649. return false;
  650. }
  651. bool Request::handle(FnMut<bool()> handler) {
  652. _handler = std::move(handler);
  653. return true;
  654. }
  655. FnMut<bool()> RequestHandler(std::vector<Command> commands) {
  656. auto request = Request(std::move(commands));
  657. RequestsStream.fire(&request);
  658. return std::move(request._handler);
  659. }
  660. FnMut<bool()> RequestHandler(Command command) {
  661. return RequestHandler(std::vector<Command>{ command });
  662. }
  663. bool Launch(Command command) {
  664. if (auto handler = RequestHandler(command)) {
  665. return handler();
  666. }
  667. return false;
  668. }
  669. bool Launch(std::vector<Command> commands) {
  670. if (Paused) {
  671. return false;
  672. } else if (auto handler = RequestHandler(std::move(commands))) {
  673. return handler();
  674. }
  675. return false;
  676. }
  677. rpl::producer<not_null<Request*>> Requests() {
  678. return RequestsStream.events();
  679. }
  680. void Start() {
  681. Data.fill();
  682. }
  683. const QStringList &Errors() {
  684. return Data.errors();
  685. }
  686. bool HandleEvent(
  687. not_null<QObject*> object,
  688. not_null<QShortcutEvent*> event) {
  689. return Launch(Data.lookup(object));
  690. }
  691. void ToggleMediaShortcuts(bool toggled) {
  692. Data.toggleMedia(toggled);
  693. }
  694. void ToggleSupportShortcuts(bool toggled) {
  695. Data.toggleSupport(toggled);
  696. }
  697. void Pause() {
  698. Paused = true;
  699. }
  700. void Unpause() {
  701. Paused = false;
  702. }
  703. auto KeysDefaults()
  704. -> base::flat_map<QKeySequence, base::flat_set<Command>> {
  705. return Data.keysDefaults();
  706. }
  707. auto KeysCurrents()
  708. -> base::flat_map<QKeySequence, base::flat_set<Command>> {
  709. return Data.keysCurrents();
  710. }
  711. void Change(
  712. QKeySequence was,
  713. QKeySequence now,
  714. Command command,
  715. std::optional<Command> restore) {
  716. Data.change(was, now, command, restore);
  717. }
  718. void ResetToDefaults() {
  719. Data.resetToDefaults();
  720. }
  721. bool AllowWithoutModifiers(int key) {
  722. const auto service = {
  723. Qt::Key_Escape,
  724. Qt::Key_Tab,
  725. Qt::Key_Backtab,
  726. Qt::Key_Backspace,
  727. Qt::Key_Return,
  728. Qt::Key_Enter,
  729. Qt::Key_Insert,
  730. Qt::Key_Delete,
  731. Qt::Key_Pause,
  732. Qt::Key_Print,
  733. Qt::Key_SysReq,
  734. Qt::Key_Clear,
  735. Qt::Key_Home,
  736. Qt::Key_End,
  737. Qt::Key_Left,
  738. Qt::Key_Up,
  739. Qt::Key_Right,
  740. Qt::Key_Down,
  741. Qt::Key_PageUp,
  742. Qt::Key_PageDown,
  743. Qt::Key_Shift,
  744. Qt::Key_Control,
  745. Qt::Key_Meta,
  746. Qt::Key_Alt,
  747. Qt::Key_CapsLock,
  748. Qt::Key_NumLock,
  749. Qt::Key_ScrollLock,
  750. };
  751. return (key >= 0x80) && !ranges::contains(service, key);
  752. }
  753. void Finish() {
  754. Data.clear();
  755. }
  756. void Listen(not_null<QWidget*> widget) {
  757. Data.listen(widget);
  758. }
  759. } // namespace Shortcuts