Merge branch 'master' into git_is_pepega

This commit is contained in:
Mm2PL 2020-02-08 16:42:47 +01:00 committed by GitHub
commit d7a7df7427
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 2424 additions and 5215 deletions

View file

@ -30,12 +30,25 @@ jobs:
modules: qtwebengine
# WINDOWS
- name: Cache conan
if: startsWith(matrix.os, 'windows')
uses: actions/cache@v1
with:
key: ${{ runner.os }}-conan-root-${{ hashFiles('**/conanfile.txt') }}
path: ~/.conan
- name: Cache conan packages
if: startsWith(matrix.os, 'windows')
uses: actions/cache@v1
with:
key: ${{ runner.os }}-conan-pkg-${{ hashFiles('**/conanfile.txt') }}
path: C:/.conan/
- name: Install dependencies (Windows)
if: startsWith(matrix.os, 'windows')
run: |
REM We use this source (temporarily?) until choco has updated their version of conan
choco source add -n=AFG -s="https://api.bintray.com/nuget/anotherfoxguy/choco-pkg"
choco install conan -y
choco install conan -y -s="https://api.bintray.com/nuget/anotherfoxguy/choco-pkg"
refreshenv
shell: cmd

View file

@ -32,6 +32,7 @@ matrix:
- git config --global user.email "builds@travis-ci.com"
- git config --global user.name "Travis CI"
- export GIT_TAG=nightly-build
- export TRAVIS_TAG=nightly-build
- git tag $GIT_TAG -f
- sh ./.CI/CreateAppImage.sh
@ -83,6 +84,7 @@ matrix:
- git config --global user.email "builds@travis-ci.com"
- git config --global user.name "Travis CI"
- export GIT_TAG=nightly-build
- export TRAVIS_TAG=nightly-build
- git tag $GIT_TAG -f
deploy:

View file

@ -36,7 +36,7 @@ git submodule update --init --recursive
The code is formatted using clang format in Qt Creator. [.clang-format](src/.clang-format) contains the style file for clang format.
### Get it automated with QT Creator + Beautifier + Clang Format
1. Download LLVM: http://releases.llvm.org/6.0.1/LLVM-6.0.1-win64.exe
1. Download LLVM: https://releases.llvm.org/9.0.0/LLVM-9.0.0-win64.exe
2. During the installation, make sure to add it to your path
3. In QT Creator, select `Help` > `About Plugins` > `C++` > `Beautifier` to enable the plugin
4. Restart QT Creator

View file

@ -1,7 +1,4 @@
# Exposed build flags:
# from lib/fmt.pri
# - FMT_PREFIX ($$PWD by default)
# - FMT_SYSTEM (1 = true) (Linux only, uses pkg-config)
# from lib/websocketpp.pri
# - WEBSOCKETPP_PREFIX ($$PWD by default)
# - WEBSOCKETPP_SYSTEM (1 = true) (unix only)
@ -76,7 +73,6 @@ CONFIG(debug, debug|release) {
# Submodules
include(lib/warnings.pri)
include(lib/fmt.pri)
include(lib/humanize.pri)
include(lib/libcommuni.pri)
include(lib/websocketpp.pri)
@ -137,6 +133,7 @@ SOURCES += \
src/controllers/highlights/HighlightBlacklistModel.cpp \
src/controllers/highlights/HighlightController.cpp \
src/controllers/highlights/HighlightModel.cpp \
src/controllers/highlights/HighlightPhrase.cpp \
src/controllers/highlights/UserHighlightModel.cpp \
src/controllers/ignores/IgnoreController.cpp \
src/controllers/ignores/IgnoreModel.cpp \
@ -170,6 +167,7 @@ SOURCES += \
src/providers/bttv/BttvEmotes.cpp \
src/providers/bttv/LoadBttvChannelEmote.cpp \
src/providers/chatterino/ChatterinoBadges.cpp \
src/providers/colors/ColorProvider.cpp \
src/providers/emoji/Emojis.cpp \
src/providers/ffz/FfzEmotes.cpp \
src/providers/irc/AbstractIrcServer.cpp \
@ -214,6 +212,7 @@ SOURCES += \
src/singletons/TooltipPreviewImage.cpp \
src/singletons/Updates.cpp \
src/singletons/WindowManager.cpp \
src/util/Clipboard.cpp \
src/util/DebugCount.cpp \
src/util/FormatTime.cpp \
src/util/FunctionEventFilter.cpp \
@ -232,6 +231,7 @@ SOURCES += \
src/widgets/BasePopup.cpp \
src/widgets/BaseWidget.cpp \
src/widgets/BaseWindow.cpp \
src/widgets/dialogs/ColorPickerDialog.cpp \
src/widgets/dialogs/EmotePopup.cpp \
src/widgets/dialogs/IrcConnectionEditor.cpp \
src/widgets/dialogs/LastRunCrashDialog.cpp \
@ -247,12 +247,14 @@ SOURCES += \
src/widgets/dialogs/WelcomeDialog.cpp \
src/widgets/helper/Button.cpp \
src/widgets/helper/ChannelView.cpp \
src/widgets/helper/ColorButton.cpp \
src/widgets/helper/ComboBoxItemDelegate.cpp \
src/widgets/helper/DebugPopup.cpp \
src/widgets/helper/EditableModelView.cpp \
src/widgets/helper/EffectLabel.cpp \
src/widgets/helper/NotebookButton.cpp \
src/widgets/helper/NotebookTab.cpp \
src/widgets/helper/QColorPicker.cpp \
src/widgets/helper/ResizingTextEdit.cpp \
src/widgets/helper/ScrollbarHighlight.cpp \
src/widgets/helper/SearchPopup.cpp \
@ -345,7 +347,6 @@ HEADERS += \
src/controllers/taggedusers/TaggedUsersModel.hpp \
src/debug/AssertInGuiThread.hpp \
src/debug/Benchmark.hpp \
src/debug/Log.hpp \
src/ForwardDecl.hpp \
src/messages/Emote.hpp \
src/messages/Image.hpp \
@ -371,6 +372,7 @@ HEADERS += \
src/providers/bttv/BttvEmotes.hpp \
src/providers/bttv/LoadBttvChannelEmote.hpp \
src/providers/chatterino/ChatterinoBadges.hpp \
src/providers/colors/ColorProvider.hpp \
src/providers/emoji/Emojis.hpp \
src/providers/ffz/FfzEmotes.hpp \
src/providers/irc/AbstractIrcServer.hpp \
@ -418,6 +420,7 @@ HEADERS += \
src/singletons/Updates.hpp \
src/singletons/WindowManager.hpp \
src/util/Clamp.hpp \
src/util/Clipboard.hpp \
src/util/CombinePath.hpp \
src/util/ConcurrentMap.hpp \
src/util/DebugCount.hpp \
@ -455,6 +458,7 @@ HEADERS += \
src/widgets/BasePopup.hpp \
src/widgets/BaseWidget.hpp \
src/widgets/BaseWindow.hpp \
src/widgets/dialogs/ColorPickerDialog.hpp \
src/widgets/dialogs/EmotePopup.hpp \
src/widgets/dialogs/IrcConnectionEditor.hpp \
src/widgets/dialogs/LastRunCrashDialog.hpp \
@ -470,6 +474,7 @@ HEADERS += \
src/widgets/dialogs/WelcomeDialog.hpp \
src/widgets/helper/Button.hpp \
src/widgets/helper/ChannelView.hpp \
src/widgets/helper/ColorButton.hpp \
src/widgets/helper/ComboBoxItemDelegate.hpp \
src/widgets/helper/CommonTexts.hpp \
src/widgets/helper/DebugPopup.hpp \
@ -478,6 +483,7 @@ HEADERS += \
src/widgets/helper/Line.hpp \
src/widgets/helper/NotebookButton.hpp \
src/widgets/helper/NotebookTab.hpp \
src/widgets/helper/QColorPicker.hpp \
src/widgets/helper/ResizingTextEdit.hpp \
src/widgets/helper/ScrollbarHighlight.hpp \
src/widgets/helper/SearchPopup.hpp \

View file

@ -1,18 +0,0 @@
# fmt
# Chatterino2 is tested with FMT 4.0
# Exposed build flags:
# - FMT_PREFIX ($$PWD by default)
# - FMT_SYSTEM (1 = true) (Linux only, uses pkg-config)
!defined(FMT_PREFIX) {
FMT_PREFIX = $$PWD
}
linux:equals(FMT_SYSTEM, "1") {
message("Building with system FMT")
PKGCONFIG += fmt
} else {
SOURCES += $$FMT_PREFIX/fmt/fmt/format.cpp
INCLUDEPATH += $$PWD/fmt/
}

View file

@ -1,535 +0,0 @@
/*
Formatting library for C++
Copyright (c) 2012 - 2016, Victor Zverovich
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "format.h"
#include <string.h>
#include <cctype>
#include <cerrno>
#include <climits>
#include <cmath>
#include <cstdarg>
#include <cstddef> // for std::ptrdiff_t
#if defined(_WIN32) && defined(__MINGW32__)
# include <cstring>
#endif
#if FMT_USE_WINDOWS_H
# if !defined(FMT_HEADER_ONLY) && !defined(WIN32_LEAN_AND_MEAN)
# define WIN32_LEAN_AND_MEAN
# endif
# if defined(NOMINMAX) || defined(FMT_WIN_MINMAX)
# include <windows.h>
# else
# define NOMINMAX
# include <windows.h>
# undef NOMINMAX
# endif
#endif
#if FMT_EXCEPTIONS
# define FMT_TRY try
# define FMT_CATCH(x) catch (x)
#else
# define FMT_TRY if (true)
# define FMT_CATCH(x) if (false)
#endif
#ifdef _MSC_VER
# pragma warning(push)
# pragma warning(disable: 4127) // conditional expression is constant
# pragma warning(disable: 4702) // unreachable code
// Disable deprecation warning for strerror. The latter is not called but
// MSVC fails to detect it.
# pragma warning(disable: 4996)
#endif
// Dummy implementations of strerror_r and strerror_s called if corresponding
// system functions are not available.
static inline fmt::internal::Null<> strerror_r(int, char *, ...) {
return fmt::internal::Null<>();
}
static inline fmt::internal::Null<> strerror_s(char *, std::size_t, ...) {
return fmt::internal::Null<>();
}
namespace fmt {
FMT_FUNC internal::RuntimeError::~RuntimeError() FMT_DTOR_NOEXCEPT {}
FMT_FUNC FormatError::~FormatError() FMT_DTOR_NOEXCEPT {}
FMT_FUNC SystemError::~SystemError() FMT_DTOR_NOEXCEPT {}
namespace {
#ifndef _MSC_VER
# define FMT_SNPRINTF snprintf
#else // _MSC_VER
inline int fmt_snprintf(char *buffer, size_t size, const char *format, ...) {
va_list args;
va_start(args, format);
int result = vsnprintf_s(buffer, size, _TRUNCATE, format, args);
va_end(args);
return result;
}
# define FMT_SNPRINTF fmt_snprintf
#endif // _MSC_VER
#if defined(_WIN32) && defined(__MINGW32__) && !defined(__NO_ISOCEXT)
# define FMT_SWPRINTF snwprintf
#else
# define FMT_SWPRINTF swprintf
#endif // defined(_WIN32) && defined(__MINGW32__) && !defined(__NO_ISOCEXT)
const char RESET_COLOR[] = "\x1b[0m";
typedef void (*FormatFunc)(Writer &, int, StringRef);
// Portable thread-safe version of strerror.
// Sets buffer to point to a string describing the error code.
// This can be either a pointer to a string stored in buffer,
// or a pointer to some static immutable string.
// Returns one of the following values:
// 0 - success
// ERANGE - buffer is not large enough to store the error message
// other - failure
// Buffer should be at least of size 1.
int safe_strerror(
int error_code, char *&buffer, std::size_t buffer_size) FMT_NOEXCEPT {
FMT_ASSERT(buffer != 0 && buffer_size != 0, "invalid buffer");
class StrError {
private:
int error_code_;
char *&buffer_;
std::size_t buffer_size_;
// A noop assignment operator to avoid bogus warnings.
void operator=(const StrError &) {}
// Handle the result of XSI-compliant version of strerror_r.
int handle(int result) {
// glibc versions before 2.13 return result in errno.
return result == -1 ? errno : result;
}
// Handle the result of GNU-specific version of strerror_r.
int handle(char *message) {
// If the buffer is full then the message is probably truncated.
if (message == buffer_ && strlen(buffer_) == buffer_size_ - 1)
return ERANGE;
buffer_ = message;
return 0;
}
// Handle the case when strerror_r is not available.
int handle(internal::Null<>) {
return fallback(strerror_s(buffer_, buffer_size_, error_code_));
}
// Fallback to strerror_s when strerror_r is not available.
int fallback(int result) {
// If the buffer is full then the message is probably truncated.
return result == 0 && strlen(buffer_) == buffer_size_ - 1 ?
ERANGE : result;
}
// Fallback to strerror if strerror_r and strerror_s are not available.
int fallback(internal::Null<>) {
errno = 0;
buffer_ = strerror(error_code_);
return errno;
}
public:
StrError(int err_code, char *&buf, std::size_t buf_size)
: error_code_(err_code), buffer_(buf), buffer_size_(buf_size) {}
int run() {
// Suppress a warning about unused strerror_r.
strerror_r(0, FMT_NULL, "");
return handle(strerror_r(error_code_, buffer_, buffer_size_));
}
};
return StrError(error_code, buffer, buffer_size).run();
}
void format_error_code(Writer &out, int error_code,
StringRef message) FMT_NOEXCEPT {
// Report error code making sure that the output fits into
// INLINE_BUFFER_SIZE to avoid dynamic memory allocation and potential
// bad_alloc.
out.clear();
static const char SEP[] = ": ";
static const char ERROR_STR[] = "error ";
// Subtract 2 to account for terminating null characters in SEP and ERROR_STR.
std::size_t error_code_size = sizeof(SEP) + sizeof(ERROR_STR) - 2;
typedef internal::IntTraits<int>::MainType MainType;
MainType abs_value = static_cast<MainType>(error_code);
if (internal::is_negative(error_code)) {
abs_value = 0 - abs_value;
++error_code_size;
}
error_code_size += internal::count_digits(abs_value);
if (message.size() <= internal::INLINE_BUFFER_SIZE - error_code_size)
out << message << SEP;
out << ERROR_STR << error_code;
assert(out.size() <= internal::INLINE_BUFFER_SIZE);
}
void report_error(FormatFunc func, int error_code,
StringRef message) FMT_NOEXCEPT {
MemoryWriter full_message;
func(full_message, error_code, message);
// Use Writer::data instead of Writer::c_str to avoid potential memory
// allocation.
std::fwrite(full_message.data(), full_message.size(), 1, stderr);
std::fputc('\n', stderr);
}
} // namespace
FMT_FUNC void SystemError::init(
int err_code, CStringRef format_str, ArgList args) {
error_code_ = err_code;
MemoryWriter w;
format_system_error(w, err_code, format(format_str, args));
std::runtime_error &base = *this;
base = std::runtime_error(w.str());
}
template <typename T>
int internal::CharTraits<char>::format_float(
char *buffer, std::size_t size, const char *format,
unsigned width, int precision, T value) {
if (width == 0) {
return precision < 0 ?
FMT_SNPRINTF(buffer, size, format, value) :
FMT_SNPRINTF(buffer, size, format, precision, value);
}
return precision < 0 ?
FMT_SNPRINTF(buffer, size, format, width, value) :
FMT_SNPRINTF(buffer, size, format, width, precision, value);
}
template <typename T>
int internal::CharTraits<wchar_t>::format_float(
wchar_t *buffer, std::size_t size, const wchar_t *format,
unsigned width, int precision, T value) {
if (width == 0) {
return precision < 0 ?
FMT_SWPRINTF(buffer, size, format, value) :
FMT_SWPRINTF(buffer, size, format, precision, value);
}
return precision < 0 ?
FMT_SWPRINTF(buffer, size, format, width, value) :
FMT_SWPRINTF(buffer, size, format, width, precision, value);
}
template <typename T>
const char internal::BasicData<T>::DIGITS[] =
"0001020304050607080910111213141516171819"
"2021222324252627282930313233343536373839"
"4041424344454647484950515253545556575859"
"6061626364656667686970717273747576777879"
"8081828384858687888990919293949596979899";
#define FMT_POWERS_OF_10(factor) \
factor * 10, \
factor * 100, \
factor * 1000, \
factor * 10000, \
factor * 100000, \
factor * 1000000, \
factor * 10000000, \
factor * 100000000, \
factor * 1000000000
template <typename T>
const uint32_t internal::BasicData<T>::POWERS_OF_10_32[] = {
0, FMT_POWERS_OF_10(1)
};
template <typename T>
const uint64_t internal::BasicData<T>::POWERS_OF_10_64[] = {
0,
FMT_POWERS_OF_10(1),
FMT_POWERS_OF_10(ULongLong(1000000000)),
// Multiply several constants instead of using a single long long constant
// to avoid warnings about C++98 not supporting long long.
ULongLong(1000000000) * ULongLong(1000000000) * 10
};
FMT_FUNC void internal::report_unknown_type(char code, const char *type) {
(void)type;
if (std::isprint(static_cast<unsigned char>(code))) {
FMT_THROW(FormatError(
format("unknown format code '{}' for {}", code, type)));
}
FMT_THROW(FormatError(
format("unknown format code '\\x{:02x}' for {}",
static_cast<unsigned>(code), type)));
}
#if FMT_USE_WINDOWS_H
FMT_FUNC internal::UTF8ToUTF16::UTF8ToUTF16(StringRef s) {
static const char ERROR_MSG[] = "cannot convert string from UTF-8 to UTF-16";
if (s.size() > INT_MAX)
FMT_THROW(WindowsError(ERROR_INVALID_PARAMETER, ERROR_MSG));
int s_size = static_cast<int>(s.size());
int length = MultiByteToWideChar(
CP_UTF8, MB_ERR_INVALID_CHARS, s.data(), s_size, FMT_NULL, 0);
if (length == 0)
FMT_THROW(WindowsError(GetLastError(), ERROR_MSG));
buffer_.resize(length + 1);
length = MultiByteToWideChar(
CP_UTF8, MB_ERR_INVALID_CHARS, s.data(), s_size, &buffer_[0], length);
if (length == 0)
FMT_THROW(WindowsError(GetLastError(), ERROR_MSG));
buffer_[length] = 0;
}
FMT_FUNC internal::UTF16ToUTF8::UTF16ToUTF8(WStringRef s) {
if (int error_code = convert(s)) {
FMT_THROW(WindowsError(error_code,
"cannot convert string from UTF-16 to UTF-8"));
}
}
FMT_FUNC int internal::UTF16ToUTF8::convert(WStringRef s) {
if (s.size() > INT_MAX)
return ERROR_INVALID_PARAMETER;
int s_size = static_cast<int>(s.size());
int length = WideCharToMultiByte(
CP_UTF8, 0, s.data(), s_size, FMT_NULL, 0, FMT_NULL, FMT_NULL);
if (length == 0)
return GetLastError();
buffer_.resize(length + 1);
length = WideCharToMultiByte(
CP_UTF8, 0, s.data(), s_size, &buffer_[0], length, FMT_NULL, FMT_NULL);
if (length == 0)
return GetLastError();
buffer_[length] = 0;
return 0;
}
FMT_FUNC void WindowsError::init(
int err_code, CStringRef format_str, ArgList args) {
error_code_ = err_code;
MemoryWriter w;
internal::format_windows_error(w, err_code, format(format_str, args));
std::runtime_error &base = *this;
base = std::runtime_error(w.str());
}
FMT_FUNC void internal::format_windows_error(
Writer &out, int error_code, StringRef message) FMT_NOEXCEPT {
FMT_TRY {
MemoryBuffer<wchar_t, INLINE_BUFFER_SIZE> buffer;
buffer.resize(INLINE_BUFFER_SIZE);
for (;;) {
wchar_t *system_message = &buffer[0];
int result = FormatMessageW(
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
FMT_NULL, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
system_message, static_cast<uint32_t>(buffer.size()), FMT_NULL);
if (result != 0) {
UTF16ToUTF8 utf8_message;
if (utf8_message.convert(system_message) == ERROR_SUCCESS) {
out << message << ": " << utf8_message;
return;
}
break;
}
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER)
break; // Can't get error message, report error code instead.
buffer.resize(buffer.size() * 2);
}
} FMT_CATCH(...) {}
fmt::format_error_code(out, error_code, message); // 'fmt::' is for bcc32.
}
#endif // FMT_USE_WINDOWS_H
FMT_FUNC void format_system_error(
Writer &out, int error_code, StringRef message) FMT_NOEXCEPT {
FMT_TRY {
internal::MemoryBuffer<char, internal::INLINE_BUFFER_SIZE> buffer;
buffer.resize(internal::INLINE_BUFFER_SIZE);
for (;;) {
char *system_message = &buffer[0];
int result = safe_strerror(error_code, system_message, buffer.size());
if (result == 0) {
out << message << ": " << system_message;
return;
}
if (result != ERANGE)
break; // Can't get error message, report error code instead.
buffer.resize(buffer.size() * 2);
}
} FMT_CATCH(...) {}
fmt::format_error_code(out, error_code, message); // 'fmt::' is for bcc32.
}
template <typename Char>
void internal::ArgMap<Char>::init(const ArgList &args) {
if (!map_.empty())
return;
typedef internal::NamedArg<Char> NamedArg;
const NamedArg *named_arg = FMT_NULL;
bool use_values =
args.type(ArgList::MAX_PACKED_ARGS - 1) == internal::Arg::NONE;
if (use_values) {
for (unsigned i = 0;/*nothing*/; ++i) {
internal::Arg::Type arg_type = args.type(i);
switch (arg_type) {
case internal::Arg::NONE:
return;
case internal::Arg::NAMED_ARG:
named_arg = static_cast<const NamedArg*>(args.values_[i].pointer);
map_.push_back(Pair(named_arg->name, *named_arg));
break;
default:
/*nothing*/;
}
}
return;
}
for (unsigned i = 0; i != ArgList::MAX_PACKED_ARGS; ++i) {
internal::Arg::Type arg_type = args.type(i);
if (arg_type == internal::Arg::NAMED_ARG) {
named_arg = static_cast<const NamedArg*>(args.args_[i].pointer);
map_.push_back(Pair(named_arg->name, *named_arg));
}
}
for (unsigned i = ArgList::MAX_PACKED_ARGS;/*nothing*/; ++i) {
switch (args.args_[i].type) {
case internal::Arg::NONE:
return;
case internal::Arg::NAMED_ARG:
named_arg = static_cast<const NamedArg*>(args.args_[i].pointer);
map_.push_back(Pair(named_arg->name, *named_arg));
break;
default:
/*nothing*/;
}
}
}
template <typename Char>
void internal::FixedBuffer<Char>::grow(std::size_t) {
FMT_THROW(std::runtime_error("buffer overflow"));
}
FMT_FUNC internal::Arg internal::FormatterBase::do_get_arg(
unsigned arg_index, const char *&error) {
internal::Arg arg = args_[arg_index];
switch (arg.type) {
case internal::Arg::NONE:
error = "argument index out of range";
break;
case internal::Arg::NAMED_ARG:
arg = *static_cast<const internal::Arg*>(arg.pointer);
break;
default:
/*nothing*/;
}
return arg;
}
FMT_FUNC void report_system_error(
int error_code, fmt::StringRef message) FMT_NOEXCEPT {
// 'fmt::' is for bcc32.
report_error(format_system_error, error_code, message);
}
#if FMT_USE_WINDOWS_H
FMT_FUNC void report_windows_error(
int error_code, fmt::StringRef message) FMT_NOEXCEPT {
// 'fmt::' is for bcc32.
report_error(internal::format_windows_error, error_code, message);
}
#endif
FMT_FUNC void print(std::FILE *f, CStringRef format_str, ArgList args) {
MemoryWriter w;
w.write(format_str, args);
std::fwrite(w.data(), 1, w.size(), f);
}
FMT_FUNC void print(CStringRef format_str, ArgList args) {
print(stdout, format_str, args);
}
FMT_FUNC void print_colored(Color c, CStringRef format, ArgList args) {
char escape[] = "\x1b[30m";
escape[3] = static_cast<char>('0' + c);
std::fputs(escape, stdout);
print(format, args);
std::fputs(RESET_COLOR, stdout);
}
#ifndef FMT_HEADER_ONLY
template struct internal::BasicData<void>;
// Explicit instantiations for char.
template void internal::FixedBuffer<char>::grow(std::size_t);
template void internal::ArgMap<char>::init(const ArgList &args);
template FMT_API int internal::CharTraits<char>::format_float(
char *buffer, std::size_t size, const char *format,
unsigned width, int precision, double value);
template FMT_API int internal::CharTraits<char>::format_float(
char *buffer, std::size_t size, const char *format,
unsigned width, int precision, long double value);
// Explicit instantiations for wchar_t.
template void internal::FixedBuffer<wchar_t>::grow(std::size_t);
template void internal::ArgMap<wchar_t>::init(const ArgList &args);
template FMT_API int internal::CharTraits<wchar_t>::format_float(
wchar_t *buffer, std::size_t size, const wchar_t *format,
unsigned width, int precision, double value);
template FMT_API int internal::CharTraits<wchar_t>::format_float(
wchar_t *buffer, std::size_t size, const wchar_t *format,
unsigned width, int precision, long double value);
#endif // FMT_HEADER_ONLY
} // namespace fmt
#ifdef _MSC_VER
# pragma warning(pop)
#endif

File diff suppressed because it is too large Load diff

View file

@ -1,23 +0,0 @@
Copyright (c) 2012 - 2016, Victor Zverovich
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -40,7 +40,6 @@
<file>icon.png</file>
<file>licenses/boost_boost.txt</file>
<file>licenses/emoji-data-source.txt</file>
<file>licenses/fmt_bsd2.txt</file>
<file>licenses/libcommuni_BSD3.txt</file>
<file>licenses/openssl.txt</file>
<file>licenses/pajlada_settings.txt</file>

View file

@ -11,7 +11,6 @@
#include "controllers/notifications/NotificationController.hpp"
#include "controllers/pings/PingController.hpp"
#include "controllers/taggedusers/TaggedUsersController.hpp"
#include "debug/Log.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp"
@ -158,11 +157,11 @@ void Application::initNm(Paths &paths)
void Application::initPubsub()
{
this->twitch.pubsub->signals_.whisper.sent.connect([](const auto &msg) {
log("WHISPER SENT LOL"); //
qDebug() << "WHISPER SENT LOL"; //
});
this->twitch.pubsub->signals_.whisper.received.connect([](const auto &msg) {
log("WHISPER RECEIVED LOL"); //
qDebug() << "WHISPER RECEIVED LOL"; //
});
this->twitch.pubsub->signals_.moderation.chatCleared.connect(

View file

@ -1,7 +1,6 @@
#pragma once
#ifdef __cplusplus
# include <fmt/format.h>
# include <irccommand.h>
# include <ircconnection.h>
# include <rapidjson/document.h>

View file

@ -1,9 +1,9 @@
#include "common/Channel.hpp"
#include "Application.hpp"
#include "debug/Log.hpp"
#include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/IrcMessageHandler.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/Logging.hpp"
#include "singletons/Settings.hpp"

View file

@ -6,7 +6,6 @@
#include "controllers/accounts/AccountController.hpp"
#include "controllers/commands/CommandController.hpp"
#include "debug/Benchmark.hpp"
#include "debug/Log.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Emotes.hpp"

View file

@ -1,6 +1,5 @@
#include "DownloadManager.hpp"
#include "debug/Log.hpp"
#include "singletons/Paths.hpp"
#include <QDesktopServices>
@ -41,7 +40,7 @@ void DownloadManager::setFile(QString fileURL, const QString &channelName)
void DownloadManager::onDownloadProgress(qint64 bytesRead, qint64 bytesTotal)
{
log("Download progress: {}/{}", bytesRead, bytesTotal);
qDebug() << "Download progress: " << bytesRead << "/" << bytesTotal;
}
void DownloadManager::onFinished(QNetworkReply *reply)

View file

@ -61,7 +61,7 @@ Env::Env()
"https://i.nuuls.com/upload"))
, twitchServerHost(
readStringEnv("CHATTERINO2_TWITCH_SERVER_HOST", "irc.chat.twitch.tv"))
, twitchServerPort(readPortEnv("CHATTERINO2_TWITCH_SERVER_PORT", 6697))
, twitchServerPort(readPortEnv("CHATTERINO2_TWITCH_SERVER_PORT", 443))
, twitchServerSecure(readBoolEnv("CHATTERINO2_TWITCH_SERVER_SECURE", true))
{
}

View file

@ -4,7 +4,6 @@
#include "common/NetworkResult.hpp"
#include "common/Outcome.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "debug/Log.hpp"
#include "singletons/Paths.hpp"
#include "util/DebugCount.hpp"
#include "util/PostToThread.hpp"
@ -124,7 +123,7 @@ void loadUncached(const std::shared_ptr<NetworkData> &data)
if (reply == nullptr)
{
log("Unhandled request type");
qDebug() << "Unhandled request type";
return;
}
@ -132,7 +131,7 @@ void loadUncached(const std::shared_ptr<NetworkData> &data)
{
QObject::connect(
data->timer_, &QTimer::timeout, worker, [reply, data]() {
log("Aborted!");
qDebug() << "Aborted!";
reply->abort();
if (data->onError_)
{

View file

@ -4,7 +4,6 @@
#include "common/Outcome.hpp"
#include "common/Version.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "debug/Log.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include "singletons/Paths.hpp"
#include "util/DebugCount.hpp"

View file

@ -1,7 +1,5 @@
#include "common/NetworkResult.hpp"
#include "debug/Log.hpp"
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <QJsonDocument>
@ -45,8 +43,9 @@ rapidjson::Document NetworkResult::parseRapidJson() const
if (result.Code() != rapidjson::kParseErrorNone)
{
log("JSON parse error: {} ({})",
rapidjson::GetParseError_En(result.Code()), result.Offset());
qDebug() << "JSON parse error:"
<< rapidjson::GetParseError_En(result.Code()) << "("
<< result.Offset() << ")";
return ret;
}

View file

@ -5,7 +5,6 @@
#include "controllers/accounts/AccountController.hpp"
#include "controllers/commands/Command.hpp"
#include "controllers/commands/CommandModel.hpp"
#include "debug/Log.hpp"
#include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp"
#include "messages/MessageElement.hpp"
@ -18,6 +17,7 @@
#include "singletons/Theme.hpp"
#include "util/CombinePath.hpp"
#include "widgets/dialogs/LogsPopup.hpp"
#include "widgets/dialogs/UserInfoPopup.hpp"
#include <QApplication>
#include <QFile>
@ -482,6 +482,21 @@ QString CommandController::execCommand(const QString &textNoEmoji,
channelName + "/viewercard/" + words[1]);
return "";
}
else if (commandName == "/usercard")
{
if (words.size() < 2)
{
channel->addMessage(
makeSystemMessage("Usage /usercard [user]"));
return "";
}
auto *userPopup = new UserInfoPopup;
userPopup->setData(words[1], channel);
userPopup->setActionOnFocusLoss(BaseWindow::Delete);
userPopup->move(QCursor::pos());
userPopup->show();
return "";
}
}
{

View file

@ -18,16 +18,17 @@ HighlightBlacklistUser HighlightBlacklistModel::getItemFromRow(
{
// key, regex
return HighlightBlacklistUser{row[0]->data(Qt::DisplayRole).toString(),
row[1]->data(Qt::CheckStateRole).toBool()};
return HighlightBlacklistUser{
row[Column::Pattern]->data(Qt::DisplayRole).toString(),
row[Column::UseRegex]->data(Qt::CheckStateRole).toBool()};
}
// turns a row in the model into a vector item
void HighlightBlacklistModel::getRowFromItem(const HighlightBlacklistUser &item,
std::vector<QStandardItem *> &row)
{
setStringItem(row[0], item.getPattern());
setBoolItem(row[1], item.isRegex());
setStringItem(row[Column::Pattern], item.getPattern());
setBoolItem(row[Column::UseRegex], item.isRegex());
}
} // namespace chatterino

View file

@ -13,6 +13,12 @@ class HighlightBlacklistModel : public SignalVectorModel<HighlightBlacklistUser>
{
explicit HighlightBlacklistModel(QObject *parent);
public:
enum Column {
Pattern = 0,
UseRegex = 1,
};
protected:
// turn a vector item into a model row
virtual HighlightBlacklistUser getItemFromRow(

View file

@ -8,7 +8,7 @@ namespace chatterino {
// commandmodel
HighlightModel::HighlightModel(QObject *parent)
: SignalVectorModel<HighlightPhrase>(5, parent)
: SignalVectorModel<HighlightPhrase>(7, parent)
{
}
@ -16,52 +16,103 @@ HighlightModel::HighlightModel(QObject *parent)
HighlightPhrase HighlightModel::getItemFromRow(
std::vector<QStandardItem *> &row, const HighlightPhrase &original)
{
// key, alert, sound, regex, case-sensitivity
// In order for old messages to update their highlight color, we need to
// update the highlight color here.
auto highlightColor = original.getColor();
*highlightColor =
row[Column::Color]->data(Qt::DecorationRole).value<QColor>();
return HighlightPhrase{row[0]->data(Qt::DisplayRole).toString(),
row[1]->data(Qt::CheckStateRole).toBool(),
row[2]->data(Qt::CheckStateRole).toBool(),
row[3]->data(Qt::CheckStateRole).toBool(),
row[4]->data(Qt::CheckStateRole).toBool()};
return HighlightPhrase{
row[Column::Pattern]->data(Qt::DisplayRole).toString(),
row[Column::FlashTaskbar]->data(Qt::CheckStateRole).toBool(),
row[Column::PlaySound]->data(Qt::CheckStateRole).toBool(),
row[Column::UseRegex]->data(Qt::CheckStateRole).toBool(),
row[Column::CaseSensitive]->data(Qt::CheckStateRole).toBool(),
row[Column::SoundPath]->data(Qt::UserRole).toString(),
highlightColor};
}
// turns a row in the model into a vector item
void HighlightModel::getRowFromItem(const HighlightPhrase &item,
std::vector<QStandardItem *> &row)
{
setStringItem(row[0], item.getPattern());
setBoolItem(row[1], item.getAlert());
setBoolItem(row[2], item.getSound());
setBoolItem(row[3], item.isRegex());
setBoolItem(row[4], item.isCaseSensitive());
setStringItem(row[Column::Pattern], item.getPattern());
setBoolItem(row[Column::FlashTaskbar], item.hasAlert());
setBoolItem(row[Column::PlaySound], item.hasSound());
setBoolItem(row[Column::UseRegex], item.isRegex());
setBoolItem(row[Column::CaseSensitive], item.isCaseSensitive());
setFilePathItem(row[Column::SoundPath], item.getSoundUrl());
setColorItem(row[Column::Color], *item.getColor());
}
void HighlightModel::afterInit()
{
// Highlight settings for own username
std::vector<QStandardItem *> usernameRow = this->createRow();
setBoolItem(usernameRow[0], getSettings()->enableSelfHighlight.getValue(),
true, false);
usernameRow[0]->setData("Your username (automatic)", Qt::DisplayRole);
setBoolItem(usernameRow[1],
setBoolItem(usernameRow[Column::Pattern],
getSettings()->enableSelfHighlight.getValue(), true, false);
usernameRow[Column::Pattern]->setData("Your username (automatic)",
Qt::DisplayRole);
setBoolItem(usernameRow[Column::FlashTaskbar],
getSettings()->enableSelfHighlightTaskbar.getValue(), true,
false);
setBoolItem(usernameRow[2],
setBoolItem(usernameRow[Column::PlaySound],
getSettings()->enableSelfHighlightSound.getValue(), true,
false);
usernameRow[3]->setFlags(0);
usernameRow[Column::UseRegex]->setFlags(0);
usernameRow[Column::CaseSensitive]->setFlags(0);
QUrl selfSound = QUrl(getSettings()->selfHighlightSoundUrl.getValue());
setFilePathItem(usernameRow[Column::SoundPath], selfSound);
auto selfColor = ColorProvider::instance().color(ColorType::SelfHighlight);
setColorItem(usernameRow[Column::Color], *selfColor);
this->insertCustomRow(usernameRow, 0);
// Highlight settings for whispers
std::vector<QStandardItem *> whisperRow = this->createRow();
setBoolItem(whisperRow[0], getSettings()->enableWhisperHighlight.getValue(),
true, false);
whisperRow[0]->setData("Whispers", Qt::DisplayRole);
setBoolItem(whisperRow[1],
setBoolItem(whisperRow[Column::Pattern],
getSettings()->enableWhisperHighlight.getValue(), true, false);
whisperRow[Column::Pattern]->setData("Whispers", Qt::DisplayRole);
setBoolItem(whisperRow[Column::FlashTaskbar],
getSettings()->enableWhisperHighlightTaskbar.getValue(), true,
false);
setBoolItem(whisperRow[2],
setBoolItem(whisperRow[Column::PlaySound],
getSettings()->enableWhisperHighlightSound.getValue(), true,
false);
whisperRow[3]->setFlags(0);
whisperRow[Column::UseRegex]->setFlags(0);
whisperRow[Column::CaseSensitive]->setFlags(0);
QUrl whisperSound =
QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
setFilePathItem(whisperRow[Column::SoundPath], whisperSound);
auto whisperColor = ColorProvider::instance().color(ColorType::Whisper);
setColorItem(whisperRow[Column::Color], *whisperColor);
this->insertCustomRow(whisperRow, 1);
// Highlight settings for subscription messages
std::vector<QStandardItem *> subRow = this->createRow();
setBoolItem(subRow[Column::Pattern],
getSettings()->enableSubHighlight.getValue(), true, false);
subRow[Column::Pattern]->setData("Subscriptions", Qt::DisplayRole);
setBoolItem(subRow[Column::FlashTaskbar],
getSettings()->enableSubHighlightTaskbar.getValue(), true,
false);
setBoolItem(subRow[Column::PlaySound],
getSettings()->enableSubHighlightSound.getValue(), true, false);
subRow[Column::UseRegex]->setFlags(0);
subRow[Column::CaseSensitive]->setFlags(0);
QUrl subSound = QUrl(getSettings()->subHighlightSoundUrl.getValue());
setFilePathItem(subRow[Column::SoundPath], subSound);
auto subColor = ColorProvider::instance().color(ColorType::Subscription);
setColorItem(subRow[Column::Color], *subColor);
this->insertCustomRow(subRow, 2);
}
void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
@ -70,7 +121,7 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
{
switch (column)
{
case 0: {
case Column::Pattern: {
if (role == Qt::CheckStateRole)
{
if (rowIndex == 0)
@ -82,10 +133,14 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->enableWhisperHighlight.setValue(
value.toBool());
}
else if (rowIndex == 2)
{
getSettings()->enableSubHighlight.setValue(value.toBool());
}
}
}
break;
case 1: {
case Column::FlashTaskbar: {
if (role == Qt::CheckStateRole)
{
if (rowIndex == 0)
@ -98,10 +153,15 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->enableWhisperHighlightTaskbar.setValue(
value.toBool());
}
else if (rowIndex == 2)
{
getSettings()->enableSubHighlightTaskbar.setValue(
value.toBool());
}
}
}
break;
case 2: {
case Column::PlaySound: {
if (role == Qt::CheckStateRole)
{
if (rowIndex == 0)
@ -114,11 +174,62 @@ void HighlightModel::customRowSetData(const std::vector<QStandardItem *> &row,
getSettings()->enableWhisperHighlightSound.setValue(
value.toBool());
}
else if (rowIndex == 2)
{
getSettings()->enableSubHighlightSound.setValue(
value.toBool());
}
}
}
break;
case 3: {
// empty element
case Column::UseRegex: {
// Regex --> empty
}
break;
case Column::CaseSensitive: {
// Case-sensitivity --> empty
}
break;
case Column::SoundPath: {
// Custom sound file
if (role == Qt::UserRole)
{
if (rowIndex == 0)
{
getSettings()->selfHighlightSoundUrl.setValue(
value.toString());
}
else if (rowIndex == 1)
{
getSettings()->whisperHighlightSoundUrl.setValue(
value.toString());
}
else if (rowIndex == 2)
{
getSettings()->subHighlightSoundUrl.setValue(
value.toString());
}
}
}
break;
case Column::Color: {
// Custom color
if (role == Qt::DecorationRole)
{
auto colorName = value.value<QColor>().name(QColor::HexArgb);
if (rowIndex == 0)
{
getSettings()->selfHighlightColor.setValue(colorName);
}
else if (rowIndex == 1)
{
getSettings()->whisperHighlightColor.setValue(colorName);
}
else if (rowIndex == 2)
{
getSettings()->subHighlightColor.setValue(colorName);
}
}
}
break;
}

View file

@ -13,6 +13,18 @@ class HighlightModel : public SignalVectorModel<HighlightPhrase>
{
explicit HighlightModel(QObject *parent);
public:
// Used here, in HighlightingPage and in UserHighlightModel
enum Column {
Pattern = 0,
FlashTaskbar = 1,
PlaySound = 2,
UseRegex = 3,
CaseSensitive = 4,
SoundPath = 5,
Color = 6
};
protected:
// turn a vector item into a model row
virtual HighlightPhrase getItemFromRow(

View file

@ -0,0 +1,103 @@
#include "controllers/highlights/HighlightPhrase.hpp"
namespace chatterino {
bool HighlightPhrase::operator==(const HighlightPhrase &other) const
{
return std::tie(this->pattern_, this->hasSound_, this->hasAlert_,
this->isRegex_, this->isCaseSensitive_, this->soundUrl_,
this->color_) == std::tie(other.pattern_, other.hasSound_,
other.hasAlert_, other.isRegex_,
other.isCaseSensitive_,
other.soundUrl_, other.color_);
}
HighlightPhrase::HighlightPhrase(const QString &pattern, bool hasAlert,
bool hasSound, bool isRegex,
bool isCaseSensitive, const QString &soundUrl,
QColor color)
: pattern_(pattern)
, hasAlert_(hasAlert)
, hasSound_(hasSound)
, isRegex_(isRegex)
, isCaseSensitive_(isCaseSensitive)
, soundUrl_(soundUrl)
, regex_(isRegex_ ? pattern
: "\\b" + QRegularExpression::escape(pattern) + "\\b",
QRegularExpression::UseUnicodePropertiesOption |
(isCaseSensitive_ ? QRegularExpression::NoPatternOption
: QRegularExpression::CaseInsensitiveOption))
{
this->color_ = std::make_shared<QColor>(color);
}
HighlightPhrase::HighlightPhrase(const QString &pattern, bool hasAlert,
bool hasSound, bool isRegex,
bool isCaseSensitive, const QString &soundUrl,
std::shared_ptr<QColor> color)
: pattern_(pattern)
, hasAlert_(hasAlert)
, hasSound_(hasSound)
, isRegex_(isRegex)
, isCaseSensitive_(isCaseSensitive)
, soundUrl_(soundUrl)
, color_(color)
, regex_(isRegex_ ? pattern
: "\\b" + QRegularExpression::escape(pattern) + "\\b",
QRegularExpression::UseUnicodePropertiesOption |
(isCaseSensitive_ ? QRegularExpression::NoPatternOption
: QRegularExpression::CaseInsensitiveOption))
{
}
const QString &HighlightPhrase::getPattern() const
{
return this->pattern_;
}
bool HighlightPhrase::hasAlert() const
{
return this->hasAlert_;
}
bool HighlightPhrase::hasSound() const
{
return this->hasSound_;
}
bool HighlightPhrase::hasCustomSound() const
{
return !this->soundUrl_.isEmpty();
}
bool HighlightPhrase::isRegex() const
{
return this->isRegex_;
}
bool HighlightPhrase::isValid() const
{
return !this->pattern_.isEmpty() && this->regex_.isValid();
}
bool HighlightPhrase::isMatch(const QString &subject) const
{
return this->isValid() && this->regex_.match(subject).hasMatch();
}
bool HighlightPhrase::isCaseSensitive() const
{
return this->isCaseSensitive_;
}
const QUrl &HighlightPhrase::getSoundUrl() const
{
return this->soundUrl_;
}
const std::shared_ptr<QColor> HighlightPhrase::getColor() const
{
return this->color_;
}
} // namespace chatterino

View file

@ -1,5 +1,6 @@
#pragma once
#include "providers/colors/ColorProvider.hpp"
#include "util/RapidJsonSerializeQString.hpp"
#include "util/RapidjsonHelpers.hpp"
@ -12,70 +13,74 @@ namespace chatterino {
class HighlightPhrase
{
public:
bool operator==(const HighlightPhrase &other) const
{
return std::tie(this->pattern_, this->sound_, this->alert_,
this->isRegex_, this->caseSensitive_) ==
std::tie(other.pattern_, other.sound_, other.alert_,
other.isRegex_, other.caseSensitive_);
}
bool operator==(const HighlightPhrase &other) const;
HighlightPhrase(const QString &pattern, bool alert, bool sound,
bool isRegex, bool caseSensitive)
: pattern_(pattern)
, alert_(alert)
, sound_(sound)
, isRegex_(isRegex)
, caseSensitive_(caseSensitive)
, regex_(
isRegex_ ? pattern
: "\\b" + QRegularExpression::escape(pattern) + "\\b",
QRegularExpression::UseUnicodePropertiesOption |
(caseSensitive_ ? QRegularExpression::NoPatternOption
: QRegularExpression::CaseInsensitiveOption))
{
}
/**
* @brief Create a new HighlightPhrase.
*
* Use this constructor when creating a new HighlightPhrase.
*/
HighlightPhrase(const QString &pattern, bool hasAlert, bool hasSound,
bool isRegex, bool isCaseSensitive, const QString &soundUrl,
QColor color);
const QString &getPattern() const
{
return this->pattern_;
}
bool getAlert() const
{
return this->alert_;
}
bool getSound() const
{
return this->sound_;
}
bool isRegex() const
{
return this->isRegex_;
}
/**
* @brief Create a new HighlightPhrase.
*
* Use this constructor when updating an existing HighlightPhrase's color.
*/
HighlightPhrase(const QString &pattern, bool hasAlert, bool hasSound,
bool isRegex, bool isCaseSensitive, const QString &soundUrl,
std::shared_ptr<QColor> color);
bool isValid() const
{
return !this->pattern_.isEmpty() && this->regex_.isValid();
}
const QString &getPattern() const;
bool hasAlert() const;
bool isMatch(const QString &subject) const
{
return this->isValid() && this->regex_.match(subject).hasMatch();
}
/**
* @brief Check if this highlight phrase should play a sound when
* triggered.
*
* In distinction from `HighlightPhrase::hasCustomSound`, this method only
* checks whether or not ANY sound should be played when the phrase is
* triggered.
*
* To check whether a custom sound is set, use
* `HighlightPhrase::hasCustomSound` instead.
*
* @return true, if this highlight phrase should play a sound when
* triggered, false otherwise
*/
bool hasSound() const;
bool isCaseSensitive() const
{
return this->caseSensitive_;
}
/**
* @brief Check if this highlight phrase has a custom sound set.
*
* Note that this method only checks whether the path to the custom sound
* is not empty. It does not check whether the file still exists, is a
* sound file, or anything else.
*
* @return true, if the custom sound file path is not empty, false otherwise
*/
bool hasCustomSound() const;
bool isRegex() const;
bool isValid() const;
bool isMatch(const QString &subject) const;
bool isCaseSensitive() const;
const QUrl &getSoundUrl() const;
const std::shared_ptr<QColor> getColor() const;
private:
QString pattern_;
bool alert_;
bool sound_;
bool hasAlert_;
bool hasSound_;
bool isRegex_;
bool caseSensitive_;
bool isCaseSensitive_;
QUrl soundUrl_;
std::shared_ptr<QColor> color_;
QRegularExpression regex_;
};
} // namespace chatterino
namespace pajlada {
@ -88,10 +93,13 @@ struct Serialize<chatterino::HighlightPhrase> {
rapidjson::Value ret(rapidjson::kObjectType);
chatterino::rj::set(ret, "pattern", value.getPattern(), a);
chatterino::rj::set(ret, "alert", value.getAlert(), a);
chatterino::rj::set(ret, "sound", value.getSound(), a);
chatterino::rj::set(ret, "alert", value.hasAlert(), a);
chatterino::rj::set(ret, "sound", value.hasSound(), a);
chatterino::rj::set(ret, "regex", value.isRegex(), a);
chatterino::rj::set(ret, "case", value.isCaseSensitive(), a);
chatterino::rj::set(ret, "soundUrl", value.getSoundUrl().toString(), a);
chatterino::rj::set(ret, "color",
value.getColor()->name(QColor::HexArgb), a);
return ret;
}
@ -104,23 +112,30 @@ struct Deserialize<chatterino::HighlightPhrase> {
if (!value.IsObject())
{
return chatterino::HighlightPhrase(QString(), true, false, false,
false);
false, "", QColor());
}
QString _pattern;
bool _alert = true;
bool _sound = false;
bool _hasAlert = true;
bool _hasSound = false;
bool _isRegex = false;
bool _caseSensitive = false;
bool _isCaseSensitive = false;
QString _soundUrl;
QString encodedColor;
chatterino::rj::getSafe(value, "pattern", _pattern);
chatterino::rj::getSafe(value, "alert", _alert);
chatterino::rj::getSafe(value, "sound", _sound);
chatterino::rj::getSafe(value, "alert", _hasAlert);
chatterino::rj::getSafe(value, "sound", _hasSound);
chatterino::rj::getSafe(value, "regex", _isRegex);
chatterino::rj::getSafe(value, "case", _caseSensitive);
chatterino::rj::getSafe(value, "case", _isCaseSensitive);
chatterino::rj::getSafe(value, "soundUrl", _soundUrl);
chatterino::rj::getSafe(value, "color", encodedColor);
return chatterino::HighlightPhrase(_pattern, _alert, _sound, _isRegex,
_caseSensitive);
auto _color = QColor(encodedColor);
return chatterino::HighlightPhrase(_pattern, _hasAlert, _hasSound,
_isRegex, _isCaseSensitive,
_soundUrl, _color);
}
};

View file

@ -1,6 +1,7 @@
#include "UserHighlightModel.hpp"
#include "Application.hpp"
#include "controllers/highlights/HighlightModel.hpp"
#include "singletons/Settings.hpp"
#include "util/StandardItemHelper.hpp"
@ -8,7 +9,7 @@ namespace chatterino {
// commandmodel
UserHighlightModel::UserHighlightModel(QObject *parent)
: SignalVectorModel<HighlightPhrase>(5, parent)
: SignalVectorModel<HighlightPhrase>(7, parent)
{
}
@ -16,24 +17,37 @@ UserHighlightModel::UserHighlightModel(QObject *parent)
HighlightPhrase UserHighlightModel::getItemFromRow(
std::vector<QStandardItem *> &row, const HighlightPhrase &original)
{
// key, regex
using Column = HighlightModel::Column;
return HighlightPhrase{row[0]->data(Qt::DisplayRole).toString(),
row[1]->data(Qt::CheckStateRole).toBool(),
row[2]->data(Qt::CheckStateRole).toBool(),
row[3]->data(Qt::CheckStateRole).toBool(),
row[4]->data(Qt::CheckStateRole).toBool()};
// In order for old messages to update their highlight color, we need to
// update the highlight color here.
auto highlightColor = original.getColor();
*highlightColor =
row[Column::Color]->data(Qt::DecorationRole).value<QColor>();
return HighlightPhrase{
row[Column::Pattern]->data(Qt::DisplayRole).toString(),
row[Column::FlashTaskbar]->data(Qt::CheckStateRole).toBool(),
row[Column::PlaySound]->data(Qt::CheckStateRole).toBool(),
row[Column::UseRegex]->data(Qt::CheckStateRole).toBool(),
row[Column::CaseSensitive]->data(Qt::CheckStateRole).toBool(),
row[Column::SoundPath]->data(Qt::UserRole).toString(),
highlightColor};
}
// row into vector item
void UserHighlightModel::getRowFromItem(const HighlightPhrase &item,
std::vector<QStandardItem *> &row)
{
setStringItem(row[0], item.getPattern());
setBoolItem(row[1], item.getAlert());
setBoolItem(row[2], item.getSound());
setBoolItem(row[3], item.isRegex());
setBoolItem(row[4], item.isCaseSensitive());
using Column = HighlightModel::Column;
setStringItem(row[Column::Pattern], item.getPattern());
setBoolItem(row[Column::FlashTaskbar], item.hasAlert());
setBoolItem(row[Column::PlaySound], item.hasSound());
setBoolItem(row[Column::UseRegex], item.isRegex());
setBoolItem(row[Column::CaseSensitive], item.isCaseSensitive());
setFilePathItem(row[Column::SoundPath], item.getSoundUrl());
setColorItem(row[Column::Color], *item.getColor());
}
} // namespace chatterino

View file

@ -4,7 +4,6 @@
#include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "controllers/notifications/NotificationModel.hpp"
#include "debug/Log.hpp"
#include "providers/twitch/TwitchApi.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Toasts.hpp"
@ -147,12 +146,13 @@ void NotificationController::getFakeTwitchChannelLiveStatus(
TwitchApi::findUserId(channelName, [channelName, this](QString roomID) {
if (roomID.isEmpty())
{
log("[TwitchChannel:{}] Refreshing live status (Missing ID)",
channelName);
qDebug() << "[TwitchChannel" << channelName
<< "] Refreshing live status (Missing ID)";
removeFakeChannel(channelName);
return;
}
log("[TwitchChannel:{}] Refreshing live status", channelName);
qDebug() << "[TwitchChannel" << channelName
<< "] Refreshing live status";
QString url("https://api.twitch.tv/kraken/streams/" + roomID);
NetworkRequest::twitchRequest(url)
@ -160,15 +160,15 @@ void NotificationController::getFakeTwitchChannelLiveStatus(
rapidjson::Document document = result.parseRapidJson();
if (!document.IsObject())
{
log("[TwitchChannel:refreshLiveStatus]root is not an "
"object");
qDebug() << "[TwitchChannel:refreshLiveStatus] root is not "
"an object";
return Failure;
}
if (!document.HasMember("stream"))
{
log("[TwitchChannel:refreshLiveStatus] Missing stream in "
"root");
qDebug() << "[TwitchChannel:refreshLiveStatus] Missing "
"stream in root";
return Failure;
}

View file

@ -10,7 +10,8 @@ BenchmarkGuard::BenchmarkGuard(const QString &_name)
BenchmarkGuard::~BenchmarkGuard()
{
log("{} {} ms", this->name_, float(timer_.nsecsElapsed()) / 1000000.0f);
qDebug() << this->name_ << float(timer_.nsecsElapsed()) / 1000000.0f
<< "ms";
}
qreal BenchmarkGuard::getElapsedMs()

View file

@ -1,7 +1,6 @@
#pragma once
#include "debug/Log.hpp"
#include <QDebug>
#include <QElapsedTimer>
#include <boost/noncopyable.hpp>

View file

@ -1,29 +0,0 @@
#pragma once
#include "util/Helpers.hpp"
#include <QDebug>
#include <QTime>
namespace chatterino {
template <typename... Args>
inline void log(const std::string &formatString, Args &&... args)
{
qDebug().noquote() << QTime::currentTime().toString("hh:mm:ss.zzz")
<< fS(formatString, std::forward<Args>(args)...).c_str();
}
template <typename... Args>
inline void log(const char *formatString, Args &&... args)
{
log(std::string(formatString), std::forward<Args>(args)...);
}
template <typename... Args>
inline void log(const QString &formatString, Args &&... args)
{
log(formatString.toStdString(), std::forward<Args>(args)...);
}
} // namespace chatterino

View file

@ -14,7 +14,6 @@
#include "common/NetworkRequest.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "debug/Benchmark.hpp"
#include "debug/Log.hpp"
#include "singletons/Emotes.hpp"
#include "singletons/WindowManager.hpp"
#include "util/DebugCount.hpp"
@ -108,8 +107,8 @@ namespace detail {
if (reader.imageCount() == 0)
{
log("Error while reading image {}: '{}'", url.string,
reader.errorString());
qDebug() << "Error while reading image" << url.string << ": '"
<< reader.errorString() << "'";
return frames;
}
@ -127,8 +126,8 @@ namespace detail {
if (frames.size() == 0)
{
log("Error while reading image {}: '{}'", url.string,
reader.errorString());
qDebug() << "Error while reading image" << url.string << ": '"
<< reader.errorString() << "'";
}
return frames;

View file

@ -1,6 +1,9 @@
#include "messages/Message.hpp"
#include "Application.hpp"
#include "MessageElement.hpp"
#include "providers/twitch/PubsubActions.hpp"
#include "singletons/Theme.hpp"
#include "util/DebugCount.hpp"
#include "util/IrcHelpers.hpp"
@ -24,11 +27,13 @@ SBHighlight Message::getScrollBarHighlight() const
if (this->flags.has(MessageFlag::Highlighted) ||
this->flags.has(MessageFlag::HighlightedWhisper))
{
return SBHighlight(SBHighlight::Highlight);
return SBHighlight(this->highlightColor);
}
else if (this->flags.has(MessageFlag::Subscription))
else if (this->flags.has(MessageFlag::Subscription) &&
getSettings()->enableSubHighlight)
{
return SBHighlight(SBHighlight::Subscription);
return SBHighlight(
ColorProvider::instance().color(ColorType::Subscription));
}
return SBHighlight();
}

View file

@ -33,6 +33,7 @@ enum class MessageFlag : uint32_t {
Whisper = (1 << 16),
HighlightedWhisper = (1 << 17),
Debug = (1 << 18),
Similar = (1 << 19),
};
using MessageFlags = FlagsEnum<MessageFlag>;
@ -55,6 +56,7 @@ struct Message : boost::noncopyable {
QString displayName;
QString localizedName;
QString timeoutUser;
std::shared_ptr<QColor> highlightColor;
uint32_t count = 1;
std::vector<std::unique_ptr<MessageElement>> elements;

View file

@ -25,6 +25,19 @@
namespace chatterino {
namespace {
QColor blendColors(const QColor &base, const QColor &apply)
{
const qreal &alpha = apply.alphaF();
QColor result;
result.setRgbF(base.redF() * (1 - alpha) + apply.redF() * alpha,
base.greenF() * (1 - alpha) + apply.greenF() * alpha,
base.blueF() * (1 - alpha) + apply.blueF() * alpha);
return result;
}
} // namespace
MessageLayout::MessageLayout(MessagePtr message)
: message_(message)
, container_(std::make_shared<MessageLayoutContainer>())
@ -128,6 +141,12 @@ void MessageLayout::actuallyLayout(int width, MessageElementFlags flags)
continue;
}
if (getSettings()->hideSimilar &&
this->message_->flags.has(MessageFlag::Similar))
{
continue;
}
element->addToContainer(*this->container_, flags);
}
@ -249,21 +268,33 @@ void MessageLayout::updateBuffer(QPixmap *buffer, int /*messageIndex*/,
painter.setRenderHint(QPainter::SmoothPixmapTransform);
// draw background
QColor backgroundColor = app->themes->messages.backgrounds.regular;
QColor backgroundColor = [this, &app] {
if (getSettings()->alternateMessages.getValue() &&
this->flags.has(MessageLayoutFlag::AlternateBackground))
{
return app->themes->messages.backgrounds.alternate;
}
else
{
return app->themes->messages.backgrounds.regular;
}
}();
if ((this->message_->flags.has(MessageFlag::Highlighted) ||
this->message_->flags.has(MessageFlag::HighlightedWhisper)) &&
!this->flags.has(MessageLayoutFlag::IgnoreHighlights))
{
backgroundColor = app->themes->messages.backgrounds.highlighted;
// Blend highlight color with usual background color
backgroundColor =
blendColors(backgroundColor, *this->message_->highlightColor);
}
else if (this->message_->flags.has(MessageFlag::Subscription))
else if (this->message_->flags.has(MessageFlag::Subscription) &&
getSettings()->enableSubHighlight)
{
backgroundColor = app->themes->messages.backgrounds.subscription;
}
else if (getSettings()->alternateMessages.getValue() &&
this->flags.has(MessageLayoutFlag::AlternateBackground))
{
backgroundColor = app->themes->messages.backgrounds.alternate;
// Blend highlight color with usual background color
backgroundColor = blendColors(
backgroundColor,
*ColorProvider::instance().color(ColorType::Subscription));
}
else if (this->message_->flags.has(MessageFlag::AutoMod))
{

View file

@ -5,7 +5,6 @@
#include "common/Common.hpp"
#include "common/NetworkRequest.hpp"
#include "debug/Log.hpp"
#include "messages/Emote.hpp"
#include "messages/Image.hpp"
#include "messages/ImageSet.hpp"

View file

@ -0,0 +1,124 @@
#include "providers/colors/ColorProvider.hpp"
#include "controllers/highlights/HighlightController.hpp"
#include "singletons/Theme.hpp"
namespace chatterino {
const ColorProvider &ColorProvider::instance()
{
static ColorProvider instance;
return instance;
}
ColorProvider::ColorProvider()
: typeColorMap_()
, defaultColors_()
{
this->initTypeColorMap();
this->initDefaultColors();
}
const std::shared_ptr<QColor> ColorProvider::color(ColorType type) const
{
return this->typeColorMap_.at(type);
}
void ColorProvider::updateColor(ColorType type, QColor color)
{
auto colorPtr = this->typeColorMap_.at(type);
*colorPtr = color;
}
QSet<QColor> ColorProvider::recentColors() const
{
QSet<QColor> retVal;
/*
* Currently, only colors used in highlight phrases are considered. This
* may change at any point in the future.
*/
for (auto phrase : getApp()->highlights->phrases)
{
retVal.insert(*phrase.getColor());
}
for (auto userHl : getApp()->highlights->highlightedUsers)
{
retVal.insert(*userHl.getColor());
}
// Insert preset highlight colors
retVal.insert(*this->color(ColorType::SelfHighlight));
retVal.insert(*this->color(ColorType::Subscription));
retVal.insert(*this->color(ColorType::Whisper));
return retVal;
}
const std::vector<QColor> &ColorProvider::defaultColors() const
{
return this->defaultColors_;
}
void ColorProvider::initTypeColorMap()
{
// Read settings for custom highlight colors and save them in map.
// If no custom values can be found, set up default values instead.
auto backgrounds = getApp()->themes->messages.backgrounds;
QString customColor = getSettings()->selfHighlightColor;
if (QColor(customColor).isValid())
{
this->typeColorMap_.insert(
{ColorType::SelfHighlight, std::make_shared<QColor>(customColor)});
}
else
{
this->typeColorMap_.insert(
{ColorType::SelfHighlight,
std::make_shared<QColor>(backgrounds.highlighted)});
}
customColor = getSettings()->subHighlightColor;
if (QColor(customColor).isValid())
{
this->typeColorMap_.insert(
{ColorType::Subscription, std::make_shared<QColor>(customColor)});
}
else
{
this->typeColorMap_.insert(
{ColorType::Subscription,
std::make_shared<QColor>(backgrounds.subscription)});
}
customColor = getSettings()->whisperHighlightColor;
if (QColor(customColor).isValid())
{
this->typeColorMap_.insert(
{ColorType::Whisper, std::make_shared<QColor>(customColor)});
}
else
{
this->typeColorMap_.insert(
{ColorType::Whisper,
std::make_shared<QColor>(backgrounds.highlighted)});
}
}
void ColorProvider::initDefaultColors()
{
// Init default colors
this->defaultColors_.emplace_back(31, 141, 43, 127); // Green-ish
this->defaultColors_.emplace_back(28, 126, 141, 127); // Blue-ish
this->defaultColors_.emplace_back(136, 141, 49, 127); // Golden-ish
this->defaultColors_.emplace_back(143, 48, 24, 127); // Red-ish
this->defaultColors_.emplace_back(28, 141, 117, 127); // Cyan-ish
auto backgrounds = getApp()->themes->messages.backgrounds;
this->defaultColors_.push_back(backgrounds.highlighted);
this->defaultColors_.push_back(backgrounds.subscription);
}
} // namespace chatterino

View file

@ -0,0 +1,53 @@
#pragma once
namespace chatterino {
enum class ColorType { SelfHighlight, Subscription, Whisper };
class ColorProvider
{
public:
static const ColorProvider &instance();
/**
* @brief Return a std::shared_ptr to the color of the requested ColorType.
*
* If a custom color has been set for the requested ColorType, it is
* returned. If no custom color exists for the type, a default color is
* returned.
*
* We need to do this in order to be able to dynamically update the colors
* of already parsed predefined (self highlights, subscriptions,
* and whispers) highlights.
*/
const std::shared_ptr<QColor> color(ColorType type) const;
void updateColor(ColorType type, QColor color);
/**
* @brief Return a set of recently used colors used anywhere in Chatterino.
*/
QSet<QColor> recentColors() const;
/**
* @brief Return a vector of colors that are good defaults for use
* throughout the program.
*/
const std::vector<QColor> &defaultColors() const;
private:
ColorProvider();
void initTypeColorMap();
void initDefaultColors();
std::unordered_map<ColorType, std::shared_ptr<QColor>> typeColorMap_;
std::vector<QColor> defaultColors_;
};
} // namespace chatterino
// Adapted from Qt example: https://doc.qt.io/qt-5/qhash.html#qhash
inline uint qHash(const QColor &key)
{
return qHash(key.name(QColor::HexArgb));
}

View file

@ -1,7 +1,6 @@
#include "providers/emoji/Emojis.hpp"
#include "Application.hpp"
#include "debug/Log.hpp"
#include "messages/Emote.hpp"
#include "singletons/Settings.hpp"
@ -132,8 +131,9 @@ void Emojis::loadEmojis()
if (result.Code() != rapidjson::kParseErrorNone)
{
log("JSON parse error: {} ({})",
rapidjson::GetParseError_En(result.Code()), result.Offset());
qDebug() << "JSON parse error:"
<< rapidjson::GetParseError_En(result.Code()) << "("
<< result.Offset() << ")";
return;
}
@ -165,8 +165,8 @@ void Emojis::loadEmojis()
auto toneNameIt = toneNames.find(tone);
if (toneNameIt == toneNames.end())
{
log("Tone with key {} does not exist in tone names map",
tone);
qDebug() << "Tone with key" << tone.c_str()
<< "does not exist in tone names map";
continue;
}

View file

@ -4,7 +4,6 @@
#include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "debug/Log.hpp"
#include "messages/Emote.hpp"
#include "messages/Image.hpp"
@ -186,7 +185,8 @@ void FfzEmotes::loadChannel(
const QString &channelId, std::function<void(EmoteMap &&)> emoteCallback,
std::function<void(boost::optional<EmotePtr>)> modBadgeCallback)
{
log("[FFZEmotes] Reload FFZ Channel Emotes for channel {}\n", channelId);
qDebug() << "[FFZEmotes] Reload FFZ Channel Emotes for channel"
<< channelId;
NetworkRequest("https://api.frankerfacez.com/v1/room/id/" + channelId)
@ -211,13 +211,13 @@ void FfzEmotes::loadChannel(
else if (result.status() == NetworkResult::timedoutStatus)
{
// TODO: Auto retry in case of a timeout, with a delay
log("Fetching FFZ emotes for channel {} failed due to timeout",
channelId);
qDebug() << "Fetching FFZ emotes for channel" << channelId
<< "failed due to timeout";
}
else
{
log("Error fetching FFZ emotes for channel {}, error {}",
channelId, result.status());
qDebug() << "Error fetching FFZ emotes for channel" << channelId
<< ", error" << result.status();
}
})
.execute();

View file

@ -2,7 +2,6 @@
#include "common/Channel.hpp"
#include "common/Common.hpp"
#include "debug/Log.hpp"
#include "messages/LimitedQueueSnapshot.hpp"
#include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp"
@ -66,7 +65,7 @@ AbstractIrcServer::AbstractIrcServer()
if (!this->readConnection_->isConnected())
{
log("Trying to reconnect... {}", this->falloffCounter_);
qDebug() << "Trying to reconnect..." << this->falloffCounter_;
this->connect();
}
});
@ -159,11 +158,12 @@ ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName)
}
this->channels.insert(channelName, chan);
this->connections_.emplace_back(chan->destroyed.connect([this,
channelName] {
this->connections_.emplace_back(
chan->destroyed.connect([this, channelName] {
// fourtf: issues when the server itself is destroyed
log("[AbstractIrcServer::addChannel] {} was destroyed", channelName);
qDebug() << "[AbstractIrcServer::addChannel]" << channelName
<< "was destroyed";
this->channels.remove(channelName);
if (this->readConnection_)

View file

@ -1,7 +1,15 @@
#include "IrcConnection2.hpp"
#include "common/Version.hpp"
namespace chatterino {
namespace {
const auto payload = QString("chatterino/" CHATTERINO_VERSION);
} // namespace
IrcConnection::IrcConnection(QObject *parent)
: Communi::IrcConnection(parent)
{
@ -11,10 +19,17 @@ IrcConnection::IrcConnection(QObject *parent)
QObject::connect(&this->pingTimer_, &QTimer::timeout, [this] {
if (this->isConnected())
{
if (this->waitingForPong_.load())
{
// Not sending another ping as we haven't received the matching pong yet
return;
}
if (!this->recentlyReceivedMessage_.load())
{
this->sendRaw("PING chatterino/ping");
this->sendRaw("PING " + payload);
this->reconnectTimer_.start();
this->waitingForPong_ = true;
}
this->recentlyReceivedMessage_ = false;
}
@ -30,6 +45,17 @@ IrcConnection::IrcConnection(QObject *parent)
}
});
QObject::connect(this, &Communi::IrcConnection::connected, this,
[this] { this->waitingForPong_ = false; });
QObject::connect(this, &Communi::IrcConnection::pongMessageReceived,
[this](Communi::IrcPongMessage *message) {
if (message->argument() == payload)
{
this->waitingForPong_ = false;
}
});
QObject::connect(this, &Communi::IrcConnection::messageReceived,
[this](Communi::IrcMessage *) {
this->recentlyReceivedMessage_ = true;

View file

@ -18,6 +18,9 @@ private:
QTimer pingTimer_;
QTimer reconnectTimer_;
std::atomic<bool> recentlyReceivedMessage_{true};
// waitingForPong_ is set to true when we send a PING message, and back to false when we receive the matching PONG response
std::atomic<bool> waitingForPong_{false};
};
} // namespace chatterino

View file

@ -3,7 +3,6 @@
#include "Application.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/highlights/HighlightController.hpp"
#include "debug/Log.hpp"
#include "messages/LimitedQueue.hpp"
#include "messages/Message.hpp"
#include "providers/twitch/TwitchAccountManager.hpp"
@ -22,6 +21,97 @@
namespace chatterino {
static float relativeSimilarity(const QString &str1, const QString &str2)
{
// Longest Common Substring Problem
std::vector<std::vector<int>> tree(str1.size(),
std::vector<int>(str2.size(), 0));
int z = 0;
for (int i = 0; i < str1.size(); ++i)
{
for (int j = 0; j < str2.size(); ++j)
{
if (str1[i] == str2[j])
{
if (i == 0 || j == 0)
{
tree[i][j] = 1;
}
else
{
tree[i][j] = tree[i - 1][j - 1] + 1;
}
if (tree[i][j] > z)
{
z = tree[i][j];
}
}
else
{
tree[i][j] = 0;
}
}
}
return z == 0 ? 0.f : float(z) / std::max(str1.size(), str2.size());
};
float IrcMessageHandler::similarity(
MessagePtr msg, const LimitedQueueSnapshot<MessagePtr> &messages)
{
float similarityPercent = 0.0f;
int bySameUser = 0;
for (int i = 1; bySameUser < getSettings()->hideSimilarMaxMessagesToCheck;
++i)
{
if (messages.size() < i)
{
break;
}
const auto &prevMsg = messages[messages.size() - i];
if (prevMsg->parseTime.secsTo(QTime::currentTime()) >=
getSettings()->hideSimilarMaxDelay)
{
break;
}
if (msg->loginName != prevMsg->loginName)
{
continue;
}
++bySameUser;
similarityPercent = std::max(
similarityPercent,
relativeSimilarity(msg->messageText, prevMsg->messageText));
}
return similarityPercent;
}
void IrcMessageHandler::setSimilarityFlags(MessagePtr msg, ChannelPtr chan)
{
if (getSettings()->similarityEnabled)
{
bool isMyself = msg->loginName ==
getApp()->accounts->twitch.getCurrent()->getUserName();
bool hideMyself = getSettings()->hideSimilarMyself;
if (isMyself && !hideMyself)
{
return;
}
if (IrcMessageHandler::similarity(msg, chan->getMessageSnapshot()) >
getSettings()->similarityPercentage)
{
msg->flags.set(MessageFlag::Similar, true);
if (getSettings()->colorSimilarDisabled)
{
msg->flags.set(MessageFlag::Disabled, true);
}
}
}
}
static QMap<QString, QString> parseBadges(QString badgesString)
{
QMap<QString, QString> badges;
@ -134,7 +224,16 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message,
}
auto msg = builder.build();
IrcMessageHandler::setSimilarityFlags(msg, chan);
if (!msg->flags.has(MessageFlag::Similar) ||
(!getSettings()->hideSimilar &&
getSettings()->shownSimilarTriggerHighlights))
{
builder.triggerHighlights();
}
auto highlighted = msg->flags.has(MessageFlag::Highlighted);
if (!isSub)
@ -235,9 +334,8 @@ void IrcMessageHandler::handleClearChatMessage(Communi::IrcMessage *message)
if (chan->isEmpty())
{
log("[IrcMessageHandler:handleClearChatMessage] Twitch channel {} not "
"found",
chanName);
qDebug() << "[IrcMessageHandler:handleClearChatMessage] Twitch channel"
<< chanName << "not found";
return;
}
@ -300,10 +398,9 @@ void IrcMessageHandler::handleClearMessageMessage(Communi::IrcMessage *message)
if (chan->isEmpty())
{
log("[IrcMessageHandler:handleClearMessageMessage] Twitch channel {} "
"not "
"found",
chanName);
qDebug()
<< "[IrcMessageHandler:handleClearMessageMessage] Twitch channel"
<< chanName << "not found";
return;
}
@ -356,7 +453,7 @@ void IrcMessageHandler::handleUserStateMessage(Communi::IrcMessage *message)
void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *message)
{
auto app = getApp();
log("Received whisper!");
qDebug() << "Received whisper!";
MessageParseArgs args;
args.isReceivedWhisper = true;
@ -568,10 +665,8 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message)
if (channel->isEmpty())
{
log("[IrcManager:handleNoticeMessage] Channel {} not found in "
"channel "
"manager ",
channelName);
qDebug() << "[IrcManager:handleNoticeMessage] Channel"
<< channelName << "not found in channel manager";
return;
}

View file

@ -1,6 +1,7 @@
#pragma once
#include <IrcMessage>
#include "common/Channel.hpp"
#include "messages/Message.hpp"
namespace chatterino {
@ -49,6 +50,10 @@ public:
void handleJoinMessage(Communi::IrcMessage *message);
void handlePartMessage(Communi::IrcMessage *message);
static float similarity(MessagePtr msg,
const LimitedQueueSnapshot<MessagePtr> &messages);
static void setSimilarityFlags(MessagePtr message, ChannelPtr channel);
private:
void addMessage(Communi::IrcMessage *message, const QString &target,
const QString &content, TwitchIrcServer &server,

View file

@ -2,7 +2,6 @@
#include "common/Common.hpp"
#include "common/NetworkRequest.hpp"
#include "debug/Log.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include <QJsonArray>
@ -28,6 +27,13 @@ PartialTwitchUser PartialTwitchUser::byId(const QString &id)
void PartialTwitchUser::getId(std::function<void(QString)> successCallback,
const QObject *caller)
{
getId(
successCallback, [] {}, caller);
}
void PartialTwitchUser::getId(std::function<void(QString)> successCallback,
std::function<void()> failureCallback,
const QObject *caller)
{
assert(!this->username_.isEmpty());
@ -35,35 +41,38 @@ void PartialTwitchUser::getId(std::function<void(QString)> successCallback,
this->username_)
.caller(caller)
.authorizeTwitchV5(getDefaultClientID())
.onSuccess([successCallback](auto result) -> Outcome {
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
if (!root.value("users").isArray())
{
log("API Error while getting user id, users is not an array");
qDebug()
<< "API Error while getting user id, users is not an array";
failureCallback();
return Failure;
}
auto users = root.value("users").toArray();
if (users.size() != 1)
{
log("API Error while getting user id, users array size is not "
"1");
qDebug() << "API Error while getting user id, users array size "
"is not 1";
failureCallback();
return Failure;
}
if (!users[0].isObject())
{
log("API Error while getting user id, first user is not an "
"object");
qDebug() << "API Error while getting user id, first user is "
"not an object";
failureCallback();
return Failure;
}
auto firstUser = users[0].toObject();
auto id = firstUser.value("_id");
if (!id.isString())
{
log("API Error: while getting user id, first user object `_id` "
"key "
"is not a "
"string");
qDebug() << "API Error: while getting user id, first user "
"object `_id` key is not a string";
failureCallback();
return Failure;
}
successCallback(id.toString());

View file

@ -21,6 +21,10 @@ public:
void getId(std::function<void(QString)> successCallback,
const QObject *caller = nullptr);
void getId(std::function<void(QString)> successCallback,
std::function<void()> failureCallback,
const QObject *caller = nullptr);
};
} // namespace chatterino

View file

@ -1,8 +1,8 @@
#include "providers/twitch/PubsubClient.hpp"
#include "debug/Log.hpp"
#include "providers/twitch/PubsubActions.hpp"
#include "providers/twitch/PubsubHelpers.hpp"
#include "util/Helpers.hpp"
#include "util/RapidjsonHelpers.hpp"
#include <rapidjson/error/en.h>
@ -77,14 +77,14 @@ namespace detail {
return true;
}
void PubSubClient::unlistenPrefix(const std::string &prefix)
void PubSubClient::unlistenPrefix(const QString &prefix)
{
std::vector<std::string> topics;
std::vector<QString> topics;
for (auto it = this->listeners_.begin(); it != this->listeners_.end();)
{
const auto &listener = *it;
if (listener.topic.find(prefix) == 0)
if (listener.topic.startsWith(prefix))
{
topics.push_back(listener.topic);
it = this->listeners_.erase(it);
@ -116,16 +116,14 @@ namespace detail {
{
assert(this->awaitingPong_);
log("Got pong!");
this->awaitingPong_ = false;
}
bool PubSubClient::isListeningToTopic(const std::string &payload)
bool PubSubClient::isListeningToTopic(const QString &topic)
{
for (const auto &listener : this->listeners_)
{
if (listener.topic == payload)
if (listener.topic == topic)
{
return true;
}
@ -156,9 +154,8 @@ namespace detail {
if (self->awaitingPong_)
{
log("No pong response, disconnect!");
// TODO(pajlada): Label this connection as "disconnect
// me"
qDebug() << "No pong response, disconnect!";
// TODO(pajlada): Label this connection as "disconnect me"
}
});
@ -181,7 +178,8 @@ namespace detail {
if (ec)
{
log("Error sending message {}: {}", payload, ec.message());
qDebug() << "Error sending message" << payload << ":"
<< ec.message().c_str();
// TODO(pajlada): Check which error code happened and maybe
// gracefully handle it
@ -223,7 +221,7 @@ PubSub::PubSub()
if (!data.HasMember("args"))
{
log("Missing required args member");
qDebug() << "Missing required args member";
return;
}
@ -231,13 +229,13 @@ PubSub::PubSub()
if (!args.IsArray())
{
log("args member must be an array");
qDebug() << "args member must be an array";
return;
}
if (args.Size() == 0)
{
log("Missing duration argument in slowmode on");
qDebug() << "Missing duration argument in slowmode on";
return;
}
@ -245,7 +243,7 @@ PubSub::PubSub()
if (!durationArg.IsString())
{
log("Duration arg must be a string");
qDebug() << "Duration arg must be a string";
return;
}
@ -338,7 +336,7 @@ PubSub::PubSub()
}
catch (const std::runtime_error &ex)
{
log("Error parsing moderation action: {}", ex.what());
qDebug() << "Error parsing moderation action:" << ex.what();
}
action.modded = false;
@ -368,7 +366,7 @@ PubSub::PubSub()
}
catch (const std::runtime_error &ex)
{
log("Error parsing moderation action: {}", ex.what());
qDebug() << "Error parsing moderation action:" << ex.what();
}
action.modded = true;
@ -417,7 +415,7 @@ PubSub::PubSub()
}
catch (const std::runtime_error &ex)
{
log("Error parsing moderation action: {}", ex.what());
qDebug() << "Error parsing moderation action:" << ex.what();
}
};
@ -454,7 +452,7 @@ PubSub::PubSub()
}
catch (const std::runtime_error &ex)
{
log("Error parsing moderation action: {}", ex.what());
qDebug() << "Error parsing moderation action:" << ex.what();
}
};
@ -485,7 +483,7 @@ PubSub::PubSub()
}
catch (const std::runtime_error &ex)
{
log("Error parsing moderation action: {}", ex.what());
qDebug() << "Error parsing moderation action:" << ex.what();
}
};
@ -516,7 +514,7 @@ PubSub::PubSub()
}
catch (const std::runtime_error &ex)
{
log("Error parsing moderation action: {}", ex.what());
qDebug() << "Error parsing moderation action:" << ex.what();
}
};
@ -568,7 +566,7 @@ PubSub::PubSub()
}
catch (const std::runtime_error &ex)
{
log("Error parsing moderation action: {}", ex.what());
qDebug() << "Error parsing moderation action:" << ex.what();
}
};
@ -597,7 +595,7 @@ PubSub::PubSub()
}
catch (const std::runtime_error &ex)
{
log("Error parsing moderation action: {}", ex.what());
qDebug() << "Error parsing moderation action:" << ex.what();
}
};
@ -626,7 +624,7 @@ PubSub::PubSub()
}
catch (const std::runtime_error &ex)
{
log("Error parsing moderation action: {}", ex.what());
qDebug() << "Error parsing moderation action:" << ex.what();
}
};
@ -655,7 +653,7 @@ PubSub::PubSub()
}
catch (const std::runtime_error &ex)
{
log("Error parsing moderation action: {}", ex.what());
qDebug() << "Error parsing moderation action:" << ex.what();
}
};
@ -685,7 +683,7 @@ PubSub::PubSub()
}
catch (const std::runtime_error &ex)
{
log("Error parsing moderation action: {}", ex.what());
qDebug() << "Error parsing moderation action:" << ex.what();
}
};
@ -738,7 +736,7 @@ void PubSub::addClient()
if (ec)
{
log("Unable to establish connection: {}", ec.message());
qDebug() << "Unable to establish connection:" << ec.message().c_str();
return;
}
@ -753,20 +751,23 @@ void PubSub::start()
void PubSub::listenToWhispers(std::shared_ptr<TwitchAccount> account)
{
static const QString topicFormat("whispers.%1");
assert(account != nullptr);
std::string userID = account->getUserId().toStdString();
auto userID = account->getUserId();
log("Connection open!");
qDebug() << "Connection open!";
websocketpp::lib::error_code ec;
std::vector<std::string> topics({"whispers." + userID});
std::vector<QString> topics({topicFormat.arg(userID)});
this->listen(createListenMessage(topics, account));
if (ec)
{
log("Unable to send message to websocket server: {}", ec.message());
qDebug() << "Unable to send message to websocket server:"
<< ec.message().c_str();
return;
}
}
@ -783,26 +784,26 @@ void PubSub::unlistenAllModerationActions()
void PubSub::listenToChannelModerationActions(
const QString &channelID, std::shared_ptr<TwitchAccount> account)
{
static const QString topicFormat("chat_moderator_actions.%1.%2");
assert(!channelID.isEmpty());
assert(account != nullptr);
QString userID = account->getUserId();
if (userID.isEmpty())
return;
std::string topic(fS("chat_moderator_actions.{}.{}", userID, channelID));
auto topic = topicFormat.arg(userID).arg(channelID);
if (this->isListeningToTopic(topic))
{
log("We are already listening to topic {}", topic);
return;
}
log("Listen to topic {}", topic);
qDebug() << "Listen to topic" << topic;
this->listenToTopic(topic, account);
}
void PubSub::listenToTopic(const std::string &topic,
void PubSub::listenToTopic(const QString &topic,
std::shared_ptr<TwitchAccount> account)
{
auto message = createListenMessage({topic}, account);
@ -814,20 +815,19 @@ void PubSub::listen(rapidjson::Document &&msg)
{
if (this->tryListen(msg))
{
log("Successfully listened!");
return;
}
this->addClient();
log("Added to the back of the queue");
qDebug() << "Added to the back of the queue";
this->requests.emplace_back(
std::make_unique<rapidjson::Document>(std::move(msg)));
}
bool PubSub::tryListen(rapidjson::Document &msg)
{
log("tryListen with {} clients", this->clients.size());
qDebug() << "tryListen with" << this->clients.size() << "clients";
for (const auto &p : this->clients)
{
const auto &client = p.second;
@ -840,7 +840,7 @@ bool PubSub::tryListen(rapidjson::Document &msg)
return false;
}
bool PubSub::isListeningToTopic(const std::string &topic)
bool PubSub::isListeningToTopic(const QString &topic)
{
for (const auto &p : this->clients)
{
@ -865,24 +865,24 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl,
if (!res)
{
log("Error parsing message '{}' from PubSub: {}", payload,
rapidjson::GetParseError_En(res.Code()));
qDebug() << "Error parsing message '" << payload.c_str()
<< "' from PubSub:" << rapidjson::GetParseError_En(res.Code());
return;
}
if (!msg.IsObject())
{
log("Error parsing message '{}' from PubSub. Root object is not an "
"object",
payload);
qDebug() << "Error parsing message '" << payload.c_str()
<< "' from PubSub. Root object is not an "
"object";
return;
}
std::string type;
QString type;
if (!rj::getSafe(msg, "type", type))
{
log("Missing required string member `type` in message root");
qDebug() << "Missing required string member `type` in message root";
return;
}
@ -894,7 +894,7 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl,
{
if (!msg.HasMember("data"))
{
log("Missing required object member `data` in message root");
qDebug() << "Missing required object member `data` in message root";
return;
}
@ -902,7 +902,7 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl,
if (!data.IsObject())
{
log("Member `data` must be an object");
qDebug() << "Member `data` must be an object";
return;
}
@ -922,7 +922,7 @@ void PubSub::onMessage(websocketpp::connection_hdl hdl,
}
else
{
log("Unknown message type: {}", type);
qDebug() << "Unknown message type:" << type;
}
}
@ -983,7 +983,7 @@ PubSub::WebsocketContextPtr PubSub::onTLSInit(websocketpp::connection_hdl hdl)
}
catch (const std::exception &e)
{
log("Exception caught in OnTLSInit: {}", e.what());
qDebug() << "Exception caught in OnTLSInit:" << e.what();
}
return ctx;
@ -991,21 +991,21 @@ PubSub::WebsocketContextPtr PubSub::onTLSInit(websocketpp::connection_hdl hdl)
void PubSub::handleListenResponse(const rapidjson::Document &msg)
{
std::string error;
QString error;
if (rj::getSafe(msg, "error", error))
{
std::string nonce;
QString nonce;
rj::getSafe(msg, "nonce", nonce);
if (error.empty())
if (error.isEmpty())
{
log("Successfully listened to nonce {}", nonce);
qDebug() << "Successfully listened to nonce" << nonce;
// Nothing went wrong
return;
}
log("PubSub error: {} on nonce {}", error, nonce);
qDebug() << "PubSub error:" << error << "on nonce" << nonce;
return;
}
}
@ -1016,7 +1016,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData)
if (!rj::getSafe(outerData, "topic", topic))
{
log("Missing required string member `topic` in outerData");
qDebug() << "Missing required string member `topic` in outerData";
return;
}
@ -1024,7 +1024,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData)
if (!rj::getSafe(outerData, "message", payload))
{
log("Expected string message in outerData");
qDebug() << "Expected string message in outerData";
return;
}
@ -1034,8 +1034,8 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData)
if (!res)
{
log("Error parsing message '{}' from PubSub: {}", payload,
rapidjson::GetParseError_En(res.Code()));
qDebug() << "Error parsing message '" << payload.c_str()
<< "' from PubSub:" << rapidjson::GetParseError_En(res.Code());
return;
}
@ -1045,7 +1045,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData)
if (!rj::getSafe(msg, "type", whisperType))
{
log("Bad whisper data");
qDebug() << "Bad whisper data";
return;
}
@ -1063,7 +1063,7 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData)
}
else
{
log("Invalid whisper type: {}", whisperType);
qDebug() << "Invalid whisper type:" << whisperType.c_str();
assert(false);
return;
}
@ -1078,7 +1078,8 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData)
if (!rj::getSafe(data, "moderation_action", moderationAction))
{
log("Missing moderation action in data: {}", rj::stringify(data));
qDebug() << "Missing moderation action in data:"
<< rj::stringify(data).c_str();
return;
}
@ -1086,7 +1087,8 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData)
if (handlerIt == this->moderationActionHandlers.end())
{
log("No handler found for moderation action {}", moderationAction);
qDebug() << "No handler found for moderation action"
<< moderationAction.c_str();
return;
}
@ -1095,16 +1097,16 @@ void PubSub::handleMessageResponse(const rapidjson::Value &outerData)
}
else
{
log("Unknown topic: {}", topic);
qDebug() << "Unknown topic:" << topic;
return;
}
}
void PubSub::runThread()
{
log("Start pubsub manager thread");
qDebug() << "Start pubsub manager thread";
this->websocketClient.run();
log("Done with pubsub manager thread");
qDebug() << "Done with pubsub manager thread";
}
} // namespace chatterino

View file

@ -15,7 +15,6 @@
#include <map>
#include <memory>
#include <set>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
@ -33,7 +32,7 @@ using WebsocketErrorCode = websocketpp::lib::error_code;
namespace detail {
struct Listener {
std::string topic;
QString topic;
bool authed;
bool persistent;
bool confirmed = false;
@ -49,11 +48,11 @@ namespace detail {
void stop();
bool listen(rapidjson::Document &message);
void unlistenPrefix(const std::string &prefix);
void unlistenPrefix(const QString &prefix);
void handlePong();
bool isListeningToTopic(const std::string &topic);
bool isListeningToTopic(const QString &topic);
private:
void ping();
@ -135,13 +134,13 @@ public:
std::vector<std::unique_ptr<rapidjson::Document>> requests;
private:
void listenToTopic(const std::string &topic,
void listenToTopic(const QString &topic,
std::shared_ptr<TwitchAccount> account);
void listen(rapidjson::Document &&msg);
bool tryListen(rapidjson::Document &msg);
bool isListeningToTopic(const std::string &topic);
bool isListeningToTopic(const QString &topic);
void addClient();

View file

@ -46,8 +46,7 @@ bool getTargetUser(const rapidjson::Value &data, ActionUser &user)
return rj::getSafe(data, "target_user_id", user.id);
}
rapidjson::Document createListenMessage(
const std::vector<std::string> &topicsVec,
rapidjson::Document createListenMessage(const std::vector<QString> &topicsVec,
std::shared_ptr<TwitchAccount> account)
{
rapidjson::Document msg(rapidjson::kObjectType);
@ -75,8 +74,7 @@ rapidjson::Document createListenMessage(
return msg;
}
rapidjson::Document createUnlistenMessage(
const std::vector<std::string> &topicsVec)
rapidjson::Document createUnlistenMessage(const std::vector<QString> &topicsVec)
{
rapidjson::Document msg(rapidjson::kObjectType);
auto &a = msg.GetAllocator();

View file

@ -3,7 +3,6 @@
#include <boost/asio.hpp>
#include <boost/asio/steady_timer.hpp>
#include <memory>
#include "debug/Log.hpp"
#include "util/RapidjsonHelpers.hpp"
namespace chatterino {
@ -18,11 +17,10 @@ bool getCreatedByUser(const rapidjson::Value &data, ActionUser &user);
bool getTargetUser(const rapidjson::Value &data, ActionUser &user);
rapidjson::Document createListenMessage(
const std::vector<std::string> &topicsVec,
rapidjson::Document createListenMessage(const std::vector<QString> &topicsVec,
std::shared_ptr<TwitchAccount> account);
rapidjson::Document createUnlistenMessage(
const std::vector<std::string> &topicsVec);
const std::vector<QString> &topicsVec);
// Create timer using given ioService
template <typename Duration, typename Callback>
@ -35,7 +33,7 @@ void runAfter(boost::asio::io_service &ioService, Duration duration,
timer->async_wait([timer, cb](const boost::system::error_code &ec) {
if (ec)
{
log("Error in runAfter: {}", ec.message());
qDebug() << "Error in runAfter:" << ec.message().c_str();
return;
}
@ -53,7 +51,7 @@ void runAfter(std::shared_ptr<boost::asio::steady_timer> timer,
timer->async_wait([timer, cb](const boost::system::error_code &ec) {
if (ec)
{
log("Error in runAfter: {}", ec.message());
qDebug() << "Error in runAfter:" << ec.message().c_str();
return;
}

View file

@ -6,7 +6,6 @@
#include "common/Env.hpp"
#include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "debug/Log.hpp"
#include "providers/twitch/PartialTwitchUser.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include "singletons/Emotes.hpp"
@ -134,8 +133,8 @@ void TwitchAccount::loadIgnores()
TwitchUser ignoredUser;
if (!rj::getSafe(userIt->value, ignoredUser))
{
log("Error parsing twitch user JSON {}",
rj::stringify(userIt->value));
qDebug() << "Error parsing twitch user JSON"
<< rj::stringify(userIt->value).c_str();
continue;
}
@ -345,14 +344,14 @@ std::set<TwitchUser> TwitchAccount::getIgnores() const
void TwitchAccount::loadEmotes()
{
log("Loading Twitch emotes for user {}", this->getUserName());
qDebug() << "Loading Twitch emotes for user" << this->getUserName();
const auto &clientID = this->getOAuthClient();
const auto &oauthToken = this->getOAuthToken();
if (clientID.isEmpty() || oauthToken.isEmpty())
{
log("Missing Client ID or OAuth token");
qDebug() << "Missing Client ID or OAuth token";
return;
}
@ -363,7 +362,7 @@ void TwitchAccount::loadEmotes()
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
.onError([=](NetworkResult result) {
log("[TwitchAccount::loadEmotes] Error {}", result.status());
qDebug() << "[TwitchAccount::loadEmotes] Error" << result.status();
if (result.status() == 203)
{
// onFinished(FollowResult_NotFollowing);
@ -402,7 +401,8 @@ void TwitchAccount::autoModAllow(const QString msgID)
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
.onError([=](NetworkResult result) {
log("[TwitchAccounts::autoModAllow] Error {}", result.status());
qDebug() << "[TwitchAccounts::autoModAllow] Error"
<< result.status();
})
.execute();
}
@ -421,7 +421,8 @@ void TwitchAccount::autoModDeny(const QString msgID)
.authorizeTwitchV5(this->getOAuthClient(), this->getOAuthToken())
.onError([=](NetworkResult result) {
log("[TwitchAccounts::autoModDeny] Error {}", result.status());
qDebug() << "[TwitchAccounts::autoModDeny] Error"
<< result.status();
})
.execute();
}
@ -436,7 +437,7 @@ void TwitchAccount::parseEmotes(const rapidjson::Document &root)
auto emoticonSets = root.FindMember("emoticon_sets");
if (emoticonSets == root.MemberEnd() || !emoticonSets->value.IsObject())
{
log("No emoticon_sets in load emotes response");
qDebug() << "No emoticon_sets in load emotes response";
return;
}
@ -452,21 +453,21 @@ void TwitchAccount::parseEmotes(const rapidjson::Document &root)
{
if (!emoteJSON.IsObject())
{
log("Emote value was invalid");
qDebug() << "Emote value was invalid";
return;
}
uint64_t idNumber;
if (!rj::getSafe(emoteJSON, "id", idNumber))
{
log("No ID key found in Emote value");
qDebug() << "No ID key found in Emote value";
return;
}
QString _code;
if (!rj::getSafe(emoteJSON, "code", _code))
{
log("No code key found in Emote value");
qDebug() << "No code key found in Emote value";
return;
}
@ -481,6 +482,10 @@ void TwitchAccount::parseEmotes(const rapidjson::Document &root)
emoteData->emotes.emplace(code, emote);
}
std::sort(emoteSet->emotes.begin(), emoteSet->emotes.end(),
[](const TwitchEmote &l, const TwitchEmote &r) {
return l.name.string < r.name.string;
});
emoteData->emoteSets.emplace_back(emoteSet);
}
};
@ -489,7 +494,7 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet)
{
if (!emoteSet)
{
log("null emote set sent");
qDebug() << "null emote set sent";
return;
}
@ -505,7 +510,8 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet)
NetworkRequest(Env::get().twitchEmoteSetResolverUrl.arg(emoteSet->key))
.cache()
.onError([](NetworkResult result) {
log("Error code {} while loading emote set data", result.status());
qDebug() << "Error code" << result.status()
<< "while loading emote set data";
})
.onSuccess([emoteSet](auto result) -> Outcome {
auto root = result.parseRapidJson();
@ -527,7 +533,7 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr<EmoteSet> emoteSet)
return Failure;
}
log("Loaded twitch emote set data for {}!", emoteSet->key);
qDebug() << "Loaded twitch emote set data for" << emoteSet->key;
auto name = channelName;
name.detach();

View file

@ -1,7 +1,6 @@
#include "providers/twitch/TwitchAccountManager.hpp"
#include "common/Common.hpp"
#include "debug/Log.hpp"
#include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchCommon.hpp"
@ -103,23 +102,23 @@ void TwitchAccountManager::reloadUsers()
switch (this->addUser(userData))
{
case AddUserResponse::UserAlreadyExists: {
log("User {} already exists", userData.username);
qDebug() << "User" << userData.username << "already exists";
// Do nothing
}
break;
case AddUserResponse::UserValuesUpdated: {
log("User {} already exists, and values updated!",
userData.username);
qDebug() << "User" << userData.username
<< "already exists, and values updated!";
if (userData.username == this->getCurrent()->getUserName())
{
log("It was the current user, so we need to reconnect "
"stuff!");
qDebug() << "It was the current user, so we need to "
"reconnect stuff!";
this->currentUserChanged.invoke();
}
}
break;
case AddUserResponse::UserAdded: {
log("Added user {}", userData.username);
qDebug() << "Added user" << userData.username;
listUpdated = true;
}
break;
@ -140,15 +139,12 @@ void TwitchAccountManager::load()
auto user = this->findUserByUsername(newUsername);
if (user)
{
log("[AccountManager:currentUsernameChanged] User successfully "
"updated to {}",
newUsername);
qDebug() << "Twitch user updated to" << newUsername;
this->currentUser_ = user;
}
else
{
log("[AccountManager:currentUsernameChanged] User successfully "
"updated to anonymous");
qDebug() << "Twitch user updated to anonymous";
this->currentUser_ = this->anonymousUser_;
}
@ -170,11 +166,13 @@ bool TwitchAccountManager::isLoggedIn() const
bool TwitchAccountManager::removeUser(TwitchAccount *account)
{
static const QString accountFormat("/accounts/uid%1");
auto userID(account->getUserId());
if (!userID.isEmpty())
{
pajlada::Settings::SettingManager::removeSetting(
fS("/accounts/uid{}", userID));
accountFormat.arg(userID).toStdString());
}
if (account->getUserName() == this->currentUsername)

View file

@ -2,7 +2,6 @@
#include "common/Common.hpp"
#include "common/NetworkRequest.hpp"
#include "debug/Log.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include <QString>
@ -23,22 +22,23 @@ void TwitchApi::findUserId(const QString user,
auto root = result.parseJson();
if (!root.value("users").isArray())
{
log("API Error while getting user id, users is not an array");
qDebug()
<< "API Error while getting user id, users is not an array";
successCallback("");
return Failure;
}
auto users = root.value("users").toArray();
if (users.size() != 1)
{
log("API Error while getting user id, users array size is not "
"1");
qDebug() << "API Error while getting user id, users array size "
"is not 1";
successCallback("");
return Failure;
}
if (!users[0].isObject())
{
log("API Error while getting user id, first user is not an "
"object");
qDebug() << "API Error while getting user id, first user is "
"not an object";
successCallback("");
return Failure;
}
@ -46,10 +46,8 @@ void TwitchApi::findUserId(const QString user,
auto id = firstUser.value("_id");
if (!id.isString())
{
log("API Error: while getting user id, first user object `_id` "
"key "
"is not a "
"string");
qDebug() << "API Error: while getting user id, first user "
"object `_id` key is not a string";
successCallback("");
return Failure;
}
@ -73,8 +71,8 @@ void TwitchApi::findUserName(const QString userid,
auto name = root.value("name");
if (!name.isString())
{
log("API Error: while getting user name, `name` is not a "
"string");
qDebug() << "API Error: while getting user name, `name` is not "
"a string";
successCallback("");
return Failure;
}

View file

@ -7,7 +7,6 @@
#include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "debug/Log.hpp"
#include "messages/Emote.hpp"
namespace chatterino {

View file

@ -6,7 +6,6 @@
#include "common/NetworkRequest.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "controllers/notifications/NotificationController.hpp"
#include "debug/Log.hpp"
#include "messages/Message.hpp"
#include "providers/bttv/BttvEmotes.hpp"
#include "providers/bttv/LoadBttvChannelEmote.hpp"
@ -92,7 +91,7 @@ TwitchChannel::TwitchChannel(const QString &name,
, mod_(false)
, titleRefreshedTime_(QTime::currentTime().addSecs(-TITLE_REFRESH_PERIOD))
{
log("[TwitchChannel:{}] Opened", name);
qDebug() << "[TwitchChannel" << name << "] Opened";
this->liveStatusChanged.connect([this]() {
if (this->isLive() == 1)
@ -194,7 +193,8 @@ void TwitchChannel::sendMessage(const QString &message)
return;
}
log("[TwitchChannel:{}] Send message: {}", this->getName(), message);
qDebug() << "[TwitchChannel" << this->getName()
<< "] Send message:" << message;
// Do last message processing
QString parsedMessage = app->emotes->emojis.replaceShortCodes(message);
@ -491,8 +491,8 @@ void TwitchChannel::refreshLiveStatus()
if (roomID.isEmpty())
{
log("[TwitchChannel:{}] Refreshing live status (Missing ID)",
this->getName());
qDebug() << "[TwitchChannel" << this->getName()
<< "] Refreshing live status (Missing ID)";
this->setLive(false);
return;
}
@ -517,13 +517,13 @@ Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document)
{
if (!document.IsObject())
{
log("[TwitchChannel:refreshLiveStatus] root is not an object");
qDebug() << "[TwitchChannel:refreshLiveStatus] root is not an object";
return Failure;
}
if (!document.HasMember("stream"))
{
log("[TwitchChannel:refreshLiveStatus] Missing stream in root");
qDebug() << "[TwitchChannel:refreshLiveStatus] Missing stream in root";
return Failure;
}
@ -539,7 +539,8 @@ Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document)
if (!stream.HasMember("viewers") || !stream.HasMember("game") ||
!stream.HasMember("channel") || !stream.HasMember("created_at"))
{
log("[TwitchChannel:refreshLiveStatus] Missing members in stream");
qDebug()
<< "[TwitchChannel:refreshLiveStatus] Missing members in stream";
this->setLive(false);
return Failure;
}
@ -548,8 +549,8 @@ Outcome TwitchChannel::parseLiveStatus(const rapidjson::Document &document)
if (!streamChannel.IsObject() || !streamChannel.HasMember("status"))
{
log("[TwitchChannel:refreshLiveStatus] Missing member \"status\" in "
"channel");
qDebug() << "[TwitchChannel:refreshLiveStatus] Missing member "
"\"status\" in channel";
return Failure;
}
@ -743,6 +744,12 @@ void TwitchChannel::refreshCheerEmotes()
NetworkRequest::twitchRequest(url)
.onSuccess([this,
weak = weakOf<Channel>(this)](auto result) -> Outcome {
auto shared = weak.lock();
if (!shared)
{
return Failure;
}
auto cheerEmoteSets = ParseCheermoteSets(result.parseRapidJson());
std::vector<CheerEmoteSet> emoteSets;
@ -836,7 +843,7 @@ boost::optional<CheerEmote> TwitchChannel::cheerEmote(const QString &string)
int bitAmount = amount.toInt(&ok);
if (!ok)
{
log("Error parsing bit amount in cheerEmote");
qDebug() << "Error parsing bit amount in cheerEmote";
}
for (const auto &emote : set.cheerEmotes)
{

View file

@ -2,7 +2,6 @@
#include "common/NetworkRequest.hpp"
#include "debug/Benchmark.hpp"
#include "debug/Log.hpp"
#include "messages/Emote.hpp"
#include "messages/Image.hpp"
#include "util/RapidjsonHelpers.hpp"

View file

@ -1,5 +1,4 @@
#include "providers/twitch/TwitchHelpers.hpp"
#include "debug/Log.hpp"
namespace chatterino {
@ -7,7 +6,7 @@ bool trimChannelName(const QString &channelName, QString &outChannelName)
{
if (channelName.length() < 2)
{
log("channel name length below 2");
qDebug() << "channel name length below 2";
return false;
}

View file

@ -5,7 +5,6 @@
#include "controllers/highlights/HighlightController.hpp"
#include "controllers/ignores/IgnoreController.hpp"
#include "controllers/pings/PingController.hpp"
#include "debug/Log.hpp"
#include "messages/Message.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/twitch/TwitchBadges.hpp"
@ -65,6 +64,25 @@ QColor getRandomColor(const QVariant &userId)
return twitchUsernameColors[colorIndex];
}
QUrl getFallbackHighlightSound()
{
using namespace chatterino;
QString path = getSettings()->pathHighlightSound;
bool fileExists = QFileInfo::exists(path) && QFileInfo(path).isFile();
// Use fallback sound when checkbox is not checked
// or custom file doesn't exist
if (getSettings()->customHighlightSound && fileExists)
{
return QUrl::fromLocalFile(path);
}
else
{
return QUrl("qrc:/sounds/ping2.wav");
}
}
} // namespace
namespace chatterino {
@ -90,8 +108,6 @@ namespace {
QStringList parts = badgeInfo.split('/');
if (parts.size() != 2)
{
log("Skipping badge-info because it split weird: {}",
badgeInfo);
continue;
}
@ -110,7 +126,6 @@ namespace {
QStringList parts = badge.split('/');
if (parts.size() != 2)
{
log("Skipping badge because it split weird: {}", badge);
continue;
}
@ -159,8 +174,8 @@ bool TwitchMessageBuilder::isIgnored() const
{
if (phrase.isBlock() && phrase.isMatch(this->originalMessage_))
{
log("Blocking message because it contains ignored phrase {}",
phrase.getPattern());
qDebug() << "Blocking message because it contains ignored phrase"
<< phrase.getPattern();
return true;
}
}
@ -190,8 +205,8 @@ bool TwitchMessageBuilder::isIgnored() const
case ShowIgnoredUsersMessages::Never:
break;
}
log("Blocking message because it's from blocked user {}",
user.name);
qDebug() << "Blocking message because it's from blocked user"
<< user.name;
return true;
}
}
@ -237,17 +252,11 @@ void TwitchMessageBuilder::triggerHighlights()
if (auto player = getPlayer())
{
// update the media player url if necessary
QUrl highlightSoundUrl =
getSettings()->customHighlightSound
? QUrl::fromLocalFile(
getSettings()->pathHighlightSound.getValue())
: QUrl("qrc:/sounds/ping2.wav");
if (currentPlayerUrl != highlightSoundUrl)
if (currentPlayerUrl != this->highlightSoundUrl_)
{
player->setMedia(highlightSoundUrl);
player->setMedia(this->highlightSoundUrl_);
currentPlayerUrl = highlightSoundUrl;
currentPlayerUrl = this->highlightSoundUrl_;
}
player->play();
@ -427,8 +436,8 @@ void TwitchMessageBuilder::addWords(
auto emoteImage = std::get<1>(*currentTwitchEmote);
if (emoteImage == nullptr)
{
log("emoteImage nullptr {}",
std::get<2>(*currentTwitchEmote).string);
qDebug() << "emoteImage nullptr"
<< std::get<2>(*currentTwitchEmote).string;
}
this->emplace<EmoteElement>(emoteImage,
MessageElementFlag::TwitchEmote);
@ -771,7 +780,7 @@ void TwitchMessageBuilder::runIgnoreReplaces(
{
if (std::get<1>(*copy) == nullptr)
{
log("remem nullptr {}", std::get<2>(*copy).string);
qDebug() << "remem nullptr" << std::get<2>(*copy).string;
}
}
std::vector<std::tuple<int, EmotePtr, EmoteName>> v(
@ -809,7 +818,7 @@ void TwitchMessageBuilder::runIgnoreReplaces(
{
if (emote.second == nullptr)
{
log("emote null {}", emote.first.string);
qDebug() << "emote null" << emote.first.string;
}
twitchEmotes.push_back(std::tuple<int, EmotePtr, EmoteName>{
startIndex + pos, emote.second, emote.first});
@ -872,7 +881,7 @@ void TwitchMessageBuilder::runIgnoreReplaces(
{
if (std::get<1>(tup) == nullptr)
{
log("v nullptr {}", std::get<2>(tup).string);
qDebug() << "v nullptr" << std::get<2>(tup).string;
continue;
}
QRegularExpression emoteregex(
@ -941,7 +950,7 @@ void TwitchMessageBuilder::runIgnoreReplaces(
{
if (std::get<1>(tup) == nullptr)
{
log("v nullptr {}", std::get<2>(tup).string);
qDebug() << "v nullptr" << std::get<2>(tup).string;
continue;
}
QRegularExpression emoteregex(
@ -971,6 +980,39 @@ void TwitchMessageBuilder::parseHighlights()
{
auto app = getApp();
if (this->message().flags.has(MessageFlag::Subscription) &&
getSettings()->enableSubHighlight)
{
if (getSettings()->enableSubHighlightTaskbar)
{
this->highlightAlert_ = true;
}
if (getSettings()->enableSubHighlightSound)
{
this->highlightSound_ = true;
// Use custom sound if set, otherwise use fallback
if (!getSettings()->subHighlightSoundUrl.getValue().isEmpty())
{
this->highlightSoundUrl_ =
QUrl(getSettings()->subHighlightSoundUrl.getValue());
}
else
{
this->highlightSoundUrl_ = getFallbackHighlightSound();
}
}
this->message().flags.set(MessageFlag::Highlighted);
this->message().highlightColor =
ColorProvider::instance().color(ColorType::Subscription);
// This message was a subscription.
// Don't check for any other highlight phrases.
return;
}
auto currentUser = app->accounts->twitch.getCurrent();
QString currentUsername = currentUser->getUserName();
@ -981,6 +1023,39 @@ void TwitchMessageBuilder::parseHighlights()
return;
}
// Highlight because it's a whisper
if (this->args.isReceivedWhisper && getSettings()->enableWhisperHighlight)
{
if (getSettings()->enableWhisperHighlightTaskbar)
{
this->highlightAlert_ = true;
}
if (getSettings()->enableWhisperHighlightSound)
{
this->highlightSound_ = true;
// Use custom sound if set, otherwise use fallback
if (!getSettings()->whisperHighlightSoundUrl.getValue().isEmpty())
{
this->highlightSoundUrl_ =
QUrl(getSettings()->whisperHighlightSoundUrl.getValue());
}
else
{
this->highlightSoundUrl_ = getFallbackHighlightSound();
}
}
this->message().highlightColor =
ColorProvider::instance().color(ColorType::Whisper);
/*
* Do _NOT_ return yet, we might want to apply phrase/user name
* highlights (which override whisper color/sound).
*/
}
std::vector<HighlightPhrase> userHighlights =
app->highlights->highlightedUsers.cloneVector();
@ -991,29 +1066,39 @@ void TwitchMessageBuilder::parseHighlights()
{
continue;
}
log("Highlight because user {} sent a message",
this->ircMessage->nick());
if (!this->highlightVisual_)
{
this->highlightVisual_ = true;
this->message().flags.set(MessageFlag::Highlighted);
}
qDebug() << "Highlight because user" << this->ircMessage->nick()
<< "sent a message";
if (userHighlight.getAlert())
this->message().flags.set(MessageFlag::Highlighted);
this->message().highlightColor = userHighlight.getColor();
if (userHighlight.hasAlert())
{
this->highlightAlert_ = true;
}
if (userHighlight.getSound())
if (userHighlight.hasSound())
{
this->highlightSound_ = true;
// Use custom sound if set, otherwise use the fallback sound
if (userHighlight.hasCustomSound())
{
this->highlightSoundUrl_ = userHighlight.getSoundUrl();
}
else
{
this->highlightSoundUrl_ = getFallbackHighlightSound();
}
}
if (this->highlightAlert_ && this->highlightSound_)
{
// Break if no further action can be taken from other
// usernames Mostly used for regex stuff
break;
/*
* User name highlights "beat" highlight phrases: If a message has
* all attributes (color, taskbar flashing, sound) set, highlight
* phrases will not be checked.
*/
return;
}
}
@ -1032,7 +1117,9 @@ void TwitchMessageBuilder::parseHighlights()
{
HighlightPhrase selfHighlight(
currentUsername, getSettings()->enableSelfHighlightTaskbar,
getSettings()->enableSelfHighlightSound, false, false);
getSettings()->enableSelfHighlightSound, false, false,
getSettings()->selfHighlightSoundUrl.getValue(),
ColorProvider::instance().color(ColorType::SelfHighlight));
activeHighlights.emplace_back(std::move(selfHighlight));
}
@ -1044,45 +1131,43 @@ void TwitchMessageBuilder::parseHighlights()
continue;
}
log("Highlight because {} matches {}", this->originalMessage_,
highlight.getPattern());
if (!this->highlightVisual_)
{
this->highlightVisual_ = true;
this->message().flags.set(MessageFlag::Highlighted);
}
qDebug() << "Highlight because" << this->originalMessage_ << "matches"
<< highlight.getPattern();
if (highlight.getAlert())
this->message().flags.set(MessageFlag::Highlighted);
this->message().highlightColor = highlight.getColor();
if (highlight.hasAlert())
{
this->highlightAlert_ = true;
}
if (highlight.getSound())
// Only set highlightSound_ if it hasn't been set by username
// highlights already.
if (highlight.hasSound() && !this->highlightSound_)
{
this->highlightSound_ = true;
// Use custom sound if set, otherwise use fallback sound
if (highlight.hasCustomSound())
{
this->highlightSoundUrl_ = highlight.getSoundUrl();
}
else
{
this->highlightSoundUrl_ = getFallbackHighlightSound();
}
}
if (this->highlightAlert_ && this->highlightSound_)
{
// Break if no further action can be taken from other
// highlights This might change if highlights can have
// custom colors/sounds/actions
/*
* Break once no further attributes (taskbar, sound) can be
* applied.
*/
break;
}
}
// Highlight because it's a whisper
if (this->args.isReceivedWhisper && getSettings()->enableWhisperHighlight)
{
if (getSettings()->enableWhisperHighlightTaskbar)
{
this->highlightAlert_ = true;
}
if (getSettings()->enableWhisperHighlightSound)
{
this->highlightSound_ = true;
}
}
}
void TwitchMessageBuilder::appendTwitchEmote(
@ -1130,7 +1215,7 @@ void TwitchMessageBuilder::appendTwitchEmote(
start, app->emotes->twitch.getOrCreateEmote(id, name), name};
if (std::get<1>(tup) == nullptr)
{
log("nullptr {}", std::get<2>(tup).string);
qDebug() << "nullptr" << std::get<2>(tup).string;
}
vec.push_back(std::move(tup));
}
@ -1216,7 +1301,7 @@ void TwitchMessageBuilder::appendTwitchBadges()
auto badgeEmote = this->getTwitchBadge(badge);
if (!badgeEmote)
{
log("No channel/global variant found {}", badge.key_);
qDebug() << "No channel/global variant found" << badge.key_;
continue;
}
auto tooltip = (*badgeEmote)->tooltip.string;

View file

@ -92,9 +92,10 @@ private:
const bool action_ = false;
bool highlightVisual_ = false;
bool highlightAlert_ = false;
bool highlightSound_ = false;
QUrl highlightSoundUrl_;
};
} // namespace chatterino

View file

@ -1,7 +1,6 @@
#include "singletons/Logging.hpp"
#include "Application.hpp"
#include "debug/Log.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"

View file

@ -1,7 +1,6 @@
#include "singletons/Settings.hpp"
#include "Application.hpp"
#include "debug/Log.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Resources.hpp"
#include "singletons/WindowManager.hpp"

View file

@ -146,18 +146,39 @@ public:
/// Highlighting
// BoolSetting enableHighlights = {"/highlighting/enabled", true};
BoolSetting customHighlightSound = {"/highlighting/useCustomSound", false};
BoolSetting enableSelfHighlight = {
"/highlighting/selfHighlight/nameIsHighlightKeyword", true};
BoolSetting enableSelfHighlightSound = {
"/highlighting/selfHighlight/enableSound", true};
BoolSetting enableSelfHighlightTaskbar = {
"/highlighting/selfHighlight/enableTaskbarFlashing", true};
QStringSetting selfHighlightSoundUrl = {
"/highlighting/selfHighlightSoundUrl", ""};
QStringSetting selfHighlightColor = {"/highlighting/selfHighlightColor",
""};
BoolSetting enableWhisperHighlight = {
"/highlighting/whisperHighlight/whispersHighlighted", true};
BoolSetting enableWhisperHighlightSound = {
"/highlighting/whisperHighlight/enableSound", false};
BoolSetting enableWhisperHighlightTaskbar = {
"/highlighting/whisperHighlight/enableTaskbarFlashing", false};
QStringSetting whisperHighlightSoundUrl = {
"/highlighting/whisperHighlightSoundUrl", ""};
QStringSetting whisperHighlightColor = {
"/highlighting/whisperHighlightColor", ""};
BoolSetting enableSubHighlight = {
"/highlighting/subHighlight/subsHighlighted", true};
BoolSetting enableSubHighlightSound = {
"/highlighting/subHighlight/enableSound", false};
BoolSetting enableSubHighlightTaskbar = {
"/highlighting/subHighlight/enableTaskbarFlashing", false};
QStringSetting subHighlightSoundUrl = {"/highlighting/subHighlightSoundUrl",
""};
QStringSetting subHighlightColor = {"/highlighting/subHighlightColor", ""};
QStringSetting highlightColor = {"/highlighting/color", ""};
BoolSetting longAlerts = {"/highlighting/alerts", false};
@ -168,7 +189,7 @@ public:
QStringSetting logPath = {"/logging/path", ""};
QStringSetting pathHighlightSound = {"/highlighting/highlightSoundPath",
"qrc:/sounds/ping2.wav"};
""};
BoolSetting highlightAlwaysPlaySound = {"/highlighting/alwaysPlaySound",
false};
@ -228,6 +249,20 @@ public:
IntSetting lastSelectChannelTab = {"/ui/lastSelectChannelTab", 0};
IntSetting lastSelectIrcConn = {"/ui/lastSelectIrcConn", 0};
// Similarity
BoolSetting similarityEnabled = {"/similarity/similarityEnabled", false};
BoolSetting colorSimilarDisabled = {"/similarity/colorSimilarDisabled",
true};
BoolSetting hideSimilar = {"/similarity/hideSimilar", false};
BoolSetting hideSimilarMyself = {"/similarity/hideSimilarMyself", false};
BoolSetting shownSimilarTriggerHighlights = {
"/similarity/shownSimilarTriggerHighlights", false};
FloatSetting similarityPercentage = {"/similarity/similarityPercentage",
0.9f};
IntSetting hideSimilarMaxDelay = {"/similarity/hideSimilarMaxDelay", 5};
IntSetting hideSimilarMaxMessagesToCheck = {
"/similarity/hideSimilarMaxMessagesToCheck", 3};
private:
void updateModerationActions();
};

View file

@ -38,9 +38,6 @@ void Theme::actuallyUpdate(double hue, double multiplier)
this->splits.resizeHandle = QColor(0, 148, 255, 0xff);
this->splits.resizeHandleBackground = QColor(0, 148, 255, 0x50);
// Highlighted Messages: theme support quick-fix
this->messages.backgrounds.highlighted = QColor("#BD8489");
}
else
{
@ -49,11 +46,10 @@ void Theme::actuallyUpdate(double hue, double multiplier)
this->splits.resizeHandle = QColor(0, 148, 255, 0x70);
this->splits.resizeHandleBackground = QColor(0, 148, 255, 0x20);
// Highlighted Messages: theme support quick-fix
this->messages.backgrounds.highlighted = QColor("#4B282C");
}
this->messages.backgrounds.highlighted = QColor(140, 84, 89, 127);
this->splits.header.background = getColor(0, sat, flat ? 1 : 0.9);
this->splits.header.border = getColor(0, sat, flat ? 1 : 0.85);
this->splits.header.text = this->messages.textColors.regular;

View file

@ -13,7 +13,6 @@
#include "Application.hpp"
#include "debug/AssertInGuiThread.hpp"
#include "debug/Log.hpp"
#include "messages/MessageElement.hpp"
#include "providers/irc/Irc2.hpp"
#include "providers/irc/IrcChannel2.hpp"
@ -264,7 +263,7 @@ Window *WindowManager::windowAt(int index)
{
return nullptr;
}
log("getting window at bad index {}", index);
qDebug() << "getting window at bad index" << index;
return this->windows_.at(index);
}
@ -432,7 +431,7 @@ void WindowManager::initialize(Settings &settings, Paths &paths)
void WindowManager::save()
{
log("[WindowManager] Saving");
qDebug() << "[WindowManager] Saving";
assertInGuiThread();
QJsonDocument document;

View file

@ -1,7 +1,6 @@
#include "LoggingChannel.hpp"
#include "Application.hpp"
#include "debug/Log.hpp"
#include "singletons/Paths.hpp"
#include "singletons/Settings.hpp"
@ -64,13 +63,13 @@ void LoggingChannel::openLogFile()
if (!QDir().mkpath(directory))
{
log("Unable to create logging path");
qDebug() << "Unable to create logging path";
return;
}
// Open file handle to log file of current date
QString fileName = directory + QDir::separator() + baseFileName;
log("Logging to {}", fileName);
qDebug() << "Logging to" << fileName;
this->fileHandle.setFileName(fileName);
this->fileHandle.open(QIODevice::Append);

16
src/util/Clipboard.cpp Normal file
View file

@ -0,0 +1,16 @@
#include "util/Clipboard.hpp"
#include <QApplication>
namespace chatterino {
void crossPlatformCopy(const QString &text)
{
auto clipboard = QApplication::clipboard();
clipboard->setText(text);
if (clipboard->supportsSelection())
{
clipboard->setText(text, QClipboard::Selection);
}
}
} // namespace chatterino

9
src/util/Clipboard.hpp Normal file
View file

@ -0,0 +1,9 @@
#pragma once
#include <QString>
namespace chatterino {
void crossPlatformCopy(const QString &text);
} // namespace chatterino

View file

@ -1,16 +1,9 @@
#pragma once
#include <fmt/format.h>
#include <QString>
namespace chatterino {
template <typename... Args>
auto fS(Args &&... args)
{
return fmt::format(std::forward<Args>(args)...);
}
QString generateUuid();
QString formatRichLink(const QString &url, bool file = false);
@ -21,13 +14,3 @@ QString formatRichNamedLink(const QString &url, const QString &name,
QString shortenString(const QString &str, unsigned maxWidth = 50);
} // namespace chatterino
namespace fmt {
// format_arg for QString
inline void format_arg(BasicFormatter<char> &f, const char *&, const QString &v)
{
f.writer().write("{}", v.toStdString());
}
} // namespace fmt

View file

@ -5,8 +5,6 @@
#include <QSettings>
#include <QVariant>
#include "debug/Log.hpp"
namespace chatterino {
namespace {
#ifdef Q_OS_WIN
@ -63,7 +61,7 @@ namespace {
if (command.isNull())
return QString();
log(command);
qDebug() << command;
// inject switch to enable private browsing
command = injectPrivateSwitch(command);

View file

@ -22,6 +22,19 @@ static void setStringItem(QStandardItem *item, const QString &value,
(editable ? (Qt::ItemIsEditable) : 0)));
}
static void setFilePathItem(QStandardItem *item, const QUrl &value)
{
item->setData(value, Qt::UserRole);
item->setData(value.fileName(), Qt::DisplayRole);
item->setFlags(Qt::ItemFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable));
}
static void setColorItem(QStandardItem *item, const QColor &value)
{
item->setData(value, Qt::DecorationRole);
item->setFlags(Qt::ItemFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable));
}
static QStandardItem *emptyItem()
{
auto *item = new QStandardItem();

View file

@ -1,7 +1,6 @@
#include "util/StreamLink.hpp"
#include "Application.hpp"
#include "debug/Log.hpp"
#include "singletons/Settings.hpp"
#include "util/Helpers.hpp"
#include "widgets/dialogs/QualityPopup.hpp"
@ -92,7 +91,7 @@ namespace {
}
else
{
log("Error occured {}", err);
qDebug() << "Error occured" << err;
}
p->deleteLater();
@ -119,7 +118,7 @@ void getStreamQualities(const QString &channelURL,
[=](int res) {
if (res != 0)
{
log("Got error code {}", res);
qDebug() << "Got error code" << res;
// return;
}
QString lastLine = QString(p->readAllStandardOutput());

View file

@ -1,5 +1,4 @@
#include "widgets/AccountSwitchPopup.hpp"
#include "debug/Log.hpp"
#include "widgets/dialogs/SettingsDialog.hpp"
#include <QHBoxLayout>

View file

@ -2,7 +2,6 @@
#include "BaseSettings.hpp"
#include "BaseTheme.hpp"
#include "debug/Log.hpp"
#include "widgets/BaseWindow.hpp"
#include <QChildEvent>

View file

@ -3,7 +3,6 @@
#include "BaseSettings.hpp"
#include "BaseTheme.hpp"
#include "boost/algorithm/algorithm.hpp"
#include "debug/Log.hpp"
#include "util/PostToThread.hpp"
#include "util/Shortcut.hpp"
#include "util/WindowsHelper.hpp"
@ -385,7 +384,7 @@ void BaseWindow::mousePressEvent(QMouseEvent *event)
if (!recursiveCheckMouseTracking(widget))
{
log("Start moving");
qDebug() << "Start moving";
this->moving = true;
}
}
@ -402,7 +401,7 @@ void BaseWindow::mouseReleaseEvent(QMouseEvent *event)
{
if (this->moving)
{
log("Stop moving");
qDebug() << "Stop moving";
this->moving = false;
}
}

View file

@ -1,7 +1,6 @@
#include "widgets/Notebook.hpp"
#include "Application.hpp"
#include "debug/Log.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "singletons/WindowManager.hpp"

View file

@ -282,18 +282,7 @@ void Scrollbar::paintEvent(QPaintEvent *)
if (!highlight.isNull())
{
QColor color = [&] {
switch (highlight.getColor())
{
case ScrollbarHighlight::Highlight:
return getApp()
->themes->scrollbars.highlights.highlight;
case ScrollbarHighlight::Subscription:
return getApp()
->themes->scrollbars.highlights.subscription;
}
return QColor();
}();
QColor color = highlight.getColor();
switch (highlight.getStyle())
{

View file

@ -351,6 +351,11 @@ void Window::addShortcuts()
getApp()->twitch.server->getOrAddChannel(si.channelName));
splitContainer->appendSplit(split);
});
createWindowShortcut(this, "CTRL+H", [this] {
getSettings()->hideSimilar.setValue(!getSettings()->hideSimilar);
getApp()->windows->forceLayoutChannelViews();
});
}
void Window::addMenuBar()

View file

@ -0,0 +1,367 @@
#include "widgets/dialogs/ColorPickerDialog.hpp"
#include "providers/colors/ColorProvider.hpp"
#include "singletons/Theme.hpp"
namespace chatterino {
ColorPickerDialog::ColorPickerDialog(const QColor &initial, QWidget *parent)
: BasePopup(BaseWindow::EnableCustomFrame, parent)
, color_()
, dialogConfirmed_(false)
{
// This hosts the "business logic" and the dialog button box
LayoutCreator<QWidget> layoutWidget(this->getLayoutContainer());
auto layout = layoutWidget.setLayoutType<QVBoxLayout>().withoutMargin();
// 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
{
LayoutCreator<QWidget> gridCreator(new QWidget());
this->initRecentColors(gridCreator);
predef.append(gridCreator.getElement());
}
// Default colors
{
LayoutCreator<QWidget> gridCreator(new QWidget());
this->initDefaultColors(gridCreator);
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);
vbox.append(cpCreator.getElement());
}
// 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());
}
layout.append(contents.getElement());
// Dialog buttons
auto buttons =
layout.emplace<QHBoxLayout>().emplace<QDialogButtonBox>(this);
{
auto *button_ok = buttons->addButton(QDialogButtonBox::Ok);
QObject::connect(button_ok, &QPushButton::clicked,
[=](bool) { this->ok(); });
auto *button_cancel = buttons->addButton(QDialogButtonBox::Cancel);
QObject::connect(button_cancel, &QAbstractButton::clicked,
[=](bool) { this->close(); });
}
this->themeChangedEvent();
this->selectColor(initial, false);
}
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();
}
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)
{
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];
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->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->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)
{
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,
[=](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), [=](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::textEdited,
[=](const QString &text) {
QColor col(text);
if (col.isValid())
this->selectColor(col, false);
});
}
} // namespace chatterino

View file

@ -0,0 +1,110 @@
#pragma once
#include "util/LayoutCreator.hpp"
#include "widgets/BasePopup.hpp"
#include "widgets/helper/ColorButton.hpp"
#include "widgets/helper/QColorPicker.hpp"
#include <pajlada/signals/signal.hpp>
#include <array>
namespace chatterino {
/**
* @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
{
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 = nullptr);
~ColorPickerDialog();
/**
* @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;
pajlada::Signals::NoArgSignal closed;
protected:
void closeEvent(QCloseEvent *);
void themeChangedEvent();
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);
};
} // namespace chatterino

View file

@ -7,6 +7,7 @@
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Emotes.hpp"
#include "util/Shortcut.hpp"
#include "widgets/Notebook.hpp"
#include "widgets/helper/ChannelView.hpp"
@ -31,7 +32,14 @@ namespace {
if (!map.empty())
{
for (const auto &emote : map)
std::vector<std::pair<EmoteName, EmotePtr>> vec(map.begin(),
map.end());
std::sort(vec.begin(), vec.end(),
[](const std::pair<EmoteName, EmotePtr> &l,
const std::pair<EmoteName, EmotePtr> &r) {
return l.first.string < r.first.string;
});
for (const auto &emote : vec)
{
builder
.emplace<EmoteElement>(emote.second,
@ -131,6 +139,10 @@ EmotePopup::EmotePopup(QWidget *parent)
this->viewEmojis_ = makeView("Emojis");
this->loadEmojis();
createWindowShortcut(this, "CTRL+Tab", [=] { notebook->selectNextTab(); });
createWindowShortcut(this, "CTRL+Shift+Tab",
[=] { notebook->selectPreviousTab(); });
}
void EmotePopup::loadChannel(ChannelPtr _channel)

View file

@ -65,7 +65,7 @@ namespace {
// messageBox.setIcon(QMessageBox::Information);
// messageBox.setText("Successfully logged in with user <b>" +
// qS(username) + "</b>!");
auto basePath = fS("/accounts/uid{}", userID);
std::string basePath = "/accounts/uid" + userID.toStdString();
pajlada::Settings::Setting<QString>::set(basePath + "/username",
username);
pajlada::Settings::Setting<QString>::set(basePath + "/userID", userID);

View file

@ -3,7 +3,6 @@
#include "IrcMessage"
#include "common/Channel.hpp"
#include "common/NetworkRequest.hpp"
#include "debug/Log.hpp"
#include "messages/Message.hpp"
#include "providers/twitch/PartialTwitchUser.hpp"
#include "providers/twitch/TwitchChannel.hpp"
@ -55,7 +54,7 @@ void LogsPopup::getLogs()
dynamic_cast<TwitchChannel *>(this->channel_.get()))
{
this->channelName_ = twitchChannel->getName();
this->getLogviewerLogs(twitchChannel->roomId());
this->getOverrustleLogs();
return;
}
@ -63,14 +62,11 @@ void LogsPopup::getLogs()
if (!this->channelName_.isEmpty())
{
PartialTwitchUser::byName(this->channelName_)
.getId(
[=](const QString &roomID) { this->getLogviewerLogs(roomID); },
this);
this->getOverrustleLogs();
return;
}
log("Unable to get logs, no channel name or something specified");
qDebug() << "Unable to get logs, no channel name or something specified";
}
void LogsPopup::setMessages(std::vector<MessagePtr> &messages)
@ -81,57 +77,9 @@ void LogsPopup::setMessages(std::vector<MessagePtr> &messages)
SearchPopup::setChannel(logsChannel);
}
void LogsPopup::getLogviewerLogs(const QString &roomID)
{
auto url = QString("https://cbenni.com/api/logs/%1/?nick=%2&before=500")
.arg(this->channelName_, this->userName_);
NetworkRequest(url)
.caller(this)
.onError([this](NetworkResult) { this->getOverrustleLogs(); })
.onSuccess([this, roomID](auto result) -> Outcome {
auto data = result.parseJson();
std::vector<MessagePtr> messages;
QJsonValue before = data.value("before");
for (auto i : before.toArray())
{
auto messageObject = i.toObject();
QString message = messageObject.value("text").toString();
// Hacky way to fix the timestamp
message.insert(1, "historical=1;");
message.insert(1, QString("tmi-sent-ts=%10000;")
.arg(messageObject["time"].toInt()));
message.insert(1, QString("room-id=%1;").arg(roomID));
MessageParseArgs args;
auto ircMessage =
Communi::IrcMessage::fromData(message.toUtf8(), nullptr);
auto privMsg =
static_cast<Communi::IrcPrivateMessage *>(ircMessage);
TwitchMessageBuilder builder(this->channel_.get(), privMsg,
args);
builder.message().searchText = message;
messages.push_back(builder.build());
}
messages.push_back(
MessageBuilder(systemMessage,
"Logs provided by https://cbenni.com")
.release());
this->setMessages(messages);
return Success;
})
.execute();
}
void LogsPopup::getOverrustleLogs()
{
QString url =
auto url =
QString("https://overrustlelogs.net/api/v1/stalk/%1/%2.json?limit=500")
.arg(this->channelName_, this->userName_);

View file

@ -26,7 +26,6 @@ private:
void setMessages(std::vector<MessagePtr> &messages);
void getOverrustleLogs();
void getLogviewerLogs(const QString &roomID);
};
} // namespace chatterino

View file

@ -1,5 +1,4 @@
#include "QualityPopup.hpp"
#include "debug/Log.hpp"
#include "util/StreamLink.hpp"
namespace chatterino {
@ -50,7 +49,7 @@ void QualityPopup::okButtonClicked()
}
catch (const Exception &ex)
{
log("Exception caught trying to open streamlink: {}", ex.what());
qDebug() << "Exception caught trying to open streamlink:" << ex.what();
}
this->close();

View file

@ -25,6 +25,7 @@
#define TEXT_VIEWS "Views: "
#define TEXT_CREATED "Created: "
#define TEXT_USER_ID "ID: "
#define TEXT_UNAVAILABLE "(not available)"
namespace chatterino {
namespace {
@ -382,6 +383,22 @@ void UserInfoPopup::updateUserData()
{
std::weak_ptr<bool> hack = this->hack_;
const auto onIdFetchFailed = [this]() {
// this can occur when the account doesn't exist.
this->ui_.followerCountLabel->setText(TEXT_FOLLOWERS +
QString(TEXT_UNAVAILABLE));
this->ui_.viewCountLabel->setText(TEXT_VIEWS +
QString(TEXT_UNAVAILABLE));
this->ui_.createdDateLabel->setText(TEXT_CREATED +
QString(TEXT_UNAVAILABLE));
this->ui_.nameLabel->setText(this->userName_);
this->ui_.userIDLabel->setText(QString("ID") +
QString(TEXT_UNAVAILABLE));
this->ui_.userIDLabel->setProperty("copy-text",
QString(TEXT_UNAVAILABLE));
};
const auto onIdFetched = [this, hack](QString id) {
auto currentUser = getApp()->accounts->twitch.getCurrent();
@ -462,7 +479,8 @@ void UserInfoPopup::updateUserData()
this->ui_.ignoreHighlights->setChecked(isIgnoringHighlights);
};
PartialTwitchUser::byName(this->userName_).getId(onIdFetched, this);
PartialTwitchUser::byName(this->userName_)
.getId(onIdFetched, onIdFetchFailed, this);
this->ui_.follow->setEnabled(false);
this->ui_.ignore->setEnabled(false);

View file

@ -16,7 +16,6 @@
#include "common/Common.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "debug/Benchmark.hpp"
#include "debug/Log.hpp"
#include "messages/Emote.hpp"
#include "messages/LimitedQueueSnapshot.hpp"
#include "messages/Message.hpp"
@ -29,6 +28,7 @@
#include "singletons/Theme.hpp"
#include "singletons/TooltipPreviewImage.hpp"
#include "singletons/WindowManager.hpp"
#include "util/Clipboard.hpp"
#include "util/DistanceBetweenPoints.hpp"
#include "util/IncognitoBrowser.hpp"
#include "widgets/Scrollbar.hpp"
@ -65,9 +65,8 @@ namespace {
if (!image->isEmpty())
{
copyMenu->addAction(
QString(scale) + "x link", [url = image->url()] {
QApplication::clipboard()->setText(url.string);
});
QString(scale) + "x link",
[url = image->url()] { crossPlatformCopy(url.string); });
openMenu->addAction(
QString(scale) + "x link", [url = image->url()] {
QDesktopServices::openUrl(QUrl(url.string));
@ -85,9 +84,8 @@ namespace {
openMenu->addSeparator();
copyMenu->addAction(
"Copy " + name + " emote link", [url = emote.homePage] {
QApplication::clipboard()->setText(url.string); //
});
"Copy " + name + " emote link",
[url = emote.homePage] { crossPlatformCopy(url.string); });
openMenu->addAction(
"Open " + name + " emote link", [url = emote.homePage] {
QDesktopServices::openUrl(QUrl(url.string)); //
@ -125,9 +123,8 @@ ChannelView::ChannelView(BaseWidget *parent)
});
auto shortcut = new QShortcut(QKeySequence("Ctrl+C"), this);
QObject::connect(shortcut, &QShortcut::activated, [this] {
QGuiApplication::clipboard()->setText(this->getSelectedText());
});
QObject::connect(shortcut, &QShortcut::activated,
[this] { crossPlatformCopy(this->getSelectedText()); });
this->clickTimer_ = new QTimer(this);
this->clickTimer_->setSingleShot(true);
@ -651,7 +648,8 @@ void ChannelView::messageAppended(MessagePtr &message,
if (!messageFlags->has(MessageFlag::DoNotTriggerNotification))
{
if (messageFlags->has(MessageFlag::Highlighted))
if (messageFlags->has(MessageFlag::Highlighted) &&
!messageFlags->has(MessageFlag::Subscription))
{
this->tabHighlightRequested.invoke(HighlightState::Highlighted);
}
@ -740,9 +738,8 @@ void ChannelView::messageReplaced(size_t index, MessagePtr &replacement)
auto snapshot = this->messages_.getSnapshot();
if (index >= snapshot.size())
{
log("Tried to replace out of bounds message. Index: {}. "
"Length: {}",
index, snapshot.size());
qDebug() << "Tried to replace out of bounds message. Index:" << index
<< ". Length:" << snapshot.size();
return;
}
@ -1554,8 +1551,7 @@ void ChannelView::addContextMenuItems(
menu->addAction("Open link incognito",
[url] { openLinkIncognito(url); });
}
menu->addAction("Copy link",
[url] { QApplication::clipboard()->setText(url); });
menu->addAction("Copy link", [url] { crossPlatformCopy(url); });
menu->addSeparator();
}
@ -1563,9 +1559,8 @@ void ChannelView::addContextMenuItems(
// Copy actions
if (!this->selection_.isEmpty())
{
menu->addAction("Copy selection", [this] {
QGuiApplication::clipboard()->setText(this->getSelectedText());
});
menu->addAction("Copy selection",
[this] { crossPlatformCopy(this->getSelectedText()); });
}
menu->addAction("Copy message", [layout] {
@ -1573,14 +1568,14 @@ void ChannelView::addContextMenuItems(
layout->addSelectionText(copyString, 0, INT_MAX,
CopyMode::OnlyTextAndEmotes);
QGuiApplication::clipboard()->setText(copyString);
crossPlatformCopy(copyString);
});
menu->addAction("Copy full message", [layout] {
QString copyString;
layout->addSelectionText(copyString);
QGuiApplication::clipboard()->setText(copyString);
crossPlatformCopy(copyString);
});
// Open in new split.

View file

@ -0,0 +1,23 @@
#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

View file

@ -0,0 +1,18 @@
#pragma once
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

View file

@ -2,7 +2,6 @@
#include "Application.hpp"
#include "common/Common.hpp"
#include "debug/Log.hpp"
#include "singletons/Fonts.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"

View file

@ -0,0 +1,290 @@
/****************************************************************************
**
** 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>
/*
* 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

View file

@ -0,0 +1,130 @@
/****************************************************************************
**
** 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 <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();
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();
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

View file

@ -1,24 +1,27 @@
#include "widgets/helper/ScrollbarHighlight.hpp"
#include "Application.hpp"
#include "singletons/Theme.hpp"
#include "widgets/Scrollbar.hpp"
namespace chatterino {
ScrollbarHighlight::ScrollbarHighlight()
: color_(Color::Highlight)
: color_(std::make_shared<QColor>())
, style_(Style::None)
{
}
ScrollbarHighlight::ScrollbarHighlight(Color color, Style style)
ScrollbarHighlight::ScrollbarHighlight(const std::shared_ptr<QColor> color,
Style style)
: color_(color)
, style_(style)
{
}
ScrollbarHighlight::Color ScrollbarHighlight::getColor() const
QColor ScrollbarHighlight::getColor() const
{
return this->color_;
return *this->color_;
}
ScrollbarHighlight::Style ScrollbarHighlight::getStyle() const

View file

@ -6,17 +6,25 @@ class ScrollbarHighlight
{
public:
enum Style : char { None, Default, Line };
enum Color : char { Highlight, Subscription };
/**
* @brief Constructs an invalid ScrollbarHighlight.
*
* A highlight constructed this way will not show on the scrollbar.
* For these, use the static ScrollbarHighlight::newSubscription and
* ScrollbarHighlight::newHighlight methods.
*/
ScrollbarHighlight();
ScrollbarHighlight(Color color, Style style = Default);
Color getColor() const;
ScrollbarHighlight(const std::shared_ptr<QColor> color,
Style style = Default);
QColor getColor() const;
Style getStyle() const;
bool isNull() const;
private:
Color color_;
std::shared_ptr<QColor> color_;
Style style_;
};

View file

@ -2,7 +2,6 @@
#include "common/Modes.hpp"
#include "common/Version.hpp"
#include "debug/Log.hpp"
#include "util/LayoutCreator.hpp"
#include "util/RemoveScrollAreaBackground.hpp"
#include "widgets/helper/SignalLabel.hpp"
@ -102,8 +101,6 @@ AboutPage::AboutPage()
":/licenses/qt_lgpl-3.0.txt");
addLicense(form.getElement(), "Boost", "https://www.boost.org/",
":/licenses/boost_boost.txt");
addLicense(form.getElement(), "Fmt", "http://fmtlib.net/",
":/licenses/fmt_bsd2.txt");
addLicense(form.getElement(), "LibCommuni",
"https://github.com/communi/libcommuni",
":/licenses/libcommuni_BSD3.txt");
@ -166,7 +163,7 @@ AboutPage::AboutPage()
if (contributorParts.size() != 4)
{
log("Missing parts in line '{}'", line);
qDebug() << "Missing parts in line" << line;
continue;
}

View file

@ -547,6 +547,33 @@ void GeneralPage::initLayout(SettingsLayout &layout)
QDesktopServices::openUrl(getPaths()->rootAppDataDirectory);
});
layout.addTitle("Similarity");
layout.addDescription(
"Hides or grays out similar messages from the same user");
layout.addCheckbox("Similarity enabled", s.similarityEnabled);
layout.addCheckbox("Gray out similar messages", s.colorSimilarDisabled);
layout.addCheckbox("Hide similar messages (Ctrl + H)", s.hideSimilar);
layout.addCheckbox("Hide or gray out my own similar messages",
s.hideSimilarMyself);
layout.addCheckbox("Shown similar messages trigger highlights",
s.shownSimilarTriggerHighlights);
layout.addDropdown<float>(
"Similarity percentage", {"0.5", "0.75", "0.9"}, s.similarityPercentage,
[](auto val) { return QString::number(val); },
[](auto args) { return fuzzyToFloat(args.value, 0.9f); });
s.hideSimilar.connect(
[]() { getApp()->windows->forceLayoutChannelViews(); }, false);
layout.addDropdown<int>(
"Similar messages max delay in seconds",
{"5", "10", "15", "30", "60", "120"}, s.hideSimilarMaxDelay,
[](auto val) { return QString::number(val); },
[](auto args) { return fuzzyToInt(args.value, 5); });
layout.addDropdown<int>(
"Similar messages max previous messages to check",
{"1", "2", "3", "4", "5"}, s.hideSimilarMaxMessagesToCheck,
[](auto val) { return QString::number(val); },
[](auto args) { return fuzzyToInt(args.value, 3); });
// invisible element for width
auto inv = new BaseWidget(this);
inv->setScaleIndependantWidth(500);

View file

@ -5,11 +5,11 @@
#include "controllers/highlights/HighlightController.hpp"
#include "controllers/highlights/HighlightModel.hpp"
#include "controllers/highlights/UserHighlightModel.hpp"
#include "debug/Log.hpp"
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "util/LayoutCreator.hpp"
#include "util/StandardItemHelper.hpp"
#include "widgets/helper/EditableModelView.hpp"
#include "widgets/dialogs/ColorPickerDialog.hpp"
#include <QFileDialog>
#include <QHeaderView>
@ -46,8 +46,10 @@ HighlightingPage::HighlightingPage()
// HIGHLIGHTS
auto highlights = tabs.appendTab(new QVBoxLayout, "Messages");
{
highlights.emplace<QLabel>("Messages can be highlighted if "
"they match a certain pattern.");
highlights.emplace<QLabel>(
"Messages can be highlighted if they match a certain "
"pattern.\n"
"NOTE: User highlights will override phrase highlights.");
EditableModelView *view =
highlights
@ -57,7 +59,8 @@ HighlightingPage::HighlightingPage()
view->addRegexHelpLink();
view->setTitles({"Pattern", "Flash\ntaskbar", "Play\nsound",
"Enable\nregex", "Case-\nsensitive"});
"Enable\nregex", "Case-\nsensitive",
"Custom\nsound", "Color"});
view->getTableView()->horizontalHeader()->setSectionResizeMode(
QHeaderView::Fixed);
view->getTableView()->horizontalHeader()->setSectionResizeMode(
@ -72,14 +75,22 @@ HighlightingPage::HighlightingPage()
view->addButtonPressed.connect([] {
getApp()->highlights->phrases.appendItem(HighlightPhrase{
"my phrase", true, false, false, false});
"my phrase", true, false, false, false, "",
*ColorProvider::instance().color(
ColorType::SelfHighlight)});
});
QObject::connect(view->getTableView(), &QTableView::clicked,
[this, view](const QModelIndex &clicked) {
this->tableCellClicked(clicked, view);
});
}
auto pingUsers = tabs.appendTab(new QVBoxLayout, "Users");
{
pingUsers.emplace<QLabel>(
"Messages from a certain user can be highlighted.");
"Messages from a certain user can be highlighted.\n"
"NOTE: User highlights will override phrase highlights.");
EditableModelView *view =
pingUsers
.emplace<EditableModelView>(
@ -90,9 +101,10 @@ HighlightingPage::HighlightingPage()
view->getTableView()->horizontalHeader()->hideSection(4);
// Case-sensitivity doesn't make sense for user names so it is
// set to "false" by default & no checkbox is shown
// set to "false" by default & the column is hidden
view->setTitles({"Username", "Flash\ntaskbar", "Play\nsound",
"Enable\nregex"});
"Enable\nregex", "Case-\nsensitive",
"Custom\nsound", "Color"});
view->getTableView()->horizontalHeader()->setSectionResizeMode(
QHeaderView::Fixed);
view->getTableView()->horizontalHeader()->setSectionResizeMode(
@ -108,7 +120,14 @@ HighlightingPage::HighlightingPage()
view->addButtonPressed.connect([] {
getApp()->highlights->highlightedUsers.appendItem(
HighlightPhrase{"highlighted user", true, false, false,
false});
false, "",
*ColorProvider::instance().color(
ColorType::SelfHighlight)});
});
QObject::connect(view->getTableView(), &QTableView::clicked,
[this, view](const QModelIndex &clicked) {
this->tableCellClicked(clicked, view);
});
}
@ -148,16 +167,32 @@ HighlightingPage::HighlightingPage()
// MISC
auto customSound = layout.emplace<QHBoxLayout>().withoutMargin();
{
customSound.append(this->createCheckBox(
"Custom sound", getSettings()->customHighlightSound));
auto fallbackSound = customSound.append(this->createCheckBox(
"Fallback sound (played when no other sound is set)",
getSettings()->customHighlightSound));
auto getSelectFileText = [] {
const QString value = getSettings()->pathHighlightSound;
return value.isEmpty() ? "Select custom fallback sound"
: QUrl::fromLocalFile(value).fileName();
};
auto selectFile =
customSound.emplace<QPushButton>("Select custom sound file");
QObject::connect(selectFile.getElement(), &QPushButton::clicked,
this, [this] {
customSound.emplace<QPushButton>(getSelectFileText());
QObject::connect(
selectFile.getElement(), &QPushButton::clicked, this,
[=]() mutable {
auto fileName = QFileDialog::getOpenFileName(
this, tr("Open Sound"), "",
tr("Audio Files (*.mp3 *.wav)"));
getSettings()->pathHighlightSound = fileName;
selectFile.getElement()->setText(getSelectFileText());
// Set check box according to updated value
fallbackSound->setCheckState(
fileName.isEmpty() ? Qt::Unchecked : Qt::Checked);
});
}
@ -172,4 +207,71 @@ HighlightingPage::HighlightingPage()
this->disabledUsersChangedTimer_.setSingleShot(true);
}
void HighlightingPage::tableCellClicked(const QModelIndex &clicked,
EditableModelView *view)
{
using Column = HighlightModel::Column;
if (clicked.column() == Column::SoundPath)
{
auto fileUrl = QFileDialog::getOpenFileUrl(
this, tr("Open Sound"), QUrl(), tr("Audio Files (*.mp3 *.wav)"));
view->getModel()->setData(clicked, fileUrl, Qt::UserRole);
view->getModel()->setData(clicked, fileUrl.fileName(), Qt::DisplayRole);
// Enable custom sound check box if user set a sound
if (!fileUrl.isEmpty())
{
QModelIndex checkBox = clicked.siblingAtColumn(Column::PlaySound);
view->getModel()->setData(checkBox, Qt::Checked,
Qt::CheckStateRole);
}
}
else if (clicked.column() == Column::Color)
{
auto initial =
view->getModel()->data(clicked, Qt::DecorationRole).value<QColor>();
auto dialog = new ColorPickerDialog(initial, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();
dialog->closed.connect([=] {
QColor selected = dialog->selectedColor();
if (selected.isValid())
{
view->getModel()->setData(clicked, selected,
Qt::DecorationRole);
// Hacky (?) way to figure out what tab the cell was clicked in
const bool fromMessages = (dynamic_cast<HighlightModel *>(
view->getModel()) != nullptr);
if (fromMessages)
{
/*
* For preset highlights in the "Messages" tab, we need to
* manually update the color map.
*/
auto instance = ColorProvider::instance();
switch (clicked.row())
{
case 0:
instance.updateColor(ColorType::SelfHighlight,
selected);
break;
case 1:
instance.updateColor(ColorType::Whisper, selected);
break;
case 2:
instance.updateColor(ColorType::Subscription,
selected);
break;
}
}
}
});
}
}
} // namespace chatterino

View file

@ -1,5 +1,6 @@
#pragma once
#include "widgets/helper/EditableModelView.hpp"
#include "widgets/settingspages/SettingsPage.hpp"
#include <QAbstractTableModel>
@ -17,6 +18,8 @@ public:
private:
QTimer disabledUsersChangedTimer_;
void tableCellClicked(const QModelIndex &clicked, EditableModelView *view);
};
} // namespace chatterino

View file

@ -26,6 +26,10 @@ KeyboardSettingsPage::KeyboardSettingsPage()
form->addRow(new QLabel("Ctrl + Shift + T"), new QLabel("Create new tab"));
form->addRow(new QLabel("Ctrl + Shift + W"),
new QLabel("Close current tab"));
form->addRow(
new QLabel("Ctrl + H"),
new QLabel(
"Hide/Show similar messages (Enable in General under Similarity)"));
form->addItem(new QSpacerItem(16, 16));
form->addRow(new QLabel("Ctrl + 1/2/3/..."),

View file

@ -4,7 +4,6 @@
#include "common/Env.hpp"
#include "common/NetworkRequest.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "debug/Log.hpp"
#include "providers/twitch/EmoteValue.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
@ -13,6 +12,7 @@
#include "singletons/Theme.hpp"
#include "singletons/WindowManager.hpp"
#include "util/NuulsUploader.hpp"
#include "util/Clipboard.hpp"
#include "util/Shortcut.hpp"
#include "util/StreamLink.hpp"
#include "widgets/Notebook.hpp"
@ -537,7 +537,7 @@ void Split::openInStreamlink()
}
catch (const Exception &ex)
{
log("Error in doOpenStreamlink: {}", ex.what());
qDebug() << "Error in doOpenStreamlink:" << ex.what();
}
}
@ -667,7 +667,7 @@ void Split::openSubPage()
void Split::copyToClipboard()
{
QApplication::clipboard()->setText(this->view_->getSelectedText());
crossPlatformCopy(this->view_->getSelectedText());
}
void Split::showSearch()

View file

@ -577,11 +577,16 @@ void SplitHeader::updateModerationModeIcon()
auto channel = this->split_->getChannel();
auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get());
if (twitchChannel != nullptr && twitchChannel->hasModRights())
if (twitchChannel != nullptr &&
(twitchChannel->hasModRights() || moderationMode))
{
this->moderationButton_->show();
}
else
{
this->moderationButton_->hide();
}
}
void SplitHeader::paintEvent(QPaintEvent *)
{