Save custom shortcuts to disk.

This commit is contained in:
John Preston 2025-02-07 14:46:51 +04:00
parent 86096db02d
commit 5ebdf3ed39
10 changed files with 172 additions and 71 deletions

View file

@ -1,6 +1,7 @@
// This is a list of your own shortcuts for Telegram Desktop // This is a list of your own shortcuts for Telegram Desktop
// You can see full list of commands in the 'shortcuts-default.json' file // You can see full list of commands in the 'shortcuts-default.json' file
// Place a null value instead of a command string to switch the shortcut off // Place a null value instead of a command string to switch the shortcut off
// You can also edit them in Settings > Chat Settings > Keyboard Shortcuts.
[ [
// { // {

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,004 B

View file

@ -643,7 +643,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_chat_corner_reaction" = "Reaction button on messages"; "lng_settings_chat_corner_reaction" = "Reaction button on messages";
"lng_settings_shortcuts" = "Keyboard shortcuts"; "lng_settings_shortcuts" = "Keyboard shortcuts";
"lng_shortcuts_reset" = "Reset to default"; "lng_shortcuts_reset" = "Reset to default";
"lng_shortcuts_recording" = "Recording...";
"lng_shortcuts_add_another" = "Add another";
"lng_shortcuts_close" = "Close the window"; "lng_shortcuts_close" = "Close the window";
"lng_shortcuts_lock" = "Lock the application"; "lng_shortcuts_lock" = "Lock the application";
"lng_shortcuts_minimize" = "Minimize the window"; "lng_shortcuts_minimize" = "Minimize the window";
@ -677,7 +681,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_shortcuts_archive_chat" = "Archive chat"; "lng_shortcuts_archive_chat" = "Archive chat";
"lng_shortcuts_media_fullscreen" = "Toggle video fullscreen"; "lng_shortcuts_media_fullscreen" = "Toggle video fullscreen";
"lng_shortcuts_show_chat_menu" = "Show chat menu"; "lng_shortcuts_show_chat_menu" = "Show chat menu";
"lng_shortcuts_recording" = "Recording...";
"lng_settings_chat_reactions_title" = "Quick Reaction"; "lng_settings_chat_reactions_title" = "Quick Reaction";
"lng_settings_chat_reactions_subtitle" = "Choose your favorite reaction"; "lng_settings_chat_reactions_subtitle" = "Choose your favorite reaction";

View file

@ -113,45 +113,15 @@ const auto CommandByName = base::flat_map<QString, Command>{
// //
}; };
const auto CommandNames = base::flat_map<Command, QString>{ const base::flat_map<Command, QString> &CommandNames() {
{ Command::Close , u"close_telegram"_q }, static const auto result = [&] {
{ Command::Lock , u"lock_telegram"_q }, auto result = base::flat_map<Command, QString>();
{ Command::Minimize , u"minimize_telegram"_q }, for (const auto &[name, command] : CommandByName) {
{ Command::Quit , u"quit_telegram"_q }, result.emplace(command, name);
}
{ Command::MediaPlay , u"media_play"_q }, return result;
{ Command::MediaPause , u"media_pause"_q }, }();
{ Command::MediaPlayPause , u"media_playpause"_q }, return result;
{ 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 },
{ Command::ShowChatMenu , u"show_chat_menu"_q },
}; };
[[maybe_unused]] constexpr auto kNoValue = { [[maybe_unused]] constexpr auto kNoValue = {
@ -176,18 +146,22 @@ public:
[[nodiscard]] const QStringList &errors() const; [[nodiscard]] const QStringList &errors() const;
[[nodiscard]] base::flat_map<QKeySequence, Command> keysDefaults() const; [[nodiscard]] auto keysDefaults() const
[[nodiscard]] base::flat_map<QKeySequence, Command> keysCurrents() const; -> base::flat_map<QKeySequence, base::flat_set<Command>>;
[[nodiscard]] auto keysCurrents() const
-> base::flat_map<QKeySequence, base::flat_set<Command>>;
void change( void change(
QKeySequence was, QKeySequence was,
QKeySequence now, QKeySequence now,
Command command, Command command,
std::optional<Command> restore); std::optional<Command> restore);
void resetToDefaults();
private: private:
void fillDefaults(); void fillDefaults();
void writeDefaultFile(); void writeDefaultFile();
void writeCustomFile();
bool readCustomFile(); bool readCustomFile();
void set(const QString &keys, Command command, bool replace = false); void set(const QString &keys, Command command, bool replace = false);
@ -204,7 +178,7 @@ private:
base::flat_multi_map<not_null<QObject*>, Command> _commandByObject; base::flat_multi_map<not_null<QObject*>, Command> _commandByObject;
std::vector<QPointer<QWidget>> _listened; std::vector<QPointer<QWidget>> _listened;
base::flat_map<QKeySequence, Command> _defaults; base::flat_map<QKeySequence, base::flat_set<Command>> _defaults;
base::flat_set<QAction*> _mediaShortcuts; base::flat_set<QAction*> _mediaShortcuts;
base::flat_set<QAction*> _supportShortcuts; base::flat_set<QAction*> _supportShortcuts;
@ -295,16 +269,19 @@ const QStringList &Manager::errors() const {
return _errors; return _errors;
} }
base::flat_map<QKeySequence, Command> Manager::keysDefaults() const { auto Manager::keysDefaults() const
-> base::flat_map<QKeySequence, base::flat_set<Command>> {
return _defaults; return _defaults;
} }
base::flat_map<QKeySequence, Command> Manager::keysCurrents() const { auto Manager::keysCurrents() const
auto result = base::flat_map<QKeySequence, Command>(); -> base::flat_map<QKeySequence, base::flat_set<Command>> {
auto result = base::flat_map<QKeySequence, base::flat_set<Command>>();
for (const auto &[keys, command] : _shortcuts) { for (const auto &[keys, command] : _shortcuts) {
const auto i = _commandByObject.findFirst(command); auto i = _commandByObject.findFirst(command);
if (i != _commandByObject.end()) { const auto end = _commandByObject.end();
result.emplace(keys, i->second); for (; i != end && (i->first == command); ++i) {
result[keys].emplace(i->second);
} }
} }
return result; return result;
@ -325,6 +302,19 @@ void Manager::change(
Assert(!was.isEmpty()); Assert(!was.isEmpty());
set(was, *restore, true); set(was, *restore, true);
} }
writeCustomFile();
}
void Manager::resetToDefaults() {
while (!_shortcuts.empty()) {
remove(_shortcuts.begin()->first);
}
for (const auto &[sequence, commands] : _defaults) {
for (const auto command : commands) {
set(sequence, command, false);
}
}
writeCustomFile();
} }
std::vector<Command> Manager::lookup(not_null<QObject*> object) const { std::vector<Command> Manager::lookup(not_null<QObject*> object) const {
@ -529,8 +519,8 @@ void Manager::writeDefaultFile() {
auto i = _commandByObject.findFirst(object); auto i = _commandByObject.findFirst(object);
const auto end = _commandByObject.end(); const auto end = _commandByObject.end();
for (; i != end && i->first == object; ++i) { for (; i != end && i->first == object; ++i) {
const auto j = CommandNames.find(i->second); const auto j = CommandNames().find(i->second);
if (j != CommandNames.end()) { if (j != CommandNames().end()) {
QJsonObject entry; QJsonObject entry;
entry.insert(u"keys"_q, sequence.toString().toLower()); entry.insert(u"keys"_q, sequence.toString().toLower());
entry.insert(u"command"_q, j->second); entry.insert(u"command"_q, j->second);
@ -551,6 +541,55 @@ void Manager::writeDefaultFile() {
} }
} }
auto document = QJsonDocument();
document.setArray(shortcuts);
file.write(document.toJson(QJsonDocument::Indented));
}
void Manager::writeCustomFile() {
auto shortcuts = QJsonArray();
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 d = _defaults.find(sequence);
if (d == _defaults.end() || !d->second.contains(i->second)) {
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);
}
}
}
}
for (const auto &[sequence, command] : _defaults) {
if (!_shortcuts.contains(sequence)) {
QJsonObject entry;
entry.insert(u"keys"_q, sequence.toString().toLower());
entry.insert(u"command"_q, QJsonValue());
shortcuts.append(entry);
}
}
if (shortcuts.isEmpty()) {
WriteDefaultCustomFile();
return;
}
auto file = QFile(CustomFilePath());
if (!file.open(QIODevice::WriteOnly)) {
LOG(("Shortcut Warning: could not write custom shortcuts file."));
return;
}
const char *customHeader = R"HEADER(
// This is a list of changed shortcuts for Telegram Desktop
// You can edit them in Settings > Chat Settings > Keyboard Shortcuts.
)HEADER";
file.write(customHeader);
auto document = QJsonDocument(); auto document = QJsonDocument();
document.setArray(shortcuts); document.setArray(shortcuts);
@ -632,7 +671,7 @@ void Manager::remove(const QKeySequence &keys) {
void Manager::unregister(base::unique_qptr<QAction> shortcut) { void Manager::unregister(base::unique_qptr<QAction> shortcut) {
if (shortcut) { if (shortcut) {
_commandByObject.erase(shortcut.get()); _commandByObject.removeAll(shortcut.get());
_mediaShortcuts.erase(shortcut.get()); _mediaShortcuts.erase(shortcut.get());
_supportShortcuts.erase(shortcut.get()); _supportShortcuts.erase(shortcut.get());
} }
@ -720,11 +759,13 @@ void Unpause() {
Paused = false; Paused = false;
} }
base::flat_map<QKeySequence, Command> KeysDefaults() { auto KeysDefaults()
-> base::flat_map<QKeySequence, base::flat_set<Command>> {
return Data.keysDefaults(); return Data.keysDefaults();
} }
base::flat_map<QKeySequence, Command> KeysCurrents() { auto KeysCurrents()
-> base::flat_map<QKeySequence, base::flat_set<Command>> {
return Data.keysCurrents(); return Data.keysCurrents();
} }
@ -736,6 +777,10 @@ void Change(
Data.change(was, now, command, restore); Data.change(was, now, command, restore);
} }
void ResetToDefaults() {
Data.resetToDefaults();
}
bool AllowWithoutModifiers(int key) { bool AllowWithoutModifiers(int key) {
const auto service = { const auto service = {
Qt::Key_Escape, Qt::Key_Escape,

View file

@ -142,14 +142,17 @@ void ToggleSupportShortcuts(bool toggled);
void Pause(); void Pause();
void Unpause(); void Unpause();
[[nodiscard]] base::flat_map<QKeySequence, Command> KeysDefaults(); [[nodiscard]] auto KeysDefaults()
[[nodiscard]] base::flat_map<QKeySequence, Command> KeysCurrents(); -> base::flat_map<QKeySequence, base::flat_set<Command>>;
[[nodiscard]] auto KeysCurrents()
-> base::flat_map<QKeySequence, base::flat_set<Command>>;
void Change( void Change(
QKeySequence was, QKeySequence was,
QKeySequence now, QKeySequence now,
Command command, Command command,
std::optional<Command> restore = {}); std::optional<Command> restore = {});
void ResetToDefaults();
[[nodiscard]] bool AllowWithoutModifiers(int key); [[nodiscard]] bool AllowWithoutModifiers(int key);

View file

@ -1009,7 +1009,7 @@ void SetupMessages(
inner, inner,
tr::lng_settings_shortcuts(), tr::lng_settings_shortcuts(),
st::settingsButton, st::settingsButton,
{ &st::menuIconBotCommands } { &st::menuIconShortcut }
)->addClickHandler([=] { )->addClickHandler([=] {
showOther(Shortcuts::Id()); showOther(Shortcuts::Id());
}); });

View file

@ -14,9 +14,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/text/text_utilities.h" #include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h" #include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h" #include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/slide_wrap.h" #include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h" #include "ui/wrap/vertical_layout.h"
#include "ui/vertical_list.h" #include "ui/vertical_list.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h" #include "styles/style_settings.h"
namespace Settings { namespace Settings {
@ -135,6 +137,7 @@ struct Labeled {
rpl::variable<bool> modified; rpl::variable<bool> modified;
rpl::variable<Button*> recording; rpl::variable<Button*> recording;
rpl::variable<QKeySequence> lastKey; rpl::variable<QKeySequence> lastKey;
Fn<void(S::Command command)> showMenuFor;
}; };
const auto state = content->lifetime().make_state<State>(); const auto state = content->lifetime().make_state<State>();
const auto labeled = Entries(); const auto labeled = Entries();
@ -144,17 +147,21 @@ struct Labeled {
return Entry{ labeled.command, std::move(labeled.label) }; return Entry{ labeled.command, std::move(labeled.label) };
}) | ranges::to_vector; }) | ranges::to_vector;
for (const auto &[keys, command] : defaults) { for (const auto &[keys, commands] : defaults) {
const auto i = ranges::find(entries, command, &Entry::command); for (const auto command : commands) {
if (i != end(entries)) { const auto i = ranges::find(entries, command, &Entry::command);
i->original.push_back(keys); if (i != end(entries)) {
i->original.push_back(keys);
}
} }
} }
for (const auto &[keys, command] : currents) { for (const auto &[keys, commands] : currents) {
const auto i = ranges::find(entries, command, &Entry::command); for (const auto command : commands) {
if (i != end(entries)) { const auto i = ranges::find(entries, command, &Entry::command);
i->now.push_back(keys); if (i != end(entries)) {
i->now.push_back(keys);
}
} }
} }
@ -173,6 +180,7 @@ struct Labeled {
}; };
checkModified(); checkModified();
const auto menu = std::make_shared<QPointer<Ui::PopupMenu>>();
const auto fill = [=](Entry &entry) { const auto fill = [=](Entry &entry) {
auto index = 0; auto index = 0;
if (entry.original.empty()) { if (entry.original.empty()) {
@ -184,6 +192,7 @@ struct Labeled {
for (const auto &now : entry.now) { for (const auto &now : entry.now) {
if (index < entry.buttons.size()) { if (index < entry.buttons.size()) {
entry.buttons[index]->key = now; entry.buttons[index]->key = now;
entry.buttons[index]->removed = false;
} else { } else {
auto button = std::make_unique<Button>(Button{ auto button = std::make_unique<Button>(Button{
.command = entry.command, .command = entry.command,
@ -239,10 +248,21 @@ struct Labeled {
}, keys->lifetime()); }, keys->lifetime());
keys->setAttribute(Qt::WA_TransparentForMouseEvents); keys->setAttribute(Qt::WA_TransparentForMouseEvents);
widget->setClickedCallback([=] { widget->setAcceptBoth(true);
S::Pause(); widget->clicks(
state->recording = raw; ) | rpl::start_with_next([=](Qt::MouseButton button) {
}); if (const auto strong = *menu) {
strong->hideMenu();
return;
}
if (button == Qt::RightButton) {
state->showMenuFor(raw->command);
} else {
S::Pause();
state->recording = raw;
}
}, widget->lifetime());
button->widget.reset(widget); button->widget.reset(widget);
entry.buttons.push_back(std::move(button)); entry.buttons.push_back(std::move(button));
} }
@ -252,6 +272,29 @@ struct Labeled {
entry.buttons.pop_back(); entry.buttons.pop_back();
} }
}; };
state->showMenuFor = [=](S::Command command) {
*menu = Ui::CreateChild<Ui::PopupMenu>(
content,
st::popupMenuWithIcons);
(*menu)->addAction(tr::lng_shortcuts_add_another(tr::now), [=] {
const auto i = ranges::find(
state->entries,
command,
&Entry::command);
if (i != end(state->entries)) {
S::Pause();
const auto j = ranges::find(i->now, QKeySequence());
if (j != end(i->now)) {
state->recording = i->buttons[j - begin(i->now)].get();
} else {
i->now.push_back(QKeySequence());
fill(*i);
state->recording = i->buttons.back().get();
}
}
}, &st::menuIconTopics);
(*menu)->popup(QCursor::pos());
};
const auto stopRecording = [=](std::optional<QKeySequence> result = {}) { const auto stopRecording = [=](std::optional<QKeySequence> result = {}) {
const auto button = state->recording.current(); const auto button = state->recording.current();
@ -268,10 +311,11 @@ struct Labeled {
auto was = button->key.current(); auto was = button->key.current();
const auto now = result.value_or(was); const auto now = result.value_or(was);
if (now == was) { if (now == was) {
if (!result || !button->removed.current()) { if (!now.isEmpty() && (!result || !button->removed.current())) {
return; return;
} }
was = QKeySequence(); was = QKeySequence();
button->removed = false;
} }
auto changed = false; auto changed = false;
@ -335,7 +379,10 @@ struct Labeled {
|| k == Qt::Key_Meta) { || k == Qt::Key_Meta) {
return base::EventFilterResult::Cancel; return base::EventFilterResult::Cancel;
} else if (!m && !clear && !S::AllowWithoutModifiers(k)) { } else if (!m && !clear && !S::AllowWithoutModifiers(k)) {
stopRecording(); if (k != Qt::Key_Escape) {
// Intercept this KeyPress event.
stopRecording();
}
return base::EventFilterResult::Cancel; return base::EventFilterResult::Cancel;
} }
stopRecording(clear ? QKeySequence() : QKeySequence(k | m)); stopRecording(clear ? QKeySequence() : QKeySequence(k | m));
@ -372,6 +419,7 @@ struct Labeled {
} }
} }
checkModified(); checkModified();
S::ResetToDefaults();
}); });
AddSkip(modifiedInner); AddSkip(modifiedInner);
AddDivider(modifiedInner); AddDivider(modifiedInner);

View file

@ -175,6 +175,7 @@ menuIconTradable: icon {{ "menu/tradable", menuIconColor }};
menuIconUnique: icon {{ "menu/unique", menuIconColor }}; menuIconUnique: icon {{ "menu/unique", menuIconColor }};
menuIconNftWear: icon {{ "menu/nft_wear", menuIconColor }}; menuIconNftWear: icon {{ "menu/nft_wear", menuIconColor }};
menuIconNftTakeOff: icon {{ "menu/nft_takeoff", menuIconColor }}; menuIconNftTakeOff: icon {{ "menu/nft_takeoff", menuIconColor }};
menuIconShortcut: icon {{ "menu/shortcut", menuIconColor }};
menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }}; menuIconTTLAny: icon {{ "menu/auto_delete_plain", menuIconColor }};
menuIconTTLAnyTextPosition: point(11px, 22px); menuIconTTLAnyTextPosition: point(11px, 22px);