mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Merge remote-tracking branch 'origin/master' into fix/stop_windows_code_from_abort()ing_our_process
This commit is contained in:
commit
60a913e4ae
|
@ -65,5 +65,11 @@ CheckOptions:
|
|||
- key: readability-identifier-naming.LocalPointerIgnoredRegexp
|
||||
value: ^L$
|
||||
|
||||
# Benchmarks
|
||||
- key: readability-identifier-naming.FunctionIgnoredRegexp
|
||||
value: ^BM_[^_]+$
|
||||
- key: readability-identifier-naming.ClassIgnoredRegexp
|
||||
value: ^BM_[^_]+$
|
||||
|
||||
- key: misc-const-correctness.AnalyzeValues
|
||||
value: false
|
||||
|
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
@ -151,7 +151,7 @@ jobs:
|
|||
# WINDOWS
|
||||
- name: Enable Developer Command Prompt (Windows)
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
uses: ilammy/msvc-dev-cmd@v1.12.1
|
||||
uses: ilammy/msvc-dev-cmd@v1.13.0
|
||||
|
||||
- name: Setup conan variables (Windows)
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
|
|
2
.github/workflows/create-installer.yml
vendored
2
.github/workflows/create-installer.yml
vendored
|
@ -43,7 +43,7 @@ jobs:
|
|||
run: echo "C:\Program Files (x86)\Inno Setup 6\" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
|
||||
|
||||
- name: Enable Developer Command Prompt
|
||||
uses: ilammy/msvc-dev-cmd@v1.12.1
|
||||
uses: ilammy/msvc-dev-cmd@v1.13.0
|
||||
|
||||
- name: Build installer
|
||||
id: build-installer
|
||||
|
|
2
.github/workflows/test-windows.yml
vendored
2
.github/workflows/test-windows.yml
vendored
|
@ -63,7 +63,7 @@ jobs:
|
|||
version: ${{ matrix.qt-version }}
|
||||
|
||||
- name: Enable Developer Command Prompt
|
||||
uses: ilammy/msvc-dev-cmd@v1.12.1
|
||||
uses: ilammy/msvc-dev-cmd@v1.13.0
|
||||
|
||||
- name: Setup conan variables
|
||||
if: startsWith(matrix.os, 'windows')
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# JSON resources should not be prettified...
|
||||
resources/*.json
|
||||
benchmarks/resources/*.json
|
||||
tests/resources/*.json
|
||||
# ...themes should be prettified for readability.
|
||||
!resources/themes/*.json
|
||||
|
||||
|
@ -7,6 +9,7 @@ resources/*.json
|
|||
lib/*/
|
||||
conan-pkgs/*/
|
||||
cmake/sanitizers-cmake/
|
||||
tools/crash-handler
|
||||
|
||||
# Build folders
|
||||
*build-*/
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
- Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922)
|
||||
- Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026)
|
||||
- Major: Show restricted chat messages and suspicious treatment updates. (#5056)
|
||||
- Major: Show restricted chat messages and suspicious treatment updates. (#5056, #5060)
|
||||
- Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809)
|
||||
- Minor: The account switcher is now styled to match your theme. (#4817)
|
||||
- Minor: Add an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795)
|
||||
|
@ -20,6 +20,8 @@
|
|||
- Minor: Add a new completion API for experimental plugins feature. (#5000, #5047)
|
||||
- Minor: Re-enabled _Restart on crash_ option on Windows. (#5012)
|
||||
- Minor: The whisper highlight color can now be configured through the settings. (#5053)
|
||||
- Minor: Added missing periods at various moderator messages and commands. (#5061)
|
||||
- Minor: Improved color selection and display. (#5057)
|
||||
- Bugfix: Fixed an issue where certain emojis did not send to Twitch chat correctly. (#4840)
|
||||
- Bugfix: Fixed capitalized channel names in log inclusion list not being logged. (#4848)
|
||||
- Bugfix: Trimmed custom streamlink paths on all platforms making sure you don't accidentally add spaces at the beginning or end of its path. (#4834)
|
||||
|
@ -88,6 +90,7 @@
|
|||
- Dev: Refactor `DebugCount` and add copy button to debug popup. (#4921)
|
||||
- Dev: Refactor `common/Credentials`. (#4979)
|
||||
- Dev: Refactor chat logger. (#5058)
|
||||
- Dev: Refactor Twitch PubSub client. (#5059)
|
||||
- Dev: Changed lifetime of context menus. (#4924)
|
||||
- Dev: Renamed `tools` directory to `scripts`. (#5035)
|
||||
- Dev: Refactor `ChannelView`, removing a bunch of clang-tidy warnings. (#4926)
|
||||
|
@ -108,6 +111,9 @@
|
|||
- 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: Removed duplicate scale in settings dialog. (#5069)
|
||||
- Dev: Fix `NotebookTab` emitting updates for every message. (#5068)
|
||||
- Dev: Added benchmark for parsing and building recent messages. (#5071)
|
||||
|
||||
## 2.4.6
|
||||
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
project(chatterino-benchmark)
|
||||
|
||||
set(benchmark_SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/main.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/Emojis.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/Highlights.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/FormatTime.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/Helpers.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/LimitedQueue.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/LinkParser.cpp
|
||||
src/main.cpp
|
||||
resources/bench.qrc
|
||||
|
||||
src/Emojis.cpp
|
||||
src/Highlights.cpp
|
||||
src/FormatTime.cpp
|
||||
src/Helpers.cpp
|
||||
src/LimitedQueue.cpp
|
||||
src/LinkParser.cpp
|
||||
src/RecentMessages.cpp
|
||||
# Add your new file above this line!
|
||||
)
|
||||
|
||||
|
@ -27,4 +30,5 @@ set_target_properties(${PROJECT_NAME}
|
|||
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin"
|
||||
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin"
|
||||
RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin"
|
||||
AUTORCC ON
|
||||
)
|
||||
|
|
6
benchmarks/resources/bench.qrc
Normal file
6
benchmarks/resources/bench.qrc
Normal file
|
@ -0,0 +1,6 @@
|
|||
<RCC>
|
||||
<qresource prefix="/bench">
|
||||
<file>recentmessages-nymn.json</file>
|
||||
<file>seventvemotes-nymn.json</file>
|
||||
</qresource>
|
||||
</RCC>
|
1
benchmarks/resources/recentmessages-nymn.json
Normal file
1
benchmarks/resources/recentmessages-nymn.json
Normal file
File diff suppressed because one or more lines are too long
1
benchmarks/resources/seventvemotes-nymn.json
Normal file
1
benchmarks/resources/seventvemotes-nymn.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -145,16 +145,13 @@ static void BM_EmojiParsing(benchmark::State &state)
|
|||
|
||||
BENCHMARK(BM_EmojiParsing);
|
||||
|
||||
template <class... Args>
|
||||
static void BM_EmojiParsing2(benchmark::State &state, Args &&...args)
|
||||
static void BM_EmojiParsing2(benchmark::State &state, const QString &input,
|
||||
int expectedNumEmojis)
|
||||
{
|
||||
Emojis emojis;
|
||||
|
||||
emojis.load();
|
||||
|
||||
auto argsTuple = std::make_tuple(std::move(args)...);
|
||||
auto input = std::get<0>(argsTuple);
|
||||
auto expectedNumEmojis = std::get<1>(argsTuple);
|
||||
for (auto _ : state)
|
||||
{
|
||||
auto output = emojis.parse(input);
|
||||
|
|
|
@ -4,35 +4,41 @@
|
|||
|
||||
using namespace chatterino;
|
||||
|
||||
template <class... Args>
|
||||
void BM_TimeFormatting(benchmark::State &state, Args &&...args)
|
||||
void BM_TimeFormattingQString(benchmark::State &state, const QString &v)
|
||||
{
|
||||
auto args_tuple = std::make_tuple(std::move(args)...);
|
||||
for (auto _ : state)
|
||||
{
|
||||
formatTime(std::get<0>(args_tuple));
|
||||
formatTime(v);
|
||||
}
|
||||
}
|
||||
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, 0, 0);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, qs0, "0");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, 1337, 1337);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, qs1337, "1337");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, 623452, 623452);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, qs623452, "623452");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, 8345, 8345);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, qs8345, "8345");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, 314034, 314034);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, qs314034, "314034");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, 27, 27);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, qs27, "27");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, 34589, 34589);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, qs34589, "34589");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, 3659, 3659);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, qs3659, "3659");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, 1045345, 1045345);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, qs1045345, "1045345");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, 86432, 86432);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, qs86432, "86432");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, qsempty, "");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormatting, qsinvalid, "asd");
|
||||
void BM_TimeFormattingInt(benchmark::State &state, int v)
|
||||
{
|
||||
for (auto _ : state)
|
||||
{
|
||||
formatTime(v);
|
||||
}
|
||||
}
|
||||
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 0, 0);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 1045345, 1045345);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 1337, 1337);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 27, 27);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 314034, 314034);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 34589, 34589);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 3659, 3659);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 623452, 623452);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 8345, 8345);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingInt, 86432, 86432);
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs0, "0");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs1045345, "1045345");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs1337, "1337");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs27, "27");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs314034, "314034");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs34589, "34589");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs3659, "3659");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs623452, "623452");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs8345, "8345");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qs86432, "86432");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qsempty, "");
|
||||
BENCHMARK_CAPTURE(BM_TimeFormattingQString, qsinvalid, "asd");
|
||||
|
|
223
benchmarks/src/RecentMessages.cpp
Normal file
223
benchmarks/src/RecentMessages.cpp
Normal file
|
@ -0,0 +1,223 @@
|
|||
#include "common/Literals.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/highlights/HighlightController.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
#include "mocks/EmptyApplication.hpp"
|
||||
#include "mocks/TwitchIrcServer.hpp"
|
||||
#include "mocks/UserData.hpp"
|
||||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
#include "providers/chatterino/ChatterinoBadges.hpp"
|
||||
#include "providers/ffz/FfzBadges.hpp"
|
||||
#include "providers/ffz/FfzEmotes.hpp"
|
||||
#include "providers/recentmessages/Impl.hpp"
|
||||
#include "providers/seventv/SeventvBadges.hpp"
|
||||
#include "providers/seventv/SeventvEmotes.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Resources.hpp"
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QString>
|
||||
|
||||
#include <optional>
|
||||
|
||||
using namespace chatterino;
|
||||
using namespace literals;
|
||||
|
||||
namespace {
|
||||
|
||||
class MockApplication : mock::EmptyApplication
|
||||
{
|
||||
public:
|
||||
IEmotes *getEmotes() override
|
||||
{
|
||||
return &this->emotes;
|
||||
}
|
||||
|
||||
IUserDataController *getUserData() override
|
||||
{
|
||||
return &this->userData;
|
||||
}
|
||||
|
||||
AccountController *getAccounts() override
|
||||
{
|
||||
return &this->accounts;
|
||||
}
|
||||
|
||||
ITwitchIrcServer *getTwitch() override
|
||||
{
|
||||
return &this->twitch;
|
||||
}
|
||||
|
||||
ChatterinoBadges *getChatterinoBadges() override
|
||||
{
|
||||
return &this->chatterinoBadges;
|
||||
}
|
||||
|
||||
FfzBadges *getFfzBadges() override
|
||||
{
|
||||
return &this->ffzBadges;
|
||||
}
|
||||
|
||||
SeventvBadges *getSeventvBadges() override
|
||||
{
|
||||
return &this->seventvBadges;
|
||||
}
|
||||
|
||||
HighlightController *getHighlights() override
|
||||
{
|
||||
return &this->highlights;
|
||||
}
|
||||
|
||||
AccountController accounts;
|
||||
Emotes emotes;
|
||||
mock::UserDataController userData;
|
||||
mock::MockTwitchIrcServer twitch;
|
||||
ChatterinoBadges chatterinoBadges;
|
||||
FfzBadges ffzBadges;
|
||||
SeventvBadges seventvBadges;
|
||||
HighlightController highlights;
|
||||
};
|
||||
|
||||
std::optional<QJsonDocument> tryReadJsonFile(const QString &path)
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QFile::ReadOnly))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QJsonParseError e;
|
||||
auto doc = QJsonDocument::fromJson(file.readAll(), &e);
|
||||
if (e.error != QJsonParseError::NoError)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
QJsonDocument readJsonFile(const QString &path)
|
||||
{
|
||||
auto opt = tryReadJsonFile(path);
|
||||
if (!opt)
|
||||
{
|
||||
_exit(1);
|
||||
}
|
||||
return *opt;
|
||||
}
|
||||
|
||||
class RecentMessages
|
||||
{
|
||||
public:
|
||||
explicit RecentMessages(const QString &name_)
|
||||
: name(name_)
|
||||
, chan(this->name)
|
||||
{
|
||||
const auto seventvEmotes =
|
||||
tryReadJsonFile(u":/bench/seventvemotes-%1.json"_s.arg(this->name));
|
||||
const auto bttvEmotes =
|
||||
tryReadJsonFile(u":/bench/bttvemotes-%1.json"_s.arg(this->name));
|
||||
const auto ffzEmotes =
|
||||
tryReadJsonFile(u":/bench/ffzemotes-%1.json"_s.arg(this->name));
|
||||
|
||||
if (seventvEmotes)
|
||||
{
|
||||
this->chan.setSeventvEmotes(
|
||||
std::make_shared<const EmoteMap>(seventv::detail::parseEmotes(
|
||||
seventvEmotes->object()["emote_set"_L1]
|
||||
.toObject()["emotes"_L1]
|
||||
.toArray(),
|
||||
false)));
|
||||
}
|
||||
|
||||
if (bttvEmotes)
|
||||
{
|
||||
this->chan.setBttvEmotes(std::make_shared<const EmoteMap>(
|
||||
bttv::detail::parseChannelEmotes(bttvEmotes->object(),
|
||||
this->name)));
|
||||
}
|
||||
|
||||
if (ffzEmotes)
|
||||
{
|
||||
this->chan.setFfzEmotes(std::make_shared<const EmoteMap>(
|
||||
ffz::detail::parseChannelEmotes(ffzEmotes->object())));
|
||||
}
|
||||
|
||||
this->messages =
|
||||
readJsonFile(u":/bench/recentmessages-%1.json"_s.arg(this->name));
|
||||
}
|
||||
|
||||
~RecentMessages()
|
||||
{
|
||||
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
|
||||
}
|
||||
|
||||
virtual void run(benchmark::State &state) = 0;
|
||||
|
||||
protected:
|
||||
QString name;
|
||||
MockApplication app;
|
||||
TwitchChannel chan;
|
||||
QJsonDocument messages;
|
||||
};
|
||||
|
||||
class ParseRecentMessages : public RecentMessages
|
||||
{
|
||||
public:
|
||||
explicit ParseRecentMessages(const QString &name_)
|
||||
: RecentMessages(name_)
|
||||
{
|
||||
}
|
||||
|
||||
void run(benchmark::State &state)
|
||||
{
|
||||
for (auto _ : state)
|
||||
{
|
||||
auto parsed = recentmessages::detail::parseRecentMessages(
|
||||
this->messages.object());
|
||||
benchmark::DoNotOptimize(parsed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class BuildRecentMessages : public RecentMessages
|
||||
{
|
||||
public:
|
||||
explicit BuildRecentMessages(const QString &name_)
|
||||
: RecentMessages(name_)
|
||||
{
|
||||
}
|
||||
|
||||
void run(benchmark::State &state)
|
||||
{
|
||||
auto parsed = recentmessages::detail::parseRecentMessages(
|
||||
this->messages.object());
|
||||
for (auto _ : state)
|
||||
{
|
||||
auto built = recentmessages::detail::buildRecentMessages(
|
||||
parsed, &this->chan);
|
||||
benchmark::DoNotOptimize(built);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void BM_ParseRecentMessages(benchmark::State &state, const QString &name)
|
||||
{
|
||||
ParseRecentMessages bench(name);
|
||||
bench.run(state);
|
||||
}
|
||||
|
||||
void BM_BuildRecentMessages(benchmark::State &state, const QString &name)
|
||||
{
|
||||
BuildRecentMessages bench(name);
|
||||
bench.run(state);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BENCHMARK_CAPTURE(BM_ParseRecentMessages, nymn, u"nymn"_s);
|
||||
BENCHMARK_CAPTURE(BM_BuildRecentMessages, nymn, u"nymn"_s);
|
|
@ -1,3 +1,4 @@
|
|||
#include "singletons/Resources.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
|
@ -11,6 +12,8 @@ int main(int argc, char **argv)
|
|||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
initResources();
|
||||
|
||||
::benchmark::Initialize(&argc, argv);
|
||||
|
||||
// Ensure settings are initialized before any benchmarks are run
|
||||
|
|
|
@ -17,61 +17,96 @@ public:
|
|||
|
||||
Theme *getThemes() override
|
||||
{
|
||||
assert(
|
||||
false &&
|
||||
"EmptyApplication::getThemes was called without being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Fonts *getFonts() override
|
||||
{
|
||||
assert(
|
||||
false &&
|
||||
"EmptyApplication::getFonts was called without being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
IEmotes *getEmotes() override
|
||||
{
|
||||
assert(
|
||||
false &&
|
||||
"EmptyApplication::getEmotes was called without being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AccountController *getAccounts() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getAccounts was called without "
|
||||
"being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HotkeyController *getHotkeys() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getHotkeys was called without being "
|
||||
"initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
WindowManager *getWindows() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getWindows was called without being "
|
||||
"initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Toasts *getToasts() override
|
||||
{
|
||||
assert(
|
||||
false &&
|
||||
"EmptyApplication::getToasts was called without being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CrashHandler *getCrashHandler() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getCrashHandler was called without "
|
||||
"being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CommandController *getCommands() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getCommands was called without "
|
||||
"being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
NotificationController *getNotifications() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getNotifications was called without "
|
||||
"being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HighlightController *getHighlights() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getHighlights was called without "
|
||||
"being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ITwitchIrcServer *getTwitch() override
|
||||
{
|
||||
assert(
|
||||
false &&
|
||||
"EmptyApplication::getTwitch was called without being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PubSub *getTwitchPubSub() override
|
||||
{
|
||||
assert(false && "getTwitchPubSub was called without being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
@ -83,11 +118,15 @@ public:
|
|||
|
||||
ChatterinoBadges *getChatterinoBadges() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getChatterinoBadges was called "
|
||||
"without being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FfzBadges *getFfzBadges() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getFfzBadges was called without "
|
||||
"being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
@ -99,6 +138,8 @@ public:
|
|||
|
||||
IUserDataController *getUserData() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getUserData was called without "
|
||||
"being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
@ -110,11 +151,15 @@ public:
|
|||
|
||||
ITwitchLiveController *getTwitchLiveController() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getTwitchLiveController was called "
|
||||
"without being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ImageUploader *getImageUploader() override
|
||||
{
|
||||
assert(false && "EmptyApplication::getImageUploader was called without "
|
||||
"being initialized");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
|
BIN
resources/avatars/fraxx.png
Normal file
BIN
resources/avatars/fraxx.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 912 B |
|
@ -67,6 +67,7 @@ chrrs | https://github.com/chrrs | | Contributor
|
|||
4rneee | https://github.com/4rneee | | Contributor
|
||||
crazysmc | https://github.com/crazysmc | :/avatars/crazysmc.png | Contributor
|
||||
SputNikPlop | https://github.com/SputNikPlop | | Contributor
|
||||
fraxx | https://github.com/fraxxio | :/avatars/fraxx.png | Contributor
|
||||
|
||||
# If you are a contributor add yourself above this line
|
||||
|
||||
|
|
|
@ -87,6 +87,8 @@ ISoundController *makeSoundController(Settings &settings)
|
|||
}
|
||||
}
|
||||
|
||||
const QString TWITCH_PUBSUB_URL = "wss://pubsub-edge.twitch.tv";
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
@ -128,6 +130,7 @@ Application::Application(Settings &_settings, Paths &_paths, const Args &_args)
|
|||
, userData(&this->emplace<UserDataController>())
|
||||
, sound(&this->emplace<ISoundController>(makeSoundController(_settings)))
|
||||
, twitchLiveController(&this->emplace<TwitchLiveController>())
|
||||
, twitchPubSub(new PubSub(TWITCH_PUBSUB_URL))
|
||||
, logging(new Logging(_settings))
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
, plugins(&this->emplace<PluginController>())
|
||||
|
@ -144,6 +147,11 @@ Application::Application(Settings &_settings, Paths &_paths, const Args &_args)
|
|||
|
||||
Application::~Application() = default;
|
||||
|
||||
void Application::fakeDtor()
|
||||
{
|
||||
this->twitchPubSub.reset();
|
||||
}
|
||||
|
||||
void Application::initialize(Settings &settings, Paths &paths)
|
||||
{
|
||||
assert(isAppInitialized == false);
|
||||
|
@ -314,6 +322,11 @@ ITwitchIrcServer *Application::getTwitch()
|
|||
return this->twitch;
|
||||
}
|
||||
|
||||
PubSub *Application::getTwitchPubSub()
|
||||
{
|
||||
return this->twitchPubSub.get();
|
||||
}
|
||||
|
||||
Logging *Application::getChatLogger()
|
||||
{
|
||||
return this->logging.get();
|
||||
|
@ -343,7 +356,7 @@ void Application::initPubSub()
|
|||
{
|
||||
// We can safely ignore these signal connections since the twitch object will always
|
||||
// be destroyed before the Application
|
||||
std::ignore = this->twitch->pubsub->signals_.moderation.chatCleared.connect(
|
||||
std::ignore = this->twitchPubSub->moderation.chatCleared.connect(
|
||||
[this](const auto &action) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
if (chan->isEmpty())
|
||||
|
@ -352,7 +365,7 @@ void Application::initPubSub()
|
|||
}
|
||||
|
||||
QString text =
|
||||
QString("%1 cleared the chat").arg(action.source.login);
|
||||
QString("%1 cleared the chat.").arg(action.source.login);
|
||||
|
||||
auto msg = makeSystemMessage(text);
|
||||
postToThread([chan, msg] {
|
||||
|
@ -360,7 +373,7 @@ void Application::initPubSub()
|
|||
});
|
||||
});
|
||||
|
||||
std::ignore = this->twitch->pubsub->signals_.moderation.modeChanged.connect(
|
||||
std::ignore = this->twitchPubSub->moderation.modeChanged.connect(
|
||||
[this](const auto &action) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
if (chan->isEmpty())
|
||||
|
@ -369,7 +382,7 @@ void Application::initPubSub()
|
|||
}
|
||||
|
||||
QString text =
|
||||
QString("%1 turned %2 %3 mode")
|
||||
QString("%1 turned %2 %3 mode.")
|
||||
.arg(action.source.login)
|
||||
.arg(action.state == ModeChangedAction::State::On ? "on"
|
||||
: "off")
|
||||
|
@ -386,29 +399,28 @@ void Application::initPubSub()
|
|||
});
|
||||
});
|
||||
|
||||
std::ignore =
|
||||
this->twitch->pubsub->signals_.moderation.moderationStateChanged
|
||||
.connect([this](const auto &action) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::ignore = this->twitchPubSub->moderation.moderationStateChanged.connect(
|
||||
[this](const auto &action) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QString text;
|
||||
QString text;
|
||||
|
||||
text = QString("%1 %2 %3")
|
||||
.arg(action.source.login,
|
||||
(action.modded ? "modded" : "unmodded"),
|
||||
action.target.login);
|
||||
text = QString("%1 %2 %3.")
|
||||
.arg(action.source.login,
|
||||
(action.modded ? "modded" : "unmodded"),
|
||||
action.target.login);
|
||||
|
||||
auto msg = makeSystemMessage(text);
|
||||
postToThread([chan, msg] {
|
||||
chan->addMessage(msg);
|
||||
});
|
||||
auto msg = makeSystemMessage(text);
|
||||
postToThread([chan, msg] {
|
||||
chan->addMessage(msg);
|
||||
});
|
||||
});
|
||||
|
||||
std::ignore = this->twitch->pubsub->signals_.moderation.userBanned.connect(
|
||||
std::ignore = this->twitchPubSub->moderation.userBanned.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
|
@ -423,67 +435,65 @@ void Application::initPubSub()
|
|||
chan->addOrReplaceTimeout(msg.release());
|
||||
});
|
||||
});
|
||||
std::ignore =
|
||||
this->twitch->pubsub->signals_.moderation.messageDeleted.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
std::ignore = this->twitchPubSub->moderation.messageDeleted.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
if (chan->isEmpty() || getSettings()->hideDeletionActions)
|
||||
if (chan->isEmpty() || getSettings()->hideDeletionActions)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MessageBuilder msg;
|
||||
TwitchMessageBuilder::deletionMessage(action, &msg);
|
||||
msg->flags.set(MessageFlag::PubSub);
|
||||
|
||||
postToThread([chan, msg = msg.release()] {
|
||||
auto replaced = false;
|
||||
LimitedQueueSnapshot<MessagePtr> snapshot =
|
||||
chan->getMessageSnapshot();
|
||||
int snapshotLength = snapshot.size();
|
||||
|
||||
// without parens it doesn't build on windows
|
||||
int end = (std::max)(0, snapshotLength - 200);
|
||||
|
||||
for (int i = snapshotLength - 1; i >= end; --i)
|
||||
{
|
||||
return;
|
||||
auto &s = snapshot[i];
|
||||
if (!s->flags.has(MessageFlag::PubSub) &&
|
||||
s->timeoutUser == msg->timeoutUser)
|
||||
{
|
||||
chan->replaceMessage(s, msg);
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MessageBuilder msg;
|
||||
TwitchMessageBuilder::deletionMessage(action, &msg);
|
||||
msg->flags.set(MessageFlag::PubSub);
|
||||
|
||||
postToThread([chan, msg = msg.release()] {
|
||||
auto replaced = false;
|
||||
LimitedQueueSnapshot<MessagePtr> snapshot =
|
||||
chan->getMessageSnapshot();
|
||||
int snapshotLength = snapshot.size();
|
||||
|
||||
// without parens it doesn't build on windows
|
||||
int end = (std::max)(0, snapshotLength - 200);
|
||||
|
||||
for (int i = snapshotLength - 1; i >= end; --i)
|
||||
{
|
||||
auto &s = snapshot[i];
|
||||
if (!s->flags.has(MessageFlag::PubSub) &&
|
||||
s->timeoutUser == msg->timeoutUser)
|
||||
{
|
||||
chan->replaceMessage(s, msg);
|
||||
replaced = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!replaced)
|
||||
{
|
||||
chan->addMessage(msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
std::ignore =
|
||||
this->twitch->pubsub->signals_.moderation.userUnbanned.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
if (chan->isEmpty())
|
||||
if (!replaced)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto msg = MessageBuilder(action).release();
|
||||
|
||||
postToThread([chan, msg] {
|
||||
chan->addMessage(msg);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
std::ignore = this->twitchPubSub->moderation.userUnbanned.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto msg = MessageBuilder(action).release();
|
||||
|
||||
postToThread([chan, msg] {
|
||||
chan->addMessage(msg);
|
||||
});
|
||||
});
|
||||
|
||||
std::ignore =
|
||||
this->twitch->pubsub->signals_.moderation.suspiciousMessageReceived
|
||||
.connect([&](const auto &action) {
|
||||
this->twitchPubSub->moderation.suspiciousMessageReceived.connect(
|
||||
[&](const auto &action) {
|
||||
if (action.treatment ==
|
||||
PubSubLowTrustUsersMessage::Treatment::INVALID)
|
||||
{
|
||||
|
@ -515,18 +525,26 @@ void Application::initPubSub()
|
|||
return;
|
||||
}
|
||||
|
||||
postToThread([chan, action] {
|
||||
auto twitchChannel =
|
||||
std::dynamic_pointer_cast<TwitchChannel>(chan);
|
||||
if (!twitchChannel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
postToThread([twitchChannel, action] {
|
||||
const auto p =
|
||||
TwitchMessageBuilder::makeLowTrustUserMessage(
|
||||
action, chan->getName());
|
||||
chan->addMessage(p.first);
|
||||
chan->addMessage(p.second);
|
||||
action, twitchChannel->getName(),
|
||||
twitchChannel.get());
|
||||
twitchChannel->addMessage(p.first);
|
||||
twitchChannel->addMessage(p.second);
|
||||
});
|
||||
});
|
||||
|
||||
std::ignore =
|
||||
this->twitch->pubsub->signals_.moderation.suspiciousTreatmentUpdated
|
||||
.connect([&](const auto &action) {
|
||||
this->twitchPubSub->moderation.suspiciousTreatmentUpdated.connect(
|
||||
[&](const auto &action) {
|
||||
if (action.treatment ==
|
||||
PubSubLowTrustUsersMessage::Treatment::INVALID)
|
||||
{
|
||||
|
@ -562,170 +580,162 @@ void Application::initPubSub()
|
|||
});
|
||||
});
|
||||
|
||||
std::ignore =
|
||||
this->twitch->pubsub->signals_.moderation.autoModMessageCaught.connect(
|
||||
[&](const auto &msg, const QString &channelID) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(channelID);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::ignore = this->twitchPubSub->moderation.autoModMessageCaught.connect(
|
||||
[&](const auto &msg, const QString &channelID) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(channelID);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (msg.type)
|
||||
{
|
||||
case PubSubAutoModQueueMessage::Type::
|
||||
AutoModCaughtMessage: {
|
||||
if (msg.status == "PENDING")
|
||||
switch (msg.type)
|
||||
{
|
||||
case PubSubAutoModQueueMessage::Type::AutoModCaughtMessage: {
|
||||
if (msg.status == "PENDING")
|
||||
{
|
||||
AutomodAction action(msg.data, channelID);
|
||||
action.reason = QString("%1 level %2")
|
||||
.arg(msg.contentCategory)
|
||||
.arg(msg.contentLevel);
|
||||
|
||||
action.msgID = msg.messageID;
|
||||
action.message = msg.messageText;
|
||||
|
||||
// this message also contains per-word automod data, which could be implemented
|
||||
|
||||
// extract sender data manually because Twitch loves not being consistent
|
||||
QString senderDisplayName =
|
||||
msg.senderUserDisplayName; // Might be transformed later
|
||||
bool hasLocalizedName = false;
|
||||
if (!msg.senderUserDisplayName.isEmpty())
|
||||
{
|
||||
AutomodAction action(msg.data, channelID);
|
||||
action.reason = QString("%1 level %2")
|
||||
.arg(msg.contentCategory)
|
||||
.arg(msg.contentLevel);
|
||||
|
||||
action.msgID = msg.messageID;
|
||||
action.message = msg.messageText;
|
||||
|
||||
// this message also contains per-word automod data, which could be implemented
|
||||
|
||||
// extract sender data manually because Twitch loves not being consistent
|
||||
QString senderDisplayName =
|
||||
msg.senderUserDisplayName; // Might be transformed later
|
||||
bool hasLocalizedName = false;
|
||||
if (!msg.senderUserDisplayName.isEmpty())
|
||||
// check for non-ascii display names
|
||||
if (QString::compare(msg.senderUserDisplayName,
|
||||
msg.senderUserLogin,
|
||||
Qt::CaseInsensitive) != 0)
|
||||
{
|
||||
// check for non-ascii display names
|
||||
if (QString::compare(msg.senderUserDisplayName,
|
||||
msg.senderUserLogin,
|
||||
Qt::CaseInsensitive) != 0)
|
||||
{
|
||||
hasLocalizedName = true;
|
||||
}
|
||||
hasLocalizedName = true;
|
||||
}
|
||||
QColor senderColor = msg.senderUserChatColor;
|
||||
QString senderColor_;
|
||||
if (!senderColor.isValid() &&
|
||||
getSettings()->colorizeNicknames)
|
||||
{
|
||||
// color may be not present if user is a grey-name
|
||||
senderColor = getRandomColor(msg.senderUserID);
|
||||
}
|
||||
|
||||
// handle username style based on prefered setting
|
||||
switch (
|
||||
getSettings()->usernameDisplayMode.getValue())
|
||||
{
|
||||
case UsernameDisplayMode::Username: {
|
||||
if (hasLocalizedName)
|
||||
{
|
||||
senderDisplayName = msg.senderUserLogin;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UsernameDisplayMode::LocalizedName: {
|
||||
break;
|
||||
}
|
||||
case UsernameDisplayMode::
|
||||
UsernameAndLocalizedName: {
|
||||
if (hasLocalizedName)
|
||||
{
|
||||
senderDisplayName =
|
||||
QString("%1(%2)").arg(
|
||||
msg.senderUserLogin,
|
||||
msg.senderUserDisplayName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
action.target = ActionUser{
|
||||
msg.senderUserID, msg.senderUserLogin,
|
||||
senderDisplayName, senderColor};
|
||||
postToThread([chan, action] {
|
||||
const auto p =
|
||||
TwitchMessageBuilder::makeAutomodMessage(
|
||||
action, chan->getName());
|
||||
chan->addMessage(p.first);
|
||||
chan->addMessage(p.second);
|
||||
|
||||
getApp()->twitch->automodChannel->addMessage(
|
||||
p.first);
|
||||
getApp()->twitch->automodChannel->addMessage(
|
||||
p.second);
|
||||
});
|
||||
}
|
||||
// "ALLOWED" and "DENIED" statuses remain unimplemented
|
||||
// They are versions of automod_message_(denied|approved) but for mods.
|
||||
QColor senderColor = msg.senderUserChatColor;
|
||||
QString senderColor_;
|
||||
if (!senderColor.isValid() &&
|
||||
getSettings()->colorizeNicknames)
|
||||
{
|
||||
// color may be not present if user is a grey-name
|
||||
senderColor = getRandomColor(msg.senderUserID);
|
||||
}
|
||||
|
||||
// handle username style based on prefered setting
|
||||
switch (getSettings()->usernameDisplayMode.getValue())
|
||||
{
|
||||
case UsernameDisplayMode::Username: {
|
||||
if (hasLocalizedName)
|
||||
{
|
||||
senderDisplayName = msg.senderUserLogin;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case UsernameDisplayMode::LocalizedName: {
|
||||
break;
|
||||
}
|
||||
case UsernameDisplayMode::
|
||||
UsernameAndLocalizedName: {
|
||||
if (hasLocalizedName)
|
||||
{
|
||||
senderDisplayName = QString("%1(%2)").arg(
|
||||
msg.senderUserLogin,
|
||||
msg.senderUserDisplayName);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
action.target =
|
||||
ActionUser{msg.senderUserID, msg.senderUserLogin,
|
||||
senderDisplayName, senderColor};
|
||||
postToThread([chan, action] {
|
||||
const auto p =
|
||||
TwitchMessageBuilder::makeAutomodMessage(
|
||||
action, chan->getName());
|
||||
chan->addMessage(p.first);
|
||||
chan->addMessage(p.second);
|
||||
|
||||
getApp()->twitch->automodChannel->addMessage(
|
||||
p.first);
|
||||
getApp()->twitch->automodChannel->addMessage(
|
||||
p.second);
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case PubSubAutoModQueueMessage::Type::INVALID:
|
||||
default: {
|
||||
}
|
||||
break;
|
||||
// "ALLOWED" and "DENIED" statuses remain unimplemented
|
||||
// They are versions of automod_message_(denied|approved) but for mods.
|
||||
}
|
||||
break;
|
||||
|
||||
case PubSubAutoModQueueMessage::Type::INVALID:
|
||||
default: {
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
std::ignore = this->twitchPubSub->moderation.autoModMessageBlocked.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
postToThread([chan, action] {
|
||||
const auto p = TwitchMessageBuilder::makeAutomodMessage(
|
||||
action, chan->getName());
|
||||
chan->addMessage(p.first);
|
||||
chan->addMessage(p.second);
|
||||
});
|
||||
});
|
||||
|
||||
std::ignore = this->twitchPubSub->moderation.automodUserMessage.connect(
|
||||
[&](const auto &action) {
|
||||
// This condition has been set up to execute isInStreamerMode() as the last thing
|
||||
// as it could end up being expensive.
|
||||
if (getSettings()->streamerModeHideModActions && isInStreamerMode())
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto msg = MessageBuilder(action).release();
|
||||
|
||||
postToThread([chan, msg] {
|
||||
chan->addMessage(msg);
|
||||
});
|
||||
chan->deleteMessage(msg->id);
|
||||
});
|
||||
|
||||
std::ignore = this->twitchPubSub->moderation.automodInfoMessage.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
postToThread([chan, action] {
|
||||
const auto p =
|
||||
TwitchMessageBuilder::makeAutomodInfoMessage(action);
|
||||
chan->addMessage(p);
|
||||
});
|
||||
});
|
||||
|
||||
std::ignore =
|
||||
this->twitch->pubsub->signals_.moderation.autoModMessageBlocked.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
postToThread([chan, action] {
|
||||
const auto p = TwitchMessageBuilder::makeAutomodMessage(
|
||||
action, chan->getName());
|
||||
chan->addMessage(p.first);
|
||||
chan->addMessage(p.second);
|
||||
});
|
||||
});
|
||||
|
||||
std::ignore =
|
||||
this->twitch->pubsub->signals_.moderation.automodUserMessage.connect(
|
||||
[&](const auto &action) {
|
||||
// This condition has been set up to execute isInStreamerMode() as the last thing
|
||||
// as it could end up being expensive.
|
||||
if (getSettings()->streamerModeHideModActions &&
|
||||
isInStreamerMode())
|
||||
{
|
||||
return;
|
||||
}
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto msg = MessageBuilder(action).release();
|
||||
|
||||
postToThread([chan, msg] {
|
||||
chan->addMessage(msg);
|
||||
});
|
||||
chan->deleteMessage(msg->id);
|
||||
});
|
||||
|
||||
std::ignore =
|
||||
this->twitch->pubsub->signals_.moderation.automodInfoMessage.connect(
|
||||
[&](const auto &action) {
|
||||
auto chan = this->twitch->getChannelOrEmptyByID(action.roomID);
|
||||
|
||||
if (chan->isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
postToThread([chan, action] {
|
||||
const auto p =
|
||||
TwitchMessageBuilder::makeAutomodInfoMessage(action);
|
||||
chan->addMessage(p);
|
||||
});
|
||||
});
|
||||
|
||||
std::ignore = this->twitch->pubsub->signals_.pointReward.redeemed.connect(
|
||||
[&](auto &data) {
|
||||
this->twitchPubSub->pointReward.redeemed.connect([&](auto &data) {
|
||||
QString channelId = data.value("channel_id").toString();
|
||||
if (channelId.isEmpty())
|
||||
{
|
||||
|
@ -746,29 +756,19 @@ void Application::initPubSub()
|
|||
});
|
||||
});
|
||||
|
||||
this->twitch->pubsub->start();
|
||||
|
||||
auto RequestModerationActions = [this]() {
|
||||
this->twitch->pubsub->setAccount(
|
||||
getApp()->accounts->twitch.getCurrent());
|
||||
// TODO(pajlada): Unlisten to all authed topics instead of only
|
||||
// moderation topics this->twitch->pubsub->UnlistenAllAuthedTopics();
|
||||
|
||||
this->twitch->pubsub->listenToWhispers();
|
||||
};
|
||||
this->twitchPubSub->start();
|
||||
this->twitchPubSub->setAccount(this->accounts->twitch.getCurrent());
|
||||
|
||||
this->accounts->twitch.currentUserChanged.connect(
|
||||
[this] {
|
||||
this->twitch->pubsub->unlistenAllModerationActions();
|
||||
this->twitch->pubsub->unlistenAutomod();
|
||||
this->twitch->pubsub->unlistenLowTrustUsers();
|
||||
this->twitch->pubsub->unlistenWhispers();
|
||||
this->twitchPubSub->unlistenChannelModerationActions();
|
||||
this->twitchPubSub->unlistenAutomod();
|
||||
this->twitchPubSub->unlistenLowTrustUsers();
|
||||
this->twitchPubSub->unlistenChannelPointRewards();
|
||||
|
||||
this->twitchPubSub->setAccount(this->accounts->twitch.getCurrent());
|
||||
},
|
||||
boost::signals2::at_front);
|
||||
|
||||
this->accounts->twitch.currentUserChanged.connect(RequestModerationActions);
|
||||
|
||||
RequestModerationActions();
|
||||
}
|
||||
|
||||
void Application::initBttvLiveUpdates()
|
||||
|
|
|
@ -68,6 +68,7 @@ public:
|
|||
virtual HighlightController *getHighlights() = 0;
|
||||
virtual NotificationController *getNotifications() = 0;
|
||||
virtual ITwitchIrcServer *getTwitch() = 0;
|
||||
virtual PubSub *getTwitchPubSub() = 0;
|
||||
virtual Logging *getChatLogger() = 0;
|
||||
virtual ChatterinoBadges *getChatterinoBadges() = 0;
|
||||
virtual FfzBadges *getFfzBadges() = 0;
|
||||
|
@ -97,6 +98,12 @@ public:
|
|||
Application &operator=(const Application &) = delete;
|
||||
Application &operator=(Application &&) = delete;
|
||||
|
||||
/**
|
||||
* In the interim, before we remove _exit(0); from RunGui.cpp,
|
||||
* this will destroy things we know can be destroyed
|
||||
*/
|
||||
void fakeDtor();
|
||||
|
||||
void initialize(Settings &settings, Paths &paths);
|
||||
void load();
|
||||
void save();
|
||||
|
@ -128,6 +135,7 @@ public:
|
|||
|
||||
private:
|
||||
TwitchLiveController *const twitchLiveController{};
|
||||
std::unique_ptr<PubSub> twitchPubSub;
|
||||
const std::unique_ptr<Logging> logging;
|
||||
|
||||
public:
|
||||
|
@ -181,6 +189,7 @@ public:
|
|||
return this->highlights;
|
||||
}
|
||||
ITwitchIrcServer *getTwitch() override;
|
||||
PubSub *getTwitchPubSub() override;
|
||||
Logging *getChatLogger() override;
|
||||
ChatterinoBadges *getChatterinoBadges() override
|
||||
{
|
||||
|
|
|
@ -591,12 +591,25 @@ set(SOURCE_FILES
|
|||
widgets/dialogs/switcher/SwitchSplitItem.cpp
|
||||
widgets/dialogs/switcher/SwitchSplitItem.hpp
|
||||
|
||||
widgets/helper/color/AlphaSlider.cpp
|
||||
widgets/helper/color/AlphaSlider.hpp
|
||||
widgets/helper/color/Checkerboard.cpp
|
||||
widgets/helper/color/Checkerboard.hpp
|
||||
widgets/helper/color/ColorButton.cpp
|
||||
widgets/helper/color/ColorButton.hpp
|
||||
widgets/helper/color/ColorInput.cpp
|
||||
widgets/helper/color/ColorInput.hpp
|
||||
widgets/helper/color/ColorItemDelegate.cpp
|
||||
widgets/helper/color/ColorItemDelegate.hpp
|
||||
widgets/helper/color/HueSlider.cpp
|
||||
widgets/helper/color/HueSlider.hpp
|
||||
widgets/helper/color/SBCanvas.cpp
|
||||
widgets/helper/color/SBCanvas.hpp
|
||||
|
||||
widgets/helper/Button.cpp
|
||||
widgets/helper/Button.hpp
|
||||
widgets/helper/ChannelView.cpp
|
||||
widgets/helper/ChannelView.hpp
|
||||
widgets/helper/ColorButton.cpp
|
||||
widgets/helper/ColorButton.hpp
|
||||
widgets/helper/ComboBoxItemDelegate.cpp
|
||||
widgets/helper/ComboBoxItemDelegate.hpp
|
||||
widgets/helper/DebugPopup.cpp
|
||||
|
@ -611,8 +624,6 @@ set(SOURCE_FILES
|
|||
widgets/helper/NotebookButton.hpp
|
||||
widgets/helper/NotebookTab.cpp
|
||||
widgets/helper/NotebookTab.hpp
|
||||
widgets/helper/QColorPicker.cpp
|
||||
widgets/helper/QColorPicker.hpp
|
||||
widgets/helper/RegExpItemDelegate.cpp
|
||||
widgets/helper/RegExpItemDelegate.hpp
|
||||
widgets/helper/TrimRegExpValidator.cpp
|
||||
|
|
|
@ -294,5 +294,6 @@ void runGui(QApplication &a, Paths &paths, Settings &settings, const Args &args)
|
|||
// flushing windows clipboard to keep copied messages
|
||||
flushClipboard();
|
||||
#endif
|
||||
app.fakeDtor();
|
||||
}
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -66,7 +66,7 @@ QString uptime(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /uptime command only works in Twitch Channels"));
|
||||
"The /uptime command only works in Twitch Channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -188,14 +188,14 @@ QString clip(const CommandContext &ctx)
|
|||
type != Channel::Type::Twitch && type != Channel::Type::TwitchWatching)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /clip command only works in Twitch Channels"));
|
||||
"The /clip command only works in Twitch Channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /clip command only works in Twitch Channels"));
|
||||
"The /clip command only works in Twitch Channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -214,7 +214,7 @@ QString marker(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /marker command only works in Twitch channels"));
|
||||
"The /marker command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -520,7 +520,7 @@ QString unstableSetUserClientSideColor(const CommandContext &ctx)
|
|||
{
|
||||
ctx.channel->addMessage(
|
||||
makeSystemMessage("The /unstable-set-user-color command only "
|
||||
"works in Twitch channels"));
|
||||
"works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
if (ctx.words.size() < 2)
|
||||
|
|
|
@ -23,7 +23,7 @@ QString addModerator(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /mod command only works in Twitch channels"));
|
||||
"The /mod command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
if (ctx.words.size() < 2)
|
||||
|
|
|
@ -23,7 +23,7 @@ QString addVIP(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /vip command only works in Twitch channels"));
|
||||
"The /vip command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
if (ctx.words.size() < 2)
|
||||
|
|
|
@ -37,7 +37,7 @@ QString sendAnnouncement(const CommandContext &ctx)
|
|||
if (user->isAnon())
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"You must be logged in to use the /announce command"));
|
||||
"You must be logged in to use the /announce command."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
@ -133,7 +133,7 @@ QString sendBan(const CommandContext &ctx)
|
|||
if (twitchChannel == nullptr)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("The /ban command only works in Twitch channels")));
|
||||
QString("The /ban command only works in Twitch channels.")));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -196,7 +196,7 @@ QString sendBanById(const CommandContext &ctx)
|
|||
if (twitchChannel == nullptr)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("The /banid command only works in Twitch channels")));
|
||||
QString("The /banid command only works in Twitch channels.")));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -241,7 +241,7 @@ QString sendTimeout(const CommandContext &ctx)
|
|||
if (twitchChannel == nullptr)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
QString("The /timeout command only works in Twitch channels")));
|
||||
QString("The /timeout command only works in Twitch channels.")));
|
||||
return "";
|
||||
}
|
||||
const auto *usageStr =
|
||||
|
|
|
@ -27,7 +27,7 @@ QString blockUser(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /block command only works in Twitch channels"));
|
||||
"The /block command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -101,7 +101,7 @@ QString unblockUser(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /unblock command only works in Twitch channels"));
|
||||
"The /unblock command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ QString emoteOnly(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /emoteonly command only works in Twitch channels"));
|
||||
"The /emoteonly command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -140,7 +140,7 @@ QString emoteOnlyOff(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /emoteonlyoff command only works in Twitch channels"));
|
||||
"The /emoteonlyoff command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -170,7 +170,7 @@ QString subscribers(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /subscribers command only works in Twitch channels"));
|
||||
"The /subscribers command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -200,7 +200,7 @@ QString subscribersOff(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /subscribersoff command only works in Twitch channels"));
|
||||
"The /subscribersoff command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -230,7 +230,7 @@ QString slow(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /slow command only works in Twitch channels"));
|
||||
"The /slow command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -277,7 +277,7 @@ QString slowOff(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /slowoff command only works in Twitch channels"));
|
||||
"The /slowoff command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -307,7 +307,7 @@ QString followers(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /followers command only works in Twitch channels"));
|
||||
"The /followers command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -355,7 +355,7 @@ QString followersOff(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /followersoff command only works in Twitch channels"));
|
||||
"The /followersoff command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -385,7 +385,7 @@ QString uniqueChat(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /uniquechat command only works in Twitch channels"));
|
||||
"The /uniquechat command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -415,7 +415,7 @@ QString uniqueChatOff(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /uniquechatoff command only works in Twitch channels"));
|
||||
"The /uniquechatoff command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ QString chatters(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /chatters command only works in Twitch Channels"));
|
||||
"The /chatters command only works in Twitch Channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,7 @@ QString chatters(const CommandContext &ctx)
|
|||
getApp()->accounts->twitch.getCurrent()->getUserId(), 1,
|
||||
[channel{ctx.channel}](auto result) {
|
||||
channel->addMessage(
|
||||
makeSystemMessage(QString("Chatter count: %1")
|
||||
makeSystemMessage(QString("Chatter count: %1.")
|
||||
.arg(localizeNumbers(result.total))));
|
||||
},
|
||||
[channel{ctx.channel}](auto error, auto message) {
|
||||
|
@ -101,7 +101,7 @@ QString testChatters(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /test-chatters command only works in Twitch Channels"));
|
||||
"The /test-chatters command only works in Twitch Channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ QString deleteAllMessages(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /clear command only works in Twitch channels"));
|
||||
"The /clear command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -121,7 +121,7 @@ QString deleteOneMessage(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /delete command only works in Twitch channels"));
|
||||
"The /delete command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ QString getModerators(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /mods command only works in Twitch Channels"));
|
||||
"The /mods command only works in Twitch Channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ QString getVIPs(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /vips command only works in Twitch channels"));
|
||||
"The /vips command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
@ -125,7 +125,7 @@ QString startRaid(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /raid command only works in Twitch channels"));
|
||||
"The /raid command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -183,7 +183,7 @@ QString cancelRaid(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /unraid command only works in Twitch channels"));
|
||||
"The /unraid command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ QString removeModerator(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /unmod command only works in Twitch channels"));
|
||||
"The /unmod command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
if (ctx.words.size() < 2)
|
||||
|
|
|
@ -23,7 +23,7 @@ QString removeVIP(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /unvip command only works in Twitch channels"));
|
||||
"The /unvip command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
if (ctx.words.size() < 2)
|
||||
|
|
|
@ -20,7 +20,7 @@ QString sendReply(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /reply command only works in Twitch channels"));
|
||||
"The /reply command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ QString sendReply(const CommandContext &ctx)
|
|||
}
|
||||
|
||||
ctx.channel->addMessage(
|
||||
makeSystemMessage("A message from that user wasn't found"));
|
||||
makeSystemMessage("A message from that user wasn't found."));
|
||||
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ QString toggleShieldMode(const CommandContext &ctx, bool isActivating)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
QStringLiteral("The %1 command only works in Twitch channels")
|
||||
QStringLiteral("The %1 command only works in Twitch channels.")
|
||||
.arg(command)));
|
||||
return {};
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ QString toggleShieldMode(const CommandContext &ctx, bool isActivating)
|
|||
if (user->isAnon())
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
QStringLiteral("You must be logged in to use the %1 command")
|
||||
QStringLiteral("You must be logged in to use the %1 command.")
|
||||
.arg(command)));
|
||||
return {};
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ QString sendShoutout(const CommandContext &ctx)
|
|||
if (twitchChannel == nullptr)
|
||||
{
|
||||
channel->addMessage(makeSystemMessage(
|
||||
"The /shoutout command only works in Twitch channels"));
|
||||
"The /shoutout command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ QString sendShoutout(const CommandContext &ctx)
|
|||
if (currentUser->isAnon())
|
||||
{
|
||||
channel->addMessage(
|
||||
makeSystemMessage("You must be logged in to send shoutout"));
|
||||
makeSystemMessage("You must be logged in to send shoutout."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ QString startCommercial(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /commercial command only works in Twitch channels"));
|
||||
"The /commercial command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -106,7 +106,7 @@ QString startCommercial(const CommandContext &ctx)
|
|||
if (user->isAnon())
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"You must be logged in to use the /commercial command"));
|
||||
"You must be logged in to use the /commercial command."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ QString unbanUser(const CommandContext &ctx)
|
|||
if (ctx.twitchChannel == nullptr)
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
QString("The %1 command only works in Twitch channels")
|
||||
QString("The %1 command only works in Twitch channels.")
|
||||
.arg(commandName)));
|
||||
return "";
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ QString updateUserColor(const CommandContext &ctx)
|
|||
if (!ctx.channel->isTwitchChannel())
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"The /color command only works in Twitch channels"));
|
||||
"The /color command only works in Twitch channels."));
|
||||
return "";
|
||||
}
|
||||
auto user = getApp()->accounts->twitch.getCurrent();
|
||||
|
@ -31,7 +31,7 @@ QString updateUserColor(const CommandContext &ctx)
|
|||
if (user->isAnon())
|
||||
{
|
||||
ctx.channel->addMessage(makeSystemMessage(
|
||||
"You must be logged in to use the /color command"));
|
||||
"You must be logged in to use the /color command."));
|
||||
return "";
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,8 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
|
|||
* flags.whisper
|
||||
* flags.reply
|
||||
* flags.automod
|
||||
* flags.restricted
|
||||
* flags.monitored
|
||||
*
|
||||
* message.content
|
||||
* message.length
|
||||
|
@ -101,6 +103,8 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
|
|||
{"flags.whisper", m->flags.has(MessageFlag::Whisper)},
|
||||
{"flags.reply", m->flags.has(MessageFlag::ReplyMessage)},
|
||||
{"flags.automod", m->flags.has(MessageFlag::AutoMod)},
|
||||
{"flags.restricted", m->flags.has(MessageFlag::RestrictedMessage)},
|
||||
{"flags.monitored", m->flags.has(MessageFlag::MonitoredMessage)},
|
||||
|
||||
{"message.content", m->messageText},
|
||||
{"message.length", m->messageText.length()},
|
||||
|
|
|
@ -44,6 +44,8 @@ static const QMap<QString, Type> MESSAGE_TYPING_CONTEXT = {
|
|||
{"flags.whisper", Type::Bool},
|
||||
{"flags.reply", Type::Bool},
|
||||
{"flags.automod", Type::Bool},
|
||||
{"flags.restricted", Type::Bool},
|
||||
{"flags.monitored", Type::Bool},
|
||||
{"message.content", Type::String},
|
||||
{"message.length", Type::Int},
|
||||
};
|
||||
|
|
|
@ -32,6 +32,8 @@ static const QMap<QString, QString> validIdentifiersMap = {
|
|||
{"flags.whisper", "whisper message?"},
|
||||
{"flags.reply", "reply message?"},
|
||||
{"flags.automod", "automod message?"},
|
||||
{"flags.restricted", "restricted message?"},
|
||||
{"flags.monitored", "monitored message?"},
|
||||
{"message.content", "message text"},
|
||||
{"message.length", "message length"}};
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#include "UserHighlightModel.hpp"
|
||||
#include "controllers/highlights/UserHighlightModel.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "controllers/highlights/HighlightModel.hpp"
|
||||
#include "controllers/highlights/HighlightPhrase.hpp"
|
||||
#include "providers/colors/ColorProvider.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
@ -10,8 +9,6 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
using Column = HighlightModel::Column;
|
||||
|
||||
// commandmodel
|
||||
UserHighlightModel::UserHighlightModel(QObject *parent)
|
||||
: SignalVectorModel<HighlightPhrase>(Column::COUNT, parent)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/SignalVectorModel.hpp"
|
||||
#include "controllers/highlights/HighlightModel.hpp"
|
||||
|
||||
#include <QObject>
|
||||
|
||||
|
@ -12,6 +13,8 @@ class HighlightPhrase;
|
|||
class UserHighlightModel : public SignalVectorModel<HighlightPhrase>
|
||||
{
|
||||
public:
|
||||
using Column = HighlightModel::Column;
|
||||
|
||||
explicit UserHighlightModel(QObject *parent);
|
||||
|
||||
protected:
|
||||
|
|
|
@ -32,9 +32,11 @@ bool isIgnoredMessage(IgnoredMessageParameters &¶ms)
|
|||
{
|
||||
auto sourceUserID = params.twitchUserID;
|
||||
|
||||
bool isBlocked =
|
||||
getApp()->accounts->twitch.getCurrent()->blockedUserIds().contains(
|
||||
sourceUserID);
|
||||
bool isBlocked = getIApp()
|
||||
->getAccounts()
|
||||
->twitch.getCurrent()
|
||||
->blockedUserIds()
|
||||
.contains(sourceUserID);
|
||||
if (isBlocked)
|
||||
{
|
||||
switch (static_cast<ShowIgnoredUsersMessages>(
|
||||
|
|
|
@ -53,6 +53,10 @@ enum class MessageFlag : int64_t {
|
|||
/// The message caught by AutoMod containing the user who sent the message & its contents
|
||||
AutoModOffendingMessage = (1LL << 31),
|
||||
LowTrustUsers = (1LL << 32),
|
||||
/// The message is sent by a user marked as restricted with Twitch's "Low Trust"/"Suspicious User" feature
|
||||
RestrictedMessage = (1LL << 33),
|
||||
/// The message is sent by a user marked as monitor with Twitch's "Low Trust"/"Suspicious User" feature
|
||||
MonitoredMessage = (1LL << 34),
|
||||
};
|
||||
using MessageFlags = FlagsEnum<MessageFlag>;
|
||||
|
||||
|
|
|
@ -255,7 +255,8 @@ MessageBuilder::MessageBuilder(const BanAction &action, uint32_t count)
|
|||
this->emplaceSystemTextAndUpdate("banned", text);
|
||||
if (action.reason.isEmpty())
|
||||
{
|
||||
this->emplaceSystemTextAndUpdate(action.target.login, text)
|
||||
this->emplaceSystemTextAndUpdate(action.target.login + ".",
|
||||
text)
|
||||
->setLink({Link::UserInfo, action.target.login});
|
||||
}
|
||||
else
|
||||
|
@ -315,7 +316,7 @@ MessageBuilder::MessageBuilder(const UnbanAction &action)
|
|||
->setLink({Link::UserInfo, action.source.login});
|
||||
this->emplaceSystemTextAndUpdate(
|
||||
action.wasBan() ? "unbanned" : "untimedout", text);
|
||||
this->emplaceSystemTextAndUpdate(action.target.login, text)
|
||||
this->emplaceSystemTextAndUpdate(action.target.login + ".", text)
|
||||
->setLink({Link::UserInfo, action.target.login});
|
||||
|
||||
this->message().messageText = text;
|
||||
|
|
|
@ -52,6 +52,14 @@ MessageFlagsPredicate::MessageFlagsPredicate(const QString &flags, bool negate)
|
|||
{
|
||||
this->flags_.set(MessageFlag::ReplyMessage);
|
||||
}
|
||||
else if (flag == "restricted")
|
||||
{
|
||||
this->flags_.set(MessageFlag::RestrictedMessage);
|
||||
}
|
||||
else if (flag == "monitored")
|
||||
{
|
||||
this->flags_.set(MessageFlag::MonitoredMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -142,31 +142,33 @@ namespace {
|
|||
return anyModifications;
|
||||
}
|
||||
|
||||
std::pair<Outcome, EmoteMap> parseChannelEmotes(
|
||||
const QJsonObject &jsonRoot, const QString &channelDisplayName)
|
||||
{
|
||||
auto emotes = EmoteMap();
|
||||
|
||||
auto innerParse = [&jsonRoot, &emotes,
|
||||
&channelDisplayName](const char *key) {
|
||||
auto jsonEmotes = jsonRoot.value(key).toArray();
|
||||
for (auto jsonEmote_ : jsonEmotes)
|
||||
{
|
||||
auto emote = createChannelEmote(channelDisplayName,
|
||||
jsonEmote_.toObject());
|
||||
|
||||
emotes[emote.name] =
|
||||
cachedOrMake(std::move(emote.emote), emote.id);
|
||||
}
|
||||
};
|
||||
|
||||
innerParse("channelEmotes");
|
||||
innerParse("sharedEmotes");
|
||||
|
||||
return {Success, std::move(emotes)};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
using namespace bttv::detail;
|
||||
|
||||
EmoteMap bttv::detail::parseChannelEmotes(const QJsonObject &jsonRoot,
|
||||
const QString &channelDisplayName)
|
||||
{
|
||||
auto emotes = EmoteMap();
|
||||
|
||||
auto innerParse = [&jsonRoot, &emotes,
|
||||
&channelDisplayName](const char *key) {
|
||||
auto jsonEmotes = jsonRoot.value(key).toArray();
|
||||
for (auto jsonEmote_ : jsonEmotes)
|
||||
{
|
||||
auto emote =
|
||||
createChannelEmote(channelDisplayName, jsonEmote_.toObject());
|
||||
|
||||
emotes[emote.name] = cachedOrMake(std::move(emote.emote), emote.id);
|
||||
}
|
||||
};
|
||||
|
||||
innerParse("channelEmotes");
|
||||
innerParse("sharedEmotes");
|
||||
|
||||
return emotes;
|
||||
}
|
||||
|
||||
//
|
||||
// BttvEmotes
|
||||
//
|
||||
|
@ -230,14 +232,11 @@ void BttvEmotes::loadChannel(std::weak_ptr<Channel> channel,
|
|||
.timeout(20000)
|
||||
.onSuccess([callback = std::move(callback), channel, channelDisplayName,
|
||||
manualRefresh](auto result) {
|
||||
auto pair =
|
||||
auto emotes =
|
||||
parseChannelEmotes(result.parseJson(), channelDisplayName);
|
||||
bool hasEmotes = false;
|
||||
if (pair.first)
|
||||
{
|
||||
hasEmotes = !pair.second.empty();
|
||||
callback(std::move(pair.second));
|
||||
}
|
||||
bool hasEmotes = !emotes.empty();
|
||||
callback(std::move(emotes));
|
||||
|
||||
if (auto shared = channel.lock(); manualRefresh)
|
||||
{
|
||||
if (hasEmotes)
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#include "common/Aliases.hpp"
|
||||
#include "common/Atomic.hpp"
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
|
@ -15,6 +17,13 @@ class Channel;
|
|||
struct BttvLiveUpdateEmoteUpdateAddMessage;
|
||||
struct BttvLiveUpdateEmoteRemoveMessage;
|
||||
|
||||
namespace bttv::detail {
|
||||
|
||||
EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot,
|
||||
const QString &channelDisplayName);
|
||||
|
||||
} // namespace bttv::detail
|
||||
|
||||
class BttvEmotes final
|
||||
{
|
||||
static constexpr const char *globalEmoteApiUrl =
|
||||
|
|
|
@ -149,19 +149,22 @@ namespace {
|
|||
return authorityBadge;
|
||||
}
|
||||
|
||||
EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot)
|
||||
{
|
||||
auto emotes = EmoteMap();
|
||||
|
||||
for (const auto emoteSetRef : jsonRoot["sets"].toObject())
|
||||
{
|
||||
parseEmoteSetInto(emoteSetRef.toObject(), "Channel", emotes);
|
||||
}
|
||||
|
||||
return emotes;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
using namespace ffz::detail;
|
||||
|
||||
EmoteMap ffz::detail::parseChannelEmotes(const QJsonObject &jsonRoot)
|
||||
{
|
||||
auto emotes = EmoteMap();
|
||||
|
||||
for (const auto emoteSetRef : jsonRoot["sets"].toObject())
|
||||
{
|
||||
parseEmoteSetInto(emoteSetRef.toObject(), "Channel", emotes);
|
||||
}
|
||||
|
||||
return emotes;
|
||||
}
|
||||
|
||||
FfzEmotes::FfzEmotes()
|
||||
: global_(std::make_shared<EmoteMap>())
|
||||
{
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
#include "common/Aliases.hpp"
|
||||
#include "common/Atomic.hpp"
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
|
@ -13,6 +15,12 @@ using EmotePtr = std::shared_ptr<const Emote>;
|
|||
class EmoteMap;
|
||||
class Channel;
|
||||
|
||||
namespace ffz::detail {
|
||||
|
||||
EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot);
|
||||
|
||||
} // namespace ffz::detail
|
||||
|
||||
class FfzEmotes final
|
||||
{
|
||||
public:
|
||||
|
|
|
@ -128,7 +128,34 @@ bool checkEmoteVisibility(const QJsonObject &emoteData)
|
|||
return !flags.has(SeventvEmoteFlag::ContentTwitchDisallowed);
|
||||
}
|
||||
|
||||
EmoteMap parseEmotes(const QJsonArray &emoteSetEmotes, bool isGlobal)
|
||||
EmotePtr createUpdatedEmote(const EmotePtr &oldEmote,
|
||||
const EmoteUpdateDispatch &dispatch)
|
||||
{
|
||||
bool toNonAliased = oldEmote->baseName.has_value() &&
|
||||
dispatch.emoteName == oldEmote->baseName->string;
|
||||
|
||||
auto baseName = oldEmote->baseName.value_or(oldEmote->name);
|
||||
auto emote = std::make_shared<const Emote>(Emote(
|
||||
{EmoteName{dispatch.emoteName}, oldEmote->images,
|
||||
toNonAliased
|
||||
? createTooltip(dispatch.emoteName, oldEmote->author.string, false)
|
||||
: createAliasedTooltip(dispatch.emoteName, baseName.string,
|
||||
oldEmote->author.string, false),
|
||||
oldEmote->homePage, oldEmote->zeroWidth, oldEmote->id,
|
||||
oldEmote->author, makeConditionedOptional(!toNonAliased, baseName)}));
|
||||
return emote;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
using namespace seventv::eventapi;
|
||||
using namespace seventv::detail;
|
||||
using namespace literals;
|
||||
|
||||
EmoteMap seventv::detail::parseEmotes(const QJsonArray &emoteSetEmotes,
|
||||
bool isGlobal)
|
||||
{
|
||||
auto emotes = EmoteMap();
|
||||
|
||||
|
@ -158,31 +185,6 @@ EmoteMap parseEmotes(const QJsonArray &emoteSetEmotes, bool isGlobal)
|
|||
return emotes;
|
||||
}
|
||||
|
||||
EmotePtr createUpdatedEmote(const EmotePtr &oldEmote,
|
||||
const EmoteUpdateDispatch &dispatch)
|
||||
{
|
||||
bool toNonAliased = oldEmote->baseName.has_value() &&
|
||||
dispatch.emoteName == oldEmote->baseName->string;
|
||||
|
||||
auto baseName = oldEmote->baseName.value_or(oldEmote->name);
|
||||
auto emote = std::make_shared<const Emote>(Emote(
|
||||
{EmoteName{dispatch.emoteName}, oldEmote->images,
|
||||
toNonAliased
|
||||
? createTooltip(dispatch.emoteName, oldEmote->author.string, false)
|
||||
: createAliasedTooltip(dispatch.emoteName, baseName.string,
|
||||
oldEmote->author.string, false),
|
||||
oldEmote->homePage, oldEmote->zeroWidth, oldEmote->id,
|
||||
oldEmote->author, makeConditionedOptional(!toNonAliased, baseName)}));
|
||||
return emote;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
using namespace seventv::eventapi;
|
||||
using namespace literals;
|
||||
|
||||
SeventvEmotes::SeventvEmotes()
|
||||
: global_(std::make_shared<EmoteMap>())
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "common/Atomic.hpp"
|
||||
#include "common/FlagsEnum.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include <memory>
|
||||
|
@ -77,6 +78,12 @@ enum class SeventvEmoteSetFlag : uint32_t {
|
|||
};
|
||||
using SeventvEmoteSetFlags = FlagsEnum<SeventvEmoteSetFlag>;
|
||||
|
||||
namespace seventv::detail {
|
||||
|
||||
EmoteMap parseEmotes(const QJsonArray &emoteSetEmotes, bool isGlobal);
|
||||
|
||||
} // namespace seventv::detail
|
||||
|
||||
class SeventvEmotes final
|
||||
{
|
||||
public:
|
||||
|
|
|
@ -128,7 +128,7 @@ void updateReplyParticipatedStatus(const QVariantMap &tags,
|
|||
bool isNew)
|
||||
{
|
||||
const auto ¤tLogin =
|
||||
getApp()->accounts->twitch.getCurrent()->getUserName();
|
||||
getIApp()->getAccounts()->twitch.getCurrent()->getUserName();
|
||||
|
||||
if (thread->subscribed())
|
||||
{
|
||||
|
|
|
@ -22,6 +22,8 @@ PubSubClient::PubSubClient(WebsocketClient &websocketClient,
|
|||
const PubSubClientOptions &clientOptions)
|
||||
: websocketClient_(websocketClient)
|
||||
, handle_(handle)
|
||||
, heartbeatTimer_(std::make_shared<boost::asio::steady_timer>(
|
||||
this->websocketClient_.get_io_service()))
|
||||
, clientOptions_(clientOptions)
|
||||
{
|
||||
}
|
||||
|
@ -40,6 +42,7 @@ void PubSubClient::stop()
|
|||
assert(this->started_);
|
||||
|
||||
this->started_ = false;
|
||||
this->heartbeatTimer_->cancel();
|
||||
}
|
||||
|
||||
void PubSubClient::close(const std::string &reason,
|
||||
|
@ -187,8 +190,8 @@ void PubSubClient::ping()
|
|||
|
||||
auto self = this->shared_from_this();
|
||||
|
||||
runAfter(this->websocketClient_.get_io_service(),
|
||||
this->clientOptions_.pingInterval_, [self](auto timer) {
|
||||
runAfter(this->heartbeatTimer_, this->clientOptions_.pingInterval_,
|
||||
[self](auto timer) {
|
||||
if (!self->started_)
|
||||
{
|
||||
return;
|
||||
|
|
|
@ -70,6 +70,7 @@ private:
|
|||
std::atomic<bool> awaitingPong_{false};
|
||||
std::atomic<bool> started_{false};
|
||||
|
||||
std::shared_ptr<boost::asio::steady_timer> heartbeatTimer_;
|
||||
const PubSubClientOptions &clientOptions_;
|
||||
};
|
||||
|
||||
|
|
|
@ -17,12 +17,14 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
using websocketpp::lib::bind;
|
||||
using websocketpp::lib::placeholders::_1;
|
||||
using websocketpp::lib::placeholders::_2;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
|
@ -36,7 +38,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
const auto &roomID) {
|
||||
ClearChatAction action(data, roomID);
|
||||
|
||||
this->signals_.moderation.chatCleared.invoke(action);
|
||||
this->moderation.chatCleared.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["slowoff"] = [this](const auto &data,
|
||||
|
@ -46,7 +48,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.mode = ModeChangedAction::Mode::Slow;
|
||||
action.state = ModeChangedAction::State::Off;
|
||||
|
||||
this->signals_.moderation.modeChanged.invoke(action);
|
||||
this->moderation.modeChanged.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["slow"] = [this](const auto &data,
|
||||
|
@ -69,7 +71,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
|
||||
action.duration = args.at(0).toString().toUInt(&ok, 10);
|
||||
|
||||
this->signals_.moderation.modeChanged.invoke(action);
|
||||
this->moderation.modeChanged.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["r9kbetaoff"] = [this](const auto &data,
|
||||
|
@ -79,7 +81,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.mode = ModeChangedAction::Mode::R9K;
|
||||
action.state = ModeChangedAction::State::Off;
|
||||
|
||||
this->signals_.moderation.modeChanged.invoke(action);
|
||||
this->moderation.modeChanged.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["r9kbeta"] = [this](const auto &data,
|
||||
|
@ -89,7 +91,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.mode = ModeChangedAction::Mode::R9K;
|
||||
action.state = ModeChangedAction::State::On;
|
||||
|
||||
this->signals_.moderation.modeChanged.invoke(action);
|
||||
this->moderation.modeChanged.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["subscribersoff"] =
|
||||
|
@ -99,7 +101,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.mode = ModeChangedAction::Mode::SubscribersOnly;
|
||||
action.state = ModeChangedAction::State::Off;
|
||||
|
||||
this->signals_.moderation.modeChanged.invoke(action);
|
||||
this->moderation.modeChanged.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["subscribers"] = [this](const auto &data,
|
||||
|
@ -109,7 +111,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.mode = ModeChangedAction::Mode::SubscribersOnly;
|
||||
action.state = ModeChangedAction::State::On;
|
||||
|
||||
this->signals_.moderation.modeChanged.invoke(action);
|
||||
this->moderation.modeChanged.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["emoteonlyoff"] =
|
||||
|
@ -119,7 +121,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.mode = ModeChangedAction::Mode::EmoteOnly;
|
||||
action.state = ModeChangedAction::State::Off;
|
||||
|
||||
this->signals_.moderation.modeChanged.invoke(action);
|
||||
this->moderation.modeChanged.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["emoteonly"] = [this](const auto &data,
|
||||
|
@ -129,7 +131,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.mode = ModeChangedAction::Mode::EmoteOnly;
|
||||
action.state = ModeChangedAction::State::On;
|
||||
|
||||
this->signals_.moderation.modeChanged.invoke(action);
|
||||
this->moderation.modeChanged.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["unmod"] = [this](const auto &data,
|
||||
|
@ -149,7 +151,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
|
||||
action.modded = false;
|
||||
|
||||
this->signals_.moderation.moderationStateChanged.invoke(action);
|
||||
this->moderation.moderationStateChanged.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["mod"] = [this](const auto &data,
|
||||
|
@ -167,7 +169,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.target.id = data.value("target_user_id").toString();
|
||||
action.target.login = data.value("target_user_login").toString();
|
||||
|
||||
this->signals_.moderation.moderationStateChanged.invoke(action);
|
||||
this->moderation.moderationStateChanged.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["timeout"] = [this](const auto &data,
|
||||
|
@ -191,7 +193,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.duration = args[1].toString().toUInt(&ok, 10);
|
||||
action.reason = args[2].toString(); // May be omitted
|
||||
|
||||
this->signals_.moderation.userBanned.invoke(action);
|
||||
this->moderation.userBanned.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["delete"] = [this](const auto &data,
|
||||
|
@ -214,7 +216,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.messageText = args[1].toString();
|
||||
action.messageId = args[2].toString();
|
||||
|
||||
this->signals_.moderation.messageDeleted.invoke(action);
|
||||
this->moderation.messageDeleted.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["ban"] = [this](const auto &data,
|
||||
|
@ -236,7 +238,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.target.login = args[0].toString();
|
||||
action.reason = args[1].toString(); // May be omitted
|
||||
|
||||
this->signals_.moderation.userBanned.invoke(action);
|
||||
this->moderation.userBanned.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["unban"] = [this](const auto &data,
|
||||
|
@ -259,7 +261,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
|
||||
action.target.login = args[0].toString();
|
||||
|
||||
this->signals_.moderation.userUnbanned.invoke(action);
|
||||
this->moderation.userUnbanned.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["untimeout"] = [this](const auto &data,
|
||||
|
@ -282,7 +284,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
|
||||
action.target.login = args[0].toString();
|
||||
|
||||
this->signals_.moderation.userUnbanned.invoke(action);
|
||||
this->moderation.userUnbanned.invoke(action);
|
||||
};
|
||||
|
||||
/*
|
||||
|
@ -315,7 +317,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.message = args[1].toString(); // May be omitted
|
||||
action.reason = args[2].toString(); // May be omitted
|
||||
|
||||
this->signals_.moderation.autoModMessageBlocked.invoke(action);
|
||||
this->moderation.autoModMessageBlocked.invoke(action);
|
||||
};
|
||||
*/
|
||||
|
||||
|
@ -323,21 +325,21 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
[this](const auto &data, const auto &roomID) {
|
||||
AutomodInfoAction action(data, roomID);
|
||||
action.type = AutomodInfoAction::OnHold;
|
||||
this->signals_.moderation.automodInfoMessage.invoke(action);
|
||||
this->moderation.automodInfoMessage.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["automod_message_denied"] =
|
||||
[this](const auto &data, const auto &roomID) {
|
||||
AutomodInfoAction action(data, roomID);
|
||||
action.type = AutomodInfoAction::Denied;
|
||||
this->signals_.moderation.automodInfoMessage.invoke(action);
|
||||
this->moderation.automodInfoMessage.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["automod_message_approved"] =
|
||||
[this](const auto &data, const auto &roomID) {
|
||||
AutomodInfoAction action(data, roomID);
|
||||
action.type = AutomodInfoAction::Approved;
|
||||
this->signals_.moderation.automodInfoMessage.invoke(action);
|
||||
this->moderation.automodInfoMessage.invoke(action);
|
||||
};
|
||||
|
||||
this->channelTermsActionHandlers["add_permitted_term"] =
|
||||
|
@ -351,7 +353,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.message = data.value("text").toString();
|
||||
action.source.login = data.value("requester_login").toString();
|
||||
|
||||
this->signals_.moderation.automodUserMessage.invoke(action);
|
||||
this->moderation.automodUserMessage.invoke(action);
|
||||
};
|
||||
|
||||
this->channelTermsActionHandlers["add_blocked_term"] =
|
||||
|
@ -365,7 +367,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.message = data.value("text").toString();
|
||||
action.source.login = data.value("requester_login").toString();
|
||||
|
||||
this->signals_.moderation.automodUserMessage.invoke(action);
|
||||
this->moderation.automodUserMessage.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["delete_permitted_term"] =
|
||||
|
@ -385,7 +387,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
|
||||
action.message = args[0].toString();
|
||||
|
||||
this->signals_.moderation.automodUserMessage.invoke(action);
|
||||
this->moderation.automodUserMessage.invoke(action);
|
||||
};
|
||||
|
||||
this->channelTermsActionHandlers["delete_permitted_term"] =
|
||||
|
@ -399,7 +401,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.message = data.value("text").toString();
|
||||
action.source.login = data.value("requester_login").toString();
|
||||
|
||||
this->signals_.moderation.automodUserMessage.invoke(action);
|
||||
this->moderation.automodUserMessage.invoke(action);
|
||||
};
|
||||
|
||||
this->moderationActionHandlers["delete_blocked_term"] =
|
||||
|
@ -420,7 +422,7 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
|
||||
action.message = args[0].toString();
|
||||
|
||||
this->signals_.moderation.automodUserMessage.invoke(action);
|
||||
this->moderation.automodUserMessage.invoke(action);
|
||||
};
|
||||
this->channelTermsActionHandlers["delete_blocked_term"] =
|
||||
[this](const auto &data, const auto &roomID) {
|
||||
|
@ -434,21 +436,9 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
action.message = data.value("text").toString();
|
||||
action.source.login = data.value("requester_login").toString();
|
||||
|
||||
this->signals_.moderation.automodUserMessage.invoke(action);
|
||||
this->moderation.automodUserMessage.invoke(action);
|
||||
};
|
||||
|
||||
// We don't get this one anymore or anything similiar
|
||||
// We need some new topic so we can listen
|
||||
//
|
||||
//this->moderationActionHandlers["modified_automod_properties"] =
|
||||
// [this](const auto &data, const auto &roomID) {
|
||||
// // The automod settings got modified
|
||||
// AutomodUserAction action(data, roomID);
|
||||
// getCreatedByUser(data, action.source);
|
||||
// action.type = AutomodUserAction::Properties;
|
||||
// this->signals_.moderation.automodUserMessage.invoke(action);
|
||||
// };
|
||||
|
||||
this->moderationActionHandlers["denied_automod_message"] =
|
||||
[](const auto &data, const auto &roomID) {
|
||||
// This message got denied by a moderator
|
||||
|
@ -482,18 +472,17 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
|
|||
bind(&PubSub::onConnectionFail, this, ::_1));
|
||||
}
|
||||
|
||||
PubSub::~PubSub()
|
||||
{
|
||||
this->stop();
|
||||
}
|
||||
|
||||
void PubSub::setAccount(std::shared_ptr<TwitchAccount> account)
|
||||
{
|
||||
this->token_ = account->getOAuthToken();
|
||||
this->userID_ = account->getUserId();
|
||||
}
|
||||
|
||||
void PubSub::setAccountData(QString token, QString userID)
|
||||
{
|
||||
this->token_ = token;
|
||||
this->userID_ = userID;
|
||||
}
|
||||
|
||||
void PubSub::addClient()
|
||||
{
|
||||
if (this->addingClient)
|
||||
|
@ -525,104 +514,43 @@ void PubSub::start()
|
|||
{
|
||||
this->work = std::make_shared<boost::asio::io_service::work>(
|
||||
this->websocketClient.get_io_service());
|
||||
this->mainThread.reset(
|
||||
new std::thread(std::bind(&PubSub::runThread, this)));
|
||||
this->thread.reset(new std::thread(std::bind(&PubSub::runThread, this)));
|
||||
}
|
||||
|
||||
void PubSub::stop()
|
||||
{
|
||||
this->stopping_ = true;
|
||||
|
||||
for (const auto &client : this->clients)
|
||||
for (const auto &[hdl, client] : this->clients)
|
||||
{
|
||||
client.second->close("Shutting down");
|
||||
(void)hdl;
|
||||
|
||||
client->close("Shutting down");
|
||||
}
|
||||
|
||||
this->work.reset();
|
||||
|
||||
if (this->mainThread->joinable())
|
||||
if (this->thread->joinable())
|
||||
{
|
||||
this->mainThread->join();
|
||||
// NOTE: We spawn a new thread to join the websocket thread.
|
||||
// There is a case where a new client was initiated but not added to the clients list.
|
||||
// We just don't join the thread & let the operating system nuke the thread if joining fails
|
||||
// within 1s.
|
||||
// We could fix the underlying bug, but this is easier & we realistically won't use this exact code
|
||||
// for super much longer.
|
||||
auto joiner = std::async(std::launch::async, &std::thread::join,
|
||||
this->thread.get());
|
||||
if (joiner.wait_for(1s) == std::future_status::timeout)
|
||||
{
|
||||
qCWarning(chatterinoPubSub)
|
||||
<< "Thread didn't join within 1 second, rip it out";
|
||||
this->websocketClient.stop();
|
||||
}
|
||||
}
|
||||
|
||||
assert(this->clients.empty());
|
||||
}
|
||||
|
||||
void PubSub::unlistenAllModerationActions()
|
||||
{
|
||||
for (const auto &p : this->clients)
|
||||
{
|
||||
const auto &client = p.second;
|
||||
if (const auto &[topics, nonce] =
|
||||
client->unlistenPrefix("chat_moderator_actions.");
|
||||
!topics.empty())
|
||||
{
|
||||
this->registerNonce(nonce, {
|
||||
client,
|
||||
"UNLISTEN",
|
||||
topics,
|
||||
topics.size(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PubSub::unlistenAutomod()
|
||||
{
|
||||
for (const auto &p : this->clients)
|
||||
{
|
||||
const auto &client = p.second;
|
||||
if (const auto &[topics, nonce] =
|
||||
client->unlistenPrefix("automod-queue.");
|
||||
!topics.empty())
|
||||
{
|
||||
this->registerNonce(nonce, {
|
||||
client,
|
||||
"UNLISTEN",
|
||||
topics,
|
||||
topics.size(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PubSub::unlistenLowTrustUsers()
|
||||
{
|
||||
for (const auto &p : this->clients)
|
||||
{
|
||||
const auto &client = p.second;
|
||||
if (const auto &[topics, nonce] =
|
||||
client->unlistenPrefix("low-trust-users.");
|
||||
!topics.empty())
|
||||
{
|
||||
this->registerNonce(nonce, {
|
||||
client,
|
||||
"UNLISTEN",
|
||||
topics,
|
||||
topics.size(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PubSub::unlistenWhispers()
|
||||
{
|
||||
for (const auto &p : this->clients)
|
||||
{
|
||||
const auto &client = p.second;
|
||||
if (const auto &[topics, nonce] = client->unlistenPrefix("whispers.");
|
||||
!topics.empty())
|
||||
{
|
||||
this->registerNonce(nonce, {
|
||||
client,
|
||||
"UNLISTEN",
|
||||
topics,
|
||||
topics.size(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PubSub::listenToWhispers()
|
||||
{
|
||||
if (this->userID_.isEmpty())
|
||||
|
@ -642,6 +570,11 @@ bool PubSub::listenToWhispers()
|
|||
return true;
|
||||
}
|
||||
|
||||
void PubSub::unlistenWhispers()
|
||||
{
|
||||
this->unlistenPrefix("whispers.");
|
||||
}
|
||||
|
||||
void PubSub::listenToChannelModerationActions(const QString &channelID)
|
||||
{
|
||||
if (this->userID_.isEmpty())
|
||||
|
@ -666,6 +599,11 @@ void PubSub::listenToChannelModerationActions(const QString &channelID)
|
|||
this->listenToTopic(topic);
|
||||
}
|
||||
|
||||
void PubSub::unlistenChannelModerationActions()
|
||||
{
|
||||
this->unlistenPrefix("chat_moderator_actions.");
|
||||
}
|
||||
|
||||
void PubSub::listenToAutomod(const QString &channelID)
|
||||
{
|
||||
if (this->userID_.isEmpty())
|
||||
|
@ -690,6 +628,11 @@ void PubSub::listenToAutomod(const QString &channelID)
|
|||
this->listenToTopic(topic);
|
||||
}
|
||||
|
||||
void PubSub::unlistenAutomod()
|
||||
{
|
||||
this->unlistenPrefix("automod-queue.");
|
||||
}
|
||||
|
||||
void PubSub::listenToLowTrustUsers(const QString &channelID)
|
||||
{
|
||||
if (this->userID_.isEmpty())
|
||||
|
@ -714,6 +657,11 @@ void PubSub::listenToLowTrustUsers(const QString &channelID)
|
|||
this->listenToTopic(topic);
|
||||
}
|
||||
|
||||
void PubSub::unlistenLowTrustUsers()
|
||||
{
|
||||
this->unlistenPrefix("low-trust-users.");
|
||||
}
|
||||
|
||||
void PubSub::listenToChannelPointRewards(const QString &channelID)
|
||||
{
|
||||
static const QString topicFormat("community-points-channel-v1.%1");
|
||||
|
@ -730,6 +678,30 @@ void PubSub::listenToChannelPointRewards(const QString &channelID)
|
|||
this->listenToTopic(topic);
|
||||
}
|
||||
|
||||
void PubSub::unlistenChannelPointRewards()
|
||||
{
|
||||
this->unlistenPrefix("community-points-channel-v1.");
|
||||
}
|
||||
|
||||
void PubSub::unlistenPrefix(const QString &prefix)
|
||||
{
|
||||
for (const auto &p : this->clients)
|
||||
{
|
||||
const auto &client = p.second;
|
||||
if (const auto &[topics, nonce] = client->unlistenPrefix(prefix);
|
||||
!topics.empty())
|
||||
{
|
||||
NonceInfo nonceInfo{
|
||||
client,
|
||||
"UNLISTEN",
|
||||
topics,
|
||||
topics.size(),
|
||||
};
|
||||
this->registerNonce(nonce, nonceInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PubSub::listen(PubSubListenMessage msg)
|
||||
{
|
||||
if (this->tryListen(msg))
|
||||
|
@ -1083,11 +1055,11 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message)
|
|||
switch (whisperMessage.type)
|
||||
{
|
||||
case PubSubWhisperMessage::Type::WhisperReceived: {
|
||||
this->signals_.whisper.received.invoke(whisperMessage);
|
||||
this->whisper.received.invoke(whisperMessage);
|
||||
}
|
||||
break;
|
||||
case PubSubWhisperMessage::Type::WhisperSent: {
|
||||
this->signals_.whisper.sent.invoke(whisperMessage);
|
||||
this->whisper.sent.invoke(whisperMessage);
|
||||
}
|
||||
break;
|
||||
case PubSubWhisperMessage::Type::Thread: {
|
||||
|
@ -1182,7 +1154,7 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message)
|
|||
case PubSubCommunityPointsChannelV1Message::Type::RewardRedeemed: {
|
||||
auto redemption =
|
||||
innerMessage.data.value("redemption").toObject();
|
||||
this->signals_.pointReward.redeemed.invoke(redemption);
|
||||
this->pointReward.redeemed.invoke(redemption);
|
||||
}
|
||||
break;
|
||||
|
||||
|
@ -1210,8 +1182,7 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message)
|
|||
// Channel ID where the moderator actions are coming from
|
||||
auto channelID = topicParts[2];
|
||||
|
||||
this->signals_.moderation.autoModMessageCaught.invoke(innerMessage,
|
||||
channelID);
|
||||
this->moderation.autoModMessageCaught.invoke(innerMessage, channelID);
|
||||
}
|
||||
else if (topic.startsWith("low-trust-users."))
|
||||
{
|
||||
|
@ -1226,13 +1197,12 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message)
|
|||
switch (innerMessage.type)
|
||||
{
|
||||
case PubSubLowTrustUsersMessage::Type::UserMessage: {
|
||||
this->signals_.moderation.suspiciousMessageReceived.invoke(
|
||||
innerMessage);
|
||||
this->moderation.suspiciousMessageReceived.invoke(innerMessage);
|
||||
}
|
||||
break;
|
||||
|
||||
case PubSubLowTrustUsersMessage::Type::TreatmentUpdate: {
|
||||
this->signals_.moderation.suspiciousTreatmentUpdated.invoke(
|
||||
this->moderation.suspiciousTreatmentUpdated.invoke(
|
||||
innerMessage);
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -41,6 +41,13 @@ struct PubSubListenMessage;
|
|||
struct PubSubMessage;
|
||||
struct PubSubMessageMessage;
|
||||
|
||||
/**
|
||||
* This handles the Twitch PubSub connection
|
||||
*
|
||||
* Known issues:
|
||||
* - Upon closing a channel, we don't unsubscribe to its pubsub connections
|
||||
* - Stop is never called, meaning we never do a clean shutdown
|
||||
*/
|
||||
class PubSub
|
||||
{
|
||||
using WebsocketMessagePtr =
|
||||
|
@ -60,75 +67,62 @@ class PubSub
|
|||
};
|
||||
|
||||
WebsocketClient websocketClient;
|
||||
std::unique_ptr<std::thread> mainThread;
|
||||
std::unique_ptr<std::thread> thread;
|
||||
|
||||
// Account credentials
|
||||
// Set from setAccount or setAccountData
|
||||
// Set from setAccount
|
||||
QString token_;
|
||||
QString userID_;
|
||||
|
||||
public:
|
||||
PubSub(const QString &host,
|
||||
std::chrono::seconds pingInterval = std::chrono::seconds(15));
|
||||
~PubSub();
|
||||
|
||||
PubSub(const PubSub &) = delete;
|
||||
PubSub(PubSub &&) = delete;
|
||||
PubSub &operator=(const PubSub &) = delete;
|
||||
PubSub &operator=(PubSub &&) = delete;
|
||||
|
||||
void setAccount(std::shared_ptr<TwitchAccount> account);
|
||||
|
||||
void setAccountData(QString token, QString userID);
|
||||
|
||||
enum class State {
|
||||
Connected,
|
||||
Disconnected,
|
||||
};
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
bool isConnected() const
|
||||
{
|
||||
return this->state == State::Connected;
|
||||
}
|
||||
struct {
|
||||
Signal<ClearChatAction> chatCleared;
|
||||
Signal<DeleteAction> messageDeleted;
|
||||
Signal<ModeChangedAction> modeChanged;
|
||||
Signal<ModerationStateAction> moderationStateChanged;
|
||||
|
||||
Signal<BanAction> userBanned;
|
||||
Signal<UnbanAction> userUnbanned;
|
||||
|
||||
Signal<PubSubLowTrustUsersMessage> suspiciousMessageReceived;
|
||||
Signal<PubSubLowTrustUsersMessage> suspiciousTreatmentUpdated;
|
||||
|
||||
// Message caught by automod
|
||||
// channelID
|
||||
pajlada::Signals::Signal<PubSubAutoModQueueMessage, QString>
|
||||
autoModMessageCaught;
|
||||
|
||||
// Message blocked by moderator
|
||||
Signal<AutomodAction> autoModMessageBlocked;
|
||||
|
||||
Signal<AutomodUserAction> automodUserMessage;
|
||||
Signal<AutomodInfoAction> automodInfoMessage;
|
||||
} moderation;
|
||||
|
||||
struct {
|
||||
struct {
|
||||
Signal<ClearChatAction> chatCleared;
|
||||
Signal<DeleteAction> messageDeleted;
|
||||
Signal<ModeChangedAction> modeChanged;
|
||||
Signal<ModerationStateAction> moderationStateChanged;
|
||||
// Parsing should be done in PubSubManager as well,
|
||||
// but for now we just send the raw data
|
||||
Signal<const PubSubWhisperMessage &> received;
|
||||
Signal<const PubSubWhisperMessage &> sent;
|
||||
} whisper;
|
||||
|
||||
Signal<BanAction> userBanned;
|
||||
Signal<UnbanAction> userUnbanned;
|
||||
|
||||
Signal<PubSubLowTrustUsersMessage> suspiciousMessageReceived;
|
||||
Signal<PubSubLowTrustUsersMessage> suspiciousTreatmentUpdated;
|
||||
|
||||
// Message caught by automod
|
||||
// channelID
|
||||
pajlada::Signals::Signal<PubSubAutoModQueueMessage, QString>
|
||||
autoModMessageCaught;
|
||||
|
||||
// Message blocked by moderator
|
||||
Signal<AutomodAction> autoModMessageBlocked;
|
||||
|
||||
Signal<AutomodUserAction> automodUserMessage;
|
||||
Signal<AutomodInfoAction> automodInfoMessage;
|
||||
} moderation;
|
||||
|
||||
struct {
|
||||
// Parsing should be done in PubSubManager as well,
|
||||
// but for now we just send the raw data
|
||||
Signal<const PubSubWhisperMessage &> received;
|
||||
Signal<const PubSubWhisperMessage &> sent;
|
||||
} whisper;
|
||||
|
||||
struct {
|
||||
Signal<const QJsonObject &> redeemed;
|
||||
} pointReward;
|
||||
} signals_;
|
||||
|
||||
void unlistenAllModerationActions();
|
||||
void unlistenAutomod();
|
||||
void unlistenLowTrustUsers();
|
||||
void unlistenWhispers();
|
||||
struct {
|
||||
Signal<const QJsonObject &> redeemed;
|
||||
} pointReward;
|
||||
|
||||
/**
|
||||
* Listen to incoming whispers for the currently logged in user.
|
||||
|
@ -137,6 +131,7 @@ public:
|
|||
* PubSub topic: whispers.{currentUserID}
|
||||
*/
|
||||
bool listenToWhispers();
|
||||
void unlistenWhispers();
|
||||
|
||||
/**
|
||||
* Listen to moderation actions in the given channel.
|
||||
|
@ -150,6 +145,7 @@ public:
|
|||
* PubSub topic: chat_moderator_actions.{currentUserID}.{channelID}
|
||||
*/
|
||||
void listenToChannelModerationActions(const QString &channelID);
|
||||
void unlistenChannelModerationActions();
|
||||
|
||||
/**
|
||||
* Listen to Automod events in the given channel.
|
||||
|
@ -160,6 +156,7 @@ public:
|
|||
* PubSub topic: automod-queue.{currentUserID}.{channelID}
|
||||
*/
|
||||
void listenToAutomod(const QString &channelID);
|
||||
void unlistenAutomod();
|
||||
|
||||
/**
|
||||
* Listen to Low Trust events in the given channel.
|
||||
|
@ -170,6 +167,7 @@ public:
|
|||
* PubSub topic: low-trust-users.{currentUserID}.{channelID}
|
||||
*/
|
||||
void listenToLowTrustUsers(const QString &channelID);
|
||||
void unlistenLowTrustUsers();
|
||||
|
||||
/**
|
||||
* Listen to incoming channel point redemptions in the given channel.
|
||||
|
@ -178,8 +176,7 @@ public:
|
|||
* PubSub topic: community-points-channel-v1.{channelID}
|
||||
*/
|
||||
void listenToChannelPointRewards(const QString &channelID);
|
||||
|
||||
std::vector<QString> requests;
|
||||
void unlistenChannelPointRewards();
|
||||
|
||||
struct {
|
||||
std::atomic<uint32_t> connectionsClosed{0};
|
||||
|
@ -192,20 +189,26 @@ public:
|
|||
std::atomic<uint32_t> unlistenResponses{0};
|
||||
} diag;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Unlistens to all topics matching the prefix in all clients
|
||||
*/
|
||||
void unlistenPrefix(const QString &prefix);
|
||||
|
||||
void listenToTopic(const QString &topic);
|
||||
|
||||
private:
|
||||
void listen(PubSubListenMessage msg);
|
||||
bool tryListen(PubSubListenMessage msg);
|
||||
|
||||
bool isListeningToTopic(const QString &topic);
|
||||
|
||||
void addClient();
|
||||
|
||||
std::vector<QString> requests;
|
||||
|
||||
std::atomic<bool> addingClient{false};
|
||||
ExponentialBackoff<5> connectBackoff{std::chrono::milliseconds(1000)};
|
||||
|
||||
State state = State::Connected;
|
||||
|
||||
std::map<WebsocketHandle, std::shared_ptr<PubSubClient>,
|
||||
std::owner_less<WebsocketHandle>>
|
||||
clients;
|
||||
|
|
|
@ -29,6 +29,12 @@ void TwitchBadges::loadTwitchBadges()
|
|||
{
|
||||
assert(this->loaded_ == false);
|
||||
|
||||
if (!getHelix())
|
||||
{
|
||||
// This is intended for tests and benchmarks.
|
||||
return;
|
||||
}
|
||||
|
||||
getHelix()->getGlobalBadges(
|
||||
[this](auto globalBadges) {
|
||||
auto badgeSets = this->badgeSets_.access();
|
||||
|
|
|
@ -87,6 +87,13 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||
{
|
||||
qCDebug(chatterinoTwitch) << "[TwitchChannel" << name << "] Opened";
|
||||
|
||||
if (!getApp())
|
||||
{
|
||||
// This is intended for tests and benchmarks.
|
||||
// Irc, Pubsub, live-updates, and live-notifications aren't mocked there.
|
||||
return;
|
||||
}
|
||||
|
||||
this->bSignals_.emplace_back(
|
||||
getApp()->accounts->twitch.currentUserChanged.connect([this] {
|
||||
this->setMod(false);
|
||||
|
@ -219,6 +226,13 @@ TwitchChannel::TwitchChannel(const QString &name)
|
|||
|
||||
TwitchChannel::~TwitchChannel()
|
||||
{
|
||||
if (!getApp())
|
||||
{
|
||||
// This is for tests and benchmarks, where live-updates aren't mocked
|
||||
// see comment in constructor.
|
||||
return;
|
||||
}
|
||||
|
||||
getApp()->twitch->dropSeventvChannel(this->seventvUserID_,
|
||||
this->seventvEmoteSetID_);
|
||||
|
||||
|
@ -282,8 +296,9 @@ void TwitchChannel::refreshBTTVChannelEmotes(bool manualRefresh)
|
|||
weakOf<Channel>(this), this->roomId(), this->getLocalizedName(),
|
||||
[this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
|
||||
if (auto shared = weak.lock())
|
||||
this->bttvEmotes_.set(
|
||||
std::make_shared<EmoteMap>(std::move(emoteMap)));
|
||||
{
|
||||
this->setBttvEmotes(std::make_shared<const EmoteMap>(emoteMap));
|
||||
}
|
||||
},
|
||||
manualRefresh);
|
||||
}
|
||||
|
@ -300,8 +315,9 @@ void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh)
|
|||
weakOf<Channel>(this), this->roomId(),
|
||||
[this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
|
||||
if (auto shared = weak.lock())
|
||||
this->ffzEmotes_.set(
|
||||
std::make_shared<EmoteMap>(std::move(emoteMap)));
|
||||
{
|
||||
this->setFfzEmotes(std::make_shared<const EmoteMap>(emoteMap));
|
||||
}
|
||||
},
|
||||
[this, weak = weakOf<Channel>(this)](auto &&modBadge) {
|
||||
if (auto shared = weak.lock())
|
||||
|
@ -332,8 +348,8 @@ void TwitchChannel::refreshSevenTVChannelEmotes(bool manualRefresh)
|
|||
auto channelInfo) {
|
||||
if (auto shared = weak.lock())
|
||||
{
|
||||
this->seventvEmotes_.set(std::make_shared<EmoteMap>(
|
||||
std::forward<decltype(emoteMap)>(emoteMap)));
|
||||
this->setSeventvEmotes(
|
||||
std::make_shared<const EmoteMap>(emoteMap));
|
||||
this->updateSeventvData(channelInfo.userID,
|
||||
channelInfo.emoteSetID);
|
||||
this->seventvUserTwitchConnectionIndex_ =
|
||||
|
@ -343,6 +359,21 @@ void TwitchChannel::refreshSevenTVChannelEmotes(bool manualRefresh)
|
|||
manualRefresh);
|
||||
}
|
||||
|
||||
void TwitchChannel::setBttvEmotes(std::shared_ptr<const EmoteMap> &&map)
|
||||
{
|
||||
this->bttvEmotes_.set(std::move(map));
|
||||
}
|
||||
|
||||
void TwitchChannel::setFfzEmotes(std::shared_ptr<const EmoteMap> &&map)
|
||||
{
|
||||
this->ffzEmotes_.set(std::move(map));
|
||||
}
|
||||
|
||||
void TwitchChannel::setSeventvEmotes(std::shared_ptr<const EmoteMap> &&map)
|
||||
{
|
||||
this->seventvEmotes_.set(std::move(map));
|
||||
}
|
||||
|
||||
void TwitchChannel::addQueuedRedemption(const QString &rewardId,
|
||||
const QString &originalContent,
|
||||
Communi::IrcMessage *message)
|
||||
|
@ -700,9 +731,10 @@ void TwitchChannel::setStaff(bool value)
|
|||
|
||||
bool TwitchChannel::isBroadcaster() const
|
||||
{
|
||||
auto app = getApp();
|
||||
auto *app = getIApp();
|
||||
|
||||
return this->getName() == app->accounts->twitch.getCurrent()->getUserName();
|
||||
return this->getName() ==
|
||||
app->getAccounts()->twitch.getCurrent()->getUserName();
|
||||
}
|
||||
|
||||
bool TwitchChannel::hasHighRateLimit() const
|
||||
|
@ -730,8 +762,12 @@ void TwitchChannel::setRoomId(const QString &id)
|
|||
if (*this->roomID_.accessConst() != id)
|
||||
{
|
||||
*this->roomID_.access() = id;
|
||||
this->roomIdChanged();
|
||||
this->loadRecentMessages();
|
||||
// This is intended for tests and benchmarks. See comment in constructor.
|
||||
if (getApp())
|
||||
{
|
||||
this->roomIdChanged();
|
||||
this->loadRecentMessages();
|
||||
}
|
||||
this->disconnected_ = false;
|
||||
this->lastConnectedAt_ = std::chrono::system_clock::now();
|
||||
}
|
||||
|
@ -1250,15 +1286,15 @@ void TwitchChannel::refreshPubSub()
|
|||
|
||||
auto currentAccount = getApp()->accounts->twitch.getCurrent();
|
||||
|
||||
getApp()->twitch->pubsub->setAccount(currentAccount);
|
||||
getIApp()->getTwitchPubSub()->setAccount(currentAccount);
|
||||
|
||||
getApp()->twitch->pubsub->listenToChannelModerationActions(roomId);
|
||||
getIApp()->getTwitchPubSub()->listenToChannelModerationActions(roomId);
|
||||
if (this->hasModRights())
|
||||
{
|
||||
getApp()->twitch->pubsub->listenToAutomod(roomId);
|
||||
getApp()->twitch->pubsub->listenToLowTrustUsers(roomId);
|
||||
getIApp()->getTwitchPubSub()->listenToAutomod(roomId);
|
||||
getIApp()->getTwitchPubSub()->listenToLowTrustUsers(roomId);
|
||||
}
|
||||
getApp()->twitch->pubsub->listenToChannelPointRewards(roomId);
|
||||
getIApp()->getTwitchPubSub()->listenToChannelPointRewards(roomId);
|
||||
}
|
||||
|
||||
void TwitchChannel::refreshChatters()
|
||||
|
|
|
@ -158,6 +158,10 @@ public:
|
|||
void refreshFFZChannelEmotes(bool manualRefresh);
|
||||
void refreshSevenTVChannelEmotes(bool manualRefresh);
|
||||
|
||||
void setBttvEmotes(std::shared_ptr<const EmoteMap> &&map);
|
||||
void setFfzEmotes(std::shared_ptr<const EmoteMap> &&map);
|
||||
void setSeventvEmotes(std::shared_ptr<const EmoteMap> &&map);
|
||||
|
||||
const QString &seventvUserID() const;
|
||||
const QString &seventvEmoteSetID() const;
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#include "TwitchIrcServer.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
|
@ -13,7 +13,6 @@
|
|||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "providers/twitch/ChannelPointReward.hpp"
|
||||
#include "providers/twitch/IrcMessageHandler.hpp"
|
||||
#include "providers/twitch/PubSubManager.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
@ -29,7 +28,6 @@ using namespace std::chrono_literals;
|
|||
|
||||
namespace {
|
||||
|
||||
const QString TWITCH_PUBSUB_URL = "wss://pubsub-edge.twitch.tv";
|
||||
const QString BTTV_LIVE_UPDATES_URL = "wss://sockets.betterttv.net/ws";
|
||||
const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3";
|
||||
|
||||
|
@ -43,7 +41,6 @@ TwitchIrcServer::TwitchIrcServer()
|
|||
, liveChannel(new Channel("/live", Channel::Type::TwitchLive))
|
||||
, automodChannel(new Channel("/automod", Channel::Type::TwitchAutomod))
|
||||
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
|
||||
, pubsub(new PubSub(TWITCH_PUBSUB_URL))
|
||||
{
|
||||
this->initializeIrc();
|
||||
|
||||
|
@ -72,7 +69,6 @@ void TwitchIrcServer::initialize(Settings &settings, Paths &paths)
|
|||
getApp()->accounts->twitch.currentUserChanged.connect([this]() {
|
||||
postToThread([this] {
|
||||
this->connect();
|
||||
this->pubsub->setAccount(getApp()->accounts->twitch.getCurrent());
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ namespace chatterino {
|
|||
|
||||
class Settings;
|
||||
class Paths;
|
||||
class PubSub;
|
||||
class TwitchChannel;
|
||||
class BttvLiveUpdates;
|
||||
class SeventvEventAPI;
|
||||
|
@ -80,8 +79,6 @@ public:
|
|||
const ChannelPtr automodChannel;
|
||||
IndirectChannel watchingChannel;
|
||||
|
||||
// NOTE: We currently leak this
|
||||
PubSub *pubsub;
|
||||
std::unique_ptr<BttvLiveUpdates> bttvLiveUpdates;
|
||||
std::unique_ptr<SeventvEventAPI> seventvEventAPI;
|
||||
|
||||
|
|
|
@ -153,6 +153,119 @@ namespace {
|
|||
}
|
||||
}
|
||||
|
||||
std::optional<EmotePtr> getTwitchBadge(const Badge &badge,
|
||||
const TwitchChannel *twitchChannel)
|
||||
{
|
||||
if (auto channelBadge =
|
||||
twitchChannel->twitchBadge(badge.key_, badge.value_))
|
||||
{
|
||||
return channelBadge;
|
||||
}
|
||||
|
||||
if (auto globalBadge =
|
||||
TwitchBadges::instance()->badge(badge.key_, badge.value_))
|
||||
{
|
||||
return globalBadge;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void appendBadges(MessageBuilder *builder, const std::vector<Badge> &badges,
|
||||
const std::unordered_map<QString, QString> &badgeInfos,
|
||||
const TwitchChannel *twitchChannel)
|
||||
{
|
||||
if (twitchChannel == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto &badge : badges)
|
||||
{
|
||||
auto badgeEmote = getTwitchBadge(badge, twitchChannel);
|
||||
if (!badgeEmote)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
auto tooltip = (*badgeEmote)->tooltip.string;
|
||||
|
||||
if (badge.key_ == "bits")
|
||||
{
|
||||
const auto &cheerAmount = badge.value_;
|
||||
tooltip = QString("Twitch cheer %0").arg(cheerAmount);
|
||||
}
|
||||
else if (badge.key_ == "moderator" &&
|
||||
getSettings()->useCustomFfzModeratorBadges)
|
||||
{
|
||||
if (auto customModBadge = twitchChannel->ffzCustomModBadge())
|
||||
{
|
||||
builder
|
||||
->emplace<ModBadgeElement>(
|
||||
*customModBadge,
|
||||
MessageElementFlag::BadgeChannelAuthority)
|
||||
->setTooltip((*customModBadge)->tooltip.string);
|
||||
// early out, since we have to add a custom badge element here
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (badge.key_ == "vip" &&
|
||||
getSettings()->useCustomFfzVipBadges)
|
||||
{
|
||||
if (auto customVipBadge = twitchChannel->ffzCustomVipBadge())
|
||||
{
|
||||
builder
|
||||
->emplace<VipBadgeElement>(
|
||||
*customVipBadge,
|
||||
MessageElementFlag::BadgeChannelAuthority)
|
||||
->setTooltip((*customVipBadge)->tooltip.string);
|
||||
// early out, since we have to add a custom badge element here
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (badge.flag_ == MessageElementFlag::BadgeSubscription)
|
||||
{
|
||||
auto badgeInfoIt = badgeInfos.find(badge.key_);
|
||||
if (badgeInfoIt != badgeInfos.end())
|
||||
{
|
||||
// badge.value_ is 4 chars long if user is subbed on higher tier
|
||||
// (tier + amount of months with leading zero if less than 100)
|
||||
// e.g. 3054 - tier 3 4,5-year sub. 2108 - tier 2 9-year sub
|
||||
const auto &subTier =
|
||||
badge.value_.length() > 3 ? badge.value_.at(0) : '1';
|
||||
const auto &subMonths = badgeInfoIt->second;
|
||||
tooltip += QString(" (%1%2 months)")
|
||||
.arg(subTier != '1'
|
||||
? QString("Tier %1, ").arg(subTier)
|
||||
: "")
|
||||
.arg(subMonths);
|
||||
}
|
||||
}
|
||||
else if (badge.flag_ == MessageElementFlag::BadgePredictions)
|
||||
{
|
||||
auto badgeInfoIt = badgeInfos.find(badge.key_);
|
||||
if (badgeInfoIt != badgeInfos.end())
|
||||
{
|
||||
auto infoValue = badgeInfoIt->second;
|
||||
auto predictionText =
|
||||
infoValue
|
||||
.replace(R"(\s)", " ") // standard IRC escapes
|
||||
.replace(R"(\:)", ";")
|
||||
.replace(R"(\\)", R"(\)")
|
||||
.replace("⸝", ","); // twitch's comma escape
|
||||
// Careful, the first character is RIGHT LOW PARAPHRASE BRACKET or U+2E1D, which just looks like a comma
|
||||
|
||||
tooltip = QString("Predicted %1").arg(predictionText);
|
||||
}
|
||||
}
|
||||
|
||||
builder->emplace<BadgeElement>(*badgeEmote, badge.flag_)
|
||||
->setTooltip(tooltip);
|
||||
}
|
||||
|
||||
builder->message().badges = badges;
|
||||
builder->message().badgeInfos = badgeInfos;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TwitchMessageBuilder::TwitchMessageBuilder(
|
||||
|
@ -1113,24 +1226,6 @@ Outcome TwitchMessageBuilder::tryAppendEmote(const EmoteName &name)
|
|||
return Failure;
|
||||
}
|
||||
|
||||
std::optional<EmotePtr> TwitchMessageBuilder::getTwitchBadge(
|
||||
const Badge &badge) const
|
||||
{
|
||||
if (auto channelBadge =
|
||||
this->twitchChannel->twitchBadge(badge.key_, badge.value_))
|
||||
{
|
||||
return channelBadge;
|
||||
}
|
||||
|
||||
if (auto globalBadge =
|
||||
TwitchBadges::instance()->badge(badge.key_, badge.value_))
|
||||
{
|
||||
return globalBadge;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::unordered_map<QString, QString> TwitchMessageBuilder::parseBadgeInfoTag(
|
||||
const QVariantMap &tags)
|
||||
{
|
||||
|
@ -1189,88 +1284,8 @@ void TwitchMessageBuilder::appendTwitchBadges()
|
|||
}
|
||||
|
||||
auto badgeInfos = TwitchMessageBuilder::parseBadgeInfoTag(this->tags);
|
||||
auto badges = this->parseBadgeTag(this->tags);
|
||||
|
||||
for (const auto &badge : badges)
|
||||
{
|
||||
auto badgeEmote = this->getTwitchBadge(badge);
|
||||
if (!badgeEmote)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
auto tooltip = (*badgeEmote)->tooltip.string;
|
||||
|
||||
if (badge.key_ == "bits")
|
||||
{
|
||||
const auto &cheerAmount = badge.value_;
|
||||
tooltip = QString("Twitch cheer %0").arg(cheerAmount);
|
||||
}
|
||||
else if (badge.key_ == "moderator" &&
|
||||
getSettings()->useCustomFfzModeratorBadges)
|
||||
{
|
||||
if (auto customModBadge = this->twitchChannel->ffzCustomModBadge())
|
||||
{
|
||||
this->emplace<ModBadgeElement>(
|
||||
*customModBadge,
|
||||
MessageElementFlag::BadgeChannelAuthority)
|
||||
->setTooltip((*customModBadge)->tooltip.string);
|
||||
// early out, since we have to add a custom badge element here
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (badge.key_ == "vip" && getSettings()->useCustomFfzVipBadges)
|
||||
{
|
||||
if (auto customVipBadge = this->twitchChannel->ffzCustomVipBadge())
|
||||
{
|
||||
this->emplace<VipBadgeElement>(
|
||||
*customVipBadge,
|
||||
MessageElementFlag::BadgeChannelAuthority)
|
||||
->setTooltip((*customVipBadge)->tooltip.string);
|
||||
// early out, since we have to add a custom badge element here
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (badge.flag_ == MessageElementFlag::BadgeSubscription)
|
||||
{
|
||||
auto badgeInfoIt = badgeInfos.find(badge.key_);
|
||||
if (badgeInfoIt != badgeInfos.end())
|
||||
{
|
||||
// badge.value_ is 4 chars long if user is subbed on higher tier
|
||||
// (tier + amount of months with leading zero if less than 100)
|
||||
// e.g. 3054 - tier 3 4,5-year sub. 2108 - tier 2 9-year sub
|
||||
const auto &subTier =
|
||||
badge.value_.length() > 3 ? badge.value_.at(0) : '1';
|
||||
const auto &subMonths = badgeInfoIt->second;
|
||||
tooltip +=
|
||||
QString(" (%1%2 months)")
|
||||
.arg(subTier != '1' ? QString("Tier %1, ").arg(subTier)
|
||||
: "")
|
||||
.arg(subMonths);
|
||||
}
|
||||
}
|
||||
else if (badge.flag_ == MessageElementFlag::BadgePredictions)
|
||||
{
|
||||
auto badgeInfoIt = badgeInfos.find(badge.key_);
|
||||
if (badgeInfoIt != badgeInfos.end())
|
||||
{
|
||||
auto predictionText =
|
||||
badgeInfoIt->second
|
||||
.replace(R"(\s)", " ") // standard IRC escapes
|
||||
.replace(R"(\:)", ";")
|
||||
.replace(R"(\\)", R"(\)")
|
||||
.replace("⸝", ","); // twitch's comma escape
|
||||
// Careful, the first character is RIGHT LOW PARAPHRASE BRACKET or U+2E1D, which just looks like a comma
|
||||
|
||||
tooltip = QString("Predicted %1").arg(predictionText);
|
||||
}
|
||||
}
|
||||
|
||||
this->emplace<BadgeElement>(*badgeEmote, badge.flag_)
|
||||
->setTooltip(tooltip);
|
||||
}
|
||||
|
||||
this->message().badges = badges;
|
||||
this->message().badgeInfos = badgeInfos;
|
||||
auto badges = TwitchMessageBuilder::parseBadgeTag(this->tags);
|
||||
appendBadges(this, badges, badgeInfos, this->twitchChannel);
|
||||
}
|
||||
|
||||
void TwitchMessageBuilder::appendChatterinoBadges()
|
||||
|
@ -1936,6 +1951,12 @@ std::pair<MessagePtr, MessagePtr> TwitchMessageBuilder::makeAutomodMessage(
|
|||
MessagePtr TwitchMessageBuilder::makeLowTrustUpdateMessage(
|
||||
const PubSubLowTrustUsersMessage &action)
|
||||
{
|
||||
/**
|
||||
* Known issues:
|
||||
* - Non-Twitch badges are not shown
|
||||
* - Non-Twitch emotes are not shown
|
||||
*/
|
||||
|
||||
MessageBuilder builder;
|
||||
builder.emplace<TimestampElement>();
|
||||
builder.message().flags.set(MessageFlag::System);
|
||||
|
@ -2006,7 +2027,8 @@ MessagePtr TwitchMessageBuilder::makeLowTrustUpdateMessage(
|
|||
}
|
||||
|
||||
std::pair<MessagePtr, MessagePtr> TwitchMessageBuilder::makeLowTrustUserMessage(
|
||||
const PubSubLowTrustUsersMessage &action, const QString &channelName)
|
||||
const PubSubLowTrustUsersMessage &action, const QString &channelName,
|
||||
const TwitchChannel *twitchChannel)
|
||||
{
|
||||
MessageBuilder builder, builder2;
|
||||
|
||||
|
@ -2029,10 +2051,12 @@ std::pair<MessagePtr, MessagePtr> TwitchMessageBuilder::makeLowTrustUserMessage(
|
|||
if (action.treatment == PubSubLowTrustUsersMessage::Treatment::Restricted)
|
||||
{
|
||||
headerMessage = "Restricted";
|
||||
builder2.message().flags.set(MessageFlag::RestrictedMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
headerMessage = "Monitored";
|
||||
builder2.message().flags.set(MessageFlag::MonitoredMessage);
|
||||
}
|
||||
|
||||
if (action.restrictionTypes.has(
|
||||
|
@ -2089,6 +2113,9 @@ std::pair<MessagePtr, MessagePtr> TwitchMessageBuilder::makeLowTrustUserMessage(
|
|||
builder2.message().flags.set(MessageFlag::PubSub);
|
||||
builder2.message().flags.set(MessageFlag::LowTrustUsers);
|
||||
|
||||
// sender badges
|
||||
appendBadges(&builder2, action.senderBadges, {}, twitchChannel);
|
||||
|
||||
// sender username
|
||||
builder2
|
||||
.emplace<TextElement>(action.suspiciousUserDisplayName + ":",
|
||||
|
@ -2103,8 +2130,23 @@ std::pair<MessagePtr, MessagePtr> TwitchMessageBuilder::makeLowTrustUserMessage(
|
|||
->setLink({Link::UserInfo, action.suspiciousUserLogin});
|
||||
|
||||
// sender's message caught by AutoMod
|
||||
builder2.emplace<TextElement>(action.text, MessageElementFlag::Text,
|
||||
MessageColor::Text);
|
||||
for (const auto &fragment : action.fragments)
|
||||
{
|
||||
if (fragment.emoteID.isEmpty())
|
||||
{
|
||||
builder2.emplace<TextElement>(
|
||||
fragment.text, MessageElementFlag::Text, MessageColor::Text);
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto emotePtr =
|
||||
getIApp()->getEmotes()->getTwitchEmotes()->getOrCreateEmote(
|
||||
EmoteId{fragment.emoteID}, EmoteName{fragment.text});
|
||||
builder2.emplace<EmoteElement>(
|
||||
emotePtr, MessageElementFlag::TwitchEmote, MessageColor::Text);
|
||||
}
|
||||
}
|
||||
|
||||
auto text =
|
||||
QString("%1: %2").arg(action.suspiciousUserDisplayName, action.text);
|
||||
builder2.message().messageText = text;
|
||||
|
|
|
@ -95,7 +95,8 @@ public:
|
|||
static MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action);
|
||||
|
||||
static std::pair<MessagePtr, MessagePtr> makeLowTrustUserMessage(
|
||||
const PubSubLowTrustUsersMessage &action, const QString &channelName);
|
||||
const PubSubLowTrustUsersMessage &action, const QString &channelName,
|
||||
const TwitchChannel *twitchChannel);
|
||||
static MessagePtr makeLowTrustUpdateMessage(
|
||||
const PubSubLowTrustUsersMessage &action);
|
||||
|
||||
|
@ -119,7 +120,6 @@ private:
|
|||
|
||||
void runIgnoreReplaces(std::vector<TwitchEmoteOccurrence> &twitchEmotes);
|
||||
|
||||
std::optional<EmotePtr> getTwitchBadge(const Badge &badge) const;
|
||||
Outcome tryAppendEmote(const EmoteName &name) override;
|
||||
|
||||
void addWords(const QStringList &words,
|
||||
|
|
|
@ -21,8 +21,12 @@ PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root)
|
|||
{
|
||||
this->msgID = data.value("message_id").toString();
|
||||
this->sentAt = data.value("sent_at").toString();
|
||||
this->text =
|
||||
data.value("message_content").toObject().value("text").toString();
|
||||
const auto content = data.value("message_content").toObject();
|
||||
this->text = content.value("text").toString();
|
||||
for (const auto &part : content.value("fragments").toArray())
|
||||
{
|
||||
this->fragments.emplace_back(part.toObject());
|
||||
}
|
||||
|
||||
// the rest of the data is within a nested object
|
||||
data = data.value("low_trust_user").toObject();
|
||||
|
@ -35,23 +39,22 @@ PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root)
|
|||
this->suspiciousUserColor =
|
||||
QColor(sender.value("chat_color").toString());
|
||||
|
||||
std::vector<LowTrustUserChatBadge> badges;
|
||||
for (const auto &badge : sender.value("badges").toArray())
|
||||
{
|
||||
badges.emplace_back(badge.toObject());
|
||||
const auto badgeObj = badge.toObject();
|
||||
const auto badgeID = badgeObj.value("id").toString();
|
||||
const auto badgeVersion = badgeObj.value("version").toString();
|
||||
this->senderBadges.emplace_back(Badge{badgeID, badgeVersion});
|
||||
}
|
||||
this->senderBadges = badges;
|
||||
|
||||
const auto sharedValue = data.value("shared_ban_channel_ids");
|
||||
std::vector<QString> sharedIDs;
|
||||
if (!sharedValue.isNull())
|
||||
{
|
||||
for (const auto &id : sharedValue.toArray())
|
||||
{
|
||||
sharedIDs.emplace_back(id.toString());
|
||||
this->sharedBanChannelIDs.emplace_back(id.toString());
|
||||
}
|
||||
}
|
||||
this->sharedBanChannelIDs = sharedIDs;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -88,17 +91,15 @@ PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root)
|
|||
this->evasionEvaluation = oEvaluation.value();
|
||||
}
|
||||
|
||||
FlagsEnum<RestrictionType> restrictions;
|
||||
for (const auto &rType : data.value("types").toArray())
|
||||
{
|
||||
if (const auto oRestriction = magic_enum::enum_cast<RestrictionType>(
|
||||
rType.toString().toStdString());
|
||||
oRestriction.has_value())
|
||||
{
|
||||
restrictions.set(oRestriction.value());
|
||||
this->restrictionTypes.set(oRestriction.value());
|
||||
}
|
||||
}
|
||||
this->restrictionTypes = restrictions;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "providers/twitch/TwitchBadge.hpp"
|
||||
|
||||
#include <common/FlagsEnum.hpp>
|
||||
#include <magic_enum/magic_enum.hpp>
|
||||
#include <QColor>
|
||||
|
@ -8,18 +10,21 @@
|
|||
|
||||
namespace chatterino {
|
||||
|
||||
struct LowTrustUserChatBadge {
|
||||
QString id;
|
||||
QString version;
|
||||
|
||||
explicit LowTrustUserChatBadge(const QJsonObject &obj)
|
||||
: id(obj.value("id").toString())
|
||||
, version(obj.value("version").toString())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct PubSubLowTrustUsersMessage {
|
||||
struct Fragment {
|
||||
QString text;
|
||||
QString emoteID;
|
||||
|
||||
explicit Fragment(const QJsonObject &obj)
|
||||
: text(obj.value("text").toString())
|
||||
, emoteID(obj.value("emoticon")
|
||||
.toObject()
|
||||
.value("emoticonID")
|
||||
.toString())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of low trust message update
|
||||
*/
|
||||
|
@ -102,6 +107,12 @@ struct PubSubLowTrustUsersMessage {
|
|||
*/
|
||||
QString text;
|
||||
|
||||
/**
|
||||
* Pre-parsed components of the message.
|
||||
* Only used for the UserMessage type.
|
||||
*/
|
||||
std::vector<Fragment> fragments;
|
||||
|
||||
/**
|
||||
* ID of the message.
|
||||
* Only used for the UserMessage type.
|
||||
|
@ -130,7 +141,7 @@ struct PubSubLowTrustUsersMessage {
|
|||
* A list of badges of the user who sent the message.
|
||||
* Only used for the UserMessage type.
|
||||
*/
|
||||
std::vector<LowTrustUserChatBadge> senderBadges;
|
||||
std::vector<Badge> senderBadges;
|
||||
|
||||
/**
|
||||
* Stores the string value of `type`
|
||||
|
|
|
@ -476,7 +476,7 @@ void Theme::normalizeColor(QColor &color) const
|
|||
|
||||
Theme *getTheme()
|
||||
{
|
||||
return getApp()->themes;
|
||||
return getIApp()->getThemes();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include "widgets/BaseWidget.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "controllers/hotkeys/HotkeyController.hpp"
|
||||
#include "singletons/Theme.hpp"
|
||||
|
@ -17,10 +18,8 @@ namespace chatterino {
|
|||
|
||||
BaseWidget::BaseWidget(QWidget *parent, Qt::WindowFlags f)
|
||||
: QWidget(parent, f)
|
||||
, theme(getIApp()->getThemes())
|
||||
{
|
||||
// REMOVED
|
||||
this->theme = getTheme();
|
||||
|
||||
this->signalHolder_.managedConnect(this->theme->updated, [this]() {
|
||||
this->themeChangedEvent();
|
||||
|
||||
|
|
|
@ -588,11 +588,13 @@ void Notebook::updateTabVisibility()
|
|||
|
||||
void Notebook::updateTabVisibilityMenuAction()
|
||||
{
|
||||
auto toggleSeq = getApp()->hotkeys->getDisplaySequence(
|
||||
const auto *hotkeys = getIApp()->getHotkeys();
|
||||
|
||||
auto toggleSeq = hotkeys->getDisplaySequence(
|
||||
HotkeyCategory::Window, "setTabVisibility", {std::vector<QString>()});
|
||||
if (toggleSeq.isEmpty())
|
||||
{
|
||||
toggleSeq = getApp()->hotkeys->getDisplaySequence(
|
||||
toggleSeq = hotkeys->getDisplaySequence(
|
||||
HotkeyCategory::Window, "setTabVisibility", {{"toggle"}});
|
||||
}
|
||||
|
||||
|
@ -601,12 +603,12 @@ void Notebook::updateTabVisibilityMenuAction()
|
|||
// show contextual shortcuts
|
||||
if (this->getShowTabs())
|
||||
{
|
||||
toggleSeq = getApp()->hotkeys->getDisplaySequence(
|
||||
toggleSeq = hotkeys->getDisplaySequence(
|
||||
HotkeyCategory::Window, "setTabVisibility", {{"off"}});
|
||||
}
|
||||
else if (!this->getShowTabs())
|
||||
{
|
||||
toggleSeq = getApp()->hotkeys->getDisplaySequence(
|
||||
toggleSeq = hotkeys->getDisplaySequence(
|
||||
HotkeyCategory::Window, "setTabVisibility", {{"on"}});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -281,7 +281,7 @@ void Window::addDebugStuff(HotkeyController::HotkeyMap &actions)
|
|||
->toInner<PubSubCommunityPointsChannelV1Message>();
|
||||
|
||||
app->twitch->addFakeMessage(getSampleChannelRewardIRCMessage());
|
||||
app->twitch->pubsub->signals_.pointReward.redeemed.invoke(
|
||||
getIApp()->getTwitchPubSub()->pointReward.redeemed.invoke(
|
||||
oInnerMessage->data.value("redemption").toObject());
|
||||
alt = !alt;
|
||||
}
|
||||
|
@ -292,7 +292,7 @@ void Window::addDebugStuff(HotkeyController::HotkeyMap &actions)
|
|||
auto oInnerMessage =
|
||||
oMessage->toInner<PubSubMessageMessage>()
|
||||
->toInner<PubSubCommunityPointsChannelV1Message>();
|
||||
app->twitch->pubsub->signals_.pointReward.redeemed.invoke(
|
||||
getIApp()->getTwitchPubSub()->pointReward.redeemed.invoke(
|
||||
oInnerMessage->data.value("redemption").toObject());
|
||||
alt = !alt;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,66 @@
|
|||
#include "widgets/dialogs/ColorPickerDialog.hpp"
|
||||
|
||||
#include "common/Literals.hpp"
|
||||
#include "providers/colors/ColorProvider.hpp"
|
||||
#include "singletons/Theme.hpp"
|
||||
#include "util/LayoutCreator.hpp"
|
||||
#include "widgets/helper/ColorButton.hpp"
|
||||
#include "widgets/helper/QColorPicker.hpp"
|
||||
#include "widgets/helper/color/AlphaSlider.hpp"
|
||||
#include "widgets/helper/color/ColorButton.hpp"
|
||||
#include "widgets/helper/color/ColorInput.hpp"
|
||||
#include "widgets/helper/color/HueSlider.hpp"
|
||||
#include "widgets/helper/color/SBCanvas.hpp"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QLineEdit>
|
||||
#include <QSet>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino;
|
||||
|
||||
constexpr size_t COLORS_PER_ROW = 5;
|
||||
constexpr size_t MAX_RECENT_COLORS = 15;
|
||||
constexpr size_t MAX_DEFAULT_COLORS = 15;
|
||||
|
||||
QGridLayout *makeColorGrid(const auto &items, auto *self,
|
||||
std::size_t maxButtons)
|
||||
{
|
||||
auto *layout = new QGridLayout;
|
||||
|
||||
// TODO(nerix): use std::ranges::views::enumerate (C++ 23)
|
||||
for (std::size_t i = 0; auto color : items)
|
||||
{
|
||||
auto *button = new ColorButton(color);
|
||||
button->setMinimumWidth(40);
|
||||
QObject::connect(button, &ColorButton::clicked, self, [self, color]() {
|
||||
self->setColor(color);
|
||||
});
|
||||
|
||||
layout->addWidget(button, static_cast<int>(i / COLORS_PER_ROW),
|
||||
static_cast<int>(i % COLORS_PER_ROW));
|
||||
i++;
|
||||
if (i >= maxButtons)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
/// All color inputs have the same two signals and slots:
|
||||
/// `colorChanged` and `setColor`.
|
||||
/// `colorChanged` is emitted when the user changed the color (not after calling `setColor`).
|
||||
template <typename D, typename W>
|
||||
void connectSignals(D *dialog, W *widget)
|
||||
{
|
||||
QObject::connect(widget, &W::colorChanged, dialog, &D::setColor);
|
||||
QObject::connect(dialog, &D::colorChanged, widget, &W::setColor);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
ColorPickerDialog::ColorPickerDialog(const QColor &initial, QWidget *parent)
|
||||
using namespace literals;
|
||||
|
||||
ColorPickerDialog::ColorPickerDialog(QColor color, QWidget *parent)
|
||||
: BasePopup(
|
||||
{
|
||||
BaseWindow::EnableCustomFrame,
|
||||
|
@ -20,371 +68,95 @@ ColorPickerDialog::ColorPickerDialog(const QColor &initial, QWidget *parent)
|
|||
BaseWindow::BoundsCheckOnShow,
|
||||
},
|
||||
parent)
|
||||
, color_()
|
||||
, dialogConfirmed_(false)
|
||||
, color_(color)
|
||||
{
|
||||
// This hosts the "business logic" and the dialog button box
|
||||
LayoutCreator<QWidget> layoutWidget(this->getLayoutContainer());
|
||||
auto layout = layoutWidget.setLayoutType<QVBoxLayout>().withoutMargin();
|
||||
this->setWindowTitle(u"Chatterino - Color picker"_s);
|
||||
this->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
// This hosts the business logic: color picker and predefined colors
|
||||
LayoutCreator<QWidget> contentCreator(new QWidget());
|
||||
auto contents = contentCreator.setLayoutType<QHBoxLayout>();
|
||||
|
||||
// This hosts the predefined colors (and also the currently selected color)
|
||||
LayoutCreator<QWidget> predefCreator(new QWidget());
|
||||
auto predef = predefCreator.setLayoutType<QVBoxLayout>();
|
||||
|
||||
// Recently used colors
|
||||
auto *dialogContents = new QHBoxLayout;
|
||||
dialogContents->setContentsMargins(10, 10, 10, 10);
|
||||
{
|
||||
LayoutCreator<QWidget> gridCreator(new QWidget());
|
||||
this->initRecentColors(gridCreator);
|
||||
auto *buttons = new QVBoxLayout;
|
||||
buttons->addWidget(new QLabel(u"Recently used"_s));
|
||||
buttons->addLayout(makeColorGrid(
|
||||
ColorProvider::instance().recentColors(), this, MAX_RECENT_COLORS));
|
||||
|
||||
predef.append(gridCreator.getElement());
|
||||
buttons->addSpacing(10);
|
||||
|
||||
buttons->addWidget(new QLabel(u"Default colors"_s));
|
||||
buttons->addLayout(
|
||||
makeColorGrid(ColorProvider::instance().defaultColors(), this,
|
||||
MAX_DEFAULT_COLORS));
|
||||
|
||||
buttons->addStretch(1);
|
||||
buttons->addWidget(new QLabel(u"Selected"_s));
|
||||
auto *display = new ColorButton(this->color());
|
||||
QObject::connect(this, &ColorPickerDialog::colorChanged, display,
|
||||
&ColorButton::setColor);
|
||||
buttons->addWidget(display);
|
||||
|
||||
dialogContents->addLayout(buttons);
|
||||
dialogContents->addSpacing(10);
|
||||
}
|
||||
|
||||
// Default colors
|
||||
{
|
||||
LayoutCreator<QWidget> gridCreator(new QWidget());
|
||||
this->initDefaultColors(gridCreator);
|
||||
auto *controls = new QVBoxLayout;
|
||||
|
||||
predef.append(gridCreator.getElement());
|
||||
}
|
||||
|
||||
// Currently selected color
|
||||
{
|
||||
LayoutCreator<QWidget> curColorCreator(new QWidget());
|
||||
auto curColor = curColorCreator.setLayoutType<QHBoxLayout>();
|
||||
curColor.emplace<QLabel>("Selected:").assign(&this->ui_.selected.label);
|
||||
curColor.emplace<ColorButton>(initial).assign(
|
||||
&this->ui_.selected.color);
|
||||
|
||||
predef.append(curColor.getElement());
|
||||
}
|
||||
|
||||
contents.append(predef.getElement());
|
||||
|
||||
// Color picker
|
||||
{
|
||||
LayoutCreator<QWidget> obj(new QWidget());
|
||||
auto vbox = obj.setLayoutType<QVBoxLayout>();
|
||||
|
||||
// The actual color picker
|
||||
{
|
||||
LayoutCreator<QWidget> cpCreator(new QWidget());
|
||||
this->initColorPicker(cpCreator);
|
||||
auto *select = new QVBoxLayout;
|
||||
|
||||
vbox.append(cpCreator.getElement());
|
||||
auto *sbCanvas = new SBCanvas(this->color());
|
||||
auto *hueSlider = new HueSlider(this->color());
|
||||
auto *alphaSlider = new AlphaSlider(this->color());
|
||||
|
||||
connectSignals(this, sbCanvas);
|
||||
connectSignals(this, hueSlider);
|
||||
connectSignals(this, alphaSlider);
|
||||
|
||||
select->addWidget(sbCanvas, 0, Qt::AlignHCenter);
|
||||
select->addWidget(hueSlider);
|
||||
select->addWidget(alphaSlider);
|
||||
|
||||
controls->addLayout(select);
|
||||
}
|
||||
{
|
||||
auto *input = new ColorInput(this->color());
|
||||
connectSignals(this, input);
|
||||
controls->addWidget(input);
|
||||
}
|
||||
|
||||
// Spin boxes
|
||||
{
|
||||
LayoutCreator<QWidget> sbCreator(new QWidget());
|
||||
this->initSpinBoxes(sbCreator);
|
||||
|
||||
vbox.append(sbCreator.getElement());
|
||||
}
|
||||
|
||||
// HTML color
|
||||
{
|
||||
LayoutCreator<QWidget> htmlCreator(new QWidget());
|
||||
this->initHtmlColor(htmlCreator);
|
||||
|
||||
vbox.append(htmlCreator.getElement());
|
||||
}
|
||||
|
||||
contents.append(obj.getElement());
|
||||
dialogContents->addLayout(controls);
|
||||
}
|
||||
|
||||
layout.append(contents.getElement());
|
||||
auto *dialogLayout = new QVBoxLayout(this->getLayoutContainer());
|
||||
dialogLayout->addLayout(dialogContents, 1);
|
||||
dialogLayout->addStretch(1);
|
||||
|
||||
// Dialog buttons
|
||||
auto buttons =
|
||||
layout.emplace<QHBoxLayout>().emplace<QDialogButtonBox>(this);
|
||||
{
|
||||
auto *button_ok = buttons->addButton(QDialogButtonBox::Ok);
|
||||
QObject::connect(button_ok, &QPushButton::clicked, [this](bool) {
|
||||
this->ok();
|
||||
});
|
||||
auto *button_cancel = buttons->addButton(QDialogButtonBox::Cancel);
|
||||
QObject::connect(button_cancel, &QAbstractButton::clicked,
|
||||
[this](bool) {
|
||||
this->close();
|
||||
});
|
||||
}
|
||||
auto *buttonBox =
|
||||
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
|
||||
this->themeChangedEvent();
|
||||
this->selectColor(initial, false);
|
||||
QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, [this] {
|
||||
emit this->colorConfirmed(this->color());
|
||||
this->close();
|
||||
});
|
||||
QObject::connect(buttonBox, &QDialogButtonBox::rejected, this,
|
||||
&ColorPickerDialog::close);
|
||||
dialogLayout->addWidget(buttonBox, 0, Qt::AlignRight);
|
||||
}
|
||||
|
||||
void ColorPickerDialog::addShortcuts()
|
||||
QColor ColorPickerDialog::color() const
|
||||
{
|
||||
}
|
||||
|
||||
ColorPickerDialog::~ColorPickerDialog()
|
||||
{
|
||||
if (this->htmlColorValidator_)
|
||||
{
|
||||
this->htmlColorValidator_->deleteLater();
|
||||
this->htmlColorValidator_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
QColor ColorPickerDialog::selectedColor() const
|
||||
{
|
||||
if (!this->dialogConfirmed_)
|
||||
{
|
||||
// If the Cancel button was clicked, return the invalid color
|
||||
return QColor();
|
||||
}
|
||||
|
||||
return this->color_;
|
||||
}
|
||||
|
||||
void ColorPickerDialog::closeEvent(QCloseEvent *)
|
||||
{
|
||||
this->closed.invoke(this->selectedColor());
|
||||
}
|
||||
|
||||
void ColorPickerDialog::themeChangedEvent()
|
||||
{
|
||||
BaseWindow::themeChangedEvent();
|
||||
|
||||
QString textCol = this->theme->splits.input.text.name(QColor::HexRgb);
|
||||
QString bgCol = this->theme->splits.input.background.name(QColor::HexRgb);
|
||||
|
||||
// Labels
|
||||
|
||||
QString labelStyle = QString("color: %1;").arg(textCol);
|
||||
|
||||
this->ui_.recent.label->setStyleSheet(labelStyle);
|
||||
this->ui_.def.label->setStyleSheet(labelStyle);
|
||||
this->ui_.selected.label->setStyleSheet(labelStyle);
|
||||
this->ui_.picker.htmlLabel->setStyleSheet(labelStyle);
|
||||
|
||||
for (auto spinBoxLabel : this->ui_.picker.spinBoxLabels)
|
||||
{
|
||||
spinBoxLabel->setStyleSheet(labelStyle);
|
||||
}
|
||||
|
||||
this->ui_.picker.htmlEdit->setStyleSheet(
|
||||
this->theme->splits.input.styleSheet);
|
||||
|
||||
// Styling spin boxes is too much effort
|
||||
}
|
||||
|
||||
void ColorPickerDialog::selectColor(const QColor &color, bool fromColorPicker)
|
||||
void ColorPickerDialog::setColor(const QColor &color)
|
||||
{
|
||||
if (color == this->color_)
|
||||
{
|
||||
return;
|
||||
|
||||
}
|
||||
this->color_ = color;
|
||||
|
||||
// Update UI elements
|
||||
this->ui_.selected.color->setColor(this->color_);
|
||||
|
||||
/*
|
||||
* Somewhat "ugly" hack to prevent feedback loop between widgets. Since
|
||||
* this method is private, I'm okay with this being ugly.
|
||||
*/
|
||||
if (!fromColorPicker)
|
||||
{
|
||||
this->ui_.picker.colorPicker->setCol(this->color_.hslHue(),
|
||||
this->color_.hslSaturation());
|
||||
this->ui_.picker.luminancePicker->setCol(this->color_.hsvHue(),
|
||||
this->color_.hsvSaturation(),
|
||||
this->color_.value());
|
||||
}
|
||||
|
||||
this->ui_.picker.spinBoxes[SpinBox::RED]->setValue(this->color_.red());
|
||||
this->ui_.picker.spinBoxes[SpinBox::GREEN]->setValue(this->color_.green());
|
||||
this->ui_.picker.spinBoxes[SpinBox::BLUE]->setValue(this->color_.blue());
|
||||
this->ui_.picker.spinBoxes[SpinBox::ALPHA]->setValue(this->color_.alpha());
|
||||
|
||||
/*
|
||||
* Here, we are intentionally using HexRgb instead of HexArgb. Most online
|
||||
* sites (or other applications) will likely not include the alpha channel
|
||||
* in their output.
|
||||
*/
|
||||
this->ui_.picker.htmlEdit->setText(this->color_.name(QColor::HexRgb));
|
||||
}
|
||||
|
||||
void ColorPickerDialog::ok()
|
||||
{
|
||||
this->dialogConfirmed_ = true;
|
||||
this->close();
|
||||
}
|
||||
|
||||
void ColorPickerDialog::initRecentColors(LayoutCreator<QWidget> &creator)
|
||||
{
|
||||
auto grid = creator.setLayoutType<QGridLayout>();
|
||||
|
||||
auto label = this->ui_.recent.label = new QLabel("Recently used:");
|
||||
grid->addWidget(label, 0, 0, 1, -1);
|
||||
|
||||
const auto recentColors = ColorProvider::instance().recentColors();
|
||||
auto it = recentColors.begin();
|
||||
size_t ind = 0;
|
||||
while (it != recentColors.end() && ind < MAX_RECENT_COLORS)
|
||||
{
|
||||
this->ui_.recent.colors.push_back(new ColorButton(*it, this));
|
||||
auto *button = this->ui_.recent.colors[ind];
|
||||
|
||||
static_assert(RECENT_COLORS_PER_ROW != 0);
|
||||
const int rowInd = (ind / RECENT_COLORS_PER_ROW) + 1;
|
||||
const int columnInd = ind % RECENT_COLORS_PER_ROW;
|
||||
|
||||
grid->addWidget(button, rowInd, columnInd);
|
||||
|
||||
QObject::connect(button, &QPushButton::clicked, [=, this] {
|
||||
this->selectColor(button->color(), false);
|
||||
});
|
||||
|
||||
++it;
|
||||
++ind;
|
||||
}
|
||||
|
||||
auto spacer =
|
||||
new QSpacerItem(40, 20, QSizePolicy::Minimum, QSizePolicy::Expanding);
|
||||
grid->addItem(spacer, (ind / RECENT_COLORS_PER_ROW) + 2, 0, 1, 1,
|
||||
Qt::AlignTop);
|
||||
}
|
||||
|
||||
void ColorPickerDialog::initDefaultColors(LayoutCreator<QWidget> &creator)
|
||||
{
|
||||
auto grid = creator.setLayoutType<QGridLayout>();
|
||||
|
||||
auto label = this->ui_.def.label = new QLabel("Default colors:");
|
||||
grid->addWidget(label, 0, 0, 1, -1);
|
||||
|
||||
const auto defaultColors = ColorProvider::instance().defaultColors();
|
||||
auto it = defaultColors.begin();
|
||||
size_t ind = 0;
|
||||
while (it != defaultColors.end())
|
||||
{
|
||||
this->ui_.def.colors.push_back(new ColorButton(*it, this));
|
||||
auto *button = this->ui_.def.colors[ind];
|
||||
|
||||
const int rowInd = (ind / DEFAULT_COLORS_PER_ROW) + 1;
|
||||
const int columnInd = ind % DEFAULT_COLORS_PER_ROW;
|
||||
|
||||
grid->addWidget(button, rowInd, columnInd);
|
||||
|
||||
QObject::connect(button, &QPushButton::clicked, [=, this] {
|
||||
this->selectColor(button->color(), false);
|
||||
});
|
||||
|
||||
++it;
|
||||
++ind;
|
||||
}
|
||||
|
||||
auto spacer =
|
||||
new QSpacerItem(40, 20, QSizePolicy::Minimum, QSizePolicy::Expanding);
|
||||
grid->addItem(spacer, (ind / DEFAULT_COLORS_PER_ROW) + 2, 0, 1, 1,
|
||||
Qt::AlignTop);
|
||||
}
|
||||
|
||||
void ColorPickerDialog::initColorPicker(LayoutCreator<QWidget> &creator)
|
||||
{
|
||||
this->setWindowTitle("Chatterino - color picker");
|
||||
auto cpPanel = creator.setLayoutType<QHBoxLayout>();
|
||||
|
||||
/*
|
||||
* For some reason, LayoutCreator::emplace didn't work for these.
|
||||
* (Or maybe I was too dense to make it work.)
|
||||
* After trying to debug for 4 hours or so, I gave up and settled
|
||||
* for this solution.
|
||||
*/
|
||||
auto *colorPicker = new QColorPicker(this);
|
||||
this->ui_.picker.colorPicker = colorPicker;
|
||||
|
||||
auto *luminancePicker = new QColorLuminancePicker(this);
|
||||
this->ui_.picker.luminancePicker = luminancePicker;
|
||||
|
||||
cpPanel.append(colorPicker);
|
||||
cpPanel.append(luminancePicker);
|
||||
|
||||
QObject::connect(colorPicker, SIGNAL(newCol(int, int)), luminancePicker,
|
||||
SLOT(setCol(int, int)));
|
||||
|
||||
QObject::connect(
|
||||
luminancePicker, &QColorLuminancePicker::newHsv,
|
||||
[this](int h, int s, int v) {
|
||||
int alpha = this->ui_.picker.spinBoxes[SpinBox::ALPHA]->value();
|
||||
this->selectColor(QColor::fromHsv(h, s, v, alpha), true);
|
||||
});
|
||||
}
|
||||
|
||||
void ColorPickerDialog::initSpinBoxes(LayoutCreator<QWidget> &creator)
|
||||
{
|
||||
auto spinBoxes = creator.setLayoutType<QGridLayout>();
|
||||
|
||||
auto *red = this->ui_.picker.spinBoxes[SpinBox::RED] =
|
||||
new QColSpinBox(this);
|
||||
auto *green = this->ui_.picker.spinBoxes[SpinBox::GREEN] =
|
||||
new QColSpinBox(this);
|
||||
auto *blue = this->ui_.picker.spinBoxes[SpinBox::BLUE] =
|
||||
new QColSpinBox(this);
|
||||
auto *alpha = this->ui_.picker.spinBoxes[SpinBox::ALPHA] =
|
||||
new QColSpinBox(this);
|
||||
|
||||
// We need pointers to these for theme changes
|
||||
auto *redLbl = this->ui_.picker.spinBoxLabels[SpinBox::RED] =
|
||||
new QLabel("Red:");
|
||||
auto *greenLbl = this->ui_.picker.spinBoxLabels[SpinBox::GREEN] =
|
||||
new QLabel("Green:");
|
||||
auto *blueLbl = this->ui_.picker.spinBoxLabels[SpinBox::BLUE] =
|
||||
new QLabel("Blue:");
|
||||
auto *alphaLbl = this->ui_.picker.spinBoxLabels[SpinBox::ALPHA] =
|
||||
new QLabel("Alpha:");
|
||||
|
||||
spinBoxes->addWidget(redLbl, 0, 0);
|
||||
spinBoxes->addWidget(red, 0, 1);
|
||||
|
||||
spinBoxes->addWidget(greenLbl, 1, 0);
|
||||
spinBoxes->addWidget(green, 1, 1);
|
||||
|
||||
spinBoxes->addWidget(blueLbl, 2, 0);
|
||||
spinBoxes->addWidget(blue, 2, 1);
|
||||
|
||||
spinBoxes->addWidget(alphaLbl, 3, 0);
|
||||
spinBoxes->addWidget(alpha, 3, 1);
|
||||
|
||||
for (size_t i = 0; i < SpinBox::END; ++i)
|
||||
{
|
||||
QObject::connect(
|
||||
this->ui_.picker.spinBoxes[i],
|
||||
QOverload<int>::of(&QSpinBox::valueChanged), [=, this](int value) {
|
||||
this->selectColor(QColor(red->value(), green->value(),
|
||||
blue->value(), alpha->value()),
|
||||
false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ColorPickerDialog::initHtmlColor(LayoutCreator<QWidget> &creator)
|
||||
{
|
||||
auto html = creator.setLayoutType<QGridLayout>();
|
||||
|
||||
// Copied from Qt source for QColorShower
|
||||
static QRegularExpression regExp(
|
||||
QStringLiteral("#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})"));
|
||||
auto *validator = this->htmlColorValidator_ =
|
||||
new QRegularExpressionValidator(regExp, this);
|
||||
|
||||
auto *htmlLabel = this->ui_.picker.htmlLabel = new QLabel("HTML:");
|
||||
auto *htmlEdit = this->ui_.picker.htmlEdit = new QLineEdit(this);
|
||||
|
||||
htmlEdit->setValidator(validator);
|
||||
|
||||
html->addWidget(htmlLabel, 0, 0);
|
||||
html->addWidget(htmlEdit, 0, 1);
|
||||
|
||||
QObject::connect(htmlEdit, &QLineEdit::editingFinished, [this] {
|
||||
const QColor col(this->ui_.picker.htmlEdit->text());
|
||||
if (col.isValid())
|
||||
this->selectColor(col, false);
|
||||
});
|
||||
emit this->colorChanged(color);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -2,119 +2,26 @@
|
|||
|
||||
#include "widgets/BasePopup.hpp"
|
||||
|
||||
#include <pajlada/signals/signal.hpp>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QRegularExpressionValidator>
|
||||
|
||||
#include <array>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class ColorButton;
|
||||
class QColorLuminancePicker;
|
||||
class QColorPicker;
|
||||
class QColSpinBox;
|
||||
|
||||
template <class T>
|
||||
class LayoutCreator;
|
||||
|
||||
/**
|
||||
* @brief A custom color picker dialog.
|
||||
*
|
||||
* This class exists because QColorPickerDialog did not suit our use case.
|
||||
* This dialog provides buttons for recently used and default colors, as well
|
||||
* as a color picker widget identical to the one used in QColorPickerDialog.
|
||||
*/
|
||||
class ColorPickerDialog : public BasePopup
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Create a new color picker dialog that selects the initial color.
|
||||
*
|
||||
* You can connect to the ::closed signal of this instance to get notified
|
||||
* when the dialog is closed.
|
||||
*/
|
||||
ColorPickerDialog(const QColor &initial, QWidget *parent);
|
||||
ColorPickerDialog(QColor color, QWidget *parent);
|
||||
|
||||
~ColorPickerDialog() override;
|
||||
QColor color() const;
|
||||
|
||||
/**
|
||||
* @brief Return the final color selected by the user.
|
||||
*
|
||||
* Note that this method will always return the invalid color if the dialog
|
||||
* is still open, or if the dialog has not been confirmed.
|
||||
*
|
||||
* @return The color selected by the user, if the dialog was confirmed.
|
||||
* The invalid color, if the dialog has not been confirmed.
|
||||
*/
|
||||
QColor selectedColor() const;
|
||||
signals:
|
||||
void colorChanged(QColor color);
|
||||
void colorConfirmed(QColor color);
|
||||
|
||||
pajlada::Signals::Signal<QColor> closed;
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *) override;
|
||||
void themeChangedEvent() override;
|
||||
public slots:
|
||||
void setColor(const QColor &color);
|
||||
|
||||
private:
|
||||
struct {
|
||||
struct {
|
||||
QLabel *label;
|
||||
std::vector<ColorButton *> colors;
|
||||
} recent;
|
||||
|
||||
struct {
|
||||
QLabel *label;
|
||||
std::vector<ColorButton *> colors;
|
||||
} def;
|
||||
|
||||
struct {
|
||||
QLabel *label;
|
||||
ColorButton *color;
|
||||
} selected{};
|
||||
|
||||
struct {
|
||||
QColorPicker *colorPicker;
|
||||
QColorLuminancePicker *luminancePicker;
|
||||
|
||||
std::array<QLabel *, 4> spinBoxLabels;
|
||||
std::array<QColSpinBox *, 4> spinBoxes;
|
||||
|
||||
QLabel *htmlLabel;
|
||||
QLineEdit *htmlEdit;
|
||||
} picker{};
|
||||
} ui_;
|
||||
|
||||
enum SpinBox : size_t { RED = 0, GREEN = 1, BLUE = 2, ALPHA = 3, END };
|
||||
|
||||
static const size_t MAX_RECENT_COLORS = 10;
|
||||
static const size_t RECENT_COLORS_PER_ROW = 5;
|
||||
static const size_t DEFAULT_COLORS_PER_ROW = 5;
|
||||
|
||||
QColor color_;
|
||||
bool dialogConfirmed_;
|
||||
QRegularExpressionValidator *htmlColorValidator_{};
|
||||
|
||||
/**
|
||||
* @brief Update the currently selected color.
|
||||
*
|
||||
* @param color Color to update to.
|
||||
* @param fromColorPicker Whether the color update has been triggered by
|
||||
* one of the color picker widgets. This is needed
|
||||
* to prevent weird widget behavior.
|
||||
*/
|
||||
void selectColor(const QColor &color, bool fromColorPicker);
|
||||
|
||||
/// Called when the dialog is confirmed.
|
||||
void ok();
|
||||
|
||||
// Helper methods for initializing UI elements
|
||||
void initRecentColors(LayoutCreator<QWidget> &creator);
|
||||
void initDefaultColors(LayoutCreator<QWidget> &creator);
|
||||
void initColorPicker(LayoutCreator<QWidget> &creator);
|
||||
void initSpinBoxes(LayoutCreator<QWidget> &creator);
|
||||
void initHtmlColor(LayoutCreator<QWidget> &creator);
|
||||
|
||||
void addShortcuts() override;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
|
|
@ -52,7 +52,6 @@ SettingsDialog::SettingsDialog(QWidget *parent)
|
|||
this->initUi();
|
||||
this->addTabs();
|
||||
this->overrideBackgroundColor_ = QColor("#111111");
|
||||
this->scaleChangedEvent(this->scale()); // execute twice to width of item
|
||||
|
||||
this->addShortcuts();
|
||||
this->signalHolder_.managedConnect(getApp()->hotkeys->onItemsUpdated,
|
||||
|
@ -128,6 +127,8 @@ void SettingsDialog::initUi()
|
|||
.setLayoutType<QVBoxLayout>()
|
||||
.withoutMargin()
|
||||
.assign(&this->ui_.tabContainer);
|
||||
this->ui_.tabContainerContainer->setFixedWidth(
|
||||
static_cast<int>(150 * this->dpi_));
|
||||
|
||||
// right side (pages)
|
||||
centerBox.emplace<QStackedLayout>()
|
||||
|
@ -257,6 +258,7 @@ void SettingsDialog::addTab(std::function<SettingsPage *()> page,
|
|||
SettingsTabId id, Qt::Alignment alignment)
|
||||
{
|
||||
auto tab = new SettingsDialogTab(this, std::move(page), name, iconPath, id);
|
||||
tab->setFixedHeight(static_cast<int>(30 * this->dpi_));
|
||||
|
||||
this->ui_.tabContainer->addWidget(tab, 0, alignment);
|
||||
this->tabs_.push_back(tab);
|
||||
|
@ -390,7 +392,11 @@ void SettingsDialog::scaleChangedEvent(float newDpi)
|
|||
this->setStyleSheet(styleSheet);
|
||||
|
||||
if (this->ui_.tabContainerContainer)
|
||||
{
|
||||
this->ui_.tabContainerContainer->setFixedWidth(int(150 * newDpi));
|
||||
}
|
||||
|
||||
this->dpi_ = newDpi;
|
||||
}
|
||||
|
||||
void SettingsDialog::themeChangedEvent()
|
||||
|
|
|
@ -77,6 +77,7 @@ private:
|
|||
std::vector<SettingsDialogTab *> tabs_;
|
||||
SettingsDialogTab *selectedTab_{};
|
||||
SettingsDialogTab *lastSelectedByUser_{};
|
||||
float dpi_ = 1.0F;
|
||||
|
||||
friend class SettingsDialogTab;
|
||||
};
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
#include "widgets/helper/ColorButton.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
ColorButton::ColorButton(const QColor &color, QWidget *parent)
|
||||
: QPushButton(parent)
|
||||
, color_(color)
|
||||
{
|
||||
this->setColor(color_);
|
||||
}
|
||||
|
||||
const QColor &ColorButton::color() const
|
||||
{
|
||||
return this->color_;
|
||||
}
|
||||
|
||||
void ColorButton::setColor(QColor color)
|
||||
{
|
||||
this->color_ = color;
|
||||
this->setStyleSheet("background-color: " + color.name(QColor::HexArgb));
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,20 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <QPushButton>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class ColorButton : public QPushButton
|
||||
{
|
||||
public:
|
||||
ColorButton(const QColor &color, QWidget *parent = nullptr);
|
||||
|
||||
const QColor &color() const;
|
||||
|
||||
void setColor(QColor color);
|
||||
|
||||
private:
|
||||
QColor color_;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -93,8 +93,8 @@ NotebookTab::NotebookTab(Notebook *notebook)
|
|||
[this]() {
|
||||
this->notebook_->removePage(this->page);
|
||||
},
|
||||
getApp()->hotkeys->getDisplaySequence(HotkeyCategory::Window,
|
||||
"removeTab"));
|
||||
getIApp()->getHotkeys()->getDisplaySequence(HotkeyCategory::Window,
|
||||
"removeTab"));
|
||||
|
||||
this->menu_.addAction(
|
||||
"Popup Tab",
|
||||
|
@ -104,8 +104,8 @@ NotebookTab::NotebookTab(Notebook *notebook)
|
|||
container->popup();
|
||||
}
|
||||
},
|
||||
getApp()->hotkeys->getDisplaySequence(HotkeyCategory::Window, "popup",
|
||||
{{"window"}}));
|
||||
getIApp()->getHotkeys()->getDisplaySequence(HotkeyCategory::Window,
|
||||
"popup", {{"window"}}));
|
||||
|
||||
highlightNewMessagesAction_ =
|
||||
new QAction("Mark Tab as Unread on New Messages", &this->menu_);
|
||||
|
@ -196,7 +196,7 @@ int NotebookTab::normalTabWidth()
|
|||
float scale = this->scale();
|
||||
int width;
|
||||
|
||||
QFontMetrics metrics = getApp()->fonts->getFontMetrics(
|
||||
auto metrics = getIApp()->getFonts()->getFontMetrics(
|
||||
FontStyle::UiTabs, float(qreal(this->scale()) * deviceDpi(this)));
|
||||
|
||||
if (this->hasXButton())
|
||||
|
@ -346,17 +346,30 @@ bool NotebookTab::isLive() const
|
|||
|
||||
void NotebookTab::setHighlightState(HighlightState newHighlightStyle)
|
||||
{
|
||||
if (this->isSelected() || (!this->highlightEnabled_ &&
|
||||
newHighlightStyle == HighlightState::NewMessage))
|
||||
if (this->isSelected())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (this->highlightState_ != HighlightState::Highlighted)
|
||||
{
|
||||
this->highlightState_ = newHighlightStyle;
|
||||
|
||||
this->update();
|
||||
if (!this->highlightEnabled_ &&
|
||||
newHighlightStyle == HighlightState::NewMessage)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->highlightState_ == newHighlightStyle ||
|
||||
this->highlightState_ == HighlightState::Highlighted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->highlightState_ = newHighlightStyle;
|
||||
this->update();
|
||||
}
|
||||
|
||||
HighlightState NotebookTab::highlightState() const
|
||||
{
|
||||
return this->highlightState_;
|
||||
}
|
||||
|
||||
void NotebookTab::setHighlightsEnabled(const bool &newVal)
|
||||
|
@ -783,6 +796,11 @@ void NotebookTab::wheelEvent(QWheelEvent *event)
|
|||
}
|
||||
}
|
||||
|
||||
void NotebookTab::update()
|
||||
{
|
||||
Button::update();
|
||||
}
|
||||
|
||||
QRect NotebookTab::getXRect()
|
||||
{
|
||||
QRect rect = this->rect();
|
||||
|
|
|
@ -53,6 +53,8 @@ public:
|
|||
bool isLive() const;
|
||||
|
||||
void setHighlightState(HighlightState style);
|
||||
HighlightState highlightState() const;
|
||||
|
||||
void setHighlightsEnabled(const bool &newVal);
|
||||
bool hasHighlightsEnabled() const;
|
||||
|
||||
|
@ -84,6 +86,10 @@ protected:
|
|||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
|
||||
/// This exists as an alias to its base classes update, and is virtual
|
||||
/// to allow for mocking
|
||||
virtual void update();
|
||||
|
||||
private:
|
||||
void showRenameDialog();
|
||||
|
||||
|
|
|
@ -1,292 +0,0 @@
|
|||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2016 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the QtWidgets module of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:LGPL$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU Lesser General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU Lesser
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.LGPL3 included in the
|
||||
** packaging of this file. Please review the following information to
|
||||
** ensure the GNU Lesser General Public License version 3 requirements
|
||||
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 2.0 or (at your option) the GNU General
|
||||
** Public license version 3 or any later version approved by the KDE Free
|
||||
** Qt Foundation. The licenses are as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
|
||||
** https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
#include "widgets/helper/QColorPicker.hpp"
|
||||
|
||||
#include <qdrawutil.h>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
|
||||
/*
|
||||
* These classes are literally copied from the Qt source.
|
||||
* Unfortunately, they are private to the QColorDialog class so we cannot use
|
||||
* them directly.
|
||||
* If they become public at any point in the future, it should be possible to
|
||||
* replace every include of this header with the respective includes for the
|
||||
* QColorPicker, QColorLuminancePicker, and QColSpinBox classes.
|
||||
*/
|
||||
namespace chatterino {
|
||||
|
||||
int QColorLuminancePicker::y2val(int y)
|
||||
{
|
||||
int d = height() - 2 * coff - 1;
|
||||
return 255 - (y - coff) * 255 / d;
|
||||
}
|
||||
|
||||
int QColorLuminancePicker::val2y(int v)
|
||||
{
|
||||
int d = height() - 2 * coff - 1;
|
||||
return coff + (255 - v) * d / 255;
|
||||
}
|
||||
|
||||
QColorLuminancePicker::QColorLuminancePicker(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
hue = 100;
|
||||
val = 100;
|
||||
sat = 100;
|
||||
pix = 0;
|
||||
setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
|
||||
}
|
||||
|
||||
QColorLuminancePicker::~QColorLuminancePicker()
|
||||
{
|
||||
delete pix;
|
||||
}
|
||||
|
||||
void QColorLuminancePicker::mouseMoveEvent(QMouseEvent *m)
|
||||
{
|
||||
setVal(y2val(m->y()));
|
||||
}
|
||||
|
||||
void QColorLuminancePicker::mousePressEvent(QMouseEvent *m)
|
||||
{
|
||||
setVal(y2val(m->y()));
|
||||
}
|
||||
|
||||
void QColorLuminancePicker::setVal(int v)
|
||||
{
|
||||
if (val == v)
|
||||
return;
|
||||
val = qMax(0, qMin(v, 255));
|
||||
delete pix;
|
||||
pix = 0;
|
||||
repaint();
|
||||
emit newHsv(hue, sat, val);
|
||||
}
|
||||
|
||||
//receives from a hue,sat chooser and relays.
|
||||
void QColorLuminancePicker::setCol(int h, int s)
|
||||
{
|
||||
setCol(h, s, val);
|
||||
emit newHsv(h, s, val);
|
||||
}
|
||||
|
||||
QSize QColorLuminancePicker::sizeHint() const
|
||||
{
|
||||
return QSize(LUMINANCE_PICKER_WIDTH, LUMINANCE_PICKER_HEIGHT);
|
||||
}
|
||||
|
||||
void QColorLuminancePicker::paintEvent(QPaintEvent *)
|
||||
{
|
||||
int w = width() - 5;
|
||||
QRect r(0, foff, w, height() - 2 * foff);
|
||||
int wi = r.width() - 2;
|
||||
int hi = r.height() - 2;
|
||||
if (!pix || pix->height() != hi || pix->width() != wi)
|
||||
{
|
||||
delete pix;
|
||||
QImage img(wi, hi, QImage::Format_RGB32);
|
||||
int y;
|
||||
uint *pixel = (uint *)img.scanLine(0);
|
||||
for (y = 0; y < hi; y++)
|
||||
{
|
||||
uint *end = pixel + wi;
|
||||
std::fill(pixel, end,
|
||||
QColor::fromHsv(hue, sat, y2val(y + coff)).rgb());
|
||||
pixel = end;
|
||||
}
|
||||
pix = new QPixmap(QPixmap::fromImage(img));
|
||||
}
|
||||
QPainter p(this);
|
||||
p.drawPixmap(1, coff, *pix);
|
||||
const QPalette &g = palette();
|
||||
qDrawShadePanel(&p, r, g, true);
|
||||
p.setPen(g.windowText().color());
|
||||
p.setBrush(g.windowText());
|
||||
QPolygon a;
|
||||
int y = val2y(val);
|
||||
a.setPoints(3, w, y, w + 5, y + 5, w + 5, y - 5);
|
||||
p.eraseRect(w, 0, 5, height());
|
||||
p.drawPolygon(a);
|
||||
}
|
||||
|
||||
void QColorLuminancePicker::setCol(int h, int s, int v)
|
||||
{
|
||||
val = v;
|
||||
hue = h;
|
||||
sat = s;
|
||||
delete pix;
|
||||
pix = 0;
|
||||
repaint();
|
||||
}
|
||||
|
||||
QPoint QColorPicker::colPt()
|
||||
{
|
||||
QRect r = contentsRect();
|
||||
return QPoint((360 - hue) * (r.width() - 1) / 360,
|
||||
(255 - sat) * (r.height() - 1) / 255);
|
||||
}
|
||||
|
||||
int QColorPicker::huePt(const QPoint &pt)
|
||||
{
|
||||
QRect r = contentsRect();
|
||||
return 360 - pt.x() * 360 / (r.width() - 1);
|
||||
}
|
||||
|
||||
int QColorPicker::satPt(const QPoint &pt)
|
||||
{
|
||||
QRect r = contentsRect();
|
||||
return 255 - pt.y() * 255 / (r.height() - 1);
|
||||
}
|
||||
|
||||
void QColorPicker::setCol(const QPoint &pt)
|
||||
{
|
||||
setCol(huePt(pt), satPt(pt));
|
||||
}
|
||||
|
||||
QColorPicker::QColorPicker(QWidget *parent)
|
||||
: QFrame(parent)
|
||||
, crossVisible(true)
|
||||
{
|
||||
hue = 0;
|
||||
sat = 0;
|
||||
setCol(150, 255);
|
||||
setAttribute(Qt::WA_NoSystemBackground);
|
||||
setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
|
||||
}
|
||||
|
||||
QColorPicker::~QColorPicker()
|
||||
{
|
||||
}
|
||||
|
||||
void QColorPicker::setCrossVisible(bool visible)
|
||||
{
|
||||
if (crossVisible != visible)
|
||||
{
|
||||
crossVisible = visible;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
QSize QColorPicker::sizeHint() const
|
||||
{
|
||||
return QSize(COLOR_PICKER_WIDTH, COLOR_PICKER_HEIGHT);
|
||||
}
|
||||
|
||||
void QColorPicker::setCol(int h, int s)
|
||||
{
|
||||
int nhue = qMin(qMax(0, h), 359);
|
||||
int nsat = qMin(qMax(0, s), 255);
|
||||
if (nhue == hue && nsat == sat)
|
||||
return;
|
||||
QRect r(colPt(), QSize(20, 20));
|
||||
hue = nhue;
|
||||
sat = nsat;
|
||||
r = r.united(QRect(colPt(), QSize(20, 20)));
|
||||
r.translate(contentsRect().x() - 9, contentsRect().y() - 9);
|
||||
repaint(r);
|
||||
}
|
||||
|
||||
void QColorPicker::mouseMoveEvent(QMouseEvent *m)
|
||||
{
|
||||
QPoint p = m->pos() - contentsRect().topLeft();
|
||||
setCol(p);
|
||||
emit newCol(hue, sat);
|
||||
}
|
||||
|
||||
void QColorPicker::mousePressEvent(QMouseEvent *m)
|
||||
{
|
||||
QPoint p = m->pos() - contentsRect().topLeft();
|
||||
setCol(p);
|
||||
emit newCol(hue, sat);
|
||||
}
|
||||
|
||||
void QColorPicker::paintEvent(QPaintEvent *)
|
||||
{
|
||||
QPainter p(this);
|
||||
drawFrame(&p);
|
||||
QRect r = contentsRect();
|
||||
p.drawPixmap(r.topLeft(), pix);
|
||||
if (crossVisible)
|
||||
{
|
||||
QPoint pt = colPt() + r.topLeft();
|
||||
p.setPen(Qt::black);
|
||||
p.fillRect(pt.x() - 9, pt.y(), 20, 2, Qt::black);
|
||||
p.fillRect(pt.x(), pt.y() - 9, 2, 20, Qt::black);
|
||||
}
|
||||
}
|
||||
|
||||
void QColorPicker::resizeEvent(QResizeEvent *ev)
|
||||
{
|
||||
QFrame::resizeEvent(ev);
|
||||
int w = width() - frameWidth() * 2;
|
||||
int h = height() - frameWidth() * 2;
|
||||
QImage img(w, h, QImage::Format_RGB32);
|
||||
int x, y;
|
||||
uint *pixel = (uint *)img.scanLine(0);
|
||||
for (y = 0; y < h; y++)
|
||||
{
|
||||
const uint *end = pixel + w;
|
||||
x = 0;
|
||||
while (pixel < end)
|
||||
{
|
||||
QPoint p(x, y);
|
||||
QColor c;
|
||||
c.setHsv(huePt(p), satPt(p), 200);
|
||||
*pixel = c.rgb();
|
||||
++pixel;
|
||||
++x;
|
||||
}
|
||||
}
|
||||
pix = QPixmap::fromImage(img);
|
||||
}
|
||||
|
||||
QColSpinBox::QColSpinBox(QWidget *parent)
|
||||
: QSpinBox(parent)
|
||||
{
|
||||
this->setRange(0, 255);
|
||||
}
|
||||
|
||||
void QColSpinBox::setValue(int i)
|
||||
{
|
||||
const QSignalBlocker blocker(this);
|
||||
QSpinBox::setValue(i);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
|
@ -1,131 +0,0 @@
|
|||
/****************************************************************************
|
||||
**
|
||||
** Copyright (C) 2016 The Qt Company Ltd.
|
||||
** Contact: https://www.qt.io/licensing/
|
||||
**
|
||||
** This file is part of the QtWidgets module of the Qt Toolkit.
|
||||
**
|
||||
** $QT_BEGIN_LICENSE:LGPL$
|
||||
** Commercial License Usage
|
||||
** Licensees holding valid commercial Qt licenses may use this file in
|
||||
** accordance with the commercial license agreement provided with the
|
||||
** Software or, alternatively, in accordance with the terms contained in
|
||||
** a written agreement between you and The Qt Company. For licensing terms
|
||||
** and conditions see https://www.qt.io/terms-conditions. For further
|
||||
** information use the contact form at https://www.qt.io/contact-us.
|
||||
**
|
||||
** GNU Lesser General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU Lesser
|
||||
** General Public License version 3 as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.LGPL3 included in the
|
||||
** packaging of this file. Please review the following information to
|
||||
** ensure the GNU Lesser General Public License version 3 requirements
|
||||
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
|
||||
**
|
||||
** GNU General Public License Usage
|
||||
** Alternatively, this file may be used under the terms of the GNU
|
||||
** General Public License version 2.0 or (at your option) the GNU General
|
||||
** Public license version 3 or any later version approved by the KDE Free
|
||||
** Qt Foundation. The licenses are as published by the Free Software
|
||||
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
|
||||
** included in the packaging of this file. Please review the following
|
||||
** information to ensure the GNU General Public License requirements will
|
||||
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
|
||||
** https://www.gnu.org/licenses/gpl-3.0.html.
|
||||
**
|
||||
** $QT_END_LICENSE$
|
||||
**
|
||||
****************************************************************************/
|
||||
#pragma once
|
||||
|
||||
#include <QFrame>
|
||||
#include <QSpinBox>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
/*
|
||||
* These classes are literally copied from the Qt source.
|
||||
* Unfortunately, they are private to the QColorDialog class so we cannot use
|
||||
* them directly.
|
||||
* If they become public at any point in the future, it should be possible to
|
||||
* replace every include of this header with the respective includes for the
|
||||
* QColorPicker, QColorLuminancePicker, and QColSpinBox classes.
|
||||
*/
|
||||
class QColorPicker : public QFrame
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
QColorPicker(QWidget *parent);
|
||||
~QColorPicker() override;
|
||||
void setCrossVisible(bool visible);
|
||||
|
||||
public slots:
|
||||
void setCol(int h, int s);
|
||||
|
||||
signals:
|
||||
void newCol(int h, int s);
|
||||
|
||||
protected:
|
||||
QSize sizeHint() const override;
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
void mouseMoveEvent(QMouseEvent *) override;
|
||||
void mousePressEvent(QMouseEvent *) override;
|
||||
void resizeEvent(QResizeEvent *) override;
|
||||
|
||||
private:
|
||||
int hue;
|
||||
int sat;
|
||||
QPoint colPt();
|
||||
int huePt(const QPoint &pt);
|
||||
int satPt(const QPoint &pt);
|
||||
void setCol(const QPoint &pt);
|
||||
QPixmap pix;
|
||||
bool crossVisible;
|
||||
};
|
||||
|
||||
static const int COLOR_PICKER_WIDTH = 220;
|
||||
static const int COLOR_PICKER_HEIGHT = 200;
|
||||
|
||||
class QColorLuminancePicker : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
QColorLuminancePicker(QWidget *parent = 0);
|
||||
~QColorLuminancePicker() override;
|
||||
|
||||
public slots:
|
||||
void setCol(int h, int s, int v);
|
||||
void setCol(int h, int s);
|
||||
|
||||
signals:
|
||||
void newHsv(int h, int s, int v);
|
||||
|
||||
protected:
|
||||
QSize sizeHint() const override;
|
||||
void paintEvent(QPaintEvent *) override;
|
||||
void mouseMoveEvent(QMouseEvent *) override;
|
||||
void mousePressEvent(QMouseEvent *) override;
|
||||
|
||||
private:
|
||||
enum { foff = 3, coff = 4 }; //frame and contents offset
|
||||
int val;
|
||||
int hue;
|
||||
int sat;
|
||||
int y2val(int y);
|
||||
int val2y(int val);
|
||||
void setVal(int v);
|
||||
QPixmap *pix;
|
||||
};
|
||||
|
||||
static const int LUMINANCE_PICKER_WIDTH = 25;
|
||||
static const int LUMINANCE_PICKER_HEIGHT = COLOR_PICKER_HEIGHT;
|
||||
|
||||
class QColSpinBox : public QSpinBox
|
||||
{
|
||||
public:
|
||||
QColSpinBox(QWidget *parent);
|
||||
|
||||
void setValue(int i);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
163
src/widgets/helper/color/AlphaSlider.cpp
Normal file
163
src/widgets/helper/color/AlphaSlider.cpp
Normal file
|
@ -0,0 +1,163 @@
|
|||
#include "widgets/helper/color/AlphaSlider.hpp"
|
||||
|
||||
#include "widgets/helper/color/Checkerboard.hpp"
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QPainterPath>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int SLIDER_WIDTH = 256;
|
||||
constexpr int SLIDER_HEIGHT = 12;
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
AlphaSlider::AlphaSlider(QColor color, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, alpha_(color.alpha())
|
||||
, color_(color)
|
||||
{
|
||||
this->setSizePolicy({QSizePolicy::Expanding, QSizePolicy::Fixed});
|
||||
}
|
||||
|
||||
void AlphaSlider::setColor(QColor color)
|
||||
{
|
||||
if (this->color_ == color)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->alpha_ = color.alpha();
|
||||
this->color_ = color;
|
||||
this->cachedPixmap_ = {};
|
||||
this->update();
|
||||
}
|
||||
|
||||
int AlphaSlider::alpha() const
|
||||
{
|
||||
return this->alpha_;
|
||||
}
|
||||
|
||||
QSize AlphaSlider::sizeHint() const
|
||||
{
|
||||
return {SLIDER_WIDTH, SLIDER_HEIGHT};
|
||||
}
|
||||
|
||||
void AlphaSlider::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
this->updatePixmap();
|
||||
}
|
||||
|
||||
int AlphaSlider::xPosToAlpha(int xPos) const
|
||||
{
|
||||
return (xPos * 255) / (this->width() - this->height());
|
||||
}
|
||||
|
||||
void AlphaSlider::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->buttons().testFlag(Qt::MouseButton::LeftButton))
|
||||
{
|
||||
this->trackingMouseEvents_ = true;
|
||||
this->updateFromEvent(event);
|
||||
this->setFocus(Qt::FocusReason::MouseFocusReason);
|
||||
}
|
||||
}
|
||||
void AlphaSlider::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (this->trackingMouseEvents_)
|
||||
{
|
||||
this->updateFromEvent(event);
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
void AlphaSlider::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (this->trackingMouseEvents_ &&
|
||||
event->buttons().testFlag(Qt::MouseButton::LeftButton))
|
||||
{
|
||||
this->updateFromEvent(event);
|
||||
this->trackingMouseEvents_ = false;
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
|
||||
void AlphaSlider::updateFromEvent(QMouseEvent *event)
|
||||
{
|
||||
int cornerRadius = this->height() / 2;
|
||||
auto clampedX = std::clamp(event->pos().x(), cornerRadius,
|
||||
this->width() - cornerRadius);
|
||||
this->setAlpha(this->xPosToAlpha(clampedX - cornerRadius));
|
||||
}
|
||||
|
||||
void AlphaSlider::updatePixmap()
|
||||
{
|
||||
this->cachedPixmap_ = QPixmap(this->size());
|
||||
this->cachedPixmap_.fill(Qt::transparent);
|
||||
QPainter painter(&this->cachedPixmap_);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
qreal cornerRadius = (qreal)this->height() / 2.0;
|
||||
|
||||
QPainterPath mask;
|
||||
mask.addRoundedRect(QRect({0, 0}, this->size()), cornerRadius,
|
||||
cornerRadius);
|
||||
painter.setClipPath(mask);
|
||||
|
||||
drawCheckerboard(painter, this->size(), this->height() / 2);
|
||||
|
||||
QLinearGradient gradient(cornerRadius, 0.0,
|
||||
(qreal)this->width() - cornerRadius, 0.0);
|
||||
QColor start = this->color_;
|
||||
QColor end = this->color_;
|
||||
start.setAlpha(0);
|
||||
end.setAlpha(255);
|
||||
|
||||
gradient.setColorAt(0.0, start);
|
||||
gradient.setColorAt(1.0, end);
|
||||
|
||||
painter.setPen({Qt::transparent, 0});
|
||||
painter.setBrush(gradient);
|
||||
painter.drawRect(QRect({0, 0}, this->size()));
|
||||
}
|
||||
|
||||
void AlphaSlider::paintEvent(QPaintEvent * /*event*/)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
if (this->cachedPixmap_.isNull())
|
||||
{
|
||||
this->updatePixmap();
|
||||
}
|
||||
|
||||
painter.drawPixmap(this->rect().topLeft(), this->cachedPixmap_);
|
||||
|
||||
int cornerRadius = this->height() / 2;
|
||||
|
||||
QPoint circ = {
|
||||
cornerRadius +
|
||||
(this->alpha() * (this->width() - 2 * cornerRadius)) / 255,
|
||||
cornerRadius};
|
||||
auto circleColor = 0;
|
||||
painter.setPen({QColor(circleColor, circleColor, circleColor), 2});
|
||||
auto opaqueBase = this->color_;
|
||||
opaqueBase.setAlpha(255);
|
||||
painter.setBrush(opaqueBase);
|
||||
painter.drawEllipse(circ, cornerRadius - 1, cornerRadius - 1);
|
||||
}
|
||||
|
||||
void AlphaSlider::setAlpha(int alpha)
|
||||
{
|
||||
if (this->alpha_ == alpha)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->alpha_ = alpha;
|
||||
this->color_.setAlpha(alpha);
|
||||
|
||||
emit this->colorChanged(this->color_);
|
||||
this->update();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
48
src/widgets/helper/color/AlphaSlider.hpp
Normal file
48
src/widgets/helper/color/AlphaSlider.hpp
Normal file
|
@ -0,0 +1,48 @@
|
|||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class AlphaSlider : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AlphaSlider(QColor color, QWidget *parent = nullptr);
|
||||
|
||||
QSize sizeHint() const override;
|
||||
|
||||
int alpha() const;
|
||||
|
||||
signals:
|
||||
void colorChanged(QColor color) const;
|
||||
|
||||
public slots:
|
||||
void setColor(QColor color);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
int alpha_ = 255;
|
||||
QColor color_;
|
||||
|
||||
QPixmap cachedPixmap_;
|
||||
|
||||
bool trackingMouseEvents_ = false;
|
||||
|
||||
void updatePixmap();
|
||||
int xPosToAlpha(int xPos) const;
|
||||
|
||||
void updateFromEvent(QMouseEvent *event);
|
||||
|
||||
void setAlpha(int alpha);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
29
src/widgets/helper/color/Checkerboard.cpp
Normal file
29
src/widgets/helper/color/Checkerboard.cpp
Normal file
|
@ -0,0 +1,29 @@
|
|||
#include "widgets/helper/color/Checkerboard.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
void drawCheckerboard(QPainter &painter, QRect rect, int tileSize)
|
||||
{
|
||||
painter.fillRect(rect, QColor(255, 255, 255));
|
||||
|
||||
if (tileSize <= 0)
|
||||
{
|
||||
tileSize = 1;
|
||||
}
|
||||
|
||||
int overflowY = rect.height() % tileSize == 0 ? 0 : 1;
|
||||
int overflowX = rect.width() % tileSize == 0 ? 0 : 1;
|
||||
for (int row = 0; row < rect.height() / tileSize + overflowY; row++)
|
||||
{
|
||||
int offsetX = row % 2 == 0 ? 0 : 1;
|
||||
for (int col = offsetX; col < rect.width() / tileSize + overflowX;
|
||||
col += 2)
|
||||
{
|
||||
painter.fillRect(rect.x() + col * tileSize,
|
||||
rect.y() + row * tileSize, tileSize, tileSize,
|
||||
QColor(204, 204, 204));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
13
src/widgets/helper/color/Checkerboard.hpp
Normal file
13
src/widgets/helper/color/Checkerboard.hpp
Normal file
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include <QPainter>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
void drawCheckerboard(QPainter &painter, QRect rect, int tileSize = 4);
|
||||
inline void drawCheckerboard(QPainter &painter, QSize size, int tileSize = 4)
|
||||
{
|
||||
drawCheckerboard(painter, {{0, 0}, size}, tileSize);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
81
src/widgets/helper/color/ColorButton.cpp
Normal file
81
src/widgets/helper/color/ColorButton.cpp
Normal file
|
@ -0,0 +1,81 @@
|
|||
#include "widgets/helper/color/ColorButton.hpp"
|
||||
|
||||
#include "widgets/helper/color/Checkerboard.hpp"
|
||||
|
||||
#include <QPainterPath>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
ColorButton::ColorButton(QColor color, QWidget *parent)
|
||||
: QAbstractButton(parent)
|
||||
, currentColor_(color)
|
||||
{
|
||||
this->setSizePolicy({QSizePolicy::Expanding, QSizePolicy::Expanding});
|
||||
this->setMinimumSize({30, 30});
|
||||
}
|
||||
|
||||
QSize ColorButton::sizeHint() const
|
||||
{
|
||||
return {50, 30};
|
||||
}
|
||||
|
||||
void ColorButton::setColor(const QColor &color)
|
||||
{
|
||||
if (this->currentColor_ == color)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->currentColor_ = color;
|
||||
this->update();
|
||||
}
|
||||
|
||||
QColor ColorButton::color() const
|
||||
{
|
||||
return this->currentColor_;
|
||||
}
|
||||
|
||||
void ColorButton::resizeEvent(QResizeEvent * /*event*/)
|
||||
{
|
||||
this->checkerboardCacheValid_ = false;
|
||||
this->repaint();
|
||||
}
|
||||
|
||||
void ColorButton::paintEvent(QPaintEvent * /*event*/)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
auto rect = this->rect();
|
||||
|
||||
if (this->currentColor_.alpha() != 255)
|
||||
{
|
||||
if (!this->checkerboardCacheValid_)
|
||||
{
|
||||
QPixmap cache(this->size());
|
||||
cache.fill(Qt::transparent);
|
||||
|
||||
QPainter cachePainter(&cache);
|
||||
cachePainter.setRenderHint(QPainter::Antialiasing);
|
||||
QPainterPath path;
|
||||
path.addRoundedRect(QRect(1, 1, this->size().width() - 2,
|
||||
this->size().height() - 2),
|
||||
5, 5);
|
||||
cachePainter.setClipPath(path);
|
||||
|
||||
drawCheckerboard(cachePainter, this->size(),
|
||||
std::min(this->height() / 2, 10));
|
||||
cachePainter.end();
|
||||
|
||||
this->checkerboardCache_ = std::move(cache);
|
||||
this->checkerboardCacheValid_ = true;
|
||||
}
|
||||
painter.drawPixmap(rect.topLeft(), this->checkerboardCache_);
|
||||
}
|
||||
painter.setBrush(this->currentColor_);
|
||||
painter.setPen({QColor(255, 255, 255, 127), 1});
|
||||
painter.drawRoundedRect(rect.x() + 1, rect.y() + 1, rect.width() - 2,
|
||||
rect.height() - 2, 5, 5);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
33
src/widgets/helper/color/ColorButton.hpp
Normal file
33
src/widgets/helper/color/ColorButton.hpp
Normal file
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include <QAbstractButton>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class ColorButton : public QAbstractButton
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ColorButton(QColor color, QWidget *parent = nullptr);
|
||||
|
||||
QSize sizeHint() const override;
|
||||
|
||||
QColor color() const;
|
||||
|
||||
// NOLINTNEXTLINE(readability-redundant-access-specifiers)
|
||||
public slots:
|
||||
void setColor(const QColor &color);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
|
||||
private:
|
||||
QColor currentColor_;
|
||||
|
||||
QPixmap checkerboardCache_;
|
||||
bool checkerboardCacheValid_ = false;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
168
src/widgets/helper/color/ColorInput.cpp
Normal file
168
src/widgets/helper/color/ColorInput.cpp
Normal file
|
@ -0,0 +1,168 @@
|
|||
#include "widgets/helper/color/ColorInput.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// from qtools_p.h
|
||||
int fromHex(char c) noexcept
|
||||
{
|
||||
if (c >= '0' && c <= '9')
|
||||
{
|
||||
return int(c - '0');
|
||||
}
|
||||
if (c >= 'A' && c <= 'F')
|
||||
{
|
||||
return int(c - 'A' + 10);
|
||||
}
|
||||
if (c >= 'a' && c <= 'f')
|
||||
{
|
||||
return int(c - 'a' + 10);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
QColor parseHexColor(const QString &text)
|
||||
{
|
||||
if (text.length() == 5) // #rgba
|
||||
{
|
||||
auto alphaHex = fromHex(text[4].toLatin1());
|
||||
QStringView v(text);
|
||||
v.chop(1);
|
||||
QColor col(v);
|
||||
col.setAlpha(alphaHex);
|
||||
return col;
|
||||
}
|
||||
QColor col(text);
|
||||
if (col.isValid() && text.length() == 9) // #rrggbbaa
|
||||
{
|
||||
auto rgba = col.rgba();
|
||||
auto alpha = rgba & 0xff;
|
||||
QColor actual(rgba >> 8);
|
||||
actual.setAlpha((int)alpha);
|
||||
return actual;
|
||||
}
|
||||
return col;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
ColorInput::ColorInput(QColor color, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, currentColor_(color)
|
||||
, hexValidator_(QRegularExpression(
|
||||
R"(^#([A-Fa-f\d]{3,4}|[A-Fa-f\d]{6}|[A-Fa-f\d]{8})$)"))
|
||||
, layout_(this)
|
||||
{
|
||||
int row = 0;
|
||||
const auto initComponent = [&](Component &component, auto label,
|
||||
auto applyToColor) {
|
||||
component.lbl.setText(label);
|
||||
component.box.setRange(0, 255);
|
||||
QObject::connect(&component.box,
|
||||
qOverload<int>(&QSpinBox::valueChanged), this,
|
||||
[this, &component, applyToColor](int value) {
|
||||
if (component.value == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
applyToColor(this->currentColor_, value);
|
||||
|
||||
this->emitUpdate();
|
||||
});
|
||||
this->layout_.addWidget(&component.lbl, row, 0);
|
||||
this->layout_.addWidget(&component.box, row, 1);
|
||||
row++;
|
||||
};
|
||||
|
||||
initComponent(this->red_, "Red:", [](auto &color, int value) {
|
||||
color.setRed(value);
|
||||
});
|
||||
initComponent(this->green_, "Green:", [](auto &color, int value) {
|
||||
color.setGreen(value);
|
||||
});
|
||||
initComponent(this->blue_, "Red:", [](auto &color, int value) {
|
||||
color.setBlue(value);
|
||||
});
|
||||
initComponent(this->alpha_, "Alpha:", [](auto &color, int value) {
|
||||
color.setAlpha(value);
|
||||
});
|
||||
|
||||
this->hexLabel_.setText("Hex:");
|
||||
this->hexInput_.setValidator(&this->hexValidator_);
|
||||
QObject::connect(&this->hexInput_, &QLineEdit::editingFinished, [this]() {
|
||||
auto css = parseHexColor(this->hexInput_.text());
|
||||
if (!css.isValid() || this->currentColor_ == css)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->currentColor_ = css;
|
||||
this->emitUpdate();
|
||||
});
|
||||
this->layout_.addWidget(&this->hexLabel_, row, 0);
|
||||
this->layout_.addWidget(&this->hexInput_, row, 1);
|
||||
|
||||
this->updateComponents();
|
||||
}
|
||||
|
||||
void ColorInput::updateComponents()
|
||||
{
|
||||
auto color = this->currentColor_.toRgb();
|
||||
const auto updateComponent = [](Component &component, auto getValue) {
|
||||
int value = getValue();
|
||||
if (component.value != value)
|
||||
{
|
||||
component.value = value;
|
||||
component.box.setValue(value);
|
||||
}
|
||||
};
|
||||
updateComponent(this->red_, [&]() {
|
||||
return color.red();
|
||||
});
|
||||
updateComponent(this->green_, [&]() {
|
||||
return color.green();
|
||||
});
|
||||
updateComponent(this->blue_, [&]() {
|
||||
return color.blue();
|
||||
});
|
||||
updateComponent(this->alpha_, [&]() {
|
||||
return color.alpha();
|
||||
});
|
||||
|
||||
this->updateHex();
|
||||
}
|
||||
|
||||
void ColorInput::updateHex()
|
||||
{
|
||||
auto rgb = this->currentColor_.rgb();
|
||||
rgb <<= 8;
|
||||
rgb |= this->currentColor_.alpha();
|
||||
// we always need to update the CSS color
|
||||
this->hexInput_.setText(QStringLiteral("#%1").arg(rgb, 8, 16, QChar(u'0')));
|
||||
}
|
||||
|
||||
QColor ColorInput::color() const
|
||||
{
|
||||
return this->currentColor_;
|
||||
}
|
||||
|
||||
void ColorInput::setColor(QColor color)
|
||||
{
|
||||
if (this->currentColor_ == color)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->currentColor_ = color;
|
||||
this->updateComponents();
|
||||
// no emit, as we just got the updated color
|
||||
}
|
||||
|
||||
void ColorInput::emitUpdate()
|
||||
{
|
||||
this->updateComponents();
|
||||
// our components triggered this update, emit the new color
|
||||
emit this->colorChanged(this->currentColor_);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
52
src/widgets/helper/color/ColorInput.hpp
Normal file
52
src/widgets/helper/color/ColorInput.hpp
Normal file
|
@ -0,0 +1,52 @@
|
|||
#pragma once
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QSpinBox>
|
||||
#include <QWidget>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class ColorInput : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ColorInput(QColor color, QWidget *parent = nullptr);
|
||||
|
||||
QColor color() const;
|
||||
|
||||
signals:
|
||||
void colorChanged(QColor color);
|
||||
|
||||
public slots:
|
||||
void setColor(QColor color);
|
||||
|
||||
private:
|
||||
QColor currentColor_;
|
||||
|
||||
struct Component {
|
||||
QLabel lbl;
|
||||
QSpinBox box;
|
||||
int value = -1;
|
||||
};
|
||||
|
||||
Component red_;
|
||||
Component green_;
|
||||
Component blue_;
|
||||
Component alpha_;
|
||||
|
||||
QLabel hexLabel_;
|
||||
QLineEdit hexInput_;
|
||||
QRegularExpressionValidator hexValidator_;
|
||||
|
||||
QGridLayout layout_;
|
||||
|
||||
void updateComponents();
|
||||
void updateHex();
|
||||
|
||||
void emitUpdate();
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
35
src/widgets/helper/color/ColorItemDelegate.cpp
Normal file
35
src/widgets/helper/color/ColorItemDelegate.cpp
Normal file
|
@ -0,0 +1,35 @@
|
|||
#include "widgets/helper/color/ColorItemDelegate.hpp"
|
||||
|
||||
#include "widgets/helper/color/Checkerboard.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
ColorItemDelegate::ColorItemDelegate(QObject *parent)
|
||||
: QStyledItemDelegate(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void ColorItemDelegate::paint(QPainter *painter,
|
||||
const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const
|
||||
{
|
||||
auto data = index.data(Qt::DecorationRole);
|
||||
|
||||
if (data.type() != QVariant::Color)
|
||||
{
|
||||
return QStyledItemDelegate::paint(painter, option, index);
|
||||
}
|
||||
auto color = data.value<QColor>();
|
||||
|
||||
painter->save();
|
||||
if (color.alpha() != 255)
|
||||
{
|
||||
drawCheckerboard(*painter, option.rect,
|
||||
std::min(option.rect.height() / 2, 10));
|
||||
}
|
||||
painter->setBrush(color);
|
||||
painter->drawRect(option.rect);
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
16
src/widgets/helper/color/ColorItemDelegate.hpp
Normal file
16
src/widgets/helper/color/ColorItemDelegate.hpp
Normal file
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class ColorItemDelegate : public QStyledItemDelegate
|
||||
{
|
||||
public:
|
||||
explicit ColorItemDelegate(QObject *parent = nullptr);
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const override;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
165
src/widgets/helper/color/HueSlider.cpp
Normal file
165
src/widgets/helper/color/HueSlider.cpp
Normal file
|
@ -0,0 +1,165 @@
|
|||
#include "widgets/helper/color/HueSlider.hpp"
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int SLIDER_WIDTH = 256;
|
||||
constexpr int SLIDER_HEIGHT = 12;
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
HueSlider::HueSlider(QColor color, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
this->setColor(color);
|
||||
this->setSizePolicy({QSizePolicy::Expanding, QSizePolicy::Fixed});
|
||||
}
|
||||
|
||||
void HueSlider::setColor(QColor color)
|
||||
{
|
||||
if (this->color_ == color)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->color_ = color.toHsv();
|
||||
|
||||
auto hue = std::max(this->color_.hue(), 0);
|
||||
if (this->hue_ == hue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this->hue_ = hue;
|
||||
this->update();
|
||||
}
|
||||
|
||||
int HueSlider::hue() const
|
||||
{
|
||||
return this->hue_;
|
||||
}
|
||||
|
||||
QSize HueSlider::sizeHint() const
|
||||
{
|
||||
return {SLIDER_WIDTH, SLIDER_HEIGHT};
|
||||
}
|
||||
|
||||
void HueSlider::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
this->updatePixmap();
|
||||
}
|
||||
|
||||
int HueSlider::xPosToHue(int xPos) const
|
||||
{
|
||||
return (xPos * 359) / (this->width() - this->height());
|
||||
}
|
||||
|
||||
void HueSlider::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->buttons().testFlag(Qt::MouseButton::LeftButton))
|
||||
{
|
||||
this->trackingMouseEvents_ = true;
|
||||
this->updateFromEvent(event);
|
||||
event->accept();
|
||||
this->setFocus(Qt::FocusReason::MouseFocusReason);
|
||||
}
|
||||
}
|
||||
void HueSlider::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (this->trackingMouseEvents_)
|
||||
{
|
||||
this->updateFromEvent(event);
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
void HueSlider::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (this->trackingMouseEvents_ &&
|
||||
event->buttons().testFlag(Qt::MouseButton::LeftButton))
|
||||
{
|
||||
this->updateFromEvent(event);
|
||||
this->trackingMouseEvents_ = false;
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
|
||||
void HueSlider::updateFromEvent(QMouseEvent *event)
|
||||
{
|
||||
int cornerRadius = this->height() / 2;
|
||||
auto clampedX = std::clamp(event->pos().x(), cornerRadius,
|
||||
this->width() - cornerRadius);
|
||||
this->setHue(this->xPosToHue(clampedX - cornerRadius));
|
||||
}
|
||||
|
||||
void HueSlider::updatePixmap()
|
||||
{
|
||||
constexpr int nStops = 10;
|
||||
constexpr auto nStopsF = (qreal)nStops;
|
||||
|
||||
this->gradientPixmap_ = QPixmap(this->size());
|
||||
this->gradientPixmap_.fill(Qt::transparent);
|
||||
QPainter painter(&this->gradientPixmap_);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
qreal cornerRadius = (qreal)this->height() / 2.0;
|
||||
|
||||
QLinearGradient gradient(cornerRadius, 0.0,
|
||||
(qreal)this->width() - cornerRadius, 0.0);
|
||||
for (int i = 0; i <= nStops; i++)
|
||||
{
|
||||
gradient.setColorAt(
|
||||
(qreal)i / nStopsF,
|
||||
QColor::fromHsv(std::min((i * 360) / nStops, 359), 255, 255));
|
||||
}
|
||||
painter.setPen({Qt::transparent, 0});
|
||||
painter.setBrush(gradient);
|
||||
painter.drawRoundedRect(QRect({0, 0}, this->size()), cornerRadius,
|
||||
cornerRadius);
|
||||
}
|
||||
|
||||
void HueSlider::paintEvent(QPaintEvent * /*event*/)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
if (this->gradientPixmap_.isNull())
|
||||
{
|
||||
this->updatePixmap();
|
||||
}
|
||||
|
||||
painter.drawPixmap(this->rect().topLeft(), this->gradientPixmap_);
|
||||
|
||||
int cornerRadius = this->height() / 2;
|
||||
|
||||
QPoint circ = {
|
||||
cornerRadius + (this->hue() * (this->width() - 2 * cornerRadius)) / 360,
|
||||
cornerRadius};
|
||||
auto circleColor = 0;
|
||||
painter.setPen({QColor(circleColor, circleColor, circleColor), 2});
|
||||
painter.setBrush(QColor::fromHsv(this->hue(), 255, 255));
|
||||
painter.drawEllipse(circ, cornerRadius - 1, cornerRadius - 1);
|
||||
}
|
||||
|
||||
void HueSlider::setHue(int hue)
|
||||
{
|
||||
if (this->hue_ == hue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->hue_ = hue;
|
||||
// ugh
|
||||
int h{};
|
||||
int s{};
|
||||
int v{};
|
||||
int a{};
|
||||
this->color_.getHsv(&h, &s, &v, &a);
|
||||
this->color_.setHsv(this->hue_, s, v, a);
|
||||
|
||||
emit this->colorChanged(this->color_);
|
||||
this->update();
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
48
src/widgets/helper/color/HueSlider.hpp
Normal file
48
src/widgets/helper/color/HueSlider.hpp
Normal file
|
@ -0,0 +1,48 @@
|
|||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class HueSlider : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
HueSlider(QColor color, QWidget *parent = nullptr);
|
||||
|
||||
QSize sizeHint() const override;
|
||||
|
||||
int hue() const;
|
||||
|
||||
signals:
|
||||
void colorChanged(QColor color) const;
|
||||
|
||||
public slots:
|
||||
void setColor(QColor color);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
int hue_ = 0;
|
||||
QColor color_;
|
||||
|
||||
QPixmap gradientPixmap_;
|
||||
|
||||
bool trackingMouseEvents_ = false;
|
||||
|
||||
void updatePixmap();
|
||||
int xPosToHue(int xPos) const;
|
||||
|
||||
void updateFromEvent(QMouseEvent *event);
|
||||
|
||||
void setHue(int hue);
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
192
src/widgets/helper/color/SBCanvas.cpp
Normal file
192
src/widgets/helper/color/SBCanvas.cpp
Normal file
|
@ -0,0 +1,192 @@
|
|||
#include "widgets/helper/color/SBCanvas.hpp"
|
||||
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int PICKER_WIDTH = 256;
|
||||
constexpr int PICKER_HEIGHT = 256;
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
SBCanvas::SBCanvas(QColor color, QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
this->setColor(color);
|
||||
this->setSizePolicy({QSizePolicy::Fixed, QSizePolicy::Fixed});
|
||||
}
|
||||
|
||||
void SBCanvas::setColor(QColor color)
|
||||
{
|
||||
color = color.toHsv();
|
||||
if (this->color_ == color)
|
||||
{
|
||||
return;
|
||||
}
|
||||
this->color_ = color;
|
||||
|
||||
int h{};
|
||||
int s{};
|
||||
int v{};
|
||||
color.getHsv(&h, &s, &v);
|
||||
h = std::max(h, 0);
|
||||
|
||||
if (this->hue_ == h && this->saturation_ == s && this->brightness_ == v)
|
||||
{
|
||||
return; // alpha changed
|
||||
}
|
||||
this->hue_ = h;
|
||||
this->saturation_ = s;
|
||||
this->brightness_ = v;
|
||||
|
||||
this->gradientPixmap_ = {};
|
||||
this->update();
|
||||
}
|
||||
|
||||
int SBCanvas::saturation() const
|
||||
{
|
||||
return this->saturation_;
|
||||
}
|
||||
|
||||
int SBCanvas::brightness() const
|
||||
{
|
||||
return this->brightness_;
|
||||
}
|
||||
|
||||
QSize SBCanvas::sizeHint() const
|
||||
{
|
||||
return {PICKER_WIDTH, PICKER_HEIGHT};
|
||||
}
|
||||
|
||||
void SBCanvas::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
this->updatePixmap();
|
||||
}
|
||||
|
||||
int SBCanvas::xPosToSaturation(int xPos) const
|
||||
{
|
||||
return (xPos * 255) / this->width();
|
||||
}
|
||||
|
||||
int SBCanvas::yPosToBrightness(int yPos) const
|
||||
{
|
||||
return 255 - (yPos * 255) / this->height();
|
||||
}
|
||||
|
||||
void SBCanvas::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->buttons().testFlag(Qt::MouseButton::LeftButton))
|
||||
{
|
||||
this->trackingMouseEvents_ = true;
|
||||
this->updateFromEvent(event);
|
||||
event->accept();
|
||||
this->setFocus(Qt::FocusReason::MouseFocusReason);
|
||||
}
|
||||
}
|
||||
void SBCanvas::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (this->trackingMouseEvents_)
|
||||
{
|
||||
this->updateFromEvent(event);
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
void SBCanvas::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (this->trackingMouseEvents_ &&
|
||||
event->buttons().testFlag(Qt::MouseButton::LeftButton))
|
||||
{
|
||||
this->updateFromEvent(event);
|
||||
this->trackingMouseEvents_ = false;
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
|
||||
void SBCanvas::updateFromEvent(QMouseEvent *event)
|
||||
{
|
||||
auto clampedX = std::clamp(event->pos().x(), 0, this->width());
|
||||
auto clampedY = std::clamp(event->pos().y(), 0, this->height());
|
||||
|
||||
bool updated = this->setSaturation(this->xPosToSaturation(clampedX));
|
||||
updated |= this->setBrightness(this->yPosToBrightness(clampedY));
|
||||
|
||||
if (updated)
|
||||
{
|
||||
this->emitUpdatedColor();
|
||||
this->update();
|
||||
}
|
||||
}
|
||||
|
||||
void SBCanvas::updatePixmap()
|
||||
{
|
||||
int w = this->width();
|
||||
int h = this->height();
|
||||
QImage img(w, h, QImage::Format_RGB32);
|
||||
uint *pixel = (uint *)img.scanLine(0);
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
QColor c = QColor::fromHsv(this->hue_, this->xPosToSaturation(x),
|
||||
this->yPosToBrightness(y));
|
||||
*pixel = c.rgb();
|
||||
pixel++;
|
||||
}
|
||||
}
|
||||
this->gradientPixmap_ = QPixmap::fromImage(img);
|
||||
}
|
||||
|
||||
void SBCanvas::paintEvent(QPaintEvent * /*event*/)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
if (this->gradientPixmap_.isNull())
|
||||
{
|
||||
this->updatePixmap();
|
||||
}
|
||||
|
||||
painter.drawPixmap(this->rect().topLeft(), this->gradientPixmap_);
|
||||
|
||||
QPoint circ = {(this->saturation() * this->width()) / 256,
|
||||
((255 - this->brightness()) * this->height()) / 256};
|
||||
auto circleColor = this->brightness() >= 128 ? 50 : 200;
|
||||
painter.setPen({QColor(circleColor, circleColor, circleColor), 2});
|
||||
painter.setBrush(
|
||||
QColor::fromHsv(this->hue_, this->saturation_, this->brightness_));
|
||||
painter.drawEllipse(circ, 5, 5);
|
||||
}
|
||||
|
||||
bool SBCanvas::setSaturation(int saturation)
|
||||
{
|
||||
if (this->saturation_ == saturation)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this->saturation_ = saturation;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SBCanvas::setBrightness(int brightness)
|
||||
{
|
||||
if (this->brightness_ == brightness)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this->brightness_ = brightness;
|
||||
return true;
|
||||
}
|
||||
|
||||
void SBCanvas::emitUpdatedColor()
|
||||
{
|
||||
this->color_.setHsv(this->hue_, this->saturation_, this->brightness_,
|
||||
this->color_.alpha());
|
||||
emit this->colorChanged(this->color_);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
56
src/widgets/helper/color/SBCanvas.hpp
Normal file
56
src/widgets/helper/color/SBCanvas.hpp
Normal file
|
@ -0,0 +1,56 @@
|
|||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
/// 2D canvas for saturation (x-axis) and brightness (y-axis)
|
||||
class SBCanvas : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SBCanvas(QColor color, QWidget *parent = nullptr);
|
||||
|
||||
QSize sizeHint() const override;
|
||||
|
||||
int saturation() const;
|
||||
int brightness() const;
|
||||
|
||||
signals:
|
||||
void colorChanged(QColor color) const;
|
||||
|
||||
public slots:
|
||||
void setColor(QColor color);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mouseReleaseEvent(QMouseEvent *event) override;
|
||||
|
||||
private:
|
||||
int hue_ = 0;
|
||||
int saturation_ = 0;
|
||||
int brightness_ = 0;
|
||||
QColor color_;
|
||||
|
||||
QPixmap gradientPixmap_;
|
||||
|
||||
bool trackingMouseEvents_ = false;
|
||||
|
||||
void updatePixmap();
|
||||
int xPosToSaturation(int xPos) const;
|
||||
int yPosToBrightness(int yPos) const;
|
||||
|
||||
void updateFromEvent(QMouseEvent *event);
|
||||
|
||||
[[nodiscard]] bool setSaturation(int saturation);
|
||||
[[nodiscard]] bool setBrightness(int brightness);
|
||||
|
||||
void emitUpdatedColor();
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -4,7 +4,7 @@
|
|||
#include "util/LayoutHelper.hpp"
|
||||
#include "util/RapidJsonSerializeQString.hpp"
|
||||
#include "widgets/dialogs/ColorPickerDialog.hpp"
|
||||
#include "widgets/helper/ColorButton.hpp"
|
||||
#include "widgets/helper/color/ColorButton.hpp"
|
||||
#include "widgets/helper/Line.hpp"
|
||||
|
||||
#include <QRegularExpression>
|
||||
|
@ -214,20 +214,18 @@ ColorButton *GeneralPageView::addColorButton(
|
|||
|
||||
QObject::connect(
|
||||
colorButton, &ColorButton::clicked, [this, &setting, colorButton]() {
|
||||
auto dialog = new ColorPickerDialog(QColor(setting), this);
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->show();
|
||||
// We can safely ignore this signal connection, for now, since the
|
||||
auto *dialog = new ColorPickerDialog(QColor(setting), this);
|
||||
// colorButton & setting are never deleted and the signal is deleted
|
||||
// once the dialog is closed
|
||||
std::ignore = dialog->closed.connect(
|
||||
[&setting, colorButton](QColor selected) {
|
||||
if (selected.isValid())
|
||||
{
|
||||
setting = selected.name(QColor::HexArgb);
|
||||
colorButton->setColor(selected);
|
||||
}
|
||||
});
|
||||
QObject::connect(dialog, &ColorPickerDialog::colorConfirmed, this,
|
||||
[&setting, colorButton](auto selected) {
|
||||
if (selected.isValid())
|
||||
{
|
||||
setting = selected.name(QColor::HexArgb);
|
||||
colorButton->setColor(selected);
|
||||
}
|
||||
});
|
||||
dialog->show();
|
||||
});
|
||||
|
||||
this->groups_.back().widgets.push_back({label, {text}});
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
#include "util/LayoutCreator.hpp"
|
||||
#include "widgets/dialogs/BadgePickerDialog.hpp"
|
||||
#include "widgets/dialogs/ColorPickerDialog.hpp"
|
||||
#include "widgets/helper/color/ColorItemDelegate.hpp"
|
||||
#include "widgets/helper/EditableModelView.hpp"
|
||||
|
||||
#include <QFileDialog>
|
||||
|
@ -82,6 +83,8 @@ HighlightingPage::HighlightingPage()
|
|||
QHeaderView::Fixed);
|
||||
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||
0, QHeaderView::Stretch);
|
||||
view->getTableView()->setItemDelegateForColumn(
|
||||
HighlightModel::Column::Color, new ColorItemDelegate(view));
|
||||
|
||||
// fourtf: make class extrend BaseWidget and add this to
|
||||
// dpiChanged
|
||||
|
@ -134,6 +137,9 @@ HighlightingPage::HighlightingPage()
|
|||
QHeaderView::Fixed);
|
||||
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||
0, QHeaderView::Stretch);
|
||||
view->getTableView()->setItemDelegateForColumn(
|
||||
UserHighlightModel::Column::Color,
|
||||
new ColorItemDelegate(view));
|
||||
|
||||
// fourtf: make class extrend BaseWidget and add this to
|
||||
// dpiChanged
|
||||
|
@ -176,6 +182,9 @@ HighlightingPage::HighlightingPage()
|
|||
QHeaderView::Fixed);
|
||||
view->getTableView()->horizontalHeader()->setSectionResizeMode(
|
||||
0, QHeaderView::Stretch);
|
||||
view->getTableView()->setItemDelegateForColumn(
|
||||
BadgeHighlightModel::Column::Color,
|
||||
new ColorItemDelegate(view));
|
||||
|
||||
// fourtf: make class extrend BaseWidget and add this to
|
||||
// dpiChanged
|
||||
|
@ -330,18 +339,18 @@ void HighlightingPage::openColorDialog(const QModelIndex &clicked,
|
|||
auto initial =
|
||||
view->getModel()->data(clicked, Qt::DecorationRole).value<QColor>();
|
||||
|
||||
auto dialog = new ColorPickerDialog(initial, this);
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->show();
|
||||
// We can safely ignore this signal connection since the view and tab are never deleted
|
||||
auto *dialog = new ColorPickerDialog(initial, this);
|
||||
// TODO: The QModelIndex clicked is technically not safe to persist here since the model
|
||||
// can be changed between the color dialog being created & the color dialog being closed
|
||||
std::ignore = dialog->closed.connect([=](auto selected) {
|
||||
if (selected.isValid())
|
||||
{
|
||||
view->getModel()->setData(clicked, selected, Qt::DecorationRole);
|
||||
}
|
||||
});
|
||||
QObject::connect(dialog, &ColorPickerDialog::colorConfirmed, this,
|
||||
[=](auto selected) {
|
||||
if (selected.isValid())
|
||||
{
|
||||
view->getModel()->setData(clicked, selected,
|
||||
Qt::DecorationRole);
|
||||
}
|
||||
});
|
||||
dialog->show();
|
||||
}
|
||||
|
||||
void HighlightingPage::tableCellClicked(const QModelIndex &clicked,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue