2
0
mirror of https://github.com/telegramdesktop/tdesktop synced 2025-08-30 22:16:14 +00:00

First attempt of Ctrl+Tab/Ctrl+Shift+Tab UI.

This commit is contained in:
John Preston
2025-05-02 17:34:10 +04:00
parent a3cdae1e94
commit 0d8065fc1f
11 changed files with 498 additions and 11 deletions

View File

@@ -1663,6 +1663,8 @@ PRIVATE
window/window_adaptive.h
window/window_chat_preview.cpp
window/window_chat_preview.h
window/window_chat_switch_process.cpp
window/window_chat_switch_process.h
window/window_connecting_widget.cpp
window/window_connecting_widget.h
window/window_controller.cpp

View File

@@ -656,8 +656,20 @@ bool Application::eventFilter(QObject *object, QEvent *e) {
updateNonIdle();
} break;
case QEvent::KeyRelease: {
const auto event = static_cast<QKeyEvent*>(e);
if (Shortcuts::HandlePossibleChatSwitch(event)) {
return true;
}
} break;
case QEvent::ShortcutOverride: {
// handle shortcuts ourselves
// Ctrl+Tab/Ctrl+Shift+Tab chat switch is a special shortcut case,
// because it not only does an action on the shortcut activation,
// but also keeps the UI visible until you release the Ctrl key.
Shortcuts::HandlePossibleChatSwitch(static_cast<QKeyEvent*>(e));
// Handle all the shortcut management manually.
return true;
} break;

View File

@@ -30,6 +30,10 @@ constexpr auto kCountLimit = 256; // How many shortcuts can be in json file.
rpl::event_stream<not_null<Request*>> RequestsStream;
bool Paused/* = false*/;
Qt::Key ChatSwitchModifier/* = Qt::Key()*/;
bool ChatSwitchStarted/* = false*/;
rpl::event_stream<ChatSwitchRequest> ChatSwitchStream;
const auto AutoRepeatCommands = base::flat_set<Command>{
Command::MediaPrevious,
Command::MediaNext,
@@ -156,6 +160,7 @@ public:
void toggleMedia(bool toggled);
void toggleSupport(bool toggled);
void listen(not_null<QWidget*> widget);
[[nodiscard]] bool handles(const QKeySequence &sequence) const;
[[nodiscard]] const QStringList &errors() const;
@@ -361,6 +366,10 @@ void Manager::listen(not_null<QWidget*> widget) {
}
}
bool Manager::handles(const QKeySequence &sequence) const {
return _shortcuts.contains(sequence);
}
void Manager::pruneListened() {
for (auto i = begin(_listened); i != end(_listened);) {
if (i->data()) {
@@ -470,10 +479,6 @@ void Manager::fillDefaults() {
set(u"ctrl+pgup"_q, Command::ChatPrevious);
set(u"alt+up"_q, Command::ChatPrevious);
set(u"%1+tab"_q.arg(ctrl), Command::ChatNext);
set(u"%1+shift+tab"_q.arg(ctrl), Command::ChatPrevious);
set(u"%1+backtab"_q.arg(ctrl), Command::ChatPrevious);
set(u"ctrl+alt+home"_q, Command::ChatFirst);
set(u"ctrl+alt+end"_q, Command::ChatLast);
@@ -800,6 +805,72 @@ bool HandleEvent(
return Launch(Data.lookup(object));
}
void CancelChatSwitch(Qt::Key result) {
ChatSwitchModifier = Qt::Key();
if (ChatSwitchStarted) {
ChatSwitchStarted = false;
ChatSwitchStream.fire({ .action = result });
}
}
rpl::producer<ChatSwitchRequest> ChatSwitchRequests() {
return ChatSwitchStream.events();
}
bool HandlePossibleChatSwitch(not_null<QKeyEvent*> event) {
const auto type = event->type();
if (Paused) {
return false;
} else if (type == QEvent::ShortcutOverride) {
const auto key = Qt::Key(event->key());
if (key == Qt::Key_Escape) {
CancelChatSwitch(Qt::Key_Escape);
return false;
} else if (key == Qt::Key_Return || key == Qt::Key_Enter) {
CancelChatSwitch(Qt::Key_Enter);
return false;
}
const auto ctrl = Platform::IsMac()
? Qt::MetaModifier
: Qt::ControlModifier;
if (Data.handles(ctrl | Qt::ShiftModifier | Qt::Key_Tab)
&& Data.handles(QKeySequence(ctrl | Qt::Key_Tab))
&& Data.handles(QKeySequence(ctrl | Qt::Key_Backtab))) {
return false;
} else if (key == Qt::Key_Control || key == Qt::Key_Meta) {
ChatSwitchModifier = key;
} else if (key == Qt::Key_Tab || key == Qt::Key_Backtab) {
const auto modifiers = event->modifiers();
if (modifiers & ctrl) {
if (Data.handles(modifiers | key)) {
return false;
}
if (ChatSwitchModifier == Qt::Key()) {
ChatSwitchModifier = Platform::IsMac()
? Qt::Key_Meta
: Qt::Key_Control;
}
const auto action = (modifiers & Qt::ShiftModifier)
? Qt::Key_Backtab
: key;
const auto started = !std::exchange(ChatSwitchStarted, true);
ChatSwitchStream.fire({
.action = action,
.started = started,
});
return true;
}
}
} else if (type == QEvent::KeyRelease) {
const auto key = Qt::Key(event->key());
if (key == ChatSwitchModifier) {
CancelChatSwitch(Qt::Key_Enter);
}
}
return false;
}
void ToggleMediaShortcuts(bool toggled) {
Data.toggleMedia(toggled);
}

View File

@@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
class QKeyEvent;
class QShortcutEvent;
namespace Shortcuts {
enum class Command {
@@ -122,7 +125,7 @@ private:
};
rpl::producer<not_null<Request*>> Requests();
[[nodiscard]] rpl::producer<not_null<Request*>> Requests();
void Start();
void Finish();
@@ -132,7 +135,15 @@ void Listen(not_null<QWidget*> widget);
bool Launch(Command command);
bool HandleEvent(not_null<QObject*> object, not_null<QShortcutEvent*> event);
const QStringList &Errors();
bool HandlePossibleChatSwitch(not_null<QKeyEvent*> event);
struct ChatSwitchRequest {
Qt::Key action = Qt::Key_Tab; // Key_Tab, Key_Backtab or Key_Escape.
bool started = false;
};
[[nodiscard]] rpl::producer<ChatSwitchRequest> ChatSwitchRequests();
[[nodiscard]] const QStringList &Errors();
// Media shortcuts are not enabled by default, because other
// applications also use them. They are enabled only when

View File

@@ -7,6 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "data/components/recent_peers.h"
#include "data/data_peer.h"
#include "data/data_session.h"
#include "history/history.h"
#include "main/main_session.h"
#include "storage/serialize_common.h"
#include "storage/serialize_peer.h"
@@ -133,4 +136,28 @@ void RecentPeers::applyLocal(QByteArray serialized) {
("Suggestions: RecentPeers read OK, count: %1").arg(_list.size()));
}
std::vector<not_null<Thread*>> RecentPeers::collectChatOpenHistory() const {
_session->local().readSearchSuggestions();
auto result = _opens;
result.reserve(result.size() + _list.size());
for (const auto &peer : _list) {
const auto history = peer->owner().history(peer);
const auto thread = not_null<Data::Thread*>(history);
if (!ranges::contains(result, thread)) {
result.push_back(thread);
}
}
return result;
}
void RecentPeers::chatOpenPush(not_null<Thread*> thread) {
const auto i = ranges::find(_opens, thread);
if (i == end(_opens)) {
_opens.insert(begin(_opens), thread);
} else if (i != begin(_opens)) {
ranges::rotate(begin(_opens), i, i + 1);
}
}
} // namespace Data

View File

@@ -13,6 +13,8 @@ class Session;
namespace Data {
class Thread;
class RecentPeers final {
public:
explicit RecentPeers(not_null<Main::Session*> session);
@@ -28,10 +30,16 @@ public:
[[nodiscard]] QByteArray serialize() const;
void applyLocal(QByteArray serialized);
[[nodiscard]] auto collectChatOpenHistory() const
-> std::vector<not_null<Thread*>>;
void chatOpenPush(not_null<Thread*> thread);
private:
const not_null<Main::Session*> _session;
std::vector<not_null<PeerData*>> _list;
std::vector<not_null<Thread*>> _opens;
rpl::event_stream<> _updates;
};

View File

@@ -331,6 +331,14 @@ ivHeightDefault: 800px;
maxWidthSharedMediaWindow: 419px;
chatSwitchMargins: margins(16px, 16px, 16px, 16px);
chatSwitchPadding: margins(12px, 12px, 12px, 12px);
chatSwitchSize: size(72px, 72px);
chatSwitchUserpic: UserpicButton(defaultUserpicButton) {
size: size(56px, 56px);
photoSize: 56px;
}
// Windows specific
winQuitIcon: icon {{ "win_quit", windowFg }};

View File

@@ -0,0 +1,225 @@
/*
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 "window/window_chat_switch_process.h"
#include "core/shortcuts.h"
#include "data/components/recent_peers.h"
#include "data/data_thread.h"
#include "main/main_session.h"
#include "ui/widgets/shadow.h"
#include "ui/controls/userpic_button.h"
#include "ui/rp_widget.h"
#include "styles/style_layers.h"
#include "styles/style_window.h"
namespace Window {
ChatSwitchProcess::ChatSwitchProcess(
not_null<Ui::RpWidget*> geometry,
not_null<Main::Session*> session,
Data::Thread *opened)
: _session(session)
, _widget(std::make_unique<Ui::RpWidget>(
geometry->parentWidget() ? geometry->parentWidget() : geometry))
, _view(Ui::CreateChild<Ui::RpWidget>(_widget.get()))\
, _bg(st::boxRadius, st::boxBg)
, _over(st::boxRadius, st::windowBgOver) {
setupWidget(geometry);
setupContent(opened);
setupView();
}
ChatSwitchProcess::~ChatSwitchProcess() = default;
rpl::producer<not_null<Data::Thread*>> ChatSwitchProcess::chosen() const {
return _chosen.events();
}
rpl::producer<> ChatSwitchProcess::closeRequests() const {
return _closeRequests.events();
}
void ChatSwitchProcess::process(const Request &request) {
Expects(_selected < int(_list.size()));
const auto count = int(_list.size());
if (request.action == Qt::Key_Escape) {
_closeRequests.fire({});
} else if (request.action == Qt::Key_Enter) {
if (_selected >= 0) {
_chosen.fire_copy(_list[_selected]);
} else {
_closeRequests.fire({});
}
} else if (request.action == Qt::Key_Tab) {
if (_selected < 0 || _selected + 1 >= count) {
setSelected(0);
} else {
setSelected(_selected + 1);
}
} else if (request.action == Qt::Key_Backtab) {
if (_selected <= 0) {
setSelected(count - 1);
} else {
setSelected(_selected - 1);
}
}
}
void ChatSwitchProcess::setSelected(int index) {
if (_selected == index || _list.empty()) {
return;
}
if (_selected >= 0) {
_entries[_selected].button->update();
}
_selected = index;
if (_selected >= 0) {
_entries[_selected].button->update();
}
}
void ChatSwitchProcess::setupWidget(not_null<Ui::RpWidget*> geometry) {
geometry->geometryValue(
) | rpl::start_with_next([=](QRect value) {
const auto parent = geometry->parentWidget();
_widget->setGeometry((parent == _widget->parentWidget())
? value
: QRect(QPoint(), value.size()));
}, _widget->lifetime());
_widget->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
_closeRequests.fire({});
}
}, _widget->lifetime());
_widget->show();
}
void ChatSwitchProcess::setupContent(Data::Thread *opened) {
_list = _session->recentPeers().collectChatOpenHistory();
if (opened) {
const auto i = ranges::find(_list, not_null(opened));
if (i == end(_list)) {
_list.insert(begin(_list), opened);
} else if (i != begin(_list)) {
ranges::rotate(begin(_list), i, i + 1);
}
_selected = 0;
}
auto index = 0;
for (const auto &thread : _list) {
const auto button = Ui::CreateChild<Ui::AbstractButton>(_view.get());
button->resize(st::chatSwitchSize);
button->paintRequest() | rpl::start_with_next([=] {
if (index == _selected) {
auto p = QPainter(button);
_over.paint(p, button->rect());
}
}, button->lifetime());
button->setClickedCallback([=] {
_chosen.fire_copy(thread);
});
button->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseMove) {
setSelected(index);
}
}, button->lifetime());
button->show();
const auto userpic = Ui::CreateChild<Ui::UserpicButton>(
button,
thread->peer(),
st::chatSwitchUserpic);
userpic->show();
userpic->move(
((button->width() - userpic->width()) / 2),
((button->height() - userpic->height()) / 2));
userpic->setAttribute(Qt::WA_TransparentForMouseEvents);
_entries.push_back({ .button = button });
++index;
}
}
void ChatSwitchProcess::setupView() {
_widget->sizeValue() | rpl::start_with_next([=](QSize size) {
layout(size);
}, _view->lifetime());
_view->show();
_view->paintRequest() | rpl::start_with_next([=](QRect clip) {
if (_outer.isEmpty()) {
return;
}
auto p = QPainter(_view);
p.translate(-_shadowed.topLeft());
Ui::Shadow::paint(p, _outer, _view->width(), st::boxRoundShadow);
_bg.paint(p, _outer);
}, _view->lifetime());
_view->events() | rpl::start_with_next([=](not_null<QEvent*> e) {
if (e->type() == QEvent::MouseButtonPress) {
e->accept();
}
}, _view->lifetime());
}
void ChatSwitchProcess::layout(QSize size) {
const auto full = QRect(QPoint(), size);
const auto outer = full.marginsRemoved(st::chatSwitchMargins);
auto inner = outer.marginsRemoved(st::chatSwitchPadding);
const auto available = inner.width();
const auto canPerRow = (available / st::chatSwitchSize.width());
if (canPerRow < 1 || _list.empty()) {
return;
}
const auto count = int(_list.size());
const auto rows = (count + canPerRow - 1) / canPerRow;
const auto minPerRow = count / rows;
const auto wideRows = (count - (minPerRow * rows));
const auto maxPerRow = wideRows ? (minPerRow + 1) : minPerRow;
const auto narrowShift = wideRows ? (st::chatSwitchSize.width() / 2) : 0;
const auto width = maxPerRow * st::chatSwitchSize.width();
const auto height = rows * st::chatSwitchSize.height();
size = QSize(width, height);
_inner = QRect(
(full.width() - width) / 2,
(full.height() - height) / 2,
width,
height);
_outer = _inner.marginsAdded(st::chatSwitchPadding);
const auto padding = st::boxRoundShadow.extend + st::chatSwitchPadding;
auto index = 0;
auto top = padding.top();
for (auto row = 0; row != rows; ++row) {
const auto columns = (row < wideRows) ? maxPerRow : minPerRow;
auto left = padding.left() + ((row < wideRows) ? 0 : narrowShift);
for (auto column = 0; column != columns; ++column) {
auto &entry = _entries[index++];
entry.button->moveToLeft(left, top, _inner.width());
left += st::chatSwitchSize.width();
}
top += st::chatSwitchSize.height();
}
_shadowed = _outer.marginsAdded(st::boxRoundShadow.extend);
_view->setGeometry(_shadowed);
}
rpl::lifetime &ChatSwitchProcess::lifetime() {
return _lifetime;
}
} // namespace Window

View File

@@ -0,0 +1,82 @@
/*
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 "ui/round_rect.h"
namespace Data {
class Thread;
} // namespace Data
namespace Main {
class Session;
} // namespace Main
namespace Shortcuts {
struct ChatSwitchRequest;
} // namespace Shortcuts
namespace Ui {
class AbstractButton;
class RpWidget;
} // namespace Ui
namespace Window {
class ChatSwitchProcess final {
public:
// Create widget in geometry->parentWidget() and geometry->geometry().
ChatSwitchProcess(
not_null<Ui::RpWidget*> geometry,
not_null<Main::Session*> session,
Data::Thread *opened);
~ChatSwitchProcess();
[[nodiscard]] rpl::producer<not_null<Data::Thread*>> chosen() const;
[[nodiscard]] rpl::producer<> closeRequests() const;
using Request = Shortcuts::ChatSwitchRequest;
void process(const Request &request);
[[nodiscard]] rpl::lifetime &lifetime();
private:
struct Entry {
not_null<Ui::AbstractButton*> button;
};
void setupWidget(not_null<Ui::RpWidget*> geometry);
void setupContent(Data::Thread *opened);
void setupView();
void layout(QSize size);
void setSelected(int index);
const not_null<Main::Session*> _session;
const std::unique_ptr<Ui::RpWidget> _widget;
const not_null<Ui::RpWidget*> _view;
QRect _shadowed;
QRect _outer;
QRect _inner;
Ui::RoundRect _bg;
Ui::RoundRect _over;
std::vector<not_null<Data::Thread*>> _list;
std::vector<Entry> _entries;
int _selected = -1;
rpl::event_stream<not_null<Data::Thread*>> _chosen;
rpl::event_stream<> _closeRequests;
rpl::lifetime _lifetime;
};
} // namespace Window

View File

@@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "boxes/peers/replace_boost_box.h"
#include "boxes/delete_messages_box.h"
#include "window/window_chat_preview.h"
#include "window/window_chat_switch_process.h"
#include "window/window_controller.h"
#include "window/window_filters_menu.h"
#include "window/window_separate_id.h"
@@ -32,6 +33,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_subsection_tabs.h"
#include "media/player/media_player_instance.h"
#include "media/view/media_view_open_common.h"
#include "data/components/recent_peers.h"
#include "data/stickers/data_custom_emoji.h"
#include "data/data_document_resolver.h"
#include "data/data_download_manager.h"
@@ -1735,18 +1737,50 @@ void SessionController::init() {
}
void SessionController::setupShortcuts() {
Shortcuts::Requests(
using namespace Shortcuts;
ChatSwitchRequests(
) | rpl::filter([=](const ChatSwitchRequest &request) {
return !window().locked()
&& (_chatSwitchProcess
|| (request.started
&& (Core::App().activeWindow() == &window())));
}) | rpl::start_with_next([=](const ChatSwitchRequest &request) {
if (!_chatSwitchProcess) {
_chatSwitchProcess = std::make_unique<ChatSwitchProcess>(
widget()->bodyWidget(),
&session(),
activeChatCurrent().thread());
const auto close = [this, raw = _chatSwitchProcess.get()] {
if (_chatSwitchProcess.get() == raw) {
base::take(_chatSwitchProcess);
}
};
_chatSwitchProcess->chosen(
) | rpl::start_with_next([=](not_null<Data::Thread*> thread) {
close();
jumpToChatListEntry({ Dialogs::Key(thread), FullMsgId() });
}, _chatSwitchProcess->lifetime());
_chatSwitchProcess->closeRequests(
) | rpl::start_with_next(close, _chatSwitchProcess->lifetime());
}
_chatSwitchProcess->process(request);
}, _lifetime);
Requests(
) | rpl::filter([=] {
return (Core::App().activeWindow() == &window())
&& !isLayerShown()
&& !window().locked();
}) | rpl::start_with_next([=](not_null<Shortcuts::Request*> request) {
using C = Shortcuts::Command;
}) | rpl::start_with_next([=](not_null<Request*> request) {
using C = Command;
const auto app = &Core::App();
const auto accountsCount = int(app->domain().accounts().size());
auto &&accounts = ranges::views::zip(
Shortcuts::kShowAccount,
kShowAccount,
ranges::views::ints(0, accountsCount));
for (const auto &[command, index] : accounts) {
request->check(command) && request->handle([=] {
@@ -2033,6 +2067,10 @@ void SessionController::setActiveChatEntry(Dialogs::RowDescriptor row) {
{ anim::type::normal, anim::activation::background });
}, _activeHistoryLifetime);
}
if (const auto thread = row.key.thread()) {
session().recentPeers().chatOpenPush(thread);
}
}
if (session().supportMode()) {
pushToChatEntryHistory(row);

View File

@@ -99,6 +99,7 @@ class SectionMemento;
class Controller;
class FiltersMenu;
class ChatPreviewManager;
class ChatSwitchProcess;
struct PeerByLinkInfo;
struct SeparateId;
@@ -784,6 +785,8 @@ private:
std::deque<std::shared_ptr<Ui::ChatTheme>> _lastUsedCustomChatThemes;
rpl::variable<PeerThemeOverride> _peerThemeOverride;
std::unique_ptr<ChatSwitchProcess> _chatSwitchProcess;
base::has_weak_ptr _storyOpenGuard;
QString _premiumRef;