settings_shortcuts.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  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 "settings/settings_shortcuts.h"
  8. #include "base/event_filter.h"
  9. #include "core/application.h"
  10. #include "core/shortcuts.h"
  11. #include "lang/lang_keys.h"
  12. #include "ui/text/text_utilities.h"
  13. #include "ui/widgets/buttons.h"
  14. #include "ui/widgets/labels.h"
  15. #include "ui/widgets/popup_menu.h"
  16. #include "ui/wrap/slide_wrap.h"
  17. #include "ui/wrap/vertical_layout.h"
  18. #include "ui/vertical_list.h"
  19. #include "styles/style_menu_icons.h"
  20. #include "styles/style_settings.h"
  21. #include <private/qkeymapper_p.h>
  22. namespace Settings {
  23. namespace {
  24. namespace S = ::Shortcuts;
  25. struct Labeled {
  26. S::Command command = {};
  27. rpl::producer<QString> label;
  28. };
  29. [[nodiscard]] std::vector<Labeled> Entries() {
  30. using C = S::Command;
  31. const auto pinned = [](int index) {
  32. return tr::lng_shortcuts_chat_pinned_n(
  33. lt_index,
  34. rpl::single(QString::number(index)));
  35. };
  36. const auto account = [](int index) {
  37. return tr::lng_shortcuts_show_account_n(
  38. lt_index,
  39. rpl::single(QString::number(index)));
  40. };
  41. const auto folder = [](int index) {
  42. return tr::lng_shortcuts_show_folder_n(
  43. lt_index,
  44. rpl::single(QString::number(index)));
  45. };
  46. const auto separator = Labeled{ C(), nullptr };
  47. return {
  48. { C::Close, tr::lng_shortcuts_close() },
  49. { C::Lock, tr::lng_shortcuts_lock() },
  50. { C::Minimize, tr::lng_shortcuts_minimize() },
  51. { C::Quit, tr::lng_shortcuts_quit() },
  52. separator,
  53. { C::Search, tr::lng_shortcuts_search() },
  54. separator,
  55. { C::ChatPrevious, tr::lng_shortcuts_chat_previous() },
  56. { C::ChatNext, tr::lng_shortcuts_chat_next() },
  57. { C::ChatFirst, tr::lng_shortcuts_chat_first() },
  58. { C::ChatLast, tr::lng_shortcuts_chat_last() },
  59. { C::ChatSelf, tr::lng_shortcuts_chat_self() },
  60. separator,
  61. { C::ChatPinned1, pinned(1) },
  62. { C::ChatPinned2, pinned(2) },
  63. { C::ChatPinned3, pinned(3) },
  64. { C::ChatPinned4, pinned(4) },
  65. { C::ChatPinned5, pinned(5) },
  66. { C::ChatPinned6, pinned(6) },
  67. { C::ChatPinned7, pinned(7) },
  68. { C::ChatPinned8, pinned(8) },
  69. separator,
  70. { C::ShowAccount1, account(1) },
  71. { C::ShowAccount2, account(2) },
  72. { C::ShowAccount3, account(3) },
  73. { C::ShowAccount4, account(4) },
  74. { C::ShowAccount5, account(5) },
  75. { C::ShowAccount6, account(6) },
  76. separator,
  77. { C::ShowAllChats, tr::lng_shortcuts_show_all_chats() },
  78. { C::ShowFolder1, folder(1) },
  79. { C::ShowFolder2, folder(2) },
  80. { C::ShowFolder3, folder(3) },
  81. { C::ShowFolder4, folder(4) },
  82. { C::ShowFolder5, folder(5) },
  83. { C::ShowFolder6, folder(6) },
  84. { C::ShowFolderLast, tr::lng_shortcuts_show_folder_last() },
  85. { C::FolderNext, tr::lng_shortcuts_folder_next() },
  86. { C::FolderPrevious, tr::lng_shortcuts_folder_previous() },
  87. { C::ShowArchive, tr::lng_shortcuts_archive() },
  88. { C::ShowContacts, tr::lng_shortcuts_contacts() },
  89. separator,
  90. { C::ReadChat, tr::lng_shortcuts_read_chat() },
  91. { C::ArchiveChat, tr::lng_shortcuts_archive_chat() },
  92. { C::ShowScheduled, tr::lng_shortcuts_scheduled() },
  93. { C::ShowChatMenu, tr::lng_shortcuts_show_chat_menu() },
  94. { C::ShowChatPreview, tr::lng_shortcuts_show_chat_preview() },
  95. separator,
  96. { C::JustSendMessage, tr::lng_shortcuts_just_send() },
  97. { C::SendSilentMessage, tr::lng_shortcuts_silent_send() },
  98. { C::ScheduleMessage, tr::lng_shortcuts_schedule() },
  99. separator,
  100. { C::MediaViewerFullscreen, tr::lng_shortcuts_media_fullscreen() },
  101. separator,
  102. { C::MediaPlay, tr::lng_shortcuts_media_play() },
  103. { C::MediaPause, tr::lng_shortcuts_media_pause() },
  104. { C::MediaPlayPause, tr::lng_shortcuts_media_play_pause() },
  105. { C::MediaStop, tr::lng_shortcuts_media_stop() },
  106. { C::MediaPrevious, tr::lng_shortcuts_media_previous() },
  107. { C::MediaNext, tr::lng_shortcuts_media_next() },
  108. };
  109. }
  110. [[nodiscard]] QString ToString(const QKeySequence &key) {
  111. auto result = key.toString();
  112. #ifdef Q_OS_MAC
  113. result = result.replace(u"Ctrl+"_q, QString() + QChar(0x2318));
  114. result = result.replace(u"Meta+"_q, QString() + QChar(0x2303));
  115. result = result.replace(u"Alt+"_q, QString() + QChar(0x2325));
  116. result = result.replace(u"Shift+"_q, QString() + QChar(0x21E7));
  117. #endif // Q_OS_MAC
  118. return result;
  119. }
  120. [[nodiscard]] Fn<void()> SetupShortcutsContent(
  121. not_null<Window::SessionController*> controller,
  122. not_null<Ui::VerticalLayout*> content) {
  123. const auto &defaults = S::KeysDefaults();
  124. const auto &currents = S::KeysCurrents();
  125. struct Button {
  126. S::Command command;
  127. std::unique_ptr<Ui::SettingsButton> widget;
  128. rpl::variable<QKeySequence> key;
  129. rpl::variable<bool> removed;
  130. };
  131. struct Entry {
  132. S::Command command;
  133. rpl::producer<QString> label;
  134. std::vector<QKeySequence> original;
  135. std::vector<QKeySequence> now;
  136. Ui::VerticalLayout *wrap = nullptr;
  137. std::vector<std::unique_ptr<Button>> buttons;
  138. };
  139. struct State {
  140. std::vector<Entry> entries;
  141. rpl::variable<bool> modified;
  142. rpl::variable<Button*> recording;
  143. rpl::variable<QKeySequence> lastKey;
  144. Fn<void(S::Command command)> showMenuFor;
  145. };
  146. const auto state = content->lifetime().make_state<State>();
  147. const auto labeled = Entries();
  148. auto &entries = state->entries = ranges::views::all(
  149. labeled
  150. ) | ranges::views::transform([](Labeled labeled) {
  151. return Entry{ labeled.command, std::move(labeled.label) };
  152. }) | ranges::to_vector;
  153. for (const auto &[keys, commands] : defaults) {
  154. for (const auto command : commands) {
  155. const auto i = ranges::find(entries, command, &Entry::command);
  156. if (i != end(entries)) {
  157. i->original.push_back(keys);
  158. }
  159. }
  160. }
  161. for (const auto &[keys, commands] : currents) {
  162. for (const auto command : commands) {
  163. const auto i = ranges::find(entries, command, &Entry::command);
  164. if (i != end(entries)) {
  165. i->now.push_back(keys);
  166. }
  167. }
  168. }
  169. const auto checkModified = [=] {
  170. for (const auto &entry : state->entries) {
  171. auto original = entry.original;
  172. auto now = entry.now;
  173. ranges::sort(original);
  174. ranges::sort(now);
  175. if (original != now) {
  176. state->modified = true;
  177. return;
  178. }
  179. }
  180. state->modified = false;
  181. };
  182. checkModified();
  183. const auto menu = std::make_shared<QPointer<Ui::PopupMenu>>();
  184. const auto fill = [=](Entry &entry) {
  185. auto index = 0;
  186. if (entry.original.empty()) {
  187. entry.original.push_back(QKeySequence());
  188. }
  189. if (entry.now.empty()) {
  190. entry.now.push_back(QKeySequence());
  191. }
  192. for (const auto &now : entry.now) {
  193. if (index < entry.buttons.size()) {
  194. entry.buttons[index]->key = now;
  195. entry.buttons[index]->removed = false;
  196. } else {
  197. auto button = std::make_unique<Button>(Button{
  198. .command = entry.command,
  199. .key = now,
  200. });
  201. const auto raw = button.get();
  202. const auto widget = entry.wrap->add(
  203. object_ptr<Ui::SettingsButton>(
  204. entry.wrap,
  205. rpl::duplicate(entry.label),
  206. st::settingsButtonNoIcon));
  207. const auto keys = Ui::CreateChild<Ui::FlatLabel>(
  208. widget,
  209. st::settingsButtonNoIcon.rightLabel);
  210. keys->show();
  211. rpl::combine(
  212. widget->widthValue(),
  213. rpl::duplicate(entry.label),
  214. button->key.value(),
  215. state->recording.value(),
  216. button->removed.value()
  217. ) | rpl::start_with_next([=](
  218. int width,
  219. const QString &button,
  220. const QKeySequence &key,
  221. Button *recording,
  222. bool removed) {
  223. const auto &st = st::settingsButtonNoIcon;
  224. const auto available = width
  225. - st.padding.left()
  226. - st.padding.right()
  227. - st.style.font->width(button)
  228. - st::settingsButtonRightSkip;
  229. keys->setMarkedText((recording == raw)
  230. ? Ui::Text::Italic(
  231. tr::lng_shortcuts_recording(tr::now))
  232. : key.isEmpty()
  233. ? TextWithEntities()
  234. : removed
  235. ? Ui::Text::Wrapped(
  236. TextWithEntities{ ToString(key) },
  237. EntityType::StrikeOut)
  238. : TextWithEntities{ ToString(key) });
  239. keys->setTextColorOverride((recording == raw)
  240. ? st::boxTextFgGood->c
  241. : removed
  242. ? st::attentionButtonFg->c
  243. : std::optional<QColor>());
  244. keys->resizeToNaturalWidth(available);
  245. keys->moveToRight(
  246. st::settingsButtonRightSkip,
  247. st.padding.top());
  248. }, keys->lifetime());
  249. keys->setAttribute(Qt::WA_TransparentForMouseEvents);
  250. widget->setAcceptBoth(true);
  251. widget->clicks(
  252. ) | rpl::start_with_next([=](Qt::MouseButton button) {
  253. if (const auto strong = *menu) {
  254. strong->hideMenu();
  255. return;
  256. }
  257. if (button == Qt::RightButton) {
  258. state->showMenuFor(raw->command);
  259. } else {
  260. S::Pause();
  261. state->recording = raw;
  262. }
  263. }, widget->lifetime());
  264. button->widget.reset(widget);
  265. entry.buttons.push_back(std::move(button));
  266. }
  267. ++index;
  268. }
  269. while (entry.wrap->count() > index) {
  270. entry.buttons.pop_back();
  271. }
  272. };
  273. state->showMenuFor = [=](S::Command command) {
  274. *menu = Ui::CreateChild<Ui::PopupMenu>(
  275. content,
  276. st::popupMenuWithIcons);
  277. (*menu)->addAction(tr::lng_shortcuts_add_another(tr::now), [=] {
  278. const auto i = ranges::find(
  279. state->entries,
  280. command,
  281. &Entry::command);
  282. if (i != end(state->entries)) {
  283. S::Pause();
  284. const auto j = ranges::find(i->now, QKeySequence());
  285. if (j != end(i->now)) {
  286. state->recording = i->buttons[j - begin(i->now)].get();
  287. } else {
  288. i->now.push_back(QKeySequence());
  289. fill(*i);
  290. state->recording = i->buttons.back().get();
  291. }
  292. }
  293. }, &st::menuIconTopics);
  294. (*menu)->popup(QCursor::pos());
  295. };
  296. const auto stopRecording = [=](std::optional<QKeySequence> result = {}) {
  297. const auto button = state->recording.current();
  298. if (!button) {
  299. return;
  300. }
  301. state->recording = nullptr;
  302. InvokeQueued(content, [=] {
  303. InvokeQueued(content, [=] {
  304. // Let all the shortcut events propagate first.
  305. S::Unpause();
  306. });
  307. });
  308. auto was = button->key.current();
  309. const auto now = result.value_or(was);
  310. if (now == was) {
  311. if (!now.isEmpty() && (!result || !button->removed.current())) {
  312. return;
  313. }
  314. was = QKeySequence();
  315. button->removed = false;
  316. }
  317. auto changed = false;
  318. const auto command = button->command;
  319. for (auto &entry : state->entries) {
  320. const auto i = ranges::find(
  321. entry.buttons,
  322. button,
  323. &std::unique_ptr<Button>::get);
  324. if (i != end(entry.buttons)) {
  325. const auto index = i - begin(entry.buttons);
  326. if (now.isEmpty()) {
  327. entry.now.erase(begin(entry.now) + index);
  328. } else {
  329. const auto i = ranges::find(entry.now, now);
  330. if (i == end(entry.now)) {
  331. entry.now[index] = now;
  332. } else if (i != begin(entry.now) + index) {
  333. std::swap(entry.now[index], *i);
  334. entry.now.erase(i);
  335. }
  336. }
  337. fill(entry);
  338. checkModified();
  339. } else if (now != was) {
  340. const auto i = now.isEmpty()
  341. ? end(entry.now)
  342. : ranges::find(entry.now, now);
  343. if (i != end(entry.now)) {
  344. entry.buttons[i - begin(entry.now)]->removed = true;
  345. }
  346. const auto j = was.isEmpty()
  347. ? end(entry.now)
  348. : ranges::find(entry.now, was);
  349. if (j != end(entry.now)) {
  350. entry.buttons[j - begin(entry.now)]->removed = false;
  351. S::Change(was, now, command, entry.command);
  352. was = QKeySequence();
  353. changed = true;
  354. }
  355. }
  356. }
  357. if (!changed) {
  358. S::Change(was, now, command);
  359. }
  360. };
  361. base::install_event_filter(content, qApp, [=](not_null<QEvent*> e) {
  362. const auto type = e->type();
  363. if (type == QEvent::ShortcutOverride && state->recording.current()) {
  364. if (!content->window()->isActiveWindow()) {
  365. return base::EventFilterResult::Continue;
  366. }
  367. const auto key = static_cast<QKeyEvent*>(e.get());
  368. const auto m = key->modifiers();
  369. const auto k = key->key();
  370. const auto clear = !m
  371. && (k == Qt::Key_Backspace || k == Qt::Key_Delete);
  372. if (k == Qt::Key_Control
  373. || k == Qt::Key_Shift
  374. || k == Qt::Key_Alt
  375. || k == Qt::Key_Meta) {
  376. return base::EventFilterResult::Cancel;
  377. } else if (!m && !clear && !S::AllowWithoutModifiers(k)) {
  378. if (k != Qt::Key_Escape) {
  379. // Intercept this KeyPress event.
  380. stopRecording();
  381. }
  382. return base::EventFilterResult::Cancel;
  383. }
  384. const auto r = [&] {
  385. auto result = int(k);
  386. if (m & Qt::ShiftModifier) {
  387. const auto keys = QKeyMapper::possibleKeys(key);
  388. for (const auto &possible : keys) {
  389. #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0)
  390. if (possible.keyboardModifiers() == m) {
  391. return int(possible.key());
  392. }
  393. #else // Qt >= 6.7.0
  394. if (possible > int(m)) {
  395. return possible - int(m);
  396. }
  397. #endif // Qt < 6.7.0
  398. }
  399. }
  400. return result;
  401. }();
  402. stopRecording(clear ? QKeySequence() : QKeySequence(r | m));
  403. return base::EventFilterResult::Cancel;
  404. } else if (type == QEvent::KeyPress && state->recording.current()) {
  405. if (!content->window()->isActiveWindow()) {
  406. return base::EventFilterResult::Continue;
  407. }
  408. if (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Escape) {
  409. stopRecording();
  410. return base::EventFilterResult::Cancel;
  411. }
  412. }
  413. return base::EventFilterResult::Continue;
  414. });
  415. const auto modifiedWrap = content->add(
  416. object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
  417. content,
  418. object_ptr<Ui::VerticalLayout>(content)));
  419. const auto modifiedInner = modifiedWrap->entity();
  420. AddDivider(modifiedInner);
  421. AddSkip(modifiedInner);
  422. const auto reset = modifiedInner->add(object_ptr<Ui::SettingsButton>(
  423. modifiedInner,
  424. tr::lng_shortcuts_reset(),
  425. st::settingsButtonNoIcon));
  426. reset->setClickedCallback([=] {
  427. stopRecording();
  428. for (auto &entry : state->entries) {
  429. if (entry.now != entry.original) {
  430. entry.now = entry.original;
  431. fill(entry);
  432. }
  433. }
  434. checkModified();
  435. S::ResetToDefaults();
  436. });
  437. AddSkip(modifiedInner);
  438. AddDivider(modifiedInner);
  439. modifiedWrap->toggleOn(state->modified.value());
  440. AddSkip(content);
  441. for (auto &entry : entries) {
  442. if (!entry.label) {
  443. AddSkip(content);
  444. AddDivider(content);
  445. AddSkip(content);
  446. continue;
  447. }
  448. entry.wrap = content->add(object_ptr<Ui::VerticalLayout>(content));
  449. fill(entry);
  450. }
  451. return [=] {
  452. };
  453. }
  454. } // namespace
  455. Shortcuts::Shortcuts(
  456. QWidget *parent,
  457. not_null<Window::SessionController*> controller)
  458. : Section(parent) {
  459. setupContent(controller);
  460. }
  461. Shortcuts::~Shortcuts() {
  462. if (!Core::Quitting()) {
  463. _save();
  464. }
  465. }
  466. rpl::producer<QString> Shortcuts::title() {
  467. return tr::lng_settings_shortcuts();
  468. }
  469. void Shortcuts::setupContent(
  470. not_null<Window::SessionController*> controller) {
  471. const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
  472. _save = SetupShortcutsContent(controller, content);
  473. Ui::ResizeFitChild(this, content);
  474. }
  475. } // namespace Settings