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