diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index d2e79c54f..52cb7501d 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -138,6 +138,8 @@ PRIVATE ayu/ui/boxes/edit_deleted_mark.h ayu/ui/boxes/edit_edited_mark.cpp ayu/ui/boxes/edit_edited_mark.h + ayu/ui/boxes/font_selector.cpp + ayu/ui/boxes/font_selector.h ayu/sync/ayu_sync_controller.cpp ayu/sync/ayu_sync_controller.h ayu/sync/models.h diff --git a/Telegram/SourceFiles/ayu/ayu_infra.cpp b/Telegram/SourceFiles/ayu/ayu_infra.cpp index 6fc7009b9..66dedc164 100644 --- a/Telegram/SourceFiles/ayu/ayu_infra.cpp +++ b/Telegram/SourceFiles/ayu/ayu_infra.cpp @@ -11,6 +11,7 @@ #include "ayu/ui/ayu_lottie.h" #include "ayu/database/ayu_database.h" #include "lang/lang_instance.h" +#include "ayu/ayu_settings.h" namespace AyuInfra { @@ -27,18 +28,29 @@ void initLang() CustomLangPack::currentInstance()->fetchCustomLangPack(langPackId, langPackBaseId); } -void initLottie() { +void initLottie() +{ AyuUi::setLottieImpl(std::make_shared()); } -void initDatabase() { +void initDatabase() +{ AyuDatabase::initialize(); } +void initFonts() +{ + auto ayuSettings = AyuSettings::getInstance(); + + AyuFonts::setCommonFont(ayuSettings.commonFont); + AyuFonts::setMonoFont(ayuSettings.monoFont); +} + void init() { initLang(); initLottie(); + initFonts(); initDatabase(); } diff --git a/Telegram/SourceFiles/ayu/ayu_settings.cpp b/Telegram/SourceFiles/ayu/ayu_settings.cpp index 9c8cfe780..fa247806f 100644 --- a/Telegram/SourceFiles/ayu/ayu_settings.cpp +++ b/Telegram/SourceFiles/ayu/ayu_settings.cpp @@ -273,6 +273,16 @@ void AyuGramSettings::set_showGhostToggleInDrawer(bool val) showGhostToggleInDrawer = val; } +void AyuGramSettings::set_commonFont(QString val) +{ + commonFont = val; +} + +void AyuGramSettings::set_monoFont(QString val) +{ + monoFont = val; +} + void AyuGramSettings::set_showPeerId(int val) { showPeerId = val; diff --git a/Telegram/SourceFiles/ayu/ayu_settings.h b/Telegram/SourceFiles/ayu/ayu_settings.h index 9fc35ffe3..8dd7d1271 100644 --- a/Telegram/SourceFiles/ayu/ayu_settings.h +++ b/Telegram/SourceFiles/ayu/ayu_settings.h @@ -51,6 +51,8 @@ public: editedMark = tr::lng_edited(tr::now); recentStickersCount = 20; showGhostToggleInDrawer = true; + commonFont = ""; + monoFont = ""; /* * showPeerId = 0 means no ID shown @@ -86,6 +88,8 @@ public: QString editedMark; int recentStickersCount; bool showGhostToggleInDrawer; + QString commonFont; + QString monoFont; int showPeerId; bool hideAllChatsFolder; bool showMessageSeconds; @@ -132,6 +136,10 @@ public: void set_showGhostToggleInDrawer(bool val); + void set_commonFont(QString val); + + void set_monoFont(QString val); + void set_showPeerId(int val); void set_showMessageSeconds(bool val); @@ -165,6 +173,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( editedMark, recentStickersCount, showGhostToggleInDrawer, + commonFont, + monoFont, showPeerId, showMessageSeconds, hideAllChatsFolder, diff --git a/Telegram/SourceFiles/ayu/ui/boxes/font_selector.cpp b/Telegram/SourceFiles/ayu/ui/boxes/font_selector.cpp new file mode 100644 index 000000000..3e515749b --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/boxes/font_selector.cpp @@ -0,0 +1,1230 @@ +// +// Created by MaxPlays on 12/09/2023. +// +/* +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 "boxes/language_box.h" + +#include "data/data_peer_values.h" +#include "ui/widgets/checkbox.h" +#include "ui/widgets/labels.h" +#include "ui/widgets/multi_select.h" +#include "ui/widgets/scroll_area.h" +#include "ui/widgets/dropdown_menu.h" +#include "ui/widgets/box_content_divider.h" +#include "ui/text/text_entity.h" +#include "ui/wrap/vertical_layout.h" +#include "ui/wrap/slide_wrap.h" +#include "ui/effects/ripple_animation.h" +#include "ui/toast/toast.h" +#include "ui/text/text_options.h" +#include "ui/painter.h" +#include "storage/localstorage.h" +#include "boxes/premium_preview_box.h" +#include "ui/boxes/confirm_box.h" +#include "main/main_session.h" +#include "mainwidget.h" +#include "mainwindow.h" +#include "core/application.h" +#include "lang/lang_instance.h" +#include "lang/lang_cloud_manager.h" +#include "settings/settings_common.h" +#include "spellcheck/spellcheck_types.h" +#include "window/window_session_controller.h" +#include "styles/style_layers.h" +#include "styles/style_boxes.h" +#include "styles/style_info.h" +#include "styles/style_passport.h" +#include "styles/style_chat_helpers.h" +#include "font_selector.h" + +#include +#include +#include + +#include + +struct Font +{ + QString FontName; + QString id; +}; + +namespace AyuUi +{ + +class Rows: public Ui::RpWidget +{ +public: + Rows( + QWidget *parent, + const std::vector &data, + const QString &chosen); + + void filter(const QString &query); + + int count() const; + + int selected() const; + + void setSelected(int selected); + + rpl::producer hasSelection() const; + + rpl::producer isEmpty() const; + + void activateSelected(); + + rpl::producer activations() const; + + void changeChosen(const QString &chosen); + + Ui::ScrollToRequest rowScrollRequest(int index) const; + + static int DefaultRowHeight(); + +protected: + int resizeGetHeight(int newWidth) override; + + void paintEvent(QPaintEvent *e) override; + + void mouseMoveEvent(QMouseEvent *e) override; + + void mousePressEvent(QMouseEvent *e) override; + + void mouseReleaseEvent(QMouseEvent *e) override; + + void leaveEventHook(QEvent *e) override; + +private: + struct Row + { + Font data; + Ui::Text::String title = {st::boxWideWidth / 2}; + Ui::Text::String description = {st::boxWideWidth / 2}; + int top = 0; + int height = 0; + mutable std::unique_ptr ripple; + mutable std::unique_ptr menuToggleRipple; + bool menuToggleForceRippled = false; + int titleHeight = 0; + int descriptionHeight = 0; + QStringList keywords; + std::unique_ptr check; + bool removed = false; + }; + + struct RowSelection + { + int index = 0; + + inline bool operator==(const RowSelection &other) const + { + return (index == other.index); + } + }; + + struct MenuSelection + { + int index = 0; + + inline bool operator==(const MenuSelection &other) const + { + return (index == other.index); + } + }; + + using Selection = std::variant; + + void updateSelected(Selection selected); + + void updatePressed(Selection pressed); + + Rows::Row &rowByIndex(int index); + + const Rows::Row &rowByIndex(int index) const; + + Rows::Row &rowBySelection(Selection selected); + + const Rows::Row &rowBySelection(Selection selected) const; + + std::unique_ptr &rippleBySelection( + Selection selected); + + [[maybe_unused]] const std::unique_ptr &rippleBySelection( + Selection selected) const; + + std::unique_ptr &rippleBySelection( + not_null row, + Selection selected); + + [[maybe_unused]] const std::unique_ptr &rippleBySelection( + not_null row, + Selection selected) const; + + void addRipple(Selection selected, QPoint position); + + void ensureRippleBySelection(Selection selected); + + void ensureRippleBySelection(not_null row, Selection selected); + + int indexFromSelection(Selection selected) const; + + int countAvailableWidth() const; + + int countAvailableWidth(int newWidth) const; + + QRect menuToggleArea() const; + + QRect menuToggleArea(not_null row) const; + + void repaint(Selection selected); + + void repaint(int index); + + void repaint(const Row &row); + + void repaintChecked(not_null row); + + void activateByIndex(int index); + + void showMenu(int index); + + void setForceRippled(not_null row, bool rippled); + + bool canShare(not_null row) const; + + bool canRemove(not_null row) const; + + bool hasMenu(not_null row) const; + + void share(not_null row) const; + + void remove(not_null row); + + void restore(not_null row); + + std::vector _rows; + std::vector> _filtered; + Selection _selected; + Selection _pressed; + QString _chosen; + QStringList _query; + + bool _areOfficial = false; + bool _mouseSelection = false; + QPoint _globalMousePosition; + base::unique_qptr _menu; + int _menuShownIndex = -1; + bool _menuOtherEntered = false; + + rpl::event_stream _hasSelection; + rpl::event_stream _activations; + rpl::event_stream _isEmpty; + +}; + +class Content: public Ui::RpWidget +{ +public: + Content( + QWidget *parent, + const std::vector &fonts); + + Ui::ScrollToRequest jump(int rows); + + void filter(const QString &query); + + rpl::producer activations() const; + + void changeChosen(const QString &chosen); + + void activateBySubmit(); + +private: + void setupContent( + const std::vector &fonts); + + Fn _jump; + Fn _filter; + Fn()> _activations; + Fn _changeChosen; + Fn _activateBySubmit; + +}; + +std::vector PrepareFonts() +{ + auto fonts = std::vector(); + QFontDatabase base; + + for (const auto &font: base.families()) { + Font fontItem = { + .FontName=font, + .id=font + }; + + fonts.push_back(fontItem); + } + + + return fonts; +} + +Rows::Rows( + QWidget *parent, + const std::vector &data, + const QString &chosen) + : RpWidget(parent), _chosen(chosen) +{ + const auto descriptionOptions = TextParseOptions{ + TextParseMultiline, + 0, + 0, + Qt::LayoutDirectionAuto + }; + _rows.reserve(data.size()); + for (const auto &item: data) { + _rows.push_back(Row{item}); + auto &row = _rows.back(); + row.check = std::make_unique( + st::langsRadio, + (row.data.id == _chosen), + [=, row = &row] + { repaint(*row); }); + row.title.setText( + st::defaultTextStyle, + item.FontName, + Ui::NameTextOptions()); + row.keywords = TextUtilities::PrepareSearchWords( + item.FontName + ' ' + item.id); + } + resizeToWidth(width()); + setAttribute(Qt::WA_MouseTracking); + update(); +} + +void Rows::mouseMoveEvent(QMouseEvent *e) +{ + const auto position = e->globalPos(); + if (_menu) { + const auto rect = (_menuShownIndex >= 0) + ? menuToggleArea(&rowByIndex(_menuShownIndex)) + : QRect(); + if (rect.contains(e->pos())) { + if (!_menuOtherEntered) { + _menuOtherEntered = true; + _menu->otherEnter(); + } + } + else { + if (_menuOtherEntered) { + _menuOtherEntered = false; + _menu->otherLeave(); + } + } + } + if (!_mouseSelection && position == _globalMousePosition) { + return; + } + _mouseSelection = true; + _globalMousePosition = position; + const auto index = [&] + { + const auto y = e->pos().y(); + if (y < 0) { + return -1; + } + for (auto i = 0, till = count(); i != till; ++i) { + const auto &row = rowByIndex(i); + if (row.top + row.height > y) { + return i; + } + } + return -1; + }(); + const auto row = (index >= 0) ? &rowByIndex(index) : nullptr; + const auto inMenuToggle = (index >= 0 && hasMenu(row)) + ? menuToggleArea(row).contains(e->pos()) + : false; + if (index < 0) { + updateSelected({}); + } + else if (inMenuToggle) { + updateSelected(MenuSelection{index}); + } + else if (!row->removed) { + updateSelected(RowSelection{index}); + } + else { + updateSelected({}); + } +} + +void Rows::mousePressEvent(QMouseEvent *e) +{ + updatePressed(_selected); + if (!v::is_null(_pressed) + && !rowBySelection(_pressed).menuToggleForceRippled) { + addRipple(_pressed, e->pos()); + } +} + +QRect Rows::menuToggleArea() const +{ + const auto size = st::topBarSearch.width; + const auto top = (DefaultRowHeight() - size) / 2; + const auto skip = st::boxScroll.width + - st::boxScroll.deltax + + top; + const auto left = width() - skip - size; + return QRect(left, top, size, size); +} + +QRect Rows::menuToggleArea(not_null row) const +{ + return menuToggleArea().translated(0, row->top); +} + +void Rows::addRipple(Selection selected, QPoint position) +{ + Expects(!v::is_null(selected)); + + ensureRippleBySelection(selected); + + const auto menu = v::is(selected); + const auto &row = rowBySelection(selected); + const auto menuArea = menuToggleArea(&row); + auto &ripple = rippleBySelection(&row, selected); + const auto topleft = menu ? menuArea.topLeft() : QPoint(0, row.top); + ripple->add(position - topleft); +} + +void Rows::ensureRippleBySelection(Selection selected) +{ + ensureRippleBySelection(&rowBySelection(selected), selected); +} + +void Rows::ensureRippleBySelection(not_null row, Selection selected) +{ + auto &ripple = rippleBySelection(row, selected); + if (ripple) { + return; + } + const auto menu = v::is(selected); + const auto menuArea = menuToggleArea(row); + auto mask = menu + ? Ui::RippleAnimation::EllipseMask(menuArea.size()) + : Ui::RippleAnimation::RectMask({width(), row->height}); + ripple = std::make_unique( + st::defaultRippleAnimation, + std::move(mask), + [=] + { repaintChecked(row); }); +} + +void Rows::mouseReleaseEvent(QMouseEvent *e) +{ + if (_menu && e->button() == Qt::LeftButton) { + if (_menu->isHiding()) { + _menu->otherEnter(); + } + else { + _menu->otherLeave(); + } + } + const auto pressed = _pressed; + updatePressed({}); + if (pressed == _selected) { + v::match(pressed, [&](RowSelection data) + { + activateByIndex(data.index); + }, [&](MenuSelection data) + { + showMenu(data.index); + }, [](v::null_t) + {}); + } +} + +bool Rows::canShare(not_null row) const +{ + return !_areOfficial && !row->data.id.startsWith('#'); +} + +bool Rows::canRemove(not_null row) const +{ + return !_areOfficial && !row->check->checked(); +} + +bool Rows::hasMenu(not_null row) const +{ + return canShare(row) || canRemove(row); +} + +void Rows::share(not_null row) const +{ + const auto link = u"https://t.me/setlanguage/"_q + row->data.id; + QGuiApplication::clipboard()->setText(link); + Ui::Toast::Show(tr::lng_username_copied(tr::now)); +} + +void Rows::remove(not_null row) +{ + row->removed = true; + Local::removeRecentLanguage(row->data.id); +} + +void Rows::restore(not_null row) +{ + row->removed = false; +} + +void Rows::showMenu(int index) +{ + const auto row = &rowByIndex(index); +} + +void Rows::setForceRippled(not_null row, bool rippled) +{ + if (row->menuToggleForceRippled != rippled) { + row->menuToggleForceRippled = rippled; + auto &ripple = rippleBySelection(row, MenuSelection{}); + if (row->menuToggleForceRippled) { + ensureRippleBySelection(row, MenuSelection{}); + if (ripple->empty()) { + ripple->addFading(); + } + else { + ripple->lastUnstop(); + } + } + else { + if (ripple) { + ripple->lastStop(); + } + } + } + repaint(*row); +} + +void Rows::activateByIndex(int index) +{ + _activations.fire_copy(rowByIndex(index).data); +} + +void Rows::leaveEventHook(QEvent *e) +{ + updateSelected({}); + if (_menu && _menuOtherEntered) { + _menuOtherEntered = false; + _menu->otherLeave(); + } +} + +void Rows::filter(const QString &query) +{ + updateSelected({}); + updatePressed({}); + _menu = nullptr; + _menuShownIndex = -1; + + _query = TextUtilities::PrepareSearchWords(query); + + const auto skip = []( + const QStringList &haystack, + const QStringList &needles) + { + const auto find = []( + const QStringList &haystack, + const QString &needle) + { + for (const auto &item: haystack) { + if (item.startsWith(needle)) { + return true; + } + } + return false; + }; + for (const auto &needle: needles) { + if (!find(haystack, needle)) { + return true; + } + } + return false; + }; + + if (!_query.isEmpty()) { + _filtered.clear(); + _filtered.reserve(_rows.size()); + for (auto &row: _rows) { + if (!skip(row.keywords, _query)) { + _filtered.push_back(&row); + } + else { + row.ripple = nullptr; + } + } + } + + resizeToWidth(width()); + Ui::SendPendingMoveResizeEvents(this); + + _isEmpty.fire(count() == 0); +} + +int Rows::count() const +{ + return _query.isEmpty() ? _rows.size() : _filtered.size(); +} + +int Rows::indexFromSelection(Selection selected) const +{ + return v::match(selected, [&](RowSelection data) + { + return data.index; + }, [&](MenuSelection data) + { + return data.index; + }, [](v::null_t) + { + return -1; + }); +} + +int Rows::selected() const +{ + return indexFromSelection(_selected); +} + +void Rows::activateSelected() +{ + const auto index = selected(); + if (index >= 0) { + activateByIndex(index); + } +} + +rpl::producer Rows::activations() const +{ + return _activations.events(); +} + +void Rows::changeChosen(const QString &chosen) +{ + for (const auto &row: _rows) { + row.check->setChecked(row.data.id == chosen, anim::type::normal); + } +} + +void Rows::setSelected(int selected) +{ + _mouseSelection = false; + const auto limit = count(); + if (selected >= 0 && selected < limit) { + updateSelected(RowSelection{selected}); + } + else { + updateSelected({}); + } +} + +rpl::producer Rows::hasSelection() const +{ + return _hasSelection.events(); +} + +rpl::producer Rows::isEmpty() const +{ + return _isEmpty.events_starting_with( + count() == 0 + ) | rpl::distinct_until_changed(); +} + +void Rows::repaint(Selection selected) +{ + v::match(selected, [](v::null_t) + { + }, [&](const auto &data) + { + repaint(data.index); + }); +} + +void Rows::repaint(int index) +{ + if (index >= 0) { + repaint(rowByIndex(index)); + } +} + +void Rows::repaint(const Row &row) +{ + update(0, row.top, width(), row.height); +} + +void Rows::repaintChecked(not_null row) +{ + const auto found = (ranges::find(_filtered, row) != end(_filtered)); + if (_query.isEmpty() || found) { + repaint(*row); + } +} + +void Rows::updateSelected(Selection selected) +{ + const auto changed = (v::is_null(_selected) != v::is_null(selected)); + repaint(_selected); + _selected = selected; + repaint(_selected); + if (changed) { + _hasSelection.fire(!v::is_null(_selected)); + } +} + +void Rows::updatePressed(Selection pressed) +{ + if (!v::is_null(_pressed)) { + if (!rowBySelection(_pressed).menuToggleForceRippled) { + if (const auto ripple = rippleBySelection(_pressed).get()) { + ripple->lastStop(); + } + } + } + _pressed = pressed; +} + +Rows::Row &Rows::rowByIndex(int index) +{ + Expects(index >= 0 && index < count()); + + return _query.isEmpty() ? _rows[index] : *_filtered[index]; +} + +const Rows::Row &Rows::rowByIndex(int index) const +{ + Expects(index >= 0 && index < count()); + + return _query.isEmpty() ? _rows[index] : *_filtered[index]; +} + +Rows::Row &Rows::rowBySelection(Selection selected) +{ + return rowByIndex(indexFromSelection(selected)); +} + +const Rows::Row &Rows::rowBySelection(Selection selected) const +{ + return rowByIndex(indexFromSelection(selected)); +} + +std::unique_ptr &Rows::rippleBySelection( + Selection selected) +{ + return rippleBySelection(&rowBySelection(selected), selected); +} + +const std::unique_ptr &Rows::rippleBySelection( + Selection selected) const +{ + return rippleBySelection(&rowBySelection(selected), selected); +} + +std::unique_ptr &Rows::rippleBySelection( + not_null row, + Selection selected) +{ + return v::is(selected) + ? row->menuToggleRipple + : row->ripple; +} + +const std::unique_ptr &Rows::rippleBySelection( + not_null row, + Selection selected) const +{ + return const_cast(this)->rippleBySelection( + const_cast(row.get()), + selected); +} + +Ui::ScrollToRequest Rows::rowScrollRequest(int index) const +{ + const auto &row = rowByIndex(index); + return Ui::ScrollToRequest(row.top, row.top + row.height); +} + +int Rows::DefaultRowHeight() +{ + return st::passportRowPadding.top() + + st::semiboldFont->height + + st::passportRowSkip + + st::normalFont->height + + st::passportRowPadding.bottom(); +} + +int Rows::resizeGetHeight(int newWidth) +{ + const auto availableWidth = countAvailableWidth(newWidth); + auto result = 0; + for (auto i = 0, till = count(); i != till; ++i) { + auto &row = rowByIndex(i); + row.top = result; + row.titleHeight = row.title.countHeight(availableWidth); + row.descriptionHeight = row.description.countHeight(availableWidth); + row.height = st::passportRowPadding.top() + + row.titleHeight + + st::passportRowSkip + + row.descriptionHeight + + st::passportRowPadding.bottom(); + result += row.height; + } + return result; +} + +int Rows::countAvailableWidth(int newWidth) const +{ + const auto right = width() - menuToggleArea().x(); + return newWidth + - st::passportRowPadding.left() + - st::langsRadio.diameter + - st::passportRowPadding.left() + - right + - st::passportRowIconSkip; +} + +int Rows::countAvailableWidth() const +{ + return countAvailableWidth(width()); +} + +void Rows::paintEvent(QPaintEvent *e) +{ + Painter p(this); + + const auto clip = e->rect(); + + const auto checkLeft = st::passportRowPadding.left(); + const auto left = checkLeft + + st::langsRadio.diameter + + st::passportRowPadding.left(); + const auto availableWidth = countAvailableWidth(); + const auto menu = menuToggleArea(); + const auto selectedIndex = (_menuShownIndex >= 0) + ? _menuShownIndex + : indexFromSelection(!v::is_null(_pressed) ? _pressed : _selected); + for (auto i = 0, till = count(); i != till; ++i) { + const auto &row = rowByIndex(i); + if (row.top + row.height <= clip.y()) { + continue; + } + else if (row.top >= clip.y() + clip.height()) { + break; + } + p.setOpacity(row.removed ? st::stickersRowDisabledOpacity : 1.); + p.translate(0, row.top); + const auto guard = gsl::finally([&] + { p.translate(0, -row.top); }); + + const auto selected = (selectedIndex == i); + if (selected && !row.removed) { + p.fillRect(0, 0, width(), row.height, st::windowBgOver); + } + + if (row.ripple) { + row.ripple->paint(p, 0, 0, width()); + if (row.ripple->empty()) { + row.ripple.reset(); + } + } + + const auto checkTop = (row.height - st::defaultRadio.diameter) / 2; + row.check->paint(p, checkLeft, checkTop, width()); + + auto top = st::passportRowPadding.top(); + + p.setPen(st::passportRowTitleFg); + row.title.drawLeft(p, left, top, availableWidth, width()); + top += row.titleHeight + st::passportRowSkip; + + p.setPen(selected ? st::windowSubTextFgOver : st::windowSubTextFg); + row.description.drawLeft(p, left, top, availableWidth, width()); + top += row.descriptionHeight + st::passportRowPadding.bottom(); + } +} + +Content::Content( + QWidget *parent, + const std::vector &fonts) + : RpWidget(parent) +{ + setupContent(fonts); +} + +void Content::setupContent( + const std::vector &fonts) +{ + using namespace rpl::mappers; + + const auto current = AyuFonts::getCommonFont(); + const auto content = Ui::CreateChild(this); + const auto add = [&](const std::vector &list) + { + if (list.empty()) { + return (Rows *) nullptr; + } + const auto wrap = content->add( + object_ptr>( + content, + object_ptr(content))); + const auto inner = wrap->entity(); + inner->add(object_ptr( + inner, + st::defaultBox.margin.top())); + const auto rows = inner->add(object_ptr( + inner, + list, + current)); + inner->add(object_ptr( + inner, + st::defaultBox.margin.top())); + + rows->isEmpty() | rpl::start_with_next([=](bool empty) + { + wrap->toggle(!empty, anim::type::instant); + }, rows->lifetime()); + + return rows; + }; + const auto main = add(fonts); + const auto divider = content->add( + object_ptr>( + content, + object_ptr(content))); + const auto empty = content->add( + object_ptr>( + content, + object_ptr( + content, + st::membersAbout.style.font->height * 9))); + const auto label = Ui::CreateChild( + empty->entity(), + rpl::single(qs("No fonts found.")), + st::membersAbout); + empty->entity()->sizeValue( + ) | rpl::start_with_next([=](QSize size) + { + label->move( + (size.width() - label->width()) / 2, + (size.height() - label->height()) / 2); + }, label->lifetime()); + + empty->toggleOn( + main ? main->isEmpty() : rpl::single(true), + anim::type::instant); + + Ui::ResizeFitChild(this, content); + + + divider->hide(anim::type::instant); + + const auto count = [](Rows *widget) + { + return widget ? widget->count() : 0; + }; + const auto selected = [](Rows *widget) + { + return widget ? widget->selected() : -1; + }; + const auto rowsCount = [=] + { + return count(main); + }; + const auto selectedIndex = [=] + { + if (const auto index = selected(main); index >= 0) { + return index; + } + + return -1; + }; + const auto setSelectedIndex = [=](int index) + { + const auto first = count(main); + if (index >= first) { + if (main) { + main->setSelected(-1); + } + } + else { + if (main) { + main->setSelected(index); + } + } + }; + const auto selectedCoords = [=] + { + const auto coords = [=](Rows *rows, int index) + { + const auto result = rows->rowScrollRequest(index); + const auto shift = rows->mapToGlobal({0, 0}).y() + - mapToGlobal({0, 0}).y(); + return Ui::ScrollToRequest( + result.ymin + shift, + result.ymax + shift); + }; + if (const auto index = selected(main); index >= 0) { + return coords(main, index); + } + return Ui::ScrollToRequest(-1, -1); + }; + _jump = [=](int rows) + { + const auto count = rowsCount(); + const auto now = selectedIndex(); + if (now >= 0) { + const auto changed = now + rows; + if (changed < 0) { + setSelectedIndex((now > 0) ? 0 : -1); + } + else if (changed >= count) { + setSelectedIndex(count - 1); + } + else { + setSelectedIndex(changed); + } + } + else if (rows > 0) { + setSelectedIndex(0); + } + return selectedCoords(); + }; + const auto filter = [](Rows *widget, const QString &query) + { + if (widget) { + widget->filter(query); + } + }; + _filter = [=](const QString &query) + { + filter(main, query); + }; + _activations = [=] + { + if (!main) { + return rpl::never() | rpl::type_erased(); + } + return rpl::merge( + main->activations() + ) | rpl::type_erased(); + }; + _changeChosen = [=](const QString &chosen) + { + if (main) { + main->changeChosen(chosen); + } + }; + _activateBySubmit = [=] + { + if (selectedIndex() < 0) { + _jump(1); + } + if (main) { + main->activateSelected(); + } + }; +}; + +void Content::filter(const QString &query) +{ + _filter(query); +} + +rpl::producer Content::activations() const +{ + return _activations(); +} + +void Content::changeChosen(const QString &chosen) +{ + _changeChosen(chosen); +} + +void Content::activateBySubmit() +{ + _activateBySubmit(); +} + +Ui::ScrollToRequest Content::jump(int rows) +{ + return _jump(rows); +} + +} // namespace + +AyuUi::FontSelectorBox::FontSelectorBox(QWidget *, Window::SessionController *controller, Fn hook) + : _controller(controller), _hook(hook) +{ +} + +void AyuUi::FontSelectorBox::prepare() +{ + addButton(tr::lng_box_ok(), [=] + { + + _hook(_selectedFont); + + _controller->show(Ui::MakeConfirmBox({ + .text = tr::lng_settings_need_restart(), + .confirmed = [] + { Core::Restart(); }, + .confirmText = tr::lng_settings_restart_now(), + .cancelText = tr::lng_settings_restart_later(), + })); + closeBox(); + }); + + addLeftButton(tr::ayu_BoxActionReset(), [=] + { + _hook(qs("")); + + _controller->show(Ui::MakeConfirmBox({ + .text = tr::lng_settings_need_restart(), + .confirmed = [] + { Core::Restart(); }, + .confirmText = tr::lng_settings_restart_now(), + .cancelText = tr::lng_settings_restart_later(), + })); + + + closeBox(); + }); + + setTitle(rpl::single(qs("Font customisation"))); + + const auto topContainer = Ui::CreateChild(this); + setupTop(topContainer); + const auto select = topContainer->add( + object_ptr( + topContainer, + st::defaultMultiSelect, + tr::lng_participant_filter())); + topContainer->resizeToWidth(st::boxWidth); + + using namespace rpl::mappers; + + const auto fonts = AyuUi::PrepareFonts(); + const auto inner = setInnerWidget( + object_ptr(this, fonts), + st::boxScroll, + topContainer->height()); + inner->resizeToWidth(st::boxWidth); + + const auto max = lifetime().make_state(0); + rpl::combine( + inner->heightValue(), + topContainer->heightValue(), + _1 + _2 + ) | rpl::start_with_next([=](int height) + { + accumulate_max(*max, height); + setDimensions(st::boxWidth, qMin(*max, st::boxMaxListHeight)); + }, inner->lifetime()); + topContainer->heightValue( + ) | rpl::start_with_next([=](int height) + { + setInnerTopSkip(height); + }, inner->lifetime()); + + select->setSubmittedCallback([=](Qt::KeyboardModifiers) + { + inner->activateBySubmit(); + }); + select->setQueryChangedCallback([=](const QString &query) + { + inner->filter(query); + }); + select->setCancelledCallback([=] + { + select->clearQuery(); + }); + + inner->activations( + ) | rpl::start_with_next([=](const Font &font) + { + if (inner) { + inner->changeChosen(font.id); + _selectedFont = font.id; + } + }, inner->lifetime()); + + _setInnerFocus = [=] + { + select->setInnerFocus(); + }; + _jump = [=](int rows) + { + return inner->jump(rows); + }; +} + +void AyuUi::FontSelectorBox::setupTop(not_null container) +{ + if (!_controller) { + return; + } +} + +void AyuUi::FontSelectorBox::keyPressEvent(QKeyEvent *e) +{ + const auto key = e->key(); + if (key == Qt::Key_Escape) { + closeBox(); + return; + } + const auto selected = [&] + { + if (key == Qt::Key_Up) { + return _jump(-1); + } + else if (key == Qt::Key_Down) { + return _jump(1); + } + else if (key == Qt::Key_PageUp) { + return _jump(-rowsInPage()); + } + else if (key == Qt::Key_PageDown) { + return _jump(rowsInPage()); + } + return Ui::ScrollToRequest(-1, -1); + }(); + if (selected.ymin >= 0 && selected.ymax >= 0) { + scrollToY(selected.ymin, selected.ymax); + } +} + +int AyuUi::FontSelectorBox::rowsInPage() const +{ + return std::max(height() / AyuUi::Rows::DefaultRowHeight(), 1); +} + +void AyuUi::FontSelectorBox::setInnerFocus() +{ + _setInnerFocus(); +} + +base::binary_guard +AyuUi::FontSelectorBox::Show(Window::SessionController *controller, const Fn hook) +{ + auto result = base::binary_guard(); + + Ui::show(Box(controller, hook)); + + return result; + +} diff --git a/Telegram/SourceFiles/ayu/ui/boxes/font_selector.h b/Telegram/SourceFiles/ayu/ui/boxes/font_selector.h new file mode 100644 index 000000000..7d685b7c3 --- /dev/null +++ b/Telegram/SourceFiles/ayu/ui/boxes/font_selector.h @@ -0,0 +1,73 @@ +// +// Created by MaxPlays on 12/09/2023. +// + +#ifndef TELEGRAM_FONT_SELECTOR_H +#define TELEGRAM_FONT_SELECTOR_H + +/* +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 "lang/lang_cloud_manager.h" +#include "boxes/abstract_box.h" +#include "base/binary_guard.h" + +struct LanguageId; +struct Font; + +namespace Ui +{ + +class MultiSelect; +struct ScrollToRequest; +class VerticalLayout; + +} // namespace Ui + +namespace Window +{ + +class SessionController; + +} // namespace Window +namespace AyuUi +{ + +class FontSelectorBox: public Ui::BoxContent +{ +public: + FontSelectorBox(QWidget *, Window::SessionController *controller, Fn hook); + + void setInnerFocus() override; + + static base::binary_guard Show(Window::SessionController *controller, const Fn hook); +private: + QString _selectedFont; + +protected: + void prepare() override; + + void keyPressEvent(QKeyEvent *e) override; + +private: + void setupTop(not_null container); + + [[nodiscard]] int rowsInPage() const; + + Window::SessionController *_controller = nullptr; + rpl::event_stream _translateChatTurnOff; + Fn _setInnerFocus; + Fn _jump; + Fn _hook; + +}; + +} + +#endif //TELEGRAM_FONT_SELECTOR_H diff --git a/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.cpp b/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.cpp index 8bc73f604..390210266 100644 --- a/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.cpp +++ b/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.cpp @@ -10,6 +10,7 @@ #include "ayu/sync/ayu_sync_controller.h" #include "ayu/ui/boxes/edit_deleted_mark.h" #include "ayu/ui/boxes/edit_edited_mark.h" +#include "ayu/ui/boxes/font_selector.h" #include "apiwrap.h" #include "lang_auto.h" @@ -108,7 +109,7 @@ not_null AddInnerToggle( [](const auto &v) { return v->checked(); }); }; - for (const auto &innerCheck : state->innerChecks) { + for (const auto &innerCheck: state->innerChecks) { innerCheck->checkedChanges( ) | rpl::to_empty | start_to_stream( state->anyChanges, @@ -259,7 +260,7 @@ not_null AddInnerToggle( { if (!handleLocked()) { const auto checked = !checkView->checked(); - for (const auto &innerCheck : state->innerChecks) { + for (const auto &innerCheck: state->innerChecks) { innerCheck->setChecked(checked, anim::type::normal); } } @@ -344,7 +345,8 @@ void Ayu::AddPlatformOption( } } -void Ayu::SetupGhostModeToggle(not_null container) { +void Ayu::SetupGhostModeToggle(not_null container) +{ auto settings = &AyuSettings::getInstance(); const auto widget = object_ptr(this); @@ -450,7 +452,7 @@ void Ayu::SetupGhostModeToggle(not_null container) { object_ptr(container)); const auto verticalLayout = wrap->entity(); auto innerChecks = std::vector>(); - for (const auto &entry : checkboxes) { + for (const auto &entry: checkboxes) { const auto c = addCheckbox(verticalLayout, entry.checkboxLabel, entry.initial); c->checkedValue( ) | start_with_next([=](bool enabled) @@ -478,7 +480,8 @@ void Ayu::SetupGhostModeToggle(not_null container) { }, raw->lifetime()); } -void Ayu::SetupReadAfterActionToggle(not_null container) { +void Ayu::SetupReadAfterActionToggle(not_null container) +{ auto settings = &AyuSettings::getInstance(); const auto widget = object_ptr(this); @@ -570,7 +573,7 @@ void Ayu::SetupReadAfterActionToggle(not_null container) { object_ptr(container)); const auto verticalLayout = wrap->entity(); auto innerChecks = std::vector>(); - for (const auto &entry : checkboxes) { + for (const auto &entry: checkboxes) { const auto c = addCheckbox(verticalLayout, entry.checkboxLabel, entry.initial); c->checkedValue( ) | start_with_next([=](bool enabled) @@ -989,6 +992,52 @@ void Ayu::SetupExperimental(not_null container, { AddSubsectionTitle(container, tr::lng_settings_experimental()); AddPlatformOption(controller, container, StreamerMode, rpl::producer<>()); + + const auto commonButton = AddButtonWithLabel( + container, + rpl::single(qs("Customise main font")), + rpl::single( + AyuSettings::getInstance().commonFont.isEmpty() ? qs("Default") : AyuSettings::getInstance().commonFont + ), + st::settingsButtonNoIcon); + const auto commonGuard = Ui::CreateChild(commonButton.get()); + + commonButton->addClickHandler([=] + { + const auto m = commonButton->clickModifiers(); + *commonGuard = AyuUi::FontSelectorBox::Show(controller, [=](QString font) + { + auto ayuSettings = &AyuSettings::getInstance(); + + ayuSettings->set_commonFont(std::move(font)); + AyuSettings::save(); + }); + }); + + const auto monoButton = AddButtonWithLabel( + container, + rpl::single(qs("Customise mono font")), + rpl::single( + AyuSettings::getInstance().monoFont.isEmpty() ? qs("Default") + : AyuSettings::getInstance().monoFont + ), + st::settingsButtonNoIcon); + const auto monoGuard = Ui::CreateChild(monoButton.get()); + + monoButton->addClickHandler([=] + { + const auto m = monoButton->clickModifiers(); + *monoGuard = AyuUi::FontSelectorBox::Show(controller, [=](QString font) + { + auto ayuSettings = &AyuSettings::getInstance(); + + ayuSettings->set_monoFont(std::move(font)); + AyuSettings::save(); + }); + }); + + + AddDividerText(container, rpl::single(qs("Here you can customise fonts for AyuGram"))); } void Ayu::SetupAyuGramSettings(not_null container, diff --git a/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.h b/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.h index c28a5ad19..1e29ada69 100644 --- a/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.h +++ b/Telegram/SourceFiles/ayu/ui/settings/settings_ayu.h @@ -24,7 +24,7 @@ extern base::options::toggle StreamerMode; namespace Settings { -class Ayu : public Section +class Ayu: public Section { public: Ayu(QWidget *parent, not_null controller); diff --git a/Telegram/lib_ui b/Telegram/lib_ui index ab84152a6..a95896165 160000 --- a/Telegram/lib_ui +++ b/Telegram/lib_ui @@ -1 +1 @@ -Subproject commit ab84152a69c7fe03b3d3353d5a887ee23a354445 +Subproject commit a95896165567af0b7d7e511c0d4807a57cc608f6