Section for shortcuts editing.

This commit is contained in:
John Preston 2025-02-06 18:22:58 +04:00
parent c82fbefcfc
commit 0585e72c35
7 changed files with 572 additions and 4 deletions

View file

@ -1474,6 +1474,8 @@ PRIVATE
settings/settings_privacy_security.h
settings/settings_scale_preview.cpp
settings/settings_scale_preview.h
settings/settings_shortcuts.cpp
settings/settings_shortcuts.h
settings/settings_type.h
settings/settings_websites.cpp
settings/settings_websites.h

View file

@ -642,6 +642,43 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
"lng_settings_chat_quick_action_react" = "Send reaction with double click";
"lng_settings_chat_corner_reaction" = "Reaction button on messages";
"lng_settings_shortcuts" = "Keyboard shortcuts";
"lng_shortcuts_reset" = "Reset to default";
"lng_shortcuts_close" = "Close the window";
"lng_shortcuts_lock" = "Lock the application";
"lng_shortcuts_minimize" = "Minimize the window";
"lng_shortcuts_quit" = "Quit the application";
"lng_shortcuts_media_play" = "Play the media";
"lng_shortcuts_media_pause" = "Pause the media";
"lng_shortcuts_media_play_pause" = "Toggle media playback";
"lng_shortcuts_media_stop" = "Stop media playback";
"lng_shortcuts_media_previous" = "Previous track";
"lng_shortcuts_media_next" = "Next track";
"lng_shortcuts_search" = "Search messages";
"lng_shortcuts_chat_previous" = "Previous chat";
"lng_shortcuts_chat_next" = "Next chat";
"lng_shortcuts_chat_first" = "First chat";
"lng_shortcuts_chat_last" = "Last chat";
"lng_shortcuts_chat_self" = "Saved Messages";
"lng_shortcuts_chat_pinned_n" = "Pinned chat #{index}";
"lng_shortcuts_show_account_n" = "Account #{index}";
"lng_shortcuts_show_all_chats" = "All Chats folder";
"lng_shortcuts_show_folder_n" = "Folder #{index}";
"lng_shortcuts_show_folder_last" = "Last folder";
"lng_shortcuts_folder_next" = "Next folder";
"lng_shortcuts_folder_previous" = "Previous folder";
"lng_shortcuts_scheduled" = "Scheduled messages";
"lng_shortcuts_archive" = "Archived chats";
"lng_shortcuts_contacts" = "Contacts list";
"lng_shortcuts_just_send" = "Just send";
"lng_shortcuts_silent_send" = "Silent send";
"lng_shortcuts_schedule" = "Schedule";
"lng_shortcuts_read_chat" = "Mark chat as read";
"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";
"lng_settings_chat_message_reply_from" = "Bob Harris";

View file

@ -28,6 +28,7 @@ namespace {
constexpr auto kCountLimit = 256; // How many shortcuts can be in json file.
rpl::event_stream<not_null<Request*>> RequestsStream;
bool Paused/* = false*/;
const auto AutoRepeatCommands = base::flat_set<Command>{
Command::MediaPrevious,
@ -175,6 +176,9 @@ public:
[[nodiscard]] const QStringList &errors() const;
[[nodiscard]] base::flat_map<QKeySequence, Command> keysDefaults() const;
[[nodiscard]] base::flat_map<QKeySequence, Command> keysCurrents() const;
private:
void fillDefaults();
void writeDefaultFile();
@ -189,6 +193,8 @@ private:
base::flat_map<QKeySequence, base::unique_qptr<QAction>> _shortcuts;
base::flat_multi_map<not_null<QObject*>, Command> _commandByObject;
base::flat_map<QKeySequence, Command> _defaults;
base::flat_set<QAction*> _mediaShortcuts;
base::flat_set<QAction*> _supportShortcuts;
@ -278,6 +284,21 @@ const QStringList &Manager::errors() const {
return _errors;
}
base::flat_map<QKeySequence, Command> Manager::keysDefaults() const {
return _defaults;
}
base::flat_map<QKeySequence, Command> Manager::keysCurrents() const {
auto result = base::flat_map<QKeySequence, Command>();
for (const auto &[keys, command] : _shortcuts) {
const auto i = _commandByObject.findFirst(command);
if (i != _commandByObject.end()) {
result.emplace(keys, i->second);
}
}
return result;
}
std::vector<Command> Manager::lookup(not_null<QObject*> object) const {
auto result = std::vector<Command>();
auto i = _commandByObject.findFirst(object);
@ -441,6 +462,8 @@ void Manager::fillDefaults() {
set(u"ctrl+r"_q, Command::ReadChat);
set(u"ctrl+\\"_q, Command::ShowChatMenu);
_defaults = keysCurrents();
}
void Manager::writeDefaultFile() {
@ -598,7 +621,9 @@ bool Launch(Command command) {
}
bool Launch(std::vector<Command> commands) {
if (auto handler = RequestHandler(std::move(commands))) {
if (Paused) {
return false;
} else if (auto handler = RequestHandler(std::move(commands))) {
return handler();
}
return false;
@ -630,6 +655,55 @@ void ToggleSupportShortcuts(bool toggled) {
Data.toggleSupport(toggled);
}
void Pause() {
Paused = true;
}
void Unpause() {
Paused = false;
}
base::flat_map<QKeySequence, Command> KeysDefaults() {
return Data.keysDefaults();
}
base::flat_map<QKeySequence, Command> KeysCurrents() {
return Data.keysCurrents();
}
bool AllowWithoutModifiers(int key) {
const auto service = {
Qt::Key_Escape,
Qt::Key_Tab,
Qt::Key_Backtab,
Qt::Key_Backspace,
Qt::Key_Return,
Qt::Key_Enter,
Qt::Key_Insert,
Qt::Key_Delete,
Qt::Key_Pause,
Qt::Key_Print,
Qt::Key_SysReq,
Qt::Key_Clear,
Qt::Key_Home,
Qt::Key_End,
Qt::Key_Left,
Qt::Key_Up,
Qt::Key_Right,
Qt::Key_Down,
Qt::Key_PageUp,
Qt::Key_PageDown,
Qt::Key_Shift,
Qt::Key_Control,
Qt::Key_Meta,
Qt::Key_Alt,
Qt::Key_CapsLock,
Qt::Key_NumLock,
Qt::Key_ScrollLock,
};
return (key >= 0x80) && !ranges::contains(service, key);
}
void Finish() {
Data.clear();
}

View file

@ -139,4 +139,11 @@ void ToggleMediaShortcuts(bool toggled);
// have some conflicts with default input shortcuts, like Ctrl+Delete.
void ToggleSupportShortcuts(bool toggled);
void Pause();
void Unpause();
[[nodiscard]] base::flat_map<QKeySequence, Command> KeysDefaults();
[[nodiscard]] base::flat_map<QKeySequence, Command> KeysCurrents();
[[nodiscard]] bool AllowWithoutModifiers(int key);
} // namespace Shortcuts

View file

@ -11,6 +11,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "settings/settings_advanced.h"
#include "settings/settings_privacy_security.h"
#include "settings/settings_experimental.h"
#include "settings/settings_shortcuts.h"
#include "boxes/abstract_box.h"
#include "boxes/peers/edit_peer_color_box.h"
#include "boxes/connection_box.h"
@ -833,7 +834,8 @@ void SetupStickersEmoji(
void SetupMessages(
not_null<Window::SessionController*> controller,
not_null<Ui::VerticalLayout*> container) {
not_null<Ui::VerticalLayout*> container,
Fn<void(Type)> showOther) {
Ui::AddDivider(container);
Ui::AddSkip(container);
@ -1003,7 +1005,16 @@ void SetupMessages(
Core::App().saveSettingsDelayed();
}, inner->lifetime());
Ui::AddSkip(inner, st::settingsCheckboxesSkip);
AddButtonWithIcon(
inner,
tr::lng_settings_shortcuts(),
st::settingsButton,
{ &st::menuIconBotCommands }
)->addClickHandler([=] {
showOther(Shortcuts::Id());
});
Ui::AddSkip(inner);
}
void SetupArchive(
@ -1793,7 +1804,7 @@ void Chat::setupContent(not_null<Window::SessionController*> controller) {
SetupCloudThemes(controller, content);
SetupChatBackground(controller, content);
SetupStickersEmoji(controller, content);
SetupMessages(controller, content);
SetupMessages(controller, content, showOtherMethod());
Ui::AddDivider(content);
SetupSensitiveContent(controller, content, std::move(updateOnTick));
SetupArchive(controller, content);

View file

@ -0,0 +1,406 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "settings/settings_shortcuts.h"
#include "base/event_filter.h"
#include "core/application.h"
#include "core/shortcuts.h"
#include "lang/lang_keys.h"
#include "ui/text/text_utilities.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/vertical_list.h"
#include "styles/style_settings.h"
namespace Settings {
namespace {
namespace S = ::Shortcuts;
struct Labeled {
S::Command command = {};
rpl::producer<QString> label;
};
[[nodiscard]] std::vector<Labeled> Entries() {
using C = S::Command;
const auto pinned = [](int index) {
return tr::lng_shortcuts_chat_pinned_n(
lt_index,
rpl::single(QString::number(index)));
};
const auto account = [](int index) {
return tr::lng_shortcuts_show_account_n(
lt_index,
rpl::single(QString::number(index)));
};
const auto folder = [](int index) {
return tr::lng_shortcuts_show_folder_n(
lt_index,
rpl::single(QString::number(index)));
};
const auto separator = Labeled{ C(), nullptr };
return {
{ C::Close, tr::lng_shortcuts_close() },
{ C::Lock, tr::lng_shortcuts_lock() },
{ C::Minimize, tr::lng_shortcuts_minimize() },
{ C::Quit, tr::lng_shortcuts_quit() },
separator,
{ C::Search, tr::lng_shortcuts_search() },
separator,
{ C::ChatPrevious, tr::lng_shortcuts_chat_previous() },
{ C::ChatNext, tr::lng_shortcuts_chat_next() },
{ C::ChatFirst, tr::lng_shortcuts_chat_first() },
{ C::ChatLast, tr::lng_shortcuts_chat_last() },
{ C::ChatSelf, tr::lng_shortcuts_chat_self() },
separator,
{ C::ChatPinned1, pinned(1) },
{ C::ChatPinned2, pinned(2) },
{ C::ChatPinned3, pinned(3) },
{ C::ChatPinned4, pinned(4) },
{ C::ChatPinned5, pinned(5) },
{ C::ChatPinned6, pinned(6) },
{ C::ChatPinned7, pinned(7) },
{ C::ChatPinned8, pinned(8) },
separator,
{ C::ShowAccount1, account(1) },
{ C::ShowAccount2, account(2) },
{ C::ShowAccount3, account(3) },
{ C::ShowAccount4, account(4) },
{ C::ShowAccount5, account(5) },
{ C::ShowAccount6, account(6) },
separator,
{ C::ShowAllChats, tr::lng_shortcuts_show_all_chats() },
{ C::ShowFolder1, folder(1) },
{ C::ShowFolder2, folder(2) },
{ C::ShowFolder3, folder(3) },
{ C::ShowFolder4, folder(4) },
{ C::ShowFolder5, folder(5) },
{ C::ShowFolder6, folder(6) },
{ C::ShowFolderLast, tr::lng_shortcuts_show_folder_last() },
{ C::FolderNext, tr::lng_shortcuts_folder_next() },
{ C::FolderPrevious, tr::lng_shortcuts_folder_previous() },
{ C::ShowArchive, tr::lng_shortcuts_archive() },
{ C::ShowContacts, tr::lng_shortcuts_contacts() },
separator,
{ C::ReadChat, tr::lng_shortcuts_read_chat() },
{ C::ArchiveChat, tr::lng_shortcuts_archive_chat() },
{ C::ShowScheduled, tr::lng_shortcuts_scheduled() },
{ C::ShowChatMenu, tr::lng_shortcuts_show_chat_menu() },
separator,
{ C::JustSendMessage, tr::lng_shortcuts_just_send() },
{ C::SendSilentMessage, tr::lng_shortcuts_silent_send() },
{ C::ScheduleMessage, tr::lng_shortcuts_schedule() },
separator,
{ C::MediaViewerFullscreen, tr::lng_shortcuts_media_fullscreen() },
separator,
{ C::MediaPlay, tr::lng_shortcuts_media_play() },
{ C::MediaPause, tr::lng_shortcuts_media_pause() },
{ C::MediaPlayPause, tr::lng_shortcuts_media_play_pause() },
{ C::MediaStop, tr::lng_shortcuts_media_stop() },
{ C::MediaPrevious, tr::lng_shortcuts_media_previous() },
{ C::MediaNext, tr::lng_shortcuts_media_next() },
};
}
[[nodiscard]] Fn<void()> SetupShortcutsContent(
not_null<Window::SessionController*> controller,
not_null<Ui::VerticalLayout*> content) {
const auto &defaults = S::KeysDefaults();
const auto &currents = S::KeysCurrents();
struct Button {
S::Command command;
std::unique_ptr<Ui::SettingsButton> widget;
rpl::variable<QKeySequence> key;
rpl::variable<bool> removed;
};
struct Entry {
S::Command command;
rpl::producer<QString> label;
std::vector<QKeySequence> original;
std::vector<QKeySequence> now;
Ui::VerticalLayout *wrap = nullptr;
std::vector<std::unique_ptr<Button>> buttons;
};
struct State {
std::vector<Entry> entries;
rpl::variable<bool> modified;
rpl::variable<Button*> recording;
rpl::variable<QKeySequence> lastKey;
};
const auto state = content->lifetime().make_state<State>();
const auto labeled = Entries();
auto &entries = state->entries = ranges::views::all(
labeled
) | ranges::views::transform([](Labeled 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, command] : currents) {
const auto i = ranges::find(entries, command, &Entry::command);
if (i != end(entries)) {
i->now.push_back(keys);
}
}
const auto checkModified = [=] {
for (const auto &entry : state->entries) {
auto original = entry.original;
auto now = entry.now;
ranges::sort(original);
ranges::sort(now);
if (original != now) {
state->modified = true;
return;
}
}
state->modified = false;
};
checkModified();
const auto fill = [=](Entry &entry) {
auto index = 0;
if (entry.original.empty()) {
entry.original.push_back(QKeySequence());
}
if (entry.now.empty()) {
entry.now.push_back(QKeySequence());
}
for (const auto &now : entry.now) {
if (index < entry.buttons.size()) {
entry.buttons[index]->key = now;
} else {
auto button = std::make_unique<Button>(Button{
.command = entry.command,
.key = now,
});
const auto raw = button.get();
const auto widget = entry.wrap->add(
object_ptr<Ui::SettingsButton>(
entry.wrap,
rpl::duplicate(entry.label),
st::settingsButtonNoIcon));
const auto keys = Ui::CreateChild<Ui::FlatLabel>(
widget,
st::settingsButtonNoIcon.rightLabel);
keys->show();
rpl::combine(
widget->widthValue(),
rpl::duplicate(entry.label),
button->key.value(),
state->recording.value(),
button->removed.value()
) | rpl::start_with_next([=](
int width,
const QString &button,
const QKeySequence &key,
Button *recording,
bool removed) {
const auto &st = st::settingsButtonNoIcon;
const auto available = width
- st.padding.left()
- st.padding.right()
- st.style.font->width(button)
- st::settingsButtonRightSkip;
keys->setMarkedText((recording == raw)
? Ui::Text::Italic(
tr::lng_shortcuts_recording(tr::now))
: key.isEmpty()
? TextWithEntities()
: removed
? Ui::Text::Wrapped(
TextWithEntities{ key.toString() },
EntityType::StrikeOut)
: TextWithEntities{ key.toString() });
keys->setTextColorOverride(removed
? st::attentionButtonFg->c
: (recording == raw)
? st::boxTextFgGood->c
: std::optional<QColor>());
keys->resizeToNaturalWidth(available);
keys->moveToRight(
st::settingsButtonRightSkip,
st.padding.top());
}, keys->lifetime());
keys->setAttribute(Qt::WA_TransparentForMouseEvents);
widget->setClickedCallback([=] {
S::Pause();
state->recording = raw;
});
button->widget.reset(widget);
entry.buttons.push_back(std::move(button));
}
++index;
}
while (entry.wrap->count() > index) {
entry.buttons.pop_back();
}
};
const auto stopRecording = [=](std::optional<QKeySequence> result = {}) {
const auto button = state->recording.current();
if (!button) {
return;
}
state->recording = nullptr;
if (result) {
auto was = button->key.current();
const auto now = *result;
for (auto &entry : state->entries) {
const auto i = ranges::find(
entry.buttons,
button,
&std::unique_ptr<Button>::get);
if (i != end(entry.buttons)) {
const auto index = i - begin(entry.buttons);
if (now.isEmpty()) {
entry.now.erase(begin(entry.now) + index);
} else {
const auto i = ranges::find(entry.now, now);
if (i == end(entry.now)) {
entry.now[index] = now;
} else if (i != begin(entry.now) + index) {
std::swap(entry.now[index], *i);
entry.now.erase(i);
}
}
fill(entry);
checkModified();
} else if (now != was) {
const auto i = now.isEmpty()
? end(entry.now)
: ranges::find(entry.now, now);
if (i != end(entry.now)) {
entry.buttons[i - begin(entry.now)]->removed = true;
}
const auto j = was.isEmpty()
? end(entry.now)
: ranges::find(entry.now, was);
if (j != end(entry.now)) {
entry.buttons[j - begin(entry.now)]->removed = false;
was = QKeySequence();
}
}
}
}
InvokeQueued(content, [=] {
InvokeQueued(content, [=] {
// Let all the shortcut events propagate first.
S::Unpause();
});
});
};
base::install_event_filter(content, qApp, [=](not_null<QEvent*> e) {
const auto type = e->type();
if (type == QEvent::ShortcutOverride && state->recording.current()) {
const auto key = static_cast<QKeyEvent*>(e.get());
const auto m = key->modifiers();
const auto k = key->key();
const auto clear = !m
&& (k == Qt::Key_Backspace || k == Qt::Key_Delete);
if (k == Qt::Key_Control
|| k == Qt::Key_Shift
|| k == Qt::Key_Alt
|| k == Qt::Key_Meta) {
return base::EventFilterResult::Cancel;
} else if (!m && !clear && !S::AllowWithoutModifiers(k)) {
stopRecording();
return base::EventFilterResult::Cancel;
}
stopRecording(clear ? QKeySequence() : QKeySequence(k | m));
return base::EventFilterResult::Cancel;
} else if (type == QEvent::KeyPress) {
if (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Escape) {
stopRecording();
return base::EventFilterResult::Cancel;
}
}
return base::EventFilterResult::Continue;
});
const auto modifiedWrap = content->add(
object_ptr<Ui::SlideWrap<Ui::VerticalLayout>>(
content,
object_ptr<Ui::VerticalLayout>(content)));
const auto modifiedInner = modifiedWrap->entity();
AddDivider(modifiedInner);
AddSkip(modifiedInner);
const auto reset = modifiedInner->add(object_ptr<Ui::SettingsButton>(
modifiedInner,
tr::lng_shortcuts_reset(),
st::settingsButtonNoIcon));
reset->setClickedCallback([=] {
stopRecording();
for (auto &entry : state->entries) {
if (entry.now != entry.original) {
entry.now = entry.original;
fill(entry);
}
}
checkModified();
});
AddSkip(modifiedInner);
AddDivider(modifiedInner);
modifiedWrap->toggleOn(state->modified.value());
AddSkip(content);
for (auto &entry : entries) {
if (!entry.label) {
AddSkip(content);
AddDivider(content);
AddSkip(content);
continue;
}
entry.wrap = content->add(object_ptr<Ui::VerticalLayout>(content));
fill(entry);
}
return [=] {
};
}
} // namespace
Shortcuts::Shortcuts(
QWidget *parent,
not_null<Window::SessionController*> controller)
: Section(parent) {
setupContent(controller);
}
Shortcuts::~Shortcuts() {
if (!Core::Quitting()) {
_save();
}
}
rpl::producer<QString> Shortcuts::title() {
return tr::lng_settings_shortcuts();
}
void Shortcuts::setupContent(
not_null<Window::SessionController*> controller) {
const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
_save = SetupShortcutsContent(controller, content);
Ui::ResizeFitChild(this, content);
}
} // namespace Settings

View file

@ -0,0 +1,31 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "settings/settings_common_session.h"
namespace Settings {
class Shortcuts : public Section<Shortcuts> {
public:
Shortcuts(
QWidget *parent,
not_null<Window::SessionController*> controller);
~Shortcuts();
[[nodiscard]] rpl::producer<QString> title() override;
private:
void setupContent(not_null<Window::SessionController*> controller);
Fn<void()> _save;
};
} // namespace Settings