From 9583a10b88c191d6dbde6e3b08719d62118fed1f Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 11:24:09 +0100 Subject: [PATCH 01/11] fix(helix-chat): show better error messages (#5276) --- CHANGELOG.md | 2 +- src/providers/twitch/TwitchIrcServer.cpp | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd6df89d280..37080a27be6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -186,7 +186,7 @@ - Dev: Added signal to invalidate paint buffers of channel views without forcing a relayout. (#5123) - Dev: Specialize `Atomic>` if underlying standard library supports it. (#5133) - Dev: Added the `developer_name` field to the Linux AppData specification. (#5138) -- Dev: Twitch messages can be sent using Twitch's Helix API instead of IRC (disabled by default). (#5200) +- Dev: Twitch messages can be sent using Twitch's Helix API instead of IRC (disabled by default). (#5200, #5276) - Dev: Added estimation for image sizes to avoid layout shifts. (#5192) - Dev: Added the `launachable` entry to Linux AppData. (#5210) - Dev: Cleaned up and optimized resources. (#5222) diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index 9a71c89ac77..f591e8f310e 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -39,9 +39,17 @@ const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3"; void sendHelixMessage(const std::shared_ptr &channel, const QString &message, const QString &replyParentId = {}) { + auto broadcasterID = channel->roomId(); + if (broadcasterID.isEmpty()) + { + channel->addMessage(makeSystemMessage( + "Sending messages in this channel isn't possible.")); + return; + } + getHelix()->sendChatMessage( { - .broadcasterID = channel->roomId(), + .broadcasterID = broadcasterID, .senderID = getIApp()->getAccounts()->twitch.getCurrent()->getUserId(), .message = message, @@ -68,13 +76,18 @@ void sendHelixMessage(const std::shared_ptr &channel, }(); chan->addMessage(errorMessage); }, - [weak = std::weak_ptr(channel)](auto error, const auto &message) { + [weak = std::weak_ptr(channel)](auto error, auto message) { auto chan = weak.lock(); if (!chan) { return; } + if (message.isEmpty()) + { + message = "(empty message)"; + } + using Error = decltype(error); auto errorMessage = [&]() -> QString { From 84e641d5892af7d8946acd015922006c0966bac6 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 11:25:11 +0100 Subject: [PATCH 02/11] ci: run clang-tidy with Qt 6 and update action (#5273) --- .CI/setup-clang-tidy.sh | 34 ++++++ .github/workflows/clang-tidy.yml | 113 +++---------------- .github/workflows/post-clang-tidy-review.yml | 5 +- CHANGELOG.md | 1 + 4 files changed, 53 insertions(+), 100 deletions(-) create mode 100755 .CI/setup-clang-tidy.sh diff --git a/.CI/setup-clang-tidy.sh b/.CI/setup-clang-tidy.sh new file mode 100755 index 00000000000..4884285eb8e --- /dev/null +++ b/.CI/setup-clang-tidy.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -ev; + +# aqt installs into .qtinstall/Qt//gcc_64 +# This is doing the same as jurplel/install-qt-action +# See https://github.com/jurplel/install-qt-action/blob/74ca8cd6681420fc8894aed264644c7a76d7c8cb/action/src/main.ts#L52-L74 +qtpath=$(echo .qtinstall/Qt/[0-9]*/*/bin/qmake | sed -e s:/bin/qmake$::) +export LD_LIBRARY_PATH="$qtpath/lib" +export QT_ROOT_DIR=$qtpath +export QT_PLUGIN_PATH="$qtpath/plugins" +export PATH="$PATH:$(realpath "$qtpath/bin")" +export Qt6_DIR="$(realpath "$qtpath")" + +cmake -S. -Bbuild-clang-tidy \ + -DCMAKE_BUILD_TYPE=Debug \ + -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \ + -DUSE_PRECOMPILED_HEADERS=OFF \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=On \ + -DCHATTERINO_LTO=Off \ + -DCHATTERINO_PLUGINS=On \ + -DBUILD_WITH_QT6=On \ + -DBUILD_TESTS=On \ + -DBUILD_BENCHMARKS=On + +# Run MOC and UIC +# This will compile the dependencies +# Get the targets using `ninja -t targets | grep autogen` +cmake --build build-clang-tidy --parallel -t \ + Core_autogen \ + LibCommuni_autogen \ + Model_autogen \ + Util_autogen \ + chatterino-lib_autogen diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index b522bba5d3e..cf47eacaf11 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -8,60 +8,25 @@ concurrency: group: clang-tidy-${{ github.ref }} cancel-in-progress: true -env: - CHATTERINO_REQUIRE_CLEAN_GIT: On - C2_BUILD_WITH_QT6: Off - jobs: - build: + review: name: "clang-tidy ${{ matrix.os }}, Qt ${{ matrix.qt-version }})" runs-on: ${{ matrix.os }} strategy: matrix: include: - # Ubuntu 22.04, Qt 5.15 + # Ubuntu 22.04, Qt 6.6 - os: ubuntu-22.04 - qt-version: 5.15.2 - plugins: false + qt-version: 6.6.2 fail-fast: false steps: - - name: Enable plugin support - if: matrix.plugins - run: | - echo "C2_PLUGINS=ON" >> "$GITHUB_ENV" - shell: bash - - - name: Set BUILD_WITH_QT6 - if: startsWith(matrix.qt-version, '6.') - run: | - echo "C2_BUILD_WITH_QT6=ON" >> "$GITHUB_ENV" - shell: bash - - uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 # allows for tags access - - name: Install Qt5 - if: startsWith(matrix.qt-version, '5.') - uses: jurplel/install-qt-action@v3.3.0 - with: - cache: true - cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 - version: ${{ matrix.qt-version }} - - - name: Install Qt 6.5.3 imageformats - if: startsWith(matrix.qt-version, '6.') - uses: jurplel/install-qt-action@v3.3.0 - with: - cache: false - modules: qtimageformats - set-env: false - version: 6.5.3 - extra: --noarchives - - name: Install Qt6 if: startsWith(matrix.qt-version, '6.') uses: jurplel/install-qt-action@v3.3.0 @@ -70,79 +35,31 @@ jobs: cache-key-prefix: ${{ runner.os }}-QtCache-${{ matrix.qt-version }}-v2 modules: qt5compat qtimageformats version: ${{ matrix.qt-version }} - - # LINUX - - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get -y install \ - cmake \ - virtualenv \ - rapidjson-dev \ - libfuse2 \ - libssl-dev \ - libboost-dev \ - libxcb-randr0-dev \ - libboost-system-dev \ - libboost-filesystem-dev \ - libpulse-dev \ - libxkbcommon-x11-0 \ - build-essential \ - libgl1-mesa-dev \ - libxcb-icccm4 \ - libxcb-image0 \ - libxcb-keysyms1 \ - libxcb-render-util0 \ - libxcb-xinerama0 - - - name: Apply Qt5 patches - if: startsWith(matrix.qt-version, '5.') - run: | - patch "$Qt5_DIR/include/QtConcurrent/qtconcurrentthreadengine.h" .patches/qt5-on-newer-gcc.patch - shell: bash - - - name: Build - run: | - mkdir build - cd build - CXXFLAGS=-fno-sized-deallocation cmake \ - -DCMAKE_INSTALL_PREFIX=appdir/usr/ \ - -DCMAKE_BUILD_TYPE=Release \ - -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On \ - -DUSE_PRECOMPILED_HEADERS=OFF \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=On \ - -DCHATTERINO_LTO="$C2_ENABLE_LTO" \ - -DCHATTERINO_PLUGINS="$C2_PLUGINS" \ - -DBUILD_WITH_QT6="$C2_BUILD_WITH_QT6" \ - .. - shell: bash + dir: ${{ github.workspace }}/.qtinstall + set-env: false - name: clang-tidy review timeout-minutes: 20 - uses: ZedThree/clang-tidy-review@v0.17.3 + uses: ZedThree/clang-tidy-review@v0.18.0 with: build_dir: build-clang-tidy config_file: ".clang-tidy" split_workflow: true exclude: "lib/*,tools/crash-handler/*" cmake_command: >- - cmake -S. -Bbuild-clang-tidy - -DCMAKE_BUILD_TYPE=Release - -DPAJLADA_SETTINGS_USE_BOOST_FILESYSTEM=On - -DUSE_PRECOMPILED_HEADERS=OFF - -DCMAKE_EXPORT_COMPILE_COMMANDS=On - -DCHATTERINO_LTO=Off - -DCHATTERINO_PLUGINS=On - -DBUILD_WITH_QT6=Off - -DBUILD_TESTS=On - -DBUILD_BENCHMARKS=On + ./.CI/setup-clang-tidy.sh apt_packages: >- - qttools5-dev, qt5-image-formats-plugins, libqt5svg5-dev, libsecret-1-dev, libboost-dev, libboost-system-dev, libboost-filesystem-dev, libssl-dev, rapidjson-dev, - libbenchmark-dev + libbenchmark-dev, + build-essential, + libgl1-mesa-dev, libgstreamer-gl1.0-0, libpulse-dev, + libxcb-glx0, libxcb-icccm4, libxcb-image0, libxcb-keysyms1, libxcb-randr0, + libxcb-render-util0, libxcb-render0, libxcb-shape0, libxcb-shm0, libxcb-sync1, + libxcb-util1, libxcb-xfixes0, libxcb-xinerama0, libxcb1, libxkbcommon-dev, + libxkbcommon-x11-0, libxcb-xkb-dev, libxcb-cursor0 - name: clang-tidy-review upload - uses: ZedThree/clang-tidy-review/upload@v0.17.3 + uses: ZedThree/clang-tidy-review/upload@v0.18.0 diff --git a/.github/workflows/post-clang-tidy-review.yml b/.github/workflows/post-clang-tidy-review.yml index 2f9b6b3d9fa..6c39a93a7ea 100644 --- a/.github/workflows/post-clang-tidy-review.yml +++ b/.github/workflows/post-clang-tidy-review.yml @@ -8,12 +8,13 @@ on: - completed jobs: - build: + post: runs-on: ubuntu-latest # Only when a build succeeds if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: - - uses: ZedThree/clang-tidy-review/post@v0.17.3 + - uses: ZedThree/clang-tidy-review/post@v0.18.0 with: lgtm_comment_body: "" + num_comments_as_exitcode: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 37080a27be6..b8287d63a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -194,6 +194,7 @@ - Dev: Cleaned up unused code in `MessageElement` and `MessageLayoutElement`. (#5225) - Dev: Adapted `magic_enum` to Qt's Utf-16 strings. (#5258) - Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254) +- Dev: `clang-tidy` CI now uses Qt 6. (#5273) ## 2.4.6 From 09b2c53383af4e4e35f402a909573675d51e09ad Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 11:56:51 +0100 Subject: [PATCH 03/11] fix: rerender when unpausing (#5265) --- CHANGELOG.md | 1 + src/widgets/helper/ChannelView.cpp | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8287d63a05..1ce0f4f60fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ - Bugfix: Fixed split header tooltips showing in the wrong position on Windows. (#5230) - Bugfix: Fixed split header tooltips appearing too tall. (#5232) - Bugfix: Fixed past messages not showing in the search popup after adding a channel. (#5248) +- Bugfix: Fixed pause indicator not disappearing in some cases. (#5265) - Bugfix: Fixed the usercard popup not floating on tiling WMs on Linux when "Automatically close user popup when it loses focus" setting is enabled. (#3511) - Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056) - Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246) diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 1454b4999de..9c45c6d9ab1 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -536,6 +536,8 @@ void ChannelView::updatePauses() this->pauseScrollMaximumOffset_ = 0; this->queueLayout(); + // make sure we re-render + this->update(); } else if (std::any_of(this->pauses_.begin(), this->pauses_.end(), [](auto &&value) { @@ -560,8 +562,9 @@ void ChannelView::updatePauses() { /// Start the timer this->pauseEnd_ = pauseEnd; - this->pauseTimer_.start( - duration_cast(pauseEnd - SteadyClock::now())); + auto duration = + duration_cast(pauseEnd - SteadyClock::now()); + this->pauseTimer_.start(std::max(duration, 0ms)); } } } From 69bdac9936cd981679a8f5d2bf15b7873292b83f Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 30 Mar 2024 12:28:49 +0100 Subject: [PATCH 04/11] Add `reward.cost` `reward.id`, `reward.title` filter variables (#5275) --- CHANGELOG.md | 1 + src/controllers/filters/lang/Filter.cpp | 12 ++++++++++++ src/controllers/filters/lang/Filter.hpp | 3 +++ src/controllers/filters/lang/Tokenizer.hpp | 6 +++++- src/messages/Message.hpp | 3 +++ src/providers/twitch/TwitchMessageBuilder.cpp | 2 ++ src/widgets/helper/ChannelView.cpp | 14 ++++++++++++++ 7 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce0f4f60fa..b82b51b8a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ - Minor: Image links now reflect the scale of their image instead of an internal label. (#5201) - Minor: IPC files are now stored in the Chatterino directory instead of system directories on Windows. (#5226) - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) +- Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) diff --git a/src/controllers/filters/lang/Filter.cpp b/src/controllers/filters/lang/Filter.cpp index 7ae61991a90..ef0cfd15ce2 100644 --- a/src/controllers/filters/lang/Filter.cpp +++ b/src/controllers/filters/lang/Filter.cpp @@ -120,6 +120,18 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) vars["channel.live"] = false; } } + if (m->reward != nullptr) + { + vars["reward.title"] = m->reward->title; + vars["reward.cost"] = m->reward->cost; + vars["reward.id"] = m->reward->id; + } + else + { + vars["reward.title"] = ""; + vars["reward.cost"] = -1; + vars["reward.id"] = ""; + } return vars; } diff --git a/src/controllers/filters/lang/Filter.hpp b/src/controllers/filters/lang/Filter.hpp index c8afbd76916..01d7a765e9d 100644 --- a/src/controllers/filters/lang/Filter.hpp +++ b/src/controllers/filters/lang/Filter.hpp @@ -48,6 +48,9 @@ static const QMap MESSAGE_TYPING_CONTEXT = { {"flags.monitored", Type::Bool}, {"message.content", Type::String}, {"message.length", Type::Int}, + {"reward.title", Type::String}, + {"reward.cost", Type::Int}, + {"reward.id", Type::String}, }; ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel); diff --git a/src/controllers/filters/lang/Tokenizer.hpp b/src/controllers/filters/lang/Tokenizer.hpp index 2fbc5fd9536..6ca9d373ceb 100644 --- a/src/controllers/filters/lang/Tokenizer.hpp +++ b/src/controllers/filters/lang/Tokenizer.hpp @@ -35,7 +35,11 @@ static const QMap validIdentifiersMap = { {"flags.restricted", "restricted message?"}, {"flags.monitored", "monitored message?"}, {"message.content", "message text"}, - {"message.length", "message length"}}; + {"message.length", "message length"}, + {"reward.title", "point reward title"}, + {"reward.cost", "point reward cost"}, + {"reward.id", "point reward id"}, +}; // clang-format off static const QRegularExpression tokenRegex( diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index b9e0b2321ec..bdbe120ddaa 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -1,6 +1,7 @@ #pragma once #include "common/FlagsEnum.hpp" +#include "providers/twitch/ChannelPointReward.hpp" #include "util/QStringHash.hpp" #include @@ -107,6 +108,8 @@ struct Message { std::vector> elements; ScrollbarHighlight getScrollBarHighlight() const; + + std::shared_ptr reward = nullptr; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 88c0f671e74..524d0375de5 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -1625,6 +1625,8 @@ void TwitchMessageBuilder::appendChannelPointRewardMessage( builder->message().messageText = textList.join(" "); builder->message().searchText = textList.join(" "); builder->message().loginName = reward.user.login; + + builder->message().reward = std::make_shared(reward); } void TwitchMessageBuilder::liveMessage(const QString &channelName, diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 9c45c6d9ab1..2795e5ffe7b 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -268,6 +268,20 @@ void addHiddenContextMenuItems(QMenu *menu, jsonObject["searchText"] = message->searchText; jsonObject["messageText"] = message->messageText; jsonObject["flags"] = qmagicenum::enumFlagsName(message->flags.value()); + if (message->reward) + { + QJsonObject reward; + reward["id"] = message->reward->id; + reward["title"] = message->reward->title; + reward["cost"] = message->reward->cost; + reward["isUserInputRequired"] = + message->reward->isUserInputRequired; + jsonObject["reward"] = reward; + } + else + { + jsonObject["reward"] = QJsonValue(); + } jsonDocument.setObject(jsonObject); From 2f534dc6dabe84c002ca4e54325a779a596980e9 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 30 Mar 2024 14:24:47 +0100 Subject: [PATCH 05/11] fix: override broken base sizes & scales for some Twitch emotes (#5279) --- CHANGELOG.md | 1 + src/providers/twitch/TwitchEmotes.cpp | 407 +++++++++++++++++++++++++- src/providers/twitch/TwitchEmotes.hpp | 1 - 3 files changed, 398 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b82b51b8a47..b3b0c05006d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,6 +104,7 @@ - Bugfix: Fixed popup windows not persisting between restarts. (#5081) - Bugfix: Fixed splits not retaining their focus after minimizing. (#5080) - Bugfix: Fixed _Copy message_ copying the channel name in global search. (#5106) +- Bugfix: Fixed some Twitch emotes sizes being wrong at certain zoom levels. (#5279) - Bugfix: Fixed a missing space when the image uploader provided a delete link. (#5269) - Bugfix: Reply contexts now use the color of the replied-to message. (#5145) - Bugfix: Fixed top-level window getting stuck after opening settings. (#5161, #5166) diff --git a/src/providers/twitch/TwitchEmotes.cpp b/src/providers/twitch/TwitchEmotes.cpp index 4c87e472b61..918d504a46e 100644 --- a/src/providers/twitch/TwitchEmotes.cpp +++ b/src/providers/twitch/TwitchEmotes.cpp @@ -5,6 +5,400 @@ #include "messages/Image.hpp" #include "util/QStringHash.hpp" +namespace { + +using namespace chatterino; + +Url getEmoteLink(const EmoteId &id, const QString &emoteScale) +{ + return {QString(TWITCH_EMOTE_TEMPLATE) + .replace("{id}", id.string) + .replace("{scale}", emoteScale)}; +} + +QSize getEmoteExpectedBaseSize(const EmoteId &id) +{ + // From Twitch docs - expected size for an emote (1x) + constexpr QSize defaultBaseSize(28, 28); + static std::unordered_map outliers{ + {"555555635", {21, 18}}, /* ;p */ + {"555555636", {21, 18}}, /* ;-p */ + {"555555614", {21, 18}}, /* O_o */ + {"555555641", {21, 18}}, /* :z */ + {"555555604", {21, 18}}, /* :\\ */ + {"444", {21, 18}}, /* :| */ + {"555555634", {21, 18}}, /* ;-P */ + {"439", {21, 18}}, /* ;) */ + {"555555642", {21, 18}}, /* :-z */ + {"555555613", {21, 18}}, /* :-o */ + {"555555625", {21, 18}}, /* :-p */ + {"433", {21, 18}}, /* :/ */ + {"555555622", {21, 18}}, /* :P */ + {"555555640", {21, 18}}, /* :-| */ + {"555555623", {21, 18}}, /* :-P */ + {"555555628", {21, 18}}, /* :) */ + {"555555632", {21, 18}}, /* 8-) */ + {"555555667", {20, 18}}, /* ;p */ + {"445", {21, 18}}, /* <3 */ + {"555555668", {20, 18}}, /* ;-p */ + {"555555679", {20, 18}}, /* :z */ + {"483", {20, 18}}, /* <3 */ + {"555555666", {20, 18}}, /* ;-P */ + {"497", {20, 18}}, /* O_o */ + {"555555664", {20, 18}}, /* :-p */ + {"555555671", {20, 18}}, /* :o */ + {"555555681", {20, 18}}, /* :Z */ + {"555555672", {20, 18}}, /* :-o */ + {"555555676", {20, 18}}, /* :-\\ */ + {"555555611", {21, 18}}, /* :-O */ + {"555555670", {20, 18}}, /* :-O */ + {"555555688", {20, 18}}, /* :-D */ + {"441", {21, 18}}, /* B) */ + {"555555601", {21, 18}}, /* >( */ + {"491", {20, 18}}, /* ;P */ + {"496", {20, 18}}, /* :D */ + {"492", {20, 18}}, /* :O */ + {"555555573", {24, 18}}, /* o_O */ + {"555555643", {21, 18}}, /* :Z */ + {"1898", {26, 28}}, /* ThunBeast */ + {"555555682", {20, 18}}, /* :-Z */ + {"1896", {20, 30}}, /* WholeWheat */ + {"1906", {24, 30}}, /* SoBayed */ + {"555555607", {21, 18}}, /* :-( */ + {"555555660", {20, 18}}, /* :-( */ + {"489", {20, 18}}, /* :( */ + {"495", {20, 18}}, /* :s */ + {"555555638", {21, 18}}, /* :-D */ + {"357", {28, 30}}, /* HotPokket */ + {"555555624", {21, 18}}, /* :p */ + {"73", {21, 30}}, /* DBstyle */ + {"555555674", {20, 18}}, /* :-/ */ + {"555555629", {21, 18}}, /* :-) */ + {"555555600", {24, 18}}, /* R-) */ + {"41", {19, 27}}, /* Kreygasm */ + {"555555612", {21, 18}}, /* :o */ + {"488", {29, 24}}, /* :7 */ + {"69", {41, 28}}, /* BloodTrail */ + {"555555608", {21, 18}}, /* R) */ + {"501", {20, 18}}, /* ;) */ + {"50", {18, 27}}, /* ArsonNoSexy */ + {"443", {21, 18}}, /* :D */ + {"1904", {24, 30}}, /* BigBrother */ + {"555555595", {24, 18}}, /* ;P */ + {"555555663", {20, 18}}, /* :p */ + {"555555576", {24, 18}}, /* o.o */ + {"360", {22, 30}}, /* FailFish */ + {"500", {20, 18}}, /* B) */ + {"3", {24, 18}}, /* :D */ + {"484", {20, 22}}, /* R) */ + {"555555678", {20, 18}}, /* :-| */ + {"7", {24, 18}}, /* B) */ + {"52", {32, 32}}, /* SMOrc */ + {"555555644", {21, 18}}, /* :-Z */ + {"18", {20, 27}}, /* TheRinger */ + {"49106", {27, 28}}, /* CorgiDerp */ + {"6", {24, 18}}, /* O_o */ + {"10", {24, 18}}, /* :/ */ + {"47", {24, 24}}, /* PunchTrees */ + {"555555561", {24, 18}}, /* :-D */ + {"555555564", {24, 18}}, /* :-| */ + {"13", {24, 18}}, /* ;P */ + {"555555593", {24, 18}}, /* :p */ + {"555555589", {24, 18}}, /* ;) */ + {"555555590", {24, 18}}, /* ;-) */ + {"486", {27, 42}}, /* :> */ + {"40", {21, 27}}, /* KevinTurtle */ + {"555555558", {24, 18}}, /* :( */ + {"555555597", {24, 18}}, /* ;p */ + {"555555580", {24, 18}}, /* :O */ + {"555555567", {24, 18}}, /* :Z */ + {"1", {24, 18}}, /* :) */ + {"11", {24, 18}}, /* ;) */ + {"33", {25, 32}}, /* DansGame */ + {"555555586", {24, 18}}, /* :-/ */ + {"4", {24, 18}}, /* >( */ + {"555555588", {24, 18}}, /* :-\\ */ + {"12", {24, 18}}, /* :P */ + {"555555563", {24, 18}}, /* :| */ + {"555555581", {24, 18}}, /* :-O */ + {"555555598", {24, 18}}, /* ;-p */ + {"555555596", {24, 18}}, /* ;-P */ + {"555555557", {24, 18}}, /* :-) */ + {"498", {20, 18}}, /* >( */ + {"555555680", {20, 18}}, /* :-z */ + {"555555587", {24, 18}}, /* :\\ */ + {"5", {24, 18}}, /* :| */ + {"354", {20, 30}}, /* 4Head */ + {"555555562", {24, 18}}, /* >( */ + {"555555594", {24, 18}}, /* :-p */ + {"490", {20, 18}}, /* :P */ + {"555555662", {20, 18}}, /* :-P */ + {"2", {24, 18}}, /* :( */ + {"1902", {27, 29}}, /* Keepo */ + {"555555627", {21, 18}}, /* ;-) */ + {"555555566", {24, 18}}, /* :-z */ + {"555555559", {24, 18}}, /* :-( */ + {"555555592", {24, 18}}, /* :-P */ + {"28", {39, 27}}, /* MrDestructoid */ + {"8", {24, 18}}, /* :O */ + {"244", {24, 30}}, /* FUNgineer */ + {"555555591", {24, 18}}, /* :P */ + {"555555585", {24, 18}}, /* :/ */ + {"494", {20, 18}}, /* :| */ + {"9", {24, 18}}, /* <3 */ + {"555555584", {24, 18}}, /* <3 */ + {"555555579", {24, 18}}, /* 8-) */ + {"14", {24, 18}}, /* R) */ + {"485", {27, 18}}, /* #/ */ + {"555555560", {24, 18}}, /* :D */ + {"86", {36, 30}}, /* BibleThump */ + {"555555578", {24, 18}}, /* B-) */ + {"17", {20, 27}}, /* StoneLightning */ + {"436", {21, 18}}, /* :O */ + {"555555675", {20, 18}}, /* :\\ */ + {"22", {19, 27}}, /* RedCoat */ + {"555555574", {24, 18}}, /* o.O */ + {"555555603", {21, 18}}, /* :-/ */ + {"1901", {24, 28}}, /* Kippa */ + {"15", {21, 27}}, /* JKanStyle */ + {"555555605", {21, 18}}, /* :-\\ */ + {"555555701", {20, 18}}, /* ;-) */ + {"487", {20, 42}}, /* <] */ + {"555555572", {24, 18}}, /* O.O */ + {"65", {40, 30}}, /* FrankerZ */ + {"25", {25, 28}}, /* Kappa */ + {"36", {36, 30}}, /* PJSalt */ + {"499", {20, 18}}, /* :) */ + {"555555565", {24, 18}}, /* :z */ + {"434", {21, 18}}, /* :( */ + {"555555577", {24, 18}}, /* B) */ + {"34", {21, 28}}, /* SwiftRage */ + {"555555575", {24, 18}}, /* o_o */ + {"92", {23, 30}}, /* PMSTwin */ + {"555555570", {24, 18}}, /* O.o */ + {"555555569", {24, 18}}, /* O_o */ + {"493", {20, 18}}, /* :/ */ + {"26", {20, 27}}, /* JonCarnage */ + {"66", {20, 27}}, /* OneHand */ + {"555555568", {24, 18}}, /* :-Z */ + {"555555599", {24, 18}}, /* R) */ + {"1900", {33, 30}}, /* RalpherZ */ + {"555555582", {24, 18}}, /* :o */ + {"1899", {22, 30}}, /* TF2John */ + {"555555633", {21, 18}}, /* ;P */ + {"16", {22, 27}}, /* OptimizePrime */ + {"30", {29, 27}}, /* BCWarrior */ + {"555555583", {24, 18}}, /* :-o */ + {"32", {21, 27}}, /* GingerPower */ + {"87", {24, 30}}, /* ShazBotstix */ + {"74", {24, 30}}, /* AsianGlow */ + {"555555571", {24, 18}}, /* O_O */ + {"46", {24, 24}}, /* SSSsss */ + }; + + auto it = outliers.find(id.string); + if (it != outliers.end()) + { + return it->second; + } + + return defaultBaseSize; +} + +qreal getEmote3xScaleFactor(const EmoteId &id) +{ + // From Twitch docs - expected size for an emote (1x) + constexpr qreal default3xScaleFactor = 0.25; + static std::unordered_map outliers{ + {"555555635", 0.3333333333333333}, /* ;p */ + {"555555636", 0.3333333333333333}, /* ;-p */ + {"555555614", 0.3333333333333333}, /* O_o */ + {"555555641", 0.3333333333333333}, /* :z */ + {"555555604", 0.3333333333333333}, /* :\\ */ + {"444", 0.3333333333333333}, /* :| */ + {"555555634", 0.3333333333333333}, /* ;-P */ + {"439", 0.3333333333333333}, /* ;) */ + {"555555642", 0.3333333333333333}, /* :-z */ + {"555555613", 0.3333333333333333}, /* :-o */ + {"555555625", 0.3333333333333333}, /* :-p */ + {"433", 0.3333333333333333}, /* :/ */ + {"555555622", 0.3333333333333333}, /* :P */ + {"555555640", 0.3333333333333333}, /* :-| */ + {"555555623", 0.3333333333333333}, /* :-P */ + {"555555628", 0.3333333333333333}, /* :) */ + {"555555632", 0.3333333333333333}, /* 8-) */ + {"555555667", 0.3333333333333333}, /* ;p */ + {"445", 0.3333333333333333}, /* <3 */ + {"555555668", 0.3333333333333333}, /* ;-p */ + {"555555679", 0.3333333333333333}, /* :z */ + {"483", 0.3333333333333333}, /* <3 */ + {"555555666", 0.3333333333333333}, /* ;-P */ + {"497", 0.3333333333333333}, /* O_o */ + {"555555664", 0.3333333333333333}, /* :-p */ + {"555555671", 0.3333333333333333}, /* :o */ + {"555555681", 0.3333333333333333}, /* :Z */ + {"555555672", 0.3333333333333333}, /* :-o */ + {"555555676", 0.3333333333333333}, /* :-\\ */ + {"555555611", 0.3333333333333333}, /* :-O */ + {"555555670", 0.3333333333333333}, /* :-O */ + {"555555688", 0.3333333333333333}, /* :-D */ + {"441", 0.3333333333333333}, /* B) */ + {"555555601", 0.3333333333333333}, /* >( */ + {"491", 0.3333333333333333}, /* ;P */ + {"496", 0.3333333333333333}, /* :D */ + {"492", 0.3333333333333333}, /* :O */ + {"555555573", 0.3333333333333333}, /* o_O */ + {"555555643", 0.3333333333333333}, /* :Z */ + {"1898", 0.3333333333333333}, /* ThunBeast */ + {"555555682", 0.3333333333333333}, /* :-Z */ + {"1896", 0.3333333333333333}, /* WholeWheat */ + {"1906", 0.3333333333333333}, /* SoBayed */ + {"555555607", 0.3333333333333333}, /* :-( */ + {"555555660", 0.3333333333333333}, /* :-( */ + {"489", 0.3333333333333333}, /* :( */ + {"495", 0.3333333333333333}, /* :s */ + {"555555638", 0.3333333333333333}, /* :-D */ + {"357", 0.3333333333333333}, /* HotPokket */ + {"555555624", 0.3333333333333333}, /* :p */ + {"73", 0.3333333333333333}, /* DBstyle */ + {"555555674", 0.3333333333333333}, /* :-/ */ + {"555555629", 0.3333333333333333}, /* :-) */ + {"555555600", 0.3333333333333333}, /* R-) */ + {"41", 0.3333333333333333}, /* Kreygasm */ + {"555555612", 0.3333333333333333}, /* :o */ + {"488", 0.3333333333333333}, /* :7 */ + {"69", 0.3333333333333333}, /* BloodTrail */ + {"555555608", 0.3333333333333333}, /* R) */ + {"501", 0.3333333333333333}, /* ;) */ + {"50", 0.3333333333333333}, /* ArsonNoSexy */ + {"443", 0.3333333333333333}, /* :D */ + {"1904", 0.3333333333333333}, /* BigBrother */ + {"555555595", 0.3333333333333333}, /* ;P */ + {"555555663", 0.3333333333333333}, /* :p */ + {"555555576", 0.3333333333333333}, /* o.o */ + {"360", 0.3333333333333333}, /* FailFish */ + {"500", 0.3333333333333333}, /* B) */ + {"3", 0.3333333333333333}, /* :D */ + {"484", 0.3333333333333333}, /* R) */ + {"555555678", 0.3333333333333333}, /* :-| */ + {"7", 0.3333333333333333}, /* B) */ + {"52", 0.3333333333333333}, /* SMOrc */ + {"555555644", 0.3333333333333333}, /* :-Z */ + {"18", 0.3333333333333333}, /* TheRinger */ + {"49106", 0.3333333333333333}, /* CorgiDerp */ + {"6", 0.3333333333333333}, /* O_o */ + {"10", 0.3333333333333333}, /* :/ */ + {"47", 0.3333333333333333}, /* PunchTrees */ + {"555555561", 0.3333333333333333}, /* :-D */ + {"555555564", 0.3333333333333333}, /* :-| */ + {"13", 0.3333333333333333}, /* ;P */ + {"555555593", 0.3333333333333333}, /* :p */ + {"555555589", 0.3333333333333333}, /* ;) */ + {"555555590", 0.3333333333333333}, /* ;-) */ + {"486", 0.3333333333333333}, /* :> */ + {"40", 0.3333333333333333}, /* KevinTurtle */ + {"555555558", 0.3333333333333333}, /* :( */ + {"555555597", 0.3333333333333333}, /* ;p */ + {"555555580", 0.3333333333333333}, /* :O */ + {"555555567", 0.3333333333333333}, /* :Z */ + {"1", 0.3333333333333333}, /* :) */ + {"11", 0.3333333333333333}, /* ;) */ + {"33", 0.3333333333333333}, /* DansGame */ + {"555555586", 0.3333333333333333}, /* :-/ */ + {"4", 0.3333333333333333}, /* >( */ + {"555555588", 0.3333333333333333}, /* :-\\ */ + {"12", 0.3333333333333333}, /* :P */ + {"555555563", 0.3333333333333333}, /* :| */ + {"555555581", 0.3333333333333333}, /* :-O */ + {"555555598", 0.3333333333333333}, /* ;-p */ + {"555555596", 0.3333333333333333}, /* ;-P */ + {"555555557", 0.3333333333333333}, /* :-) */ + {"498", 0.3333333333333333}, /* >( */ + {"555555680", 0.3333333333333333}, /* :-z */ + {"555555587", 0.3333333333333333}, /* :\\ */ + {"5", 0.3333333333333333}, /* :| */ + {"354", 0.3333333333333333}, /* 4Head */ + {"555555562", 0.3333333333333333}, /* >( */ + {"555555594", 0.3333333333333333}, /* :-p */ + {"490", 0.3333333333333333}, /* :P */ + {"555555662", 0.3333333333333333}, /* :-P */ + {"2", 0.3333333333333333}, /* :( */ + {"1902", 0.3333333333333333}, /* Keepo */ + {"555555627", 0.3333333333333333}, /* ;-) */ + {"555555566", 0.3333333333333333}, /* :-z */ + {"555555559", 0.3333333333333333}, /* :-( */ + {"555555592", 0.3333333333333333}, /* :-P */ + {"28", 0.3333333333333333}, /* MrDestructoid */ + {"8", 0.3333333333333333}, /* :O */ + {"244", 0.3333333333333333}, /* FUNgineer */ + {"555555591", 0.3333333333333333}, /* :P */ + {"555555585", 0.3333333333333333}, /* :/ */ + {"494", 0.3333333333333333}, /* :| */ + {"9", 0.21428571428571427}, /* <3 */ + {"555555584", 0.21428571428571427}, /* <3 */ + {"555555579", 0.3333333333333333}, /* 8-) */ + {"14", 0.3333333333333333}, /* R) */ + {"485", 0.3333333333333333}, /* #/ */ + {"555555560", 0.3333333333333333}, /* :D */ + {"86", 0.3333333333333333}, /* BibleThump */ + {"555555578", 0.3333333333333333}, /* B-) */ + {"17", 0.3333333333333333}, /* StoneLightning */ + {"436", 0.3333333333333333}, /* :O */ + {"555555675", 0.3333333333333333}, /* :\\ */ + {"22", 0.3333333333333333}, /* RedCoat */ + {"245", 0.3333333333333333}, /* ResidentSleeper */ + {"555555574", 0.3333333333333333}, /* o.O */ + {"555555603", 0.3333333333333333}, /* :-/ */ + {"1901", 0.3333333333333333}, /* Kippa */ + {"15", 0.3333333333333333}, /* JKanStyle */ + {"555555605", 0.3333333333333333}, /* :-\\ */ + {"555555701", 0.3333333333333333}, /* ;-) */ + {"487", 0.3333333333333333}, /* <] */ + {"22639", 0.3333333333333333}, /* BabyRage */ + {"555555572", 0.3333333333333333}, /* O.O */ + {"65", 0.3333333333333333}, /* FrankerZ */ + {"25", 0.3333333333333333}, /* Kappa */ + {"36", 0.3333333333333333}, /* PJSalt */ + {"499", 0.3333333333333333}, /* :) */ + {"555555565", 0.3333333333333333}, /* :z */ + {"434", 0.3333333333333333}, /* :( */ + {"555555577", 0.3333333333333333}, /* B) */ + {"34", 0.3333333333333333}, /* SwiftRage */ + {"555555575", 0.3333333333333333}, /* o_o */ + {"92", 0.3333333333333333}, /* PMSTwin */ + {"555555570", 0.3333333333333333}, /* O.o */ + {"555555569", 0.3333333333333333}, /* O_o */ + {"493", 0.3333333333333333}, /* :/ */ + {"26", 0.3333333333333333}, /* JonCarnage */ + {"66", 0.3333333333333333}, /* OneHand */ + {"973", 0.3333333333333333}, /* DAESuppy */ + {"555555568", 0.3333333333333333}, /* :-Z */ + {"555555599", 0.3333333333333333}, /* R) */ + {"1900", 0.3333333333333333}, /* RalpherZ */ + {"555555582", 0.3333333333333333}, /* :o */ + {"1899", 0.3333333333333333}, /* TF2John */ + {"555555633", 0.3333333333333333}, /* ;P */ + {"16", 0.3333333333333333}, /* OptimizePrime */ + {"30", 0.3333333333333333}, /* BCWarrior */ + {"555555583", 0.3333333333333333}, /* :-o */ + {"32", 0.3333333333333333}, /* GingerPower */ + {"87", 0.3333333333333333}, /* ShazBotstix */ + {"74", 0.3333333333333333}, /* AsianGlow */ + {"555555571", 0.3333333333333333}, /* O_O */ + {"46", 0.3333333333333333}, /* SSSsss */ + }; + + auto it = outliers.find(id.string); + if (it != outliers.end()) + { + return it->second; + } + + return default3xScaleFactor; +} + +} // namespace + namespace chatterino { QString TwitchEmotes::cleanUpEmoteCode(const QString &dirtyEmoteCode) @@ -44,14 +438,14 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id, if (!shared) { - // From Twitch docs - expected size for an emote (1x) - constexpr QSize baseSize(28, 28); + auto baseSize = getEmoteExpectedBaseSize(id); (*cache)[id] = shared = std::make_shared(Emote{ EmoteName{name}, ImageSet{ Image::fromUrl(getEmoteLink(id, "1.0"), 1, baseSize), Image::fromUrl(getEmoteLink(id, "2.0"), 0.5, baseSize * 2), - Image::fromUrl(getEmoteLink(id, "3.0"), 0.25, baseSize * 4), + Image::fromUrl(getEmoteLink(id, "3.0"), + getEmote3xScaleFactor(id), baseSize * 4), }, Tooltip{name.toHtmlEscaped() + "
Twitch Emote"}, }); @@ -60,11 +454,4 @@ EmotePtr TwitchEmotes::getOrCreateEmote(const EmoteId &id, return shared; } -Url TwitchEmotes::getEmoteLink(const EmoteId &id, const QString &emoteScale) -{ - return {QString(TWITCH_EMOTE_TEMPLATE) - .replace("{id}", id.string) - .replace("{scale}", emoteScale)}; -} - } // namespace chatterino diff --git a/src/providers/twitch/TwitchEmotes.hpp b/src/providers/twitch/TwitchEmotes.hpp index d793ce72338..17e50b11fcb 100644 --- a/src/providers/twitch/TwitchEmotes.hpp +++ b/src/providers/twitch/TwitchEmotes.hpp @@ -52,7 +52,6 @@ class TwitchEmotes : public ITwitchEmotes const EmoteName &name) override; private: - Url getEmoteLink(const EmoteId &id, const QString &emoteScale); UniqueAccess>> twitchEmotesCache_; }; From b35f10fa540562e8c524ec31ae51e3d4fb139214 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 14:50:58 +0100 Subject: [PATCH 06/11] chore: require newline at EOF (#5278) --- .clang-format | 1 + CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.clang-format b/.clang-format index 0feaad9dc10..cfbe49d31fe 100644 --- a/.clang-format +++ b/.clang-format @@ -50,3 +50,4 @@ PointerBindsToType: false SpacesBeforeTrailingComments: 2 Standard: Auto ReflowComments: false +InsertNewlineAtEOF: true diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b0c05006d..d89ebd1eec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -198,6 +198,7 @@ - Dev: Adapted `magic_enum` to Qt's Utf-16 strings. (#5258) - Dev: `NetworkManager`'s statics are now created in its `init` method. (#5254) - Dev: `clang-tidy` CI now uses Qt 6. (#5273) +- Dev: Enabled `InsertNewlineAtEOF` in `clang-format`. (#5278) ## 2.4.6 From d4b8feac7d86f941c09c4a1f54743d2882d9a54a Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 30 Mar 2024 15:23:02 +0100 Subject: [PATCH 07/11] lua: Change CompletionRequested handler to use an event table. (#5280) --- CHANGELOG.md | 1 + docs/chatterino.d.ts | 14 ++++++----- docs/wip-plugins.md | 6 ++--- src/controllers/plugins/LuaAPI.hpp | 26 +++++++++++++++++++- src/controllers/plugins/LuaUtilities.cpp | 14 +++++++++++ src/controllers/plugins/LuaUtilities.hpp | 2 ++ src/controllers/plugins/Plugin.hpp | 6 ++--- src/controllers/plugins/PluginController.cpp | 8 ++++-- 8 files changed, 62 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d89ebd1eec1..f9ec7507035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ - Minor: IPC files are now stored in the Chatterino directory instead of system directories on Windows. (#5226) - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) - Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275) +- Minor: Change Lua `CompletionRequested` handler to use an event table. (#5280) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) diff --git a/docs/chatterino.d.ts b/docs/chatterino.d.ts index 9bf6f57c0fb..95d2282be9f 100644 --- a/docs/chatterino.d.ts +++ b/docs/chatterino.d.ts @@ -75,6 +75,13 @@ declare module c2 { handler: (ctx: CommandContext) => void ): boolean; + class CompletionEvent { + query: string; + full_text_content: string; + cursor_position: number; + is_first_word: boolean; + } + class CompletionList { values: String[]; hide_others: boolean; @@ -84,12 +91,7 @@ declare module c2 { CompletionRequested = "CompletionRequested", } - type CbFuncCompletionsRequested = ( - query: string, - full_text_content: string, - cursor_position: number, - is_first_word: boolean - ) => CompletionList; + type CbFuncCompletionsRequested = (ev: CompletionEvent) => CompletionList; type CbFunc = T extends EventType.CompletionRequested ? CbFuncCompletionsRequested : never; diff --git a/docs/wip-plugins.md b/docs/wip-plugins.md index 1309d7bab0c..32eda387fd1 100644 --- a/docs/wip-plugins.md +++ b/docs/wip-plugins.md @@ -167,7 +167,7 @@ Limitations/known issues: #### `register_callback("CompletionRequested", handler)` -Registers a callback (`handler`) to process completions. The callback gets the following parameters: +Registers a callback (`handler`) to process completions. The callback takes a single table with the following entries: - `query`: The queried word. - `full_text_content`: The whole input. @@ -190,8 +190,8 @@ end c2.register_callback( "CompletionRequested", - function(query, full_text_content, cursor_position, is_first_word) - if ("!join"):startswith(query) then + function(event) + if ("!join"):startswith(event.query) then ---@type CompletionList return { hide_others = true, values = { "!join" } } end diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index df042b24f5c..39df152169a 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -5,6 +5,8 @@ extern "C" { # include } +# include "controllers/plugins/LuaUtilities.hpp" + # include # include @@ -55,6 +57,28 @@ struct CompletionList { bool hideOthers{}; }; +/** + * @lua@class CompletionEvent + */ +struct CompletionEvent { + /** + * @lua@field query string The word being completed + */ + QString query; + /** + * @lua@field full_text_content string Content of the text input + */ + QString full_text_content; + /** + * @lua@field cursor_position integer Position of the cursor in the text input in unicode codepoints (not bytes) + */ + int cursor_position{}; + /** + * @lua@field is_first_word boolean True if this is the first word in the input + */ + bool is_first_word{}; +}; + /** * @includefile common/Channel.hpp * @includefile controllers/plugins/api/ChannelRef.hpp @@ -74,7 +98,7 @@ int c2_register_command(lua_State *L); * Registers a callback to be invoked when completions for a term are requested. * * @lua@param type "CompletionRequested" - * @lua@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked. + * @lua@param func fun(event: CompletionEvent): CompletionList The callback to be invoked. * @exposed c2.register_callback */ int c2_register_callback(lua_State *L); diff --git a/src/controllers/plugins/LuaUtilities.cpp b/src/controllers/plugins/LuaUtilities.cpp index 9361cd1ff3e..64af18c0133 100644 --- a/src/controllers/plugins/LuaUtilities.cpp +++ b/src/controllers/plugins/LuaUtilities.cpp @@ -142,6 +142,20 @@ StackIdx push(lua_State *L, const int &b) return lua_gettop(L); } +StackIdx push(lua_State *L, const api::CompletionEvent &ev) +{ + auto idx = pushEmptyTable(L, 4); +# define PUSH(field) \ + lua::push(L, ev.field); \ + lua_setfield(L, idx, #field) + PUSH(query); + PUSH(full_text_content); + PUSH(cursor_position); + PUSH(is_first_word); +# undef PUSH + return idx; +} + bool peek(lua_State *L, int *out, StackIdx idx) { StackGuard guard(L); diff --git a/src/controllers/plugins/LuaUtilities.hpp b/src/controllers/plugins/LuaUtilities.hpp index 4c78d6edc9a..5443a751f79 100644 --- a/src/controllers/plugins/LuaUtilities.hpp +++ b/src/controllers/plugins/LuaUtilities.hpp @@ -28,6 +28,7 @@ namespace chatterino::lua { namespace api { struct CompletionList; + struct CompletionEvent; } // namespace api constexpr int ERROR_BAD_PEEK = LUA_OK - 1; @@ -66,6 +67,7 @@ StackIdx push(lua_State *L, const QString &str); StackIdx push(lua_State *L, const std::string &str); StackIdx push(lua_State *L, const bool &b); StackIdx push(lua_State *L, const int &b); +StackIdx push(lua_State *L, const api::CompletionEvent &ev); // returns OK? bool peek(lua_State *L, int *out, StackIdx idx = -1); diff --git a/src/controllers/plugins/Plugin.hpp b/src/controllers/plugins/Plugin.hpp index 4450b2a0192..2adbe9067fb 100644 --- a/src/controllers/plugins/Plugin.hpp +++ b/src/controllers/plugins/Plugin.hpp @@ -98,8 +98,8 @@ class Plugin // Note: The CallbackFunction object's destructor will remove the function from the lua stack using LuaCompletionCallback = - lua::CallbackFunction; + lua::CallbackFunction; std::optional getCompletionCallback() { if (this->state_ == nullptr || !this->error_.isNull()) @@ -123,7 +123,7 @@ class Plugin // move return std::make_optional>( + lua::api::CompletionList, lua::api::CompletionEvent>>( this->state_, lua_gettop(this->state_)); } diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index 0f23df3430d..8c2d8055619 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -433,8 +433,12 @@ std::pair PluginController::updateCustomCompletions( qCDebug(chatterinoLua) << "Processing custom completions from plugin" << name; auto &cb = *opt; - auto errOrList = - cb(query, fullTextContent, cursorPosition, isFirstWord); + auto errOrList = cb(lua::api::CompletionEvent{ + .query = query, + .full_text_content = fullTextContent, + .cursor_position = cursorPosition, + .is_first_word = isFirstWord, + }); if (std::holds_alternative(errOrList)) { guard.handled(); From c1bd5d11d057ae15e8866511bdc9645466e5ec68 Mon Sep 17 00:00:00 2001 From: nerix Date: Sat, 30 Mar 2024 22:11:52 +0100 Subject: [PATCH 08/11] refactor: improve LuaLS generator (#5283) --- CHANGELOG.md | 2 +- docs/plugin-meta.lua | 71 ++-- scripts/make_luals_meta.py | 385 ++++++++++++++------- src/controllers/plugins/LuaAPI.hpp | 2 +- src/controllers/plugins/api/ChannelRef.hpp | 11 +- 5 files changed, 307 insertions(+), 164 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ec7507035..01c49ca3401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -179,7 +179,7 @@ - Dev: Added the ability to show `ChannelView`s without a `Split`. (#4747) - Dev: Refactor Args to be less of a singleton. (#5041) - Dev: Channels without any animated elements on screen will skip updates from the GIF timer. (#5042, #5043, #5045) -- Dev: Autogenerate docs/plugin-meta.lua. (#5055) +- Dev: Autogenerate docs/plugin-meta.lua. (#5055, #5283) - Dev: Changed Ubuntu & AppImage builders to statically link Qt. (#5151) - Dev: Refactor `NetworkPrivate`. (#5063) - Dev: Refactor `Paths` & `Updates`, focusing on reducing their singletoniability. (#5092, #5102) diff --git a/docs/plugin-meta.lua b/docs/plugin-meta.lua index 2cc56af5994..7b72b46d5f3 100644 --- a/docs/plugin-meta.lua +++ b/docs/plugin-meta.lua @@ -6,22 +6,14 @@ c2 = {} ----@class IWeakResource - ---- Returns true if the channel this object points to is valid. ---- If the object expired, returns false ---- If given a non-Channel object, it errors. ----@return boolean -function IWeakResource:is_valid() end - - ----@alias LogLevel integer ----@type { Debug: LogLevel, Info: LogLevel, Warning: LogLevel, Critical: LogLevel } +---@alias c2.LogLevel integer +---@type { Debug: c2.LogLevel, Info: c2.LogLevel, Warning: c2.LogLevel, Critical: c2.LogLevel } c2.LogLevel = {} ----@alias EventType integer ----@type { CompletionRequested: EventType } +---@alias c2.EventType integer +---@type { CompletionRequested: c2.EventType } c2.EventType = {} + ---@class CommandContext ---@field words string[] The words typed when executing the command. For example `/foo bar baz` will result in `{"/foo", "bar", "baz"}`. ---@field channel Channel The channel the command was executed in. @@ -29,20 +21,31 @@ c2.EventType = {} ---@class CompletionList ---@field values string[] The completions ---@field hide_others boolean Whether other completions from Chatterino should be hidden/ignored. --- Now including data from src/common/Channel.hpp. + +---@class CompletionEvent +---@field query string The word being completed +---@field full_text_content string Content of the text input +---@field cursor_position integer Position of the cursor in the text input in unicode codepoints (not bytes) +---@field is_first_word boolean True if this is the first word in the input + +-- Begin src/common/Channel.hpp ---@alias ChannelType integer ----@type { None: ChannelType } +---@type { None: ChannelType, Direct: ChannelType, Twitch: ChannelType, TwitchWhispers: ChannelType, TwitchWatching: ChannelType, TwitchMentions: ChannelType, TwitchLive: ChannelType, TwitchAutomod: ChannelType, TwitchEnd: ChannelType, Irc: ChannelType, Misc: ChannelType } ChannelType = {} --- Back to src/controllers/plugins/LuaAPI.hpp. --- Now including data from src/controllers/plugins/api/ChannelRef.hpp. ---- This enum describes a platform for the purpose of searching for a channel. ---- Currently only Twitch is supported because identifying IRC channels is tricky. + +-- End src/common/Channel.hpp + +-- Begin src/controllers/plugins/api/ChannelRef.hpp ---@alias Platform integer +--- This enum describes a platform for the purpose of searching for a channel. +--- Currently only Twitch is supported because identifying IRC channels is tricky. ---@type { Twitch: Platform } Platform = {} ----@class Channel: IWeakResource + +---@class Channel +Channel = {} --- Returns true if the channel this object points to is valid. --- If the object expired, returns false @@ -82,11 +85,9 @@ function Channel:add_system_message(message) end --- Compares the channel Type. Note that enum values aren't guaranteed, just --- that they are equal to the exposed enum. --- ----@return bool +---@return boolean function Channel:is_twitch_channel() end ---- Twitch Channel specific functions - --- Returns a copy of the channel mode settings (subscriber only, r9k etc.) --- ---@return RoomModes @@ -119,15 +120,10 @@ function Channel:is_mod() end ---@return boolean function Channel:is_vip() end ---- Misc - ---@return string function Channel:__tostring() end ---- Static functions - --- Finds a channel by name. ---- --- Misc channels are marked as Twitch: --- - /whispers --- - /mentions @@ -142,19 +138,15 @@ function Channel.by_name(name, platform) end --- Finds a channel by the Twitch user ID of its owner. --- ----@param string id ID of the owner of the channel. +---@param id string ID of the owner of the channel. ---@return Channel? -function Channel.by_twitch_id(string) end +function Channel.by_twitch_id(id) end ---@class RoomModes ---@field unique_chat boolean You might know this as r9kbeta or robot9000. ---@field subscriber_only boolean ----@field emotes_only boolean Whether or not text is allowed in messages. - ---- Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes - ----@field unique_chat number? Time in minutes you need to follow to chat or nil. - +---@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes +---@field follower_only number? Time in minutes you need to follow to chat or nil. ---@field slow_mode number? Time in seconds you need to wait before sending messages or nil. ---@class StreamStatus @@ -164,7 +156,8 @@ function Channel.by_twitch_id(string) end ---@field title string Stream title or last stream title ---@field game_name string ---@field game_id string --- Back to src/controllers/plugins/LuaAPI.hpp. + +-- End src/controllers/plugins/api/ChannelRef.hpp --- Registers a new command called `name` which when executed will call `handler`. --- @@ -176,12 +169,12 @@ function c2.register_command(name, handler) end --- Registers a callback to be invoked when completions for a term are requested. --- ---@param type "CompletionRequested" ----@param func fun(query: string, full_text_content: string, cursor_position: integer, is_first_word: boolean): CompletionList The callback to be invoked. +---@param func fun(event: CompletionEvent): CompletionList The callback to be invoked. function c2.register_callback(type, func) end --- Writes a message to the Chatterino log. --- ----@param level LogLevel The desired level. +---@param level c2.LogLevel The desired level. ---@param ... any Values to log. Should be convertible to a string with `tostring()`. function c2.log(level, ...) end diff --git a/scripts/make_luals_meta.py b/scripts/make_luals_meta.py index 22240da1984..58a06242806 100644 --- a/scripts/make_luals_meta.py +++ b/scripts/make_luals_meta.py @@ -12,25 +12,26 @@ - Do not have any useful info on '/**' and '*/' lines. - Class members are not allowed to have non-@command lines and commands different from @lua@field -When this scripts sees "@brief", any further lines of the comment will be ignored +Only entire comment blocks are used. One comment block can describe at most one +entity (function/class/enum). Blocks without commands are ignored. Valid commands are: 1. @exposeenum [dotted.name.in_lua.last_part] Define a table with keys of the enum. Values behind those keys aren't written on purpose. - This generates three lines: - - An type alias of [last_part] to integer, - - A type description that describes available values of the enum, - - A global table definition for the num -2. @lua[@command] +2. @exposed [c2.name] + Generates a function definition line from the last `@lua@param`s. +3. @lua[@command] Writes [@command] to the file as a comment, usually this is @class, @param, @return, ... @lua@class and @lua@field have special treatment when it comes to generation of spacing new lines -3. @exposed [c2.name] - Generates a function definition line from the last `@lua@param`s. Non-command lines of comments are written with a space after '---' """ + +from io import TextIOWrapper from pathlib import Path +import re +from typing import Optional BOILERPLATE = """ ---@meta Chatterino2 @@ -41,14 +42,6 @@ c2 = {} ----@class IWeakResource - ---- Returns true if the channel this object points to is valid. ---- If the object expired, returns false ---- If given a non-Channel object, it errors. ----@return boolean -function IWeakResource:is_valid() end - """ repo_root = Path(__file__).parent.parent @@ -58,116 +51,274 @@ print("Writing to", lua_meta.relative_to(repo_root)) -def process_file(target, out): - print("Reading from", target.relative_to(repo_root)) - with target.open("r") as f: - lines = f.read().splitlines() +def strip_line(line: str): + return re.sub(r"^/\*\*|^\*|\*/$", "", line).strip() - # Are we in a doc comment? - comment: bool = False - # This is set when @brief is encountered, making the rest of the comment be - # ignored - ignore_this_comment: bool = False - - # Last `@lua@param`s seen - for @exposed generation - last_params_names: list[str] = [] - # Are we in a `@lua@class` definition? - makes newlines around @lua@class and @lua@field prettier - is_class = False - - # The name of the next enum in lua world - expose_next_enum_as: str | None = None - # Name of the current enum in c++ world, used to generate internal typenames for - current_enum_name: str | None = None - for line_num, line in enumerate(lines): - line = line.strip() - loc = f'{target.relative_to(repo_root)}:{line_num}' - if line.startswith("enum class "): - line = line.removeprefix("enum class ") - temp = line.split(" ", 2) - current_enum_name = temp[0] - if not expose_next_enum_as: - print( - f"{loc} Skipping enum {current_enum_name}, there wasn't a @exposeenum command" - ) - current_enum_name = None + +def is_comment_start(line: str): + return line.startswith("/**") + + +def is_enum_class(line: str): + return line.startswith("enum class") + + +def is_class(line: str): + return line.startswith(("class", "struct")) + + +class Reader: + lines: list[str] + line_idx: int + + def __init__(self, lines: list[str]) -> None: + self.lines = lines + self.line_idx = 0 + + def line_no(self) -> int: + """Returns the current line number (starting from 1)""" + return self.line_idx + 1 + + def has_next(self) -> bool: + """Returns true if there are lines left to read""" + return self.line_idx < len(self.lines) + + def peek_line(self) -> Optional[str]: + """Reads the line the cursor is at""" + if self.has_next(): + return self.lines[self.line_idx].strip() + return None + + def next_line(self) -> Optional[str]: + """Consumes and returns one line""" + if self.has_next(): + self.line_idx += 1 + return self.lines[self.line_idx - 1].strip() + return None + + def next_doc_comment(self) -> Optional[list[str]]: + """Reads a documentation comment (/** ... */) and advances the cursor""" + lines = [] + # find the start + while (line := self.next_line()) is not None and not is_comment_start(line): + pass + if line is None: + return None + + stripped = strip_line(line) + if stripped: + lines.append(stripped) + + if stripped.endswith("*/"): + return lines if lines else None + + while (line := self.next_line()) is not None: + if line.startswith("*/"): + break + + stripped = strip_line(line) + if not stripped: continue - current_enum_name = expose_next_enum_as.split(".", 1)[-1] - out.write("---@alias " + current_enum_name + " integer\n") - out.write("---@type { ") - # temp[1] is '{' - if len(temp) == 2: # no values on this line + + if stripped.startswith("@"): + lines.append(stripped) + continue + + if not lines: + lines.append(stripped) + else: + lines[-1] += "\n--- " + stripped + + return lines if lines else None + + def read_class_body(self) -> list[list[str]]: + """The reader must be at the first line of the class/struct body. All comments inside the class are returned.""" + items = [] + while (line := self.peek_line()) is not None: + if line.startswith("};"): + self.next_line() + break + if not is_comment_start(line): + self.next_line() + continue + doc = self.next_doc_comment() + if not doc: + break + items.append(doc) + return items + + def read_enum_variants(self) -> list[str]: + """The reader must be before an enum class definition (possibly with some comments before). It returns all variants.""" + items = [] + is_comment = False + while (line := self.peek_line()) is not None and not line.startswith("};"): + self.next_line() + if is_comment: + if line.endswith("*/"): + is_comment = False + continue + if line.startswith("/*"): + is_comment = True + continue + if line.startswith("//"): continue - line = temp[2] - - if current_enum_name is not None: - for i, tok in enumerate(line.split(" ")): - if tok == "};": - break - entry = tok.removesuffix(",") - if i != 0: - out.write(", ") - out.write(entry + ": " + current_enum_name) - out.write(" }\n" f"{expose_next_enum_as} = {{}}\n") - print(f"{loc} Wrote enum {expose_next_enum_as} => {current_enum_name}") - current_enum_name = None - expose_next_enum_as = None + if line.endswith("};"): # oneline declaration + opener = line.find("{") + 1 + closer = line.find("}") + items = [ + line.split("=", 1)[0].strip() + for line in line[opener:closer].split(",") + ] + break + if line.startswith("enum class"): + continue + + items.append(line.rstrip(",")) + + return items + + +def finish_class(out, name): + out.write(f"{name} = {{}}\n") + + +def printmsg(path: Path, line: int, message: str): + print(f"{path.relative_to(repo_root)}:{line} {message}") + + +def panic(path: Path, line: int, message: str): + printmsg(path, line, message) + exit(1) + + +def write_func(path: Path, line: int, comments: list[str], out: TextIOWrapper): + if not comments[0].startswith("@"): + out.write(f"--- {comments[0]}\n---\n") + comments = comments[1:] + params = [] + for comment in comments[:-1]: + if not comment.startswith("@lua"): + panic(path, line, f"Invalid function specification - got '{comment}'") + if comment.startswith("@lua@param"): + params.append(comment.split(" ", 2)[1]) + + out.write(f"---{comment.removeprefix('@lua')}\n") + + if not comments[-1].startswith("@exposed "): + panic(path, line, f"Invalid function exposure - got '{comments[-1]}'") + name = comments[-1].split(" ", 1)[1] + printmsg(path, line, f"function {name}") + lua_params = ", ".join(params) + out.write(f"function {name}({lua_params}) end\n\n") + + +def read_file(path: Path, out: TextIOWrapper): + print("Reading", path.relative_to(repo_root)) + with path.open("r") as f: + lines = f.read().splitlines() + + reader = Reader(lines) + while reader.has_next(): + doc_comment = reader.next_doc_comment() + if not doc_comment: + break + header_comment = None + if not doc_comment[0].startswith("@"): + if len(doc_comment) == 1: + continue + header_comment = doc_comment[0] + header = doc_comment[1:] + else: + header = doc_comment + + # include block + if header[0].startswith("@includefile "): + for comment in header: + if not comment.startswith("@includefile "): + panic( + path, + reader.line_no(), + f"Invalid include block - got line '{comment}'", + ) + filename = comment.split(" ", 1)[1] + out.write(f"-- Begin src/{filename}\n\n") + read_file(repo_root / "src" / filename, out) + out.write(f"-- End src/{filename}\n\n") continue - if line.startswith("/**"): - comment = True + # enum + if header[0].startswith("@exposeenum "): + if len(header) > 1: + panic( + path, + reader.line_no(), + f"Invalid enum exposure - one command expected, got {len(header)}", + ) + name = header[0].split(" ", 1)[1] + printmsg(path, reader.line_no(), f"enum {name}") + out.write(f"---@alias {name} integer\n") + if header_comment: + out.write(f"--- {header_comment}\n") + out.write("---@type { ") + out.write( + ", ".join( + [f"{variant}: {name}" for variant in reader.read_enum_variants()] + ) + ) + out.write(" }\n") + out.write(f"{name} = {{}}\n\n") continue - elif "*/" in line: - comment = False - ignore_this_comment = False - if not is_class: + # class + if header[0].startswith("@lua@class "): + name = header[0].split(" ", 1)[1] + classname = name.split(":")[0].strip() + printmsg(path, reader.line_no(), f"class {classname}") + + if header_comment: + out.write(f"--- {header_comment}\n") + out.write(f"---@class {name}\n") + # inline class + if len(header) > 1: + for field in header[1:]: + if not field.startswith("@lua@field "): + panic( + path, + reader.line_no(), + f"Invalid inline class exposure - all lines must be fields, got '{field}'", + ) + out.write(f"---{field.removeprefix('@lua')}\n") out.write("\n") - continue - if not comment: - continue - if ignore_this_comment: - continue - line = line.replace("*", "", 1).lstrip() - if line == "": - out.write("---\n") - elif line.startswith('@brief '): - # Doxygen comment, on a C++ only method - ignore_this_comment = True - elif line.startswith("@exposeenum "): - expose_next_enum_as = line.split(" ", 1)[1] - elif line.startswith("@exposed "): - exp = line.replace("@exposed ", "", 1) - params = ", ".join(last_params_names) - out.write(f"function {exp}({params}) end\n") - print(f"{loc} Wrote function {exp}(...)") - last_params_names = [] - elif line.startswith("@includefile "): - filename = line.replace("@includefile ", "", 1) - output.write(f"-- Now including data from src/{filename}.\n") - process_file(repo_root / 'src' / filename, output) - output.write(f'-- Back to {target.relative_to(repo_root)}.\n') - elif line.startswith("@lua"): - command = line.replace("@lua", "", 1) - if command.startswith("@param"): - last_params_names.append(command.split(" ", 2)[1]) - elif command.startswith("@class"): - print(f"{loc} Writing {command}") - if is_class: - out.write("\n") - is_class = True - elif not command.startswith("@field"): - is_class = False - - out.write("---" + command + "\n") - else: - if is_class: - is_class = False + continue + + # class definition + # save functions for later (print fields first) + funcs = [] + for comment in reader.read_class_body(): + if comment[-1].startswith("@exposed "): + funcs.append(comment) + continue + if len(comment) > 1 or not comment[0].startswith("@lua"): + continue + out.write(f"---{comment[0].removeprefix('@lua')}\n") + + if funcs: + # only define global if there are functions on the class + out.write(f"{classname} = {{}}\n\n") + else: out.write("\n") - # note the space difference from the branch above - out.write("--- " + line + "\n") + for func in funcs: + write_func(path, reader.line_no(), func, out) + continue + + # global function + if header[-1].startswith("@exposed "): + write_func(path, reader.line_no(), doc_comment, out) + continue -with lua_meta.open("w") as output: - output.write(BOILERPLATE[1:]) # skip the newline after triple quote - process_file(lua_api_file, output) +if __name__ == "__main__": + with lua_meta.open("w") as output: + output.write(BOILERPLATE[1:]) # skip the newline after triple quote + read_file(lua_api_file, output) diff --git a/src/controllers/plugins/LuaAPI.hpp b/src/controllers/plugins/LuaAPI.hpp index 39df152169a..15be99c6fa8 100644 --- a/src/controllers/plugins/LuaAPI.hpp +++ b/src/controllers/plugins/LuaAPI.hpp @@ -106,7 +106,7 @@ int c2_register_callback(lua_State *L); /** * Writes a message to the Chatterino log. * - * @lua@param level LogLevel The desired level. + * @lua@param level c2.LogLevel The desired level. * @lua@param ... any Values to log. Should be convertible to a string with `tostring()`. * @exposed c2.log */ diff --git a/src/controllers/plugins/api/ChannelRef.hpp b/src/controllers/plugins/api/ChannelRef.hpp index 29f5173d28e..abc6b421f6f 100644 --- a/src/controllers/plugins/api/ChannelRef.hpp +++ b/src/controllers/plugins/api/ChannelRef.hpp @@ -21,7 +21,7 @@ enum class LPlatform { }; /** - * @lua@class Channel: IWeakResource + * @lua@class Channel */ struct ChannelRef { static void createMetatable(lua_State *L); @@ -100,7 +100,7 @@ struct ChannelRef { * Compares the channel Type. Note that enum values aren't guaranteed, just * that they are equal to the exposed enum. * - * @lua@return bool + * @lua@return boolean * @exposed Channel:is_twitch_channel */ static int is_twitch_channel(lua_State *L); @@ -193,7 +193,7 @@ struct ChannelRef { /** * Finds a channel by the Twitch user ID of its owner. * - * @lua@param string id ID of the owner of the channel. + * @lua@param id string ID of the owner of the channel. * @lua@return Channel? * @exposed Channel.by_twitch_id */ @@ -216,13 +216,12 @@ struct LuaRoomModes { bool subscriber_only = false; /** - * @lua@field emotes_only boolean Whether or not text is allowed in messages. - * Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes + * @lua@field emotes_only boolean Whether or not text is allowed in messages. Note that "emotes" here only means Twitch emotes, not Unicode emoji, nor 3rd party text-based emotes */ bool emotes_only = false; /** - * @lua@field unique_chat number? Time in minutes you need to follow to chat or nil. + * @lua@field follower_only number? Time in minutes you need to follow to chat or nil. */ std::optional follower_only; /** From b991b957f0cc5cd491161bf96191acc5e03ebae5 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 31 Mar 2024 11:46:58 +0200 Subject: [PATCH 09/11] fix: missing rerender on clear (#5282) --- CHANGELOG.md | 1 + src/widgets/helper/ChannelView.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c49ca3401..af86fe2737d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,7 @@ - Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056) - Bugfix: Truncated IRC messages to be at most 512 bytes. (#5246) - Bugfix: Fixed a data race when disconnecting from Twitch PubSub. (#4771) +- Bugfix: Fixed messages not immediately disappearing when clearing the chat. (#5282) - Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978) - Dev: Change clang-format from v14 to v16. (#4929) - Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 2795e5ffe7b..c76373d5679 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -782,6 +782,7 @@ void ChannelView::clearMessages() this->scrollBar_->setMaximum(0); this->scrollBar_->setMinimum(0); this->queueLayout(); + this->update(); this->lastMessageHasAlternateBackground_ = false; this->lastMessageHasAlternateBackgroundReverse_ = true; From 694d53ad20d853e0d85e7c6a0a4ed57c9ee7e7ef Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 31 Mar 2024 13:07:43 +0200 Subject: [PATCH 10/11] Fix some documentations & comments (#5286) * add comments for the new reward filters * slightly improve documentation of r9k values --- src/controllers/filters/lang/Filter.cpp | 3 +++ src/widgets/settingspages/GeneralPage.cpp | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/controllers/filters/lang/Filter.cpp b/src/controllers/filters/lang/Filter.cpp index ef0cfd15ce2..9c3ecb0228b 100644 --- a/src/controllers/filters/lang/Filter.cpp +++ b/src/controllers/filters/lang/Filter.cpp @@ -50,6 +50,9 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) * message.content * message.length * + * reward.title + * reward.cost + * reward.id */ using MessageFlag = chatterino::MessageFlag; diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 74746be8ad1..d129e46eb71 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -462,7 +462,7 @@ void GeneralPage::initLayout(GeneralPageView &layout) layout.addTitle("Messages"); layout.addCheckbox( "Separate with lines", s.separateMessages, false, - "Adds a line inbetween each message to help better tell them apart."); + "Adds a line between each message to help better tell them apart."); layout.addCheckbox("Alternate background color", s.alternateMessages, false, "Slightly change the background behind every other " "message to help better tell them apart."); @@ -904,7 +904,10 @@ void GeneralPage::initLayout(GeneralPageView &layout) toggleLocalr9kShortcut + "."); layout.addCheckbox("Hide similar messages", s.similarityEnabled); //layout.addCheckbox("Gray out matches", s.colorSimilarDisabled); - layout.addCheckbox("By the same user", s.hideSimilarBySameUser); + layout.addCheckbox( + "By the same user", s.hideSimilarBySameUser, false, + "When checked, messages that are very similar to each other can still " + "be shown as long as they're sent by different users."); layout.addCheckbox("Hide my own messages", s.hideSimilarMyself); layout.addCheckbox("Receive notification sounds from hidden messages", s.shownSimilarTriggerHighlights); @@ -920,7 +923,10 @@ void GeneralPage::initLayout(GeneralPageView &layout) }, [](auto args) { return fuzzyToFloat(args.value, 0.9f); - }); + }, + true, + "A value of 0.9 means the messages need to be 90% similar to be marked " + "as similar."); layout.addDropdown( "Maximum delay between messages", {"5s", "10s", "15s", "30s", "60s", "120s"}, s.hideSimilarMaxDelay, @@ -929,7 +935,10 @@ void GeneralPage::initLayout(GeneralPageView &layout) }, [](auto args) { return fuzzyToInt(args.value, 5); - }); + }, + true, + "A value of 5s means if there's a 5s break between messages, we will " + "stop looking further through the messages for similarities."); layout.addDropdown( "Amount of previous messages to check", {"1", "2", "3", "4", "5"}, s.hideSimilarMaxMessagesToCheck, From 905aa4e923006dc989d8febbebf1da1cc512a5d5 Mon Sep 17 00:00:00 2001 From: nerix Date: Mon, 1 Apr 2024 00:04:11 +0200 Subject: [PATCH 11/11] refactor: About page (#5287) --- CHANGELOG.md | 1 + resources/avatars/anon.png | Bin 0 -> 1612 bytes resources/contributors.txt | 141 ++++++------- src/CMakeLists.txt | 3 + src/widgets/layout/FlowLayout.cpp | 252 ++++++++++++++++++++++++ src/widgets/layout/FlowLayout.hpp | 104 ++++++++++ src/widgets/settingspages/AboutPage.cpp | 60 ++++-- 7 files changed, 474 insertions(+), 87 deletions(-) create mode 100644 resources/avatars/anon.png create mode 100644 src/widgets/layout/FlowLayout.cpp create mode 100644 src/widgets/layout/FlowLayout.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index af86fe2737d..c41fd957019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ - Minor: 7TV emotes now have a 4x image rather than a 3x image. (#5209) - Minor: Add `reward.cost` `reward.id`, `reward.title` filter variables. (#5275) - Minor: Change Lua `CompletionRequested` handler to use an event table. (#5280) +- Minor: Changed the layout of the about page. (#5287) - Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840) - Bugfix: Fixed the `/shoutout` command not working with usernames starting with @'s (e.g. `/shoutout @forsen`). (#4800) - Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848) diff --git a/resources/avatars/anon.png b/resources/avatars/anon.png new file mode 100644 index 0000000000000000000000000000000000000000..b7993edcbbbed49dc84078ac0ba2c44a3c7d2d91 GIT binary patch literal 1612 zcmV-S2DABzP)C00033P)t-s00030 z|NsC0|L5oD)YR0lu&~U`%-r1EudlDWySu2UsLRXCs;a8Mz`&ZCn#IM%kB^U_prC$! ze!#%Mn3$NNqN2sc#gmhhwY9Z(cXyeYnZLikj*gC-o1444yN{2LudlD0o14A8y>4!9 ziHV7voSch`i?+75iHV7CW*rKF^!p`oFho12xDm64H=jg5_pii(Ga zhk}BFfq{X5fPjC0e|~;`dwY9&dU|$tc5`!cZfH9JB{OjlQ2V{3ADfP;vUl9`{Us+kUJ^Yixh`Tzg_|NsC0|NsB~{wJ}e5&!@Mwn;=mRCwC$np;=eOcaLqB;kxGf=5&+ zBBCgMia`A|0XZfha{m8+r8CK7jT*oTV_aQpo~vH8ect`AD5%FPC>2_gr;{O%X9X)a*Z`9rWSGF`w*{tG{>%_eyPbN%E+)M_>I2fp(=3vez# zt2XK%(J=hKh!rKR@y=Toae*o4;u?t}T zz4|<`?Bj!=%$ka1K18_j!*>**)rZ)QnVk9&yaff%G*0_xp!CNJ7!W=2ZzK4Kw4?Ir zpW(ZISAf=VK3#eK85g`uCNce#p2^@eH zFvkAzcr+Xi2mg7%EzKQ+wEn*HGI|y)Mr-zc>FFV z11{bGIRUkA7l8eS(Y=(wEfgF8zkxCJcd;K+@Pb=Nc@m&GW%HW^!8OkD38;hd2p<6R z8%N+8DRP4&wg8Kq1a7g91JszGqA%Y8&v=_SK-~<0U!Y;5Ct(Tl8OIy(l~k=|7QpIR`+*x5s@fe4i2q%HtOAShU)LMgVc_x7r@ZWyaWE&`m`P}RZf zoB*9eX}?bXL%A-yKI41O()bn(Y&R+w>bo zG05i($w$y|pj#1I&0mK*J3EJ@ds}b%3734fkYpQhY@v;5wVEa^VyEL%3S2q}mZ{&l z1sZ|T!RR;u*90iZ0ywrnU~*f}xa5(*b7^3fX0YW{&*1YG;>JRB0#*|}v-9o%WsN21 zHpYC40a%+oV+}GgtWCJQlA7Pgz*AV+UkC#S{N?ybT0h@MP>ciG%Icr%vk?Rw3ETcHL=GbQ*+pa`GVsJp zg&Dh5DUvKMv=@)U2)LqT%~o?!2^hY;Y +#include +#include +#include + +namespace { + +using namespace chatterino; + +class Linebreak : public QWidget +{ +}; + +} // namespace + +namespace chatterino { + +FlowLayout::FlowLayout(QWidget *parent, Options options) + : QLayout(parent) + , hSpace_(options.hSpacing) + , vSpace_(options.vSpacing) +{ + if (options.margin >= 0) + { + this->setContentsMargins(options.margin, options.margin, options.margin, + options.margin); + } +} + +FlowLayout::FlowLayout(Options options) + : FlowLayout(nullptr, options) +{ +} + +FlowLayout::~FlowLayout() +{ + for (auto *item : this->itemList_) + { + delete item; + } + this->itemList_ = {}; +} + +void FlowLayout::addItem(QLayoutItem *item) +{ + this->itemList_.push_back(item); +} + +void FlowLayout::addLinebreak(int height) +{ + auto *linebreak = new Linebreak; + linebreak->setFixedHeight(height); + this->addWidget(linebreak); +} + +int FlowLayout::horizontalSpacing() const +{ + if (this->hSpace_ >= 0) + { + return this->hSpace_; + } + + return this->defaultSpacing(QStyle::PM_LayoutHorizontalSpacing); +} + +void FlowLayout::setHorizontalSpacing(int value) +{ + if (this->hSpace_ == value) + { + return; + } + this->hSpace_ = value; + this->invalidate(); +} + +int FlowLayout::verticalSpacing() const +{ + if (this->vSpace_ >= 0) + { + return this->vSpace_; + } + + return this->defaultSpacing(QStyle::PM_LayoutVerticalSpacing); +} + +void FlowLayout::setVerticalSpacing(int value) +{ + if (this->vSpace_ == value) + { + return; + } + this->vSpace_ = value; + this->invalidate(); +} + +int FlowLayout::count() const +{ + return static_cast(this->itemList_.size()); +} + +QLayoutItem *FlowLayout::itemAt(int index) const +{ + if (index >= 0 && index < static_cast(this->itemList_.size())) + { + return this->itemList_[static_cast(index)]; + } + return nullptr; +} + +QLayoutItem *FlowLayout::takeAt(int index) +{ + if (index >= 0 && index < static_cast(this->itemList_.size())) + { + auto *it = this->itemList_[static_cast(index)]; + this->itemList_.erase(this->itemList_.cbegin() + + static_cast(index)); + return it; + } + return nullptr; +} + +Qt::Orientations FlowLayout::expandingDirections() const +{ + return {}; +} + +bool FlowLayout::hasHeightForWidth() const +{ + return true; +} + +int FlowLayout::heightForWidth(int width) const +{ + return this->doLayout({0, 0, width, 0}, true); +} + +void FlowLayout::setGeometry(const QRect &rect) +{ + QLayout::setGeometry(rect); + this->doLayout(rect, false); +} + +QSize FlowLayout::sizeHint() const +{ + return this->minimumSize(); +} + +QSize FlowLayout::minimumSize() const +{ + QSize size; + for (const auto *item : this->itemList_) + { + size = size.expandedTo(item->minimumSize()); + } + + const QMargins margins = contentsMargins(); + size += QSize(margins.left() + margins.right(), + margins.top() + margins.bottom()); + return size; +} + +int FlowLayout::doLayout(const QRect &rect, bool testOnly) const +{ + auto margins = this->contentsMargins(); + QRect effectiveRect = rect.adjusted(margins.left(), margins.top(), + -margins.right(), -margins.bottom()); + int x = effectiveRect.x(); + int y = effectiveRect.y(); + int lineHeight = 0; + for (QLayoutItem *item : this->itemList_) + { + auto *linebreak = dynamic_cast(item->widget()); + if (linebreak) + { + item->setGeometry({x, y, 0, linebreak->height()}); + x = effectiveRect.x(); + y = y + lineHeight + linebreak->height(); + lineHeight = 0; + continue; + } + + auto space = this->getSpacing(item); + int nextX = x + item->sizeHint().width() + space.width(); + if (nextX - space.width() > effectiveRect.right() && lineHeight > 0) + { + x = effectiveRect.x(); + y = y + lineHeight + space.height(); + nextX = x + item->sizeHint().width() + space.width(); + lineHeight = 0; + } + + if (!testOnly) + { + item->setGeometry({QPoint{x, y}, item->sizeHint()}); + } + + x = nextX; + lineHeight = qMax(lineHeight, item->sizeHint().height()); + } + + return y + lineHeight - rect.y() + margins.bottom(); +} + +int FlowLayout::defaultSpacing(QStyle::PixelMetric pm) const +{ + QObject *parent = this->parent(); + if (!parent) + { + return -1; + } + if (auto *widget = dynamic_cast(parent)) + { + return widget->style()->pixelMetric(pm, nullptr, widget); + } + if (auto *layout = dynamic_cast(parent)) + { + return layout->spacing(); + } + return -1; +} + +QSize FlowLayout::getSpacing(QLayoutItem *item) const +{ + // called if there isn't any parent or the parent can't provide any spacing + auto fallbackSpacing = [&](auto dir) { + if (auto *widget = item->widget()) + { + return widget->style()->layoutSpacing(QSizePolicy::PushButton, + QSizePolicy::PushButton, dir); + } + if (auto *layout = item->layout()) + { + return layout->spacing(); + } + return 0; + }; + + QSize spacing(this->horizontalSpacing(), this->verticalSpacing()); + if (spacing.width() == -1) + { + spacing.rwidth() = fallbackSpacing(Qt::Horizontal); + } + if (spacing.height() == -1) + { + spacing.rheight() = fallbackSpacing(Qt::Vertical); + } + return spacing; +} + +} // namespace chatterino diff --git a/src/widgets/layout/FlowLayout.hpp b/src/widgets/layout/FlowLayout.hpp new file mode 100644 index 00000000000..39a359ff143 --- /dev/null +++ b/src/widgets/layout/FlowLayout.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include +#include + +#include + +namespace chatterino { + +/// @brief A QLayout wrapping items +/// +/// Similar to a box layout that wraps its items. It's not super optimized. +/// Some computations in #doLayout() could be cached. +/// +/// This is based on the Qt flow layout example: +/// https://doc.qt.io/qt-6/qtwidgets-layouts-flowlayout-example.html +class FlowLayout : public QLayout +{ +public: + struct Options { + int margin = -1; + int hSpacing = -1; + int vSpacing = -1; + }; + + explicit FlowLayout(QWidget *parent, Options options = {-1, -1, -1}); + explicit FlowLayout(Options options = {-1, -1, -1}); + + ~FlowLayout() override; + FlowLayout(const FlowLayout &) = delete; + FlowLayout(FlowLayout &&) = delete; + FlowLayout &operator=(const FlowLayout &) = delete; + FlowLayout &operator=(FlowLayout &&) = delete; + + /// @brief Adds @a item to this layout + /// + /// Ownership of @a item is transferred. This method isn't usually called + /// in application code (use addWidget/addLayout). + /// See QLayout::addItem for more information. + void addItem(QLayoutItem *item) override; + + /// @brief Adds a linebreak to this layout + /// + /// @param height Specifies the height of the linebreak + void addLinebreak(int height = 0); + + /// @brief Spacing on the horizontal axis + /// + /// -1 if the default spacing for an item will be used. + [[nodiscard]] int horizontalSpacing() const; + + /// Setter for #horizontalSpacing(). -1 to use defaults. + void setHorizontalSpacing(int value); + + /// @brief Spacing on the vertical axis + /// + /// -1 if the default spacing for an item will be used. + [[nodiscard]] int verticalSpacing() const; + + /// Setter for #verticalSpacing(). -1 to use defaults. + void setVerticalSpacing(int value); + + /// From QLayout. This layout doesn't expand in any direction. + Qt::Orientations expandingDirections() const override; + bool hasHeightForWidth() const override; + int heightForWidth(int width) const override; + + QSize minimumSize() const override; + QSize sizeHint() const override; + + void setGeometry(const QRect &rect) override; + + int count() const override; + QLayoutItem *itemAt(int index) const override; + + /// From QLayout. Ownership is transferred to the caller + QLayoutItem *takeAt(int index) override; + +private: + /// @brief Computes the layout + /// + /// @param rect The area in which items can be layed out + /// @param testOnly If set, items won't be moved, only the total height + /// will be computed. + /// @returns The total height including margins. + int doLayout(const QRect &rect, bool testOnly) const; + + /// @brief Computes the default spacing based for items on the parent + /// + /// @param pm Either PM_LayoutHorizontalSpacing or PM_LayoutVerticalSpacing + /// for the respective direction. + /// @returns The spacing in dp, -1 if there isn't any parent + int defaultSpacing(QStyle::PixelMetric pm) const; + + /// Computes the spacing for @a item + QSize getSpacing(QLayoutItem *item) const; + + std::vector itemList_; + int hSpace_ = -1; + int vSpace_ = -1; + int lineSpacing_ = -1; +}; + +} // namespace chatterino diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 78597c5fca3..89c985c5e8d 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -8,6 +8,7 @@ #include "util/RemoveScrollAreaBackground.hpp" #include "widgets/BasePopup.hpp" #include "widgets/helper/SignalLabel.hpp" +#include "widgets/layout/FlowLayout.hpp" #include #include @@ -54,6 +55,7 @@ AboutPage::AboutPage() auto label = vbox.emplace(version.buildString() + "
" + version.runningString()); + label->setWordWrap(true); label->setOpenExternalLinks(true); label->setTextInteractionFlags(Qt::TextBrowserInteraction); } @@ -137,15 +139,15 @@ AboutPage::AboutPage() l.emplace("Facebook emojis provided by Facebook")->setOpenExternalLinks(true); l.emplace("Apple emojis provided by Apple")->setOpenExternalLinks(true); l.emplace("Google emojis provided by Google")->setOpenExternalLinks(true); - l.emplace("Emoji datasource provided by Cal Henderson" + l.emplace("Emoji datasource provided by Cal Henderson " "(show license)")->setOpenExternalLinks(true); // clang-format on } // Contributors - auto contributors = layout.emplace("Contributors"); + auto contributors = layout.emplace("People"); { - auto l = contributors.emplace(); + auto l = contributors.emplace(); QFile contributorsFile(":/contributors.txt"); contributorsFile.open(QFile::ReadOnly); @@ -166,11 +168,24 @@ AboutPage::AboutPage() continue; } + if (line.startsWith(u"@header")) + { + if (l->count() != 0) + { + l->addLinebreak(20); + } + auto *label = new QLabel(QStringLiteral("

%1

") + .arg(line.mid(8).trimmed())); + l->addWidget(label); + l->addLinebreak(8); + continue; + } + QStringList contributorParts = line.split("|"); - if (contributorParts.size() != 4) + if (contributorParts.size() != 3) { - qCDebug(chatterinoWidget) + qCWarning(chatterinoWidget) << "Missing parts in line" << line; continue; } @@ -178,39 +193,42 @@ AboutPage::AboutPage() QString username = contributorParts[0].trimmed(); QString url = contributorParts[1].trimmed(); QString avatarUrl = contributorParts[2].trimmed(); - QString role = contributorParts[3].trimmed(); auto *usernameLabel = new QLabel("" + username + ""); usernameLabel->setOpenExternalLinks(true); - auto *roleLabel = new QLabel(role); + usernameLabel->setToolTip(url); - auto contributorBox2 = l.emplace(); + auto contributorBox2 = l.emplace(); - const auto addAvatar = [&avatarUrl, &contributorBox2] { - if (!avatarUrl.isEmpty()) + const auto addAvatar = [&] { + auto *avatar = new QLabel(); + QPixmap avatarPixmap; + if (avatarUrl.isEmpty()) + { + // TODO: or anon.png + avatarPixmap.load(":/avatars/anon.png"); + } + else { - QPixmap avatarPixmap; avatarPixmap.load(avatarUrl); - - auto avatar = contributorBox2.emplace(); - avatar->setPixmap(avatarPixmap); - avatar->setFixedSize(64, 64); - avatar->setScaledContents(true); } + + avatar->setPixmap(avatarPixmap); + avatar->setFixedSize(64, 64); + avatar->setScaledContents(true); + contributorBox2->addWidget(avatar, 0, Qt::AlignCenter); }; - const auto addLabels = [&contributorBox2, &usernameLabel, - &roleLabel] { + const auto addLabels = [&] { auto *labelBox = new QVBoxLayout(); contributorBox2->addLayout(labelBox); - labelBox->addWidget(usernameLabel); - labelBox->addWidget(roleLabel); + labelBox->addWidget(usernameLabel, 0, Qt::AlignCenter); }; - addLabels(); addAvatar(); + addLabels(); } } }