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
// 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
// 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_shortcuts" = "Keyboard shortcuts";
"lng_shortcuts_reset" = "Reset to default";
"lng_shortcuts_recording" = "Recording...";
"lng_shortcuts_add_another" = "Add another";
"lng_shortcuts_close" = "Close the window";
"lng_shortcuts_lock" = "Lock the application";
"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_media_fullscreen" = "Toggle video fullscreen";
"lng_shortcuts_show_chat_menu" = "Show chat menu";
"lng_shortcuts_recording" = "Recording...";
"lng_settings_chat_reactions_title" = "Quick 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>{
{ Command::Close , u"close_telegram"_q },
{ Command::Lock , u"lock_telegram"_q },
{ Command::Minimize , u"minimize_telegram"_q },
{ Command::Quit , u"quit_telegram"_q },
{ Command::MediaPlay , u"media_play"_q },
{ Command::MediaPause , u"media_pause"_q },
{ Command::MediaPlayPause , u"media_playpause"_q },
{ 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 },
const base::flat_map<Command, QString> &CommandNames() {
static const auto result = [&] {
auto result = base::flat_map<Command, QString>();
for (const auto &[name, command] : CommandByName) {
result.emplace(command, name);
}
return result;
}();
return result;
};
[[maybe_unused]] constexpr auto kNoValue = {
@ -176,18 +146,22 @@ public:
[[nodiscard]] const QStringList &errors() const;
[[nodiscard]] base::flat_map<QKeySequence, Command> keysDefaults() const;
[[nodiscard]] base::flat_map<QKeySequence, Command> keysCurrents() const;
[[nodiscard]] auto keysDefaults() const
-> base::flat_map<QKeySequence, base::flat_set<Command>>;
[[nodiscard]] auto keysCurrents() const
-> base::flat_map<QKeySequence, base::flat_set<Command>>;
void change(
QKeySequence was,
QKeySequence now,
Command command,
std::optional<Command> restore);
void resetToDefaults();
private:
void fillDefaults();
void writeDefaultFile();
void writeCustomFile();
bool readCustomFile();
void set(const QString &keys, Command command, bool replace = false);
@ -204,7 +178,7 @@ private:
base::flat_multi_map<not_null<QObject*>, Command> _commandByObject;
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*> _supportShortcuts;
@ -295,16 +269,19 @@ const QStringList &Manager::errors() const {
return _errors;
}
base::flat_map<QKeySequence, Command> Manager::keysDefaults() const {
auto Manager::keysDefaults() const
-> base::flat_map<QKeySequence, base::flat_set<Command>> {
return _defaults;
}
base::flat_map<QKeySequence, Command> Manager::keysCurrents() const {
auto result = base::flat_map<QKeySequence, Command>();
auto Manager::keysCurrents() const
-> base::flat_map<QKeySequence, base::flat_set<Command>> {
auto result = base::flat_map<QKeySequence, base::flat_set<Command>>();
for (const auto &[keys, command] : _shortcuts) {
const auto i = _commandByObject.findFirst(command);
if (i != _commandByObject.end()) {
result.emplace(keys, i->second);
auto i = _commandByObject.findFirst(command);
const auto end = _commandByObject.end();
for (; i != end && (i->first == command); ++i) {
result[keys].emplace(i->second);
}
}
return result;
@ -325,6 +302,19 @@ void Manager::change(
Assert(!was.isEmpty());
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 {
@ -529,8 +519,8 @@ void Manager::writeDefaultFile() {
auto i = _commandByObject.findFirst(object);
const auto end = _commandByObject.end();
for (; i != end && i->first == object; ++i) {
const auto j = CommandNames.find(i->second);
if (j != CommandNames.end()) {
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);
@ -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();
document.setArray(shortcuts);
@ -632,7 +671,7 @@ void Manager::remove(const QKeySequence &keys) {
void Manager::unregister(base::unique_qptr<QAction> shortcut) {
if (shortcut) {
_commandByObject.erase(shortcut.get());
_commandByObject.removeAll(shortcut.get());
_mediaShortcuts.erase(shortcut.get());
_supportShortcuts.erase(shortcut.get());
}
@ -720,11 +759,13 @@ void Unpause() {
Paused = false;
}
base::flat_map<QKeySequence, Command> KeysDefaults() {
auto KeysDefaults()
-> base::flat_map<QKeySequence, base::flat_set<Command>> {
return Data.keysDefaults();
}
base::flat_map<QKeySequence, Command> KeysCurrents() {
auto KeysCurrents()
-> base::flat_map<QKeySequence, base::flat_set<Command>> {
return Data.keysCurrents();
}
@ -736,6 +777,10 @@ void Change(
Data.change(was, now, command, restore);
}
void ResetToDefaults() {
Data.resetToDefaults();
}
bool AllowWithoutModifiers(int key) {
const auto service = {
Qt::Key_Escape,

View file

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

View file

@ -1009,7 +1009,7 @@ void SetupMessages(
inner,
tr::lng_settings_shortcuts(),
st::settingsButton,
{ &st::menuIconBotCommands }
{ &st::menuIconShortcut }
)->addClickHandler([=] {
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/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/popup_menu.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/vertical_list.h"
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"
namespace Settings {
@ -135,6 +137,7 @@ struct Labeled {
rpl::variable<bool> modified;
rpl::variable<Button*> recording;
rpl::variable<QKeySequence> lastKey;
Fn<void(S::Command command)> showMenuFor;
};
const auto state = content->lifetime().make_state<State>();
const auto labeled = Entries();
@ -144,17 +147,21 @@ struct Labeled {
return Entry{ labeled.command, std::move(labeled.label) };
}) | ranges::to_vector;
for (const auto &[keys, command] : defaults) {
const auto i = ranges::find(entries, command, &Entry::command);
if (i != end(entries)) {
i->original.push_back(keys);
for (const auto &[keys, commands] : defaults) {
for (const auto command : commands) {
const auto i = ranges::find(entries, command, &Entry::command);
if (i != end(entries)) {
i->original.push_back(keys);
}
}
}
for (const auto &[keys, command] : currents) {
const auto i = ranges::find(entries, command, &Entry::command);
if (i != end(entries)) {
i->now.push_back(keys);
for (const auto &[keys, commands] : currents) {
for (const auto command : commands) {
const auto i = ranges::find(entries, command, &Entry::command);
if (i != end(entries)) {
i->now.push_back(keys);
}
}
}
@ -173,6 +180,7 @@ struct Labeled {
};
checkModified();
const auto menu = std::make_shared<QPointer<Ui::PopupMenu>>();
const auto fill = [=](Entry &entry) {
auto index = 0;
if (entry.original.empty()) {
@ -184,6 +192,7 @@ struct Labeled {
for (const auto &now : entry.now) {
if (index < entry.buttons.size()) {
entry.buttons[index]->key = now;
entry.buttons[index]->removed = false;
} else {
auto button = std::make_unique<Button>(Button{
.command = entry.command,
@ -239,10 +248,21 @@ struct Labeled {
}, keys->lifetime());
keys->setAttribute(Qt::WA_TransparentForMouseEvents);
widget->setClickedCallback([=] {
S::Pause();
state->recording = raw;
});
widget->setAcceptBoth(true);
widget->clicks(
) | 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);
entry.buttons.push_back(std::move(button));
}
@ -252,6 +272,29 @@ struct Labeled {
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 button = state->recording.current();
@ -268,10 +311,11 @@ struct Labeled {
auto was = button->key.current();
const auto now = result.value_or(was);
if (now == was) {
if (!result || !button->removed.current()) {
if (!now.isEmpty() && (!result || !button->removed.current())) {
return;
}
was = QKeySequence();
button->removed = false;
}
auto changed = false;
@ -335,7 +379,10 @@ struct Labeled {
|| k == Qt::Key_Meta) {
return base::EventFilterResult::Cancel;
} else if (!m && !clear && !S::AllowWithoutModifiers(k)) {
stopRecording();
if (k != Qt::Key_Escape) {
// Intercept this KeyPress event.
stopRecording();
}
return base::EventFilterResult::Cancel;
}
stopRecording(clear ? QKeySequence() : QKeySequence(k | m));
@ -372,6 +419,7 @@ struct Labeled {
}
}
checkModified();
S::ResetToDefaults();
});
AddSkip(modifiedInner);
AddDivider(modifiedInner);

View file

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