From 3c8992cac1e1a1aaceb2591a4ba88ac786dcdedc Mon Sep 17 00:00:00 2001 From: pajlada Date: Fri, 3 Jan 2020 20:51:37 +0100 Subject: [PATCH 01/22] Remove FMT dependency (#1472) All occurrences of log() have been replaced with qDebug() bonus meme: remove a bunch of std::string usages in the pubsub client Fixes #1467 --- chatterino.pro | 5 - lib/fmt.pri | 18 - lib/fmt/fmt/format.cpp | 535 --- lib/fmt/fmt/format.h | 4012 ----------------- resources/licenses/fmt_bsd2.txt | 23 - resources/resources_autogenerated.qrc | 1 - src/Application.cpp | 5 +- src/PrecompiledHeader.hpp | 1 - src/common/Channel.cpp | 1 - src/common/CompletionModel.cpp | 1 - src/common/DownloadManager.cpp | 3 +- src/common/NetworkPrivate.cpp | 5 +- src/common/NetworkRequest.cpp | 1 - src/common/NetworkResult.cpp | 7 +- .../commands/CommandController.cpp | 1 - .../notifications/NotificationController.cpp | 16 +- src/debug/Benchmark.cpp | 3 +- src/debug/Benchmark.hpp | 3 +- src/debug/Log.hpp | 29 - src/messages/Image.cpp | 9 +- src/providers/bttv/BttvEmotes.cpp | 1 - src/providers/emoji/Emojis.cpp | 10 +- src/providers/ffz/FfzEmotes.cpp | 12 +- src/providers/irc/AbstractIrcServer.cpp | 32 +- src/providers/twitch/IrcMessageHandler.cpp | 21 +- src/providers/twitch/PartialTwitchUser.cpp | 18 +- src/providers/twitch/PubsubClient.cpp | 136 +- src/providers/twitch/PubsubClient.hpp | 11 +- src/providers/twitch/PubsubHelpers.cpp | 8 +- src/providers/twitch/PubsubHelpers.hpp | 12 +- src/providers/twitch/TwitchAccount.cpp | 32 +- src/providers/twitch/TwitchAccountManager.cpp | 24 +- src/providers/twitch/TwitchApi.cpp | 22 +- src/providers/twitch/TwitchBadges.cpp | 1 - src/providers/twitch/TwitchChannel.cpp | 23 +- src/providers/twitch/TwitchEmotes.cpp | 1 - src/providers/twitch/TwitchHelpers.cpp | 3 +- src/providers/twitch/TwitchMessageBuilder.cpp | 36 +- src/singletons/Logging.cpp | 1 - src/singletons/Settings.cpp | 1 - src/singletons/WindowManager.cpp | 5 +- src/singletons/helper/LoggingChannel.cpp | 5 +- src/util/Helpers.hpp | 17 - src/util/IncognitoBrowser.cpp | 2 - src/util/StreamLink.cpp | 5 +- src/widgets/AccountSwitchPopup.cpp | 1 - src/widgets/BaseWidget.cpp | 1 - src/widgets/BaseWindow.cpp | 5 +- src/widgets/Notebook.cpp | 1 - src/widgets/dialogs/LoginDialog.cpp | 2 +- src/widgets/dialogs/LogsPopup.cpp | 3 +- src/widgets/dialogs/QualityPopup.cpp | 3 +- src/widgets/helper/ChannelView.cpp | 6 +- src/widgets/helper/NotebookTab.cpp | 1 - src/widgets/settingspages/AboutPage.cpp | 5 +- .../settingspages/HighlightingPage.cpp | 1 - src/widgets/splits/Split.cpp | 3 +- 57 files changed, 230 insertions(+), 4920 deletions(-) delete mode 100644 lib/fmt.pri delete mode 100644 lib/fmt/fmt/format.cpp delete mode 100644 lib/fmt/fmt/format.h delete mode 100644 resources/licenses/fmt_bsd2.txt delete mode 100644 src/debug/Log.hpp diff --git a/chatterino.pro b/chatterino.pro index d8b14bbed..79d36ab60 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -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) @@ -344,7 +340,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 \ diff --git a/lib/fmt.pri b/lib/fmt.pri deleted file mode 100644 index 8703d2bd2..000000000 --- a/lib/fmt.pri +++ /dev/null @@ -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/ -} diff --git a/lib/fmt/fmt/format.cpp b/lib/fmt/fmt/format.cpp deleted file mode 100644 index 09d2ea9fd..000000000 --- a/lib/fmt/fmt/format.cpp +++ /dev/null @@ -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 - -#include -#include -#include -#include -#include -#include // for std::ptrdiff_t - -#if defined(_WIN32) && defined(__MINGW32__) -# include -#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 -# else -# define NOMINMAX -# include -# 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::MainType MainType; - MainType abs_value = static_cast(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 -int internal::CharTraits::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 -int internal::CharTraits::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 -const char internal::BasicData::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 -const uint32_t internal::BasicData::POWERS_OF_10_32[] = { - 0, FMT_POWERS_OF_10(1) -}; - -template -const uint64_t internal::BasicData::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(code))) { - FMT_THROW(FormatError( - format("unknown format code '{}' for {}", code, type))); - } - FMT_THROW(FormatError( - format("unknown format code '\\x{:02x}' for {}", - static_cast(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(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(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 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(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 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 -void internal::ArgMap::init(const ArgList &args) { - if (!map_.empty()) - return; - typedef internal::NamedArg 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(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(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(args.args_[i].pointer); - map_.push_back(Pair(named_arg->name, *named_arg)); - break; - default: - /*nothing*/; - } - } -} - -template -void internal::FixedBuffer::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(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('0' + c); - std::fputs(escape, stdout); - print(format, args); - std::fputs(RESET_COLOR, stdout); -} - -#ifndef FMT_HEADER_ONLY - -template struct internal::BasicData; - -// Explicit instantiations for char. - -template void internal::FixedBuffer::grow(std::size_t); - -template void internal::ArgMap::init(const ArgList &args); - -template FMT_API int internal::CharTraits::format_float( - char *buffer, std::size_t size, const char *format, - unsigned width, int precision, double value); - -template FMT_API int internal::CharTraits::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::grow(std::size_t); - -template void internal::ArgMap::init(const ArgList &args); - -template FMT_API int internal::CharTraits::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::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 diff --git a/lib/fmt/fmt/format.h b/lib/fmt/fmt/format.h deleted file mode 100644 index 6ee9d2a21..000000000 --- a/lib/fmt/fmt/format.h +++ /dev/null @@ -1,4012 +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. - */ - -#ifndef FMT_FORMAT_H_ -#define FMT_FORMAT_H_ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include // for std::pair - -// The fmt library version in the form major * 10000 + minor * 100 + patch. -#define FMT_VERSION 40000 - -#ifdef _SECURE_SCL -# define FMT_SECURE_SCL _SECURE_SCL -#else -# define FMT_SECURE_SCL 0 -#endif - -#if FMT_SECURE_SCL -# include -#endif - -#ifdef _MSC_VER -# define FMT_MSC_VER _MSC_VER -#else -# define FMT_MSC_VER 0 -#endif - -#if FMT_MSC_VER && FMT_MSC_VER <= 1500 -typedef unsigned __int32 uint32_t; -typedef unsigned __int64 uint64_t; -typedef __int64 intmax_t; -#else -#include -#endif - -#if !defined(FMT_HEADER_ONLY) && defined(_WIN32) -# ifdef FMT_EXPORT -# define FMT_API __declspec(dllexport) -# elif defined(FMT_SHARED) -# define FMT_API __declspec(dllimport) -# endif -#endif -#ifndef FMT_API -# define FMT_API -#endif - -#ifdef __GNUC__ -# define FMT_GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__) -# define FMT_GCC_EXTENSION __extension__ -# if FMT_GCC_VERSION >= 406 -# pragma GCC diagnostic push -// Disable the warning about "long long" which is sometimes reported even -// when using __extension__. -# pragma GCC diagnostic ignored "-Wlong-long" -// Disable the warning about declaration shadowing because it affects too -// many valid cases. -# pragma GCC diagnostic ignored "-Wshadow" -// Disable the warning about implicit conversions that may change the sign of -// an integer; silencing it otherwise would require many explicit casts. -# pragma GCC diagnostic ignored "-Wsign-conversion" -# endif -# if __cplusplus >= 201103L || defined __GXX_EXPERIMENTAL_CXX0X__ -# define FMT_HAS_GXX_CXX11 1 -# endif -#else -# define FMT_GCC_EXTENSION -#endif - -#if defined(__INTEL_COMPILER) -# define FMT_ICC_VERSION __INTEL_COMPILER -#elif defined(__ICL) -# define FMT_ICC_VERSION __ICL -#endif - -#if defined(__clang__) && !defined(FMT_ICC_VERSION) -# define FMT_CLANG_VERSION (__clang_major__ * 100 + __clang_minor__) -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wdocumentation-unknown-command" -# pragma clang diagnostic ignored "-Wpadded" -#endif - -#ifdef __GNUC_LIBSTD__ -# define FMT_GNUC_LIBSTD_VERSION (__GNUC_LIBSTD__ * 100 + __GNUC_LIBSTD_MINOR__) -#endif - -#ifdef __has_feature -# define FMT_HAS_FEATURE(x) __has_feature(x) -#else -# define FMT_HAS_FEATURE(x) 0 -#endif - -#ifdef __has_builtin -# define FMT_HAS_BUILTIN(x) __has_builtin(x) -#else -# define FMT_HAS_BUILTIN(x) 0 -#endif - -#ifdef __has_cpp_attribute -# define FMT_HAS_CPP_ATTRIBUTE(x) __has_cpp_attribute(x) -#else -# define FMT_HAS_CPP_ATTRIBUTE(x) 0 -#endif - -#ifndef FMT_USE_VARIADIC_TEMPLATES -// Variadic templates are available in GCC since version 4.4 -// (http://gcc.gnu.org/projects/cxx0x.html) and in Visual C++ -// since version 2013. -# define FMT_USE_VARIADIC_TEMPLATES \ - (FMT_HAS_FEATURE(cxx_variadic_templates) || \ - (FMT_GCC_VERSION >= 404 && FMT_HAS_GXX_CXX11) || FMT_MSC_VER >= 1800) -#endif - -#ifndef FMT_USE_RVALUE_REFERENCES -// Don't use rvalue references when compiling with clang and an old libstdc++ -// as the latter doesn't provide std::move. -# if defined(FMT_GNUC_LIBSTD_VERSION) && FMT_GNUC_LIBSTD_VERSION <= 402 -# define FMT_USE_RVALUE_REFERENCES 0 -# else -# define FMT_USE_RVALUE_REFERENCES \ - (FMT_HAS_FEATURE(cxx_rvalue_references) || \ - (FMT_GCC_VERSION >= 403 && FMT_HAS_GXX_CXX11) || FMT_MSC_VER >= 1600) -# endif -#endif - -// Check if exceptions are disabled. -#if defined(__GNUC__) && !defined(__EXCEPTIONS) -# define FMT_EXCEPTIONS 0 -#endif -#if FMT_MSC_VER && !_HAS_EXCEPTIONS -# define FMT_EXCEPTIONS 0 -#endif -#ifndef FMT_EXCEPTIONS -# define FMT_EXCEPTIONS 1 -#endif - -#ifndef FMT_THROW -# if FMT_EXCEPTIONS -# define FMT_THROW(x) throw x -# else -# define FMT_THROW(x) assert(false) -# endif -#endif - -// Define FMT_USE_NOEXCEPT to make fmt use noexcept (C++11 feature). -#ifndef FMT_USE_NOEXCEPT -# define FMT_USE_NOEXCEPT 0 -#endif - -#if FMT_USE_NOEXCEPT || FMT_HAS_FEATURE(cxx_noexcept) || \ - (FMT_GCC_VERSION >= 408 && FMT_HAS_GXX_CXX11) || \ - FMT_MSC_VER >= 1900 -# define FMT_DETECTED_NOEXCEPT noexcept -#else -# define FMT_DETECTED_NOEXCEPT throw() -#endif - -#ifndef FMT_NOEXCEPT -# if FMT_EXCEPTIONS -# define FMT_NOEXCEPT FMT_DETECTED_NOEXCEPT -# else -# define FMT_NOEXCEPT -# endif -#endif - -// This is needed because GCC still uses throw() in its headers when exceptions -// are disabled. -#if FMT_GCC_VERSION -# define FMT_DTOR_NOEXCEPT FMT_DETECTED_NOEXCEPT -#else -# define FMT_DTOR_NOEXCEPT FMT_NOEXCEPT -#endif - -#ifndef FMT_OVERRIDE -# if (defined(FMT_USE_OVERRIDE) && FMT_USE_OVERRIDE) || FMT_HAS_FEATURE(cxx_override) || \ - (FMT_GCC_VERSION >= 408 && FMT_HAS_GXX_CXX11) || \ - FMT_MSC_VER >= 1900 -# define FMT_OVERRIDE override -# else -# define FMT_OVERRIDE -# endif -#endif - -#ifndef FMT_NULL -# if FMT_HAS_FEATURE(cxx_nullptr) || \ - (FMT_GCC_VERSION >= 408 && FMT_HAS_GXX_CXX11) || \ - FMT_MSC_VER >= 1600 -# define FMT_NULL nullptr -# else -# define FMT_NULL NULL -# endif -#endif - -// A macro to disallow the copy constructor and operator= functions -// This should be used in the private: declarations for a class -#ifndef FMT_USE_DELETED_FUNCTIONS -# define FMT_USE_DELETED_FUNCTIONS 0 -#endif - -#if FMT_USE_DELETED_FUNCTIONS || FMT_HAS_FEATURE(cxx_deleted_functions) || \ - (FMT_GCC_VERSION >= 404 && FMT_HAS_GXX_CXX11) || FMT_MSC_VER >= 1800 -# define FMT_DELETED_OR_UNDEFINED = delete -# define FMT_DISALLOW_COPY_AND_ASSIGN(TypeName) \ - TypeName(const TypeName&) = delete; \ - TypeName& operator=(const TypeName&) = delete -#else -# define FMT_DELETED_OR_UNDEFINED -# define FMT_DISALLOW_COPY_AND_ASSIGN(TypeName) \ - TypeName(const TypeName&); \ - TypeName& operator=(const TypeName&) -#endif - -#ifndef FMT_USE_DEFAULTED_FUNCTIONS -# define FMT_USE_DEFAULTED_FUNCTIONS 0 -#endif - -#ifndef FMT_DEFAULTED_COPY_CTOR -# if FMT_USE_DEFAULTED_FUNCTIONS || FMT_HAS_FEATURE(cxx_defaulted_functions) || \ - (FMT_GCC_VERSION >= 404 && FMT_HAS_GXX_CXX11) || FMT_MSC_VER >= 1800 -# define FMT_DEFAULTED_COPY_CTOR(TypeName) \ - TypeName(const TypeName&) = default; -# else -# define FMT_DEFAULTED_COPY_CTOR(TypeName) -# endif -#endif - -#ifndef FMT_USE_USER_DEFINED_LITERALS -// All compilers which support UDLs also support variadic templates. This -// makes the fmt::literals implementation easier. However, an explicit check -// for variadic templates is added here just in case. -// For Intel's compiler both it and the system gcc/msc must support UDLs. -# define FMT_USE_USER_DEFINED_LITERALS \ - FMT_USE_VARIADIC_TEMPLATES && FMT_USE_RVALUE_REFERENCES && \ - (FMT_HAS_FEATURE(cxx_user_literals) || \ - (FMT_GCC_VERSION >= 407 && FMT_HAS_GXX_CXX11) || FMT_MSC_VER >= 1900) && \ - (!defined(FMT_ICC_VERSION) || FMT_ICC_VERSION >= 1500) -#endif - -#ifndef FMT_USE_EXTERN_TEMPLATES -# define FMT_USE_EXTERN_TEMPLATES \ - (FMT_CLANG_VERSION >= 209 || (FMT_GCC_VERSION >= 303 && FMT_HAS_GXX_CXX11)) -#endif - -#ifdef FMT_HEADER_ONLY -// If header only do not use extern templates. -# undef FMT_USE_EXTERN_TEMPLATES -# define FMT_USE_EXTERN_TEMPLATES 0 -#endif - -#ifndef FMT_ASSERT -# define FMT_ASSERT(condition, message) assert((condition) && message) -#endif - -// __builtin_clz is broken in clang with Microsoft CodeGen: -// https://github.com/fmtlib/fmt/issues/519 -#ifndef _MSC_VER -# if FMT_GCC_VERSION >= 400 || FMT_HAS_BUILTIN(__builtin_clz) -# define FMT_BUILTIN_CLZ(n) __builtin_clz(n) -# endif - -# if FMT_GCC_VERSION >= 400 || FMT_HAS_BUILTIN(__builtin_clzll) -# define FMT_BUILTIN_CLZLL(n) __builtin_clzll(n) -# endif -#endif - -// Some compilers masquerade as both MSVC and GCC-likes or -// otherwise support __builtin_clz and __builtin_clzll, so -// only define FMT_BUILTIN_CLZ using the MSVC intrinsics -// if the clz and clzll builtins are not available. -#if FMT_MSC_VER && !defined(FMT_BUILTIN_CLZLL) && !defined(_MANAGED) -# include // _BitScanReverse, _BitScanReverse64 - -namespace fmt { -namespace internal { -# pragma intrinsic(_BitScanReverse) -inline uint32_t clz(uint32_t x) { - unsigned long r = 0; - _BitScanReverse(&r, x); - - assert(x != 0); - // Static analysis complains about using uninitialized data - // "r", but the only way that can happen is if "x" is 0, - // which the callers guarantee to not happen. -# pragma warning(suppress: 6102) - return 31 - r; -} -# define FMT_BUILTIN_CLZ(n) fmt::internal::clz(n) - -# ifdef _WIN64 -# pragma intrinsic(_BitScanReverse64) -# endif - -inline uint32_t clzll(uint64_t x) { - unsigned long r = 0; -# ifdef _WIN64 - _BitScanReverse64(&r, x); -# else - // Scan the high 32 bits. - if (_BitScanReverse(&r, static_cast(x >> 32))) - return 63 - (r + 32); - - // Scan the low 32 bits. - _BitScanReverse(&r, static_cast(x)); -# endif - - assert(x != 0); - // Static analysis complains about using uninitialized data - // "r", but the only way that can happen is if "x" is 0, - // which the callers guarantee to not happen. -# pragma warning(suppress: 6102) - return 63 - r; -} -# define FMT_BUILTIN_CLZLL(n) fmt::internal::clzll(n) -} -} -#endif - -namespace fmt { -namespace internal { -struct DummyInt { - int data[2]; - operator int() const { return 0; } -}; -typedef std::numeric_limits FPUtil; - -// Dummy implementations of system functions such as signbit and ecvt called -// if the latter are not available. -inline DummyInt signbit(...) { return DummyInt(); } -inline DummyInt _ecvt_s(...) { return DummyInt(); } -inline DummyInt isinf(...) { return DummyInt(); } -inline DummyInt _finite(...) { return DummyInt(); } -inline DummyInt isnan(...) { return DummyInt(); } -inline DummyInt _isnan(...) { return DummyInt(); } - -// A helper function to suppress bogus "conditional expression is constant" -// warnings. -template -inline T const_check(T value) { return value; } -} -} // namespace fmt - -namespace std { -// Standard permits specialization of std::numeric_limits. This specialization -// is used to resolve ambiguity between isinf and std::isinf in glibc: -// https://gcc.gnu.org/bugzilla/show_bug.cgi?id=48891 -// and the same for isnan and signbit. -template <> -class numeric_limits : - public std::numeric_limits { - public: - // Portable version of isinf. - template - static bool isinfinity(T x) { - using namespace fmt::internal; - // The resolution "priority" is: - // isinf macro > std::isinf > ::isinf > fmt::internal::isinf - if (const_check(sizeof(isinf(x)) == sizeof(bool) || - sizeof(isinf(x)) == sizeof(int))) { - return isinf(x) != 0; - } - return !_finite(static_cast(x)); - } - - // Portable version of isnan. - template - static bool isnotanumber(T x) { - using namespace fmt::internal; - if (const_check(sizeof(isnan(x)) == sizeof(bool) || - sizeof(isnan(x)) == sizeof(int))) { - return isnan(x) != 0; - } - return _isnan(static_cast(x)) != 0; - } - - // Portable version of signbit. - static bool isnegative(double x) { - using namespace fmt::internal; - if (const_check(sizeof(signbit(x)) == sizeof(bool) || - sizeof(signbit(x)) == sizeof(int))) { - return signbit(x) != 0; - } - if (x < 0) return true; - if (!isnotanumber(x)) return false; - int dec = 0, sign = 0; - char buffer[2]; // The buffer size must be >= 2 or _ecvt_s will fail. - _ecvt_s(buffer, sizeof(buffer), x, 0, &dec, &sign); - return sign != 0; - } -}; -} // namespace std - -namespace fmt { - -// Fix the warning about long long on older versions of GCC -// that don't support the diagnostic pragma. -FMT_GCC_EXTENSION typedef long long LongLong; -FMT_GCC_EXTENSION typedef unsigned long long ULongLong; - -#if FMT_USE_RVALUE_REFERENCES -using std::move; -#endif - -template -class BasicWriter; - -typedef BasicWriter Writer; -typedef BasicWriter WWriter; - -template -class ArgFormatter; - -struct FormatSpec; - -template -class BasicPrintfArgFormatter; - -template > -class BasicFormatter; - -/** - \rst - A string reference. It can be constructed from a C string or - ``std::basic_string``. - - You can use one of the following typedefs for common character types: - - +------------+-------------------------+ - | Type | Definition | - +============+=========================+ - | StringRef | BasicStringRef | - +------------+-------------------------+ - | WStringRef | BasicStringRef | - +------------+-------------------------+ - - This class is most useful as a parameter type to allow passing - different types of strings to a function, for example:: - - template - std::string format(StringRef format_str, const Args & ... args); - - format("{}", 42); - format(std::string("{}"), 42); - \endrst - */ -template -class BasicStringRef { - private: - const Char *data_; - std::size_t size_; - - public: - /** Constructs a string reference object from a C string and a size. */ - BasicStringRef(const Char *s, std::size_t size) : data_(s), size_(size) {} - - /** - \rst - Constructs a string reference object from a C string computing - the size with ``std::char_traits::length``. - \endrst - */ - BasicStringRef(const Char *s) - : data_(s), size_(std::char_traits::length(s)) {} - - /** - \rst - Constructs a string reference from a ``std::basic_string`` object. - \endrst - */ - template - BasicStringRef( - const std::basic_string, Allocator> &s) - : data_(s.c_str()), size_(s.size()) {} - - /** - \rst - Converts a string reference to an ``std::string`` object. - \endrst - */ - std::basic_string to_string() const { - return std::basic_string(data_, size_); - } - - /** Returns a pointer to the string data. */ - const Char *data() const { return data_; } - - /** Returns the string size. */ - std::size_t size() const { return size_; } - - // Lexicographically compare this string reference to other. - int compare(BasicStringRef other) const { - std::size_t size = size_ < other.size_ ? size_ : other.size_; - int result = std::char_traits::compare(data_, other.data_, size); - if (result == 0) - result = size_ == other.size_ ? 0 : (size_ < other.size_ ? -1 : 1); - return result; - } - - friend bool operator==(BasicStringRef lhs, BasicStringRef rhs) { - return lhs.compare(rhs) == 0; - } - friend bool operator!=(BasicStringRef lhs, BasicStringRef rhs) { - return lhs.compare(rhs) != 0; - } - friend bool operator<(BasicStringRef lhs, BasicStringRef rhs) { - return lhs.compare(rhs) < 0; - } - friend bool operator<=(BasicStringRef lhs, BasicStringRef rhs) { - return lhs.compare(rhs) <= 0; - } - friend bool operator>(BasicStringRef lhs, BasicStringRef rhs) { - return lhs.compare(rhs) > 0; - } - friend bool operator>=(BasicStringRef lhs, BasicStringRef rhs) { - return lhs.compare(rhs) >= 0; - } -}; - -typedef BasicStringRef StringRef; -typedef BasicStringRef WStringRef; - -/** - \rst - A reference to a null terminated string. It can be constructed from a C - string or ``std::basic_string``. - - You can use one of the following typedefs for common character types: - - +-------------+--------------------------+ - | Type | Definition | - +=============+==========================+ - | CStringRef | BasicCStringRef | - +-------------+--------------------------+ - | WCStringRef | BasicCStringRef | - +-------------+--------------------------+ - - This class is most useful as a parameter type to allow passing - different types of strings to a function, for example:: - - template - std::string format(CStringRef format_str, const Args & ... args); - - format("{}", 42); - format(std::string("{}"), 42); - \endrst - */ -template -class BasicCStringRef { - private: - const Char *data_; - - public: - /** Constructs a string reference object from a C string. */ - BasicCStringRef(const Char *s) : data_(s) {} - - /** - \rst - Constructs a string reference from a ``std::basic_string`` object. - \endrst - */ - template - BasicCStringRef( - const std::basic_string, Allocator> &s) - : data_(s.c_str()) {} - - /** Returns the pointer to a C string. */ - const Char *c_str() const { return data_; } -}; - -typedef BasicCStringRef CStringRef; -typedef BasicCStringRef WCStringRef; - -/** A formatting error such as invalid format string. */ -class FormatError : public std::runtime_error { - public: - explicit FormatError(CStringRef message) - : std::runtime_error(message.c_str()) {} - FormatError(const FormatError &ferr) : std::runtime_error(ferr) {} - FMT_API ~FormatError() FMT_DTOR_NOEXCEPT; -}; - -namespace internal { - -// MakeUnsigned::Type gives an unsigned type corresponding to integer type T. -template -struct MakeUnsigned { typedef T Type; }; - -#define FMT_SPECIALIZE_MAKE_UNSIGNED(T, U) \ - template <> \ - struct MakeUnsigned { typedef U Type; } - -FMT_SPECIALIZE_MAKE_UNSIGNED(char, unsigned char); -FMT_SPECIALIZE_MAKE_UNSIGNED(signed char, unsigned char); -FMT_SPECIALIZE_MAKE_UNSIGNED(short, unsigned short); -FMT_SPECIALIZE_MAKE_UNSIGNED(int, unsigned); -FMT_SPECIALIZE_MAKE_UNSIGNED(long, unsigned long); -FMT_SPECIALIZE_MAKE_UNSIGNED(LongLong, ULongLong); - -// Casts nonnegative integer to unsigned. -template -inline typename MakeUnsigned::Type to_unsigned(Int value) { - FMT_ASSERT(value >= 0, "negative value"); - return static_cast::Type>(value); -} - -// The number of characters to store in the MemoryBuffer object itself -// to avoid dynamic memory allocation. -enum { INLINE_BUFFER_SIZE = 500 }; - -#if FMT_SECURE_SCL -// Use checked iterator to avoid warnings on MSVC. -template -inline stdext::checked_array_iterator make_ptr(T *ptr, std::size_t size) { - return stdext::checked_array_iterator(ptr, size); -} -#else -template -inline T *make_ptr(T *ptr, std::size_t) { return ptr; } -#endif -} // namespace internal - -/** - \rst - A buffer supporting a subset of ``std::vector``'s operations. - \endrst - */ -template -class Buffer { - private: - FMT_DISALLOW_COPY_AND_ASSIGN(Buffer); - - protected: - T *ptr_; - std::size_t size_; - std::size_t capacity_; - - Buffer(T *ptr = FMT_NULL, std::size_t capacity = 0) - : ptr_(ptr), size_(0), capacity_(capacity) {} - - /** - \rst - Increases the buffer capacity to hold at least *size* elements updating - ``ptr_`` and ``capacity_``. - \endrst - */ - virtual void grow(std::size_t size) = 0; - - public: - virtual ~Buffer() {} - - /** Returns the size of this buffer. */ - std::size_t size() const { return size_; } - - /** Returns the capacity of this buffer. */ - std::size_t capacity() const { return capacity_; } - - /** - Resizes the buffer. If T is a POD type new elements may not be initialized. - */ - void resize(std::size_t new_size) { - if (new_size > capacity_) - grow(new_size); - size_ = new_size; - } - - /** - \rst - Reserves space to store at least *capacity* elements. - \endrst - */ - void reserve(std::size_t capacity) { - if (capacity > capacity_) - grow(capacity); - } - - void clear() FMT_NOEXCEPT { size_ = 0; } - - void push_back(const T &value) { - if (size_ == capacity_) - grow(size_ + 1); - ptr_[size_++] = value; - } - - /** Appends data to the end of the buffer. */ - template - void append(const U *begin, const U *end); - - T &operator[](std::size_t index) { return ptr_[index]; } - const T &operator[](std::size_t index) const { return ptr_[index]; } -}; - -template -template -void Buffer::append(const U *begin, const U *end) { - FMT_ASSERT(end >= begin, "negative value"); - std::size_t new_size = size_ + (end - begin); - if (new_size > capacity_) - grow(new_size); - std::uninitialized_copy(begin, end, - internal::make_ptr(ptr_, capacity_) + size_); - size_ = new_size; -} - -namespace internal { - -// A memory buffer for trivially copyable/constructible types with the first -// SIZE elements stored in the object itself. -template > -class MemoryBuffer : private Allocator, public Buffer { - private: - T data_[SIZE]; - - // Deallocate memory allocated by the buffer. - void deallocate() { - if (this->ptr_ != data_) Allocator::deallocate(this->ptr_, this->capacity_); - } - - protected: - void grow(std::size_t size) FMT_OVERRIDE; - - public: - explicit MemoryBuffer(const Allocator &alloc = Allocator()) - : Allocator(alloc), Buffer(data_, SIZE) {} - ~MemoryBuffer() { deallocate(); } - -#if FMT_USE_RVALUE_REFERENCES - private: - // Move data from other to this buffer. - void move(MemoryBuffer &other) { - Allocator &this_alloc = *this, &other_alloc = other; - this_alloc = std::move(other_alloc); - this->size_ = other.size_; - this->capacity_ = other.capacity_; - if (other.ptr_ == other.data_) { - this->ptr_ = data_; - std::uninitialized_copy(other.data_, other.data_ + this->size_, - make_ptr(data_, this->capacity_)); - } else { - this->ptr_ = other.ptr_; - // Set pointer to the inline array so that delete is not called - // when deallocating. - other.ptr_ = other.data_; - } - } - - public: - MemoryBuffer(MemoryBuffer &&other) { - move(other); - } - - MemoryBuffer &operator=(MemoryBuffer &&other) { - assert(this != &other); - deallocate(); - move(other); - return *this; - } -#endif - - // Returns a copy of the allocator associated with this buffer. - Allocator get_allocator() const { return *this; } -}; - -template -void MemoryBuffer::grow(std::size_t size) { - std::size_t new_capacity = this->capacity_ + this->capacity_ / 2; - if (size > new_capacity) - new_capacity = size; - T *new_ptr = this->allocate(new_capacity, FMT_NULL); - // The following code doesn't throw, so the raw pointer above doesn't leak. - std::uninitialized_copy(this->ptr_, this->ptr_ + this->size_, - make_ptr(new_ptr, new_capacity)); - std::size_t old_capacity = this->capacity_; - T *old_ptr = this->ptr_; - this->capacity_ = new_capacity; - this->ptr_ = new_ptr; - // deallocate may throw (at least in principle), but it doesn't matter since - // the buffer already uses the new storage and will deallocate it in case - // of exception. - if (old_ptr != data_) - Allocator::deallocate(old_ptr, old_capacity); -} - -// A fixed-size buffer. -template -class FixedBuffer : public fmt::Buffer { - public: - FixedBuffer(Char *array, std::size_t size) : fmt::Buffer(array, size) {} - - protected: - FMT_API void grow(std::size_t size) FMT_OVERRIDE; -}; - -template -class BasicCharTraits { - public: -#if FMT_SECURE_SCL - typedef stdext::checked_array_iterator CharPtr; -#else - typedef Char *CharPtr; -#endif - static Char cast(int value) { return static_cast(value); } -}; - -template -class CharTraits; - -template <> -class CharTraits : public BasicCharTraits { - private: - // Conversion from wchar_t to char is not allowed. - static char convert(wchar_t); - - public: - static char convert(char value) { return value; } - - // Formats a floating-point number. - template - FMT_API static int format_float(char *buffer, std::size_t size, - const char *format, unsigned width, int precision, T value); -}; - -#if FMT_USE_EXTERN_TEMPLATES -extern template int CharTraits::format_float - (char *buffer, std::size_t size, - const char* format, unsigned width, int precision, double value); -extern template int CharTraits::format_float - (char *buffer, std::size_t size, - const char* format, unsigned width, int precision, long double value); -#endif - -template <> -class CharTraits : public BasicCharTraits { - public: - static wchar_t convert(char value) { return value; } - static wchar_t convert(wchar_t value) { return value; } - - template - FMT_API static int format_float(wchar_t *buffer, std::size_t size, - const wchar_t *format, unsigned width, int precision, T value); -}; - -#if FMT_USE_EXTERN_TEMPLATES -extern template int CharTraits::format_float - (wchar_t *buffer, std::size_t size, - const wchar_t* format, unsigned width, int precision, double value); -extern template int CharTraits::format_float - (wchar_t *buffer, std::size_t size, - const wchar_t* format, unsigned width, int precision, long double value); -#endif - -// Checks if a number is negative - used to avoid warnings. -template -struct SignChecker { - template - static bool is_negative(T value) { return value < 0; } -}; - -template <> -struct SignChecker { - template - static bool is_negative(T) { return false; } -}; - -// Returns true if value is negative, false otherwise. -// Same as (value < 0) but doesn't produce warnings if T is an unsigned type. -template -inline bool is_negative(T value) { - return SignChecker::is_signed>::is_negative(value); -} - -// Selects uint32_t if FitsIn32Bits is true, uint64_t otherwise. -template -struct TypeSelector { typedef uint32_t Type; }; - -template <> -struct TypeSelector { typedef uint64_t Type; }; - -template -struct IntTraits { - // Smallest of uint32_t and uint64_t that is large enough to represent - // all values of T. - typedef typename - TypeSelector::digits <= 32>::Type MainType; -}; - -FMT_API void report_unknown_type(char code, const char *type); - -// Static data is placed in this class template to allow header-only -// configuration. -template -struct FMT_API BasicData { - static const uint32_t POWERS_OF_10_32[]; - static const uint64_t POWERS_OF_10_64[]; - static const char DIGITS[]; -}; - -#if FMT_USE_EXTERN_TEMPLATES -extern template struct BasicData; -#endif - -typedef BasicData<> Data; - -#ifdef FMT_BUILTIN_CLZLL -// Returns the number of decimal digits in n. Leading zeros are not counted -// except for n == 0 in which case count_digits returns 1. -inline unsigned count_digits(uint64_t n) { - // Based on http://graphics.stanford.edu/~seander/bithacks.html#IntegerLog10 - // and the benchmark https://github.com/localvoid/cxx-benchmark-count-digits. - int t = (64 - FMT_BUILTIN_CLZLL(n | 1)) * 1233 >> 12; - return to_unsigned(t) - (n < Data::POWERS_OF_10_64[t]) + 1; -} -#else -// Fallback version of count_digits used when __builtin_clz is not available. -inline unsigned count_digits(uint64_t n) { - unsigned count = 1; - for (;;) { - // Integer division is slow so do it for a group of four digits instead - // of for every digit. The idea comes from the talk by Alexandrescu - // "Three Optimization Tips for C++". See speed-test for a comparison. - if (n < 10) return count; - if (n < 100) return count + 1; - if (n < 1000) return count + 2; - if (n < 10000) return count + 3; - n /= 10000u; - count += 4; - } -} -#endif - -#ifdef FMT_BUILTIN_CLZ -// Optional version of count_digits for better performance on 32-bit platforms. -inline unsigned count_digits(uint32_t n) { - int t = (32 - FMT_BUILTIN_CLZ(n | 1)) * 1233 >> 12; - return to_unsigned(t) - (n < Data::POWERS_OF_10_32[t]) + 1; -} -#endif - -// A functor that doesn't add a thousands separator. -struct NoThousandsSep { - template - void operator()(Char *) {} -}; - -// A functor that adds a thousands separator. -class ThousandsSep { - private: - fmt::StringRef sep_; - - // Index of a decimal digit with the least significant digit having index 0. - unsigned digit_index_; - - public: - explicit ThousandsSep(fmt::StringRef sep) : sep_(sep), digit_index_(0) {} - - template - void operator()(Char *&buffer) { - if (++digit_index_ % 3 != 0) - return; - buffer -= sep_.size(); - std::uninitialized_copy(sep_.data(), sep_.data() + sep_.size(), - internal::make_ptr(buffer, sep_.size())); - } -}; - -// Formats a decimal unsigned integer value writing into buffer. -// thousands_sep is a functor that is called after writing each char to -// add a thousands separator if necessary. -template -inline void format_decimal(Char *buffer, UInt value, unsigned num_digits, - ThousandsSep thousands_sep) { - buffer += num_digits; - while (value >= 100) { - // Integer division is slow so do it for a group of two digits instead - // of for every digit. The idea comes from the talk by Alexandrescu - // "Three Optimization Tips for C++". See speed-test for a comparison. - unsigned index = static_cast((value % 100) * 2); - value /= 100; - *--buffer = Data::DIGITS[index + 1]; - thousands_sep(buffer); - *--buffer = Data::DIGITS[index]; - thousands_sep(buffer); - } - if (value < 10) { - *--buffer = static_cast('0' + value); - return; - } - unsigned index = static_cast(value * 2); - *--buffer = Data::DIGITS[index + 1]; - thousands_sep(buffer); - *--buffer = Data::DIGITS[index]; -} - -template -inline void format_decimal(Char *buffer, UInt value, unsigned num_digits) { - format_decimal(buffer, value, num_digits, NoThousandsSep()); - return; -} - -#ifndef _WIN32 -# define FMT_USE_WINDOWS_H 0 -#elif !defined(FMT_USE_WINDOWS_H) -# define FMT_USE_WINDOWS_H 1 -#endif - -// Define FMT_USE_WINDOWS_H to 0 to disable use of windows.h. -// All the functionality that relies on it will be disabled too. -#if FMT_USE_WINDOWS_H -// A converter from UTF-8 to UTF-16. -// It is only provided for Windows since other systems support UTF-8 natively. -class UTF8ToUTF16 { - private: - MemoryBuffer buffer_; - - public: - FMT_API explicit UTF8ToUTF16(StringRef s); - operator WStringRef() const { return WStringRef(&buffer_[0], size()); } - size_t size() const { return buffer_.size() - 1; } - const wchar_t *c_str() const { return &buffer_[0]; } - std::wstring str() const { return std::wstring(&buffer_[0], size()); } -}; - -// A converter from UTF-16 to UTF-8. -// It is only provided for Windows since other systems support UTF-8 natively. -class UTF16ToUTF8 { - private: - MemoryBuffer buffer_; - - public: - UTF16ToUTF8() {} - FMT_API explicit UTF16ToUTF8(WStringRef s); - operator StringRef() const { return StringRef(&buffer_[0], size()); } - size_t size() const { return buffer_.size() - 1; } - const char *c_str() const { return &buffer_[0]; } - std::string str() const { return std::string(&buffer_[0], size()); } - - // Performs conversion returning a system error code instead of - // throwing exception on conversion error. This method may still throw - // in case of memory allocation error. - FMT_API int convert(WStringRef s); -}; - -FMT_API void format_windows_error(fmt::Writer &out, int error_code, - fmt::StringRef message) FMT_NOEXCEPT; -#endif - -// A formatting argument value. -struct Value { - template - struct StringValue { - const Char *value; - std::size_t size; - }; - - typedef void (*FormatFunc)( - void *formatter, const void *arg, void *format_str_ptr); - - struct CustomValue { - const void *value; - FormatFunc format; - }; - - union { - int int_value; - unsigned uint_value; - LongLong long_long_value; - ULongLong ulong_long_value; - double double_value; - long double long_double_value; - const void *pointer; - StringValue string; - StringValue sstring; - StringValue ustring; - StringValue wstring; - CustomValue custom; - }; - - enum Type { - NONE, NAMED_ARG, - // Integer types should go first, - INT, UINT, LONG_LONG, ULONG_LONG, BOOL, CHAR, LAST_INTEGER_TYPE = CHAR, - // followed by floating-point types. - DOUBLE, LONG_DOUBLE, LAST_NUMERIC_TYPE = LONG_DOUBLE, - CSTRING, STRING, WSTRING, POINTER, CUSTOM - }; -}; - -// A formatting argument. It is a trivially copyable/constructible type to -// allow storage in internal::MemoryBuffer. -struct Arg : Value { - Type type; -}; - -template -struct NamedArg; -template -struct NamedArgWithType; - -template -struct Null {}; - -// A helper class template to enable or disable overloads taking wide -// characters and strings in MakeValue. -template -struct WCharHelper { - typedef Null Supported; - typedef T Unsupported; -}; - -template -struct WCharHelper { - typedef T Supported; - typedef Null Unsupported; -}; - -typedef char Yes[1]; -typedef char No[2]; - -template -T &get(); - -// These are non-members to workaround an overload resolution bug in bcc32. -Yes &convert(fmt::ULongLong); -No &convert(...); - -template -struct ConvertToIntImpl { - enum { value = ENABLE_CONVERSION }; -}; - -template -struct ConvertToIntImpl2 { - enum { value = false }; -}; - -template -struct ConvertToIntImpl2 { - enum { - // Don't convert numeric types. - value = ConvertToIntImpl::is_specialized>::value - }; -}; - -template -struct ConvertToInt { - enum { - enable_conversion = sizeof(fmt::internal::convert(get())) == sizeof(Yes) - }; - enum { value = ConvertToIntImpl2::value }; -}; - -#define FMT_DISABLE_CONVERSION_TO_INT(Type) \ - template <> \ - struct ConvertToInt { enum { value = 0 }; } - -// Silence warnings about convering float to int. -FMT_DISABLE_CONVERSION_TO_INT(float); -FMT_DISABLE_CONVERSION_TO_INT(double); -FMT_DISABLE_CONVERSION_TO_INT(long double); - -template -struct EnableIf {}; - -template -struct EnableIf { typedef T type; }; - -template -struct Conditional { typedef T type; }; - -template -struct Conditional { typedef F type; }; - -// For bcc32 which doesn't understand ! in template arguments. -template -struct Not { enum { value = 0 }; }; - -template <> -struct Not { enum { value = 1 }; }; - -template -struct FalseType { enum { value = 0 }; }; - -template struct LConvCheck { - LConvCheck(int) {} -}; - -// Returns the thousands separator for the current locale. -// We check if ``lconv`` contains ``thousands_sep`` because on Android -// ``lconv`` is stubbed as an empty struct. -template -inline StringRef thousands_sep( - LConv *lc, LConvCheck = 0) { - return lc->thousands_sep; -} - -inline fmt::StringRef thousands_sep(...) { return ""; } - -#define FMT_CONCAT(a, b) a##b - -#if FMT_GCC_VERSION >= 303 -# define FMT_UNUSED __attribute__((unused)) -#else -# define FMT_UNUSED -#endif - -#ifndef FMT_USE_STATIC_ASSERT -# define FMT_USE_STATIC_ASSERT 0 -#endif - -#if FMT_USE_STATIC_ASSERT || FMT_HAS_FEATURE(cxx_static_assert) || \ - (FMT_GCC_VERSION >= 403 && FMT_HAS_GXX_CXX11) || _MSC_VER >= 1600 -# define FMT_STATIC_ASSERT(cond, message) static_assert(cond, message) -#else -# define FMT_CONCAT_(a, b) FMT_CONCAT(a, b) -# define FMT_STATIC_ASSERT(cond, message) \ - typedef int FMT_CONCAT_(Assert, __LINE__)[(cond) ? 1 : -1] FMT_UNUSED -#endif - -template -void format_arg(Formatter &, const Char *, const T &) { - FMT_STATIC_ASSERT(FalseType::value, - "Cannot format argument. To enable the use of ostream " - "operator<< include fmt/ostream.h. Otherwise provide " - "an overload of format_arg."); -} - -// Makes an Arg object from any type. -template -class MakeValue : public Arg { - public: - typedef typename Formatter::Char Char; - - private: - // The following two methods are private to disallow formatting of - // arbitrary pointers. If you want to output a pointer cast it to - // "void *" or "const void *". In particular, this forbids formatting - // of "[const] volatile char *" which is printed as bool by iostreams. - // Do not implement! - template - MakeValue(const T *value); - template - MakeValue(T *value); - - // The following methods are private to disallow formatting of wide - // characters and strings into narrow strings as in - // fmt::format("{}", L"test"); - // To fix this, use a wide format string: fmt::format(L"{}", L"test"). -#if !FMT_MSC_VER || defined(_NATIVE_WCHAR_T_DEFINED) - MakeValue(typename WCharHelper::Unsupported); -#endif - MakeValue(typename WCharHelper::Unsupported); - MakeValue(typename WCharHelper::Unsupported); - MakeValue(typename WCharHelper::Unsupported); - MakeValue(typename WCharHelper::Unsupported); - - void set_string(StringRef str) { - string.value = str.data(); - string.size = str.size(); - } - - void set_string(WStringRef str) { - wstring.value = str.data(); - wstring.size = str.size(); - } - - // Formats an argument of a custom type, such as a user-defined class. - template - static void format_custom_arg( - void *formatter, const void *arg, void *format_str_ptr) { - format_arg(*static_cast(formatter), - *static_cast(format_str_ptr), - *static_cast(arg)); - } - - public: - MakeValue() {} - -#define FMT_MAKE_VALUE_(Type, field, TYPE, rhs) \ - MakeValue(Type value) { field = rhs; } \ - static uint64_t type(Type) { return Arg::TYPE; } - -#define FMT_MAKE_VALUE(Type, field, TYPE) \ - FMT_MAKE_VALUE_(Type, field, TYPE, value) - - FMT_MAKE_VALUE(bool, int_value, BOOL) - FMT_MAKE_VALUE(short, int_value, INT) - FMT_MAKE_VALUE(unsigned short, uint_value, UINT) - FMT_MAKE_VALUE(int, int_value, INT) - FMT_MAKE_VALUE(unsigned, uint_value, UINT) - - MakeValue(long value) { - // To minimize the number of types we need to deal with, long is - // translated either to int or to long long depending on its size. - if (const_check(sizeof(long) == sizeof(int))) - int_value = static_cast(value); - else - long_long_value = value; - } - static uint64_t type(long) { - return sizeof(long) == sizeof(int) ? Arg::INT : Arg::LONG_LONG; - } - - MakeValue(unsigned long value) { - if (const_check(sizeof(unsigned long) == sizeof(unsigned))) - uint_value = static_cast(value); - else - ulong_long_value = value; - } - static uint64_t type(unsigned long) { - return sizeof(unsigned long) == sizeof(unsigned) ? - Arg::UINT : Arg::ULONG_LONG; - } - - FMT_MAKE_VALUE(LongLong, long_long_value, LONG_LONG) - FMT_MAKE_VALUE(ULongLong, ulong_long_value, ULONG_LONG) - FMT_MAKE_VALUE(float, double_value, DOUBLE) - FMT_MAKE_VALUE(double, double_value, DOUBLE) - FMT_MAKE_VALUE(long double, long_double_value, LONG_DOUBLE) - FMT_MAKE_VALUE(signed char, int_value, INT) - FMT_MAKE_VALUE(unsigned char, uint_value, UINT) - FMT_MAKE_VALUE(char, int_value, CHAR) - -#if !defined(_MSC_VER) || defined(_NATIVE_WCHAR_T_DEFINED) - MakeValue(typename WCharHelper::Supported value) { - int_value = value; - } - static uint64_t type(wchar_t) { return Arg::CHAR; } -#endif - -#define FMT_MAKE_STR_VALUE(Type, TYPE) \ - MakeValue(Type value) { set_string(value); } \ - static uint64_t type(Type) { return Arg::TYPE; } - - FMT_MAKE_VALUE(char *, string.value, CSTRING) - FMT_MAKE_VALUE(const char *, string.value, CSTRING) - FMT_MAKE_VALUE(signed char *, sstring.value, CSTRING) - FMT_MAKE_VALUE(const signed char *, sstring.value, CSTRING) - FMT_MAKE_VALUE(unsigned char *, ustring.value, CSTRING) - FMT_MAKE_VALUE(const unsigned char *, ustring.value, CSTRING) - FMT_MAKE_STR_VALUE(const std::string &, STRING) - FMT_MAKE_STR_VALUE(StringRef, STRING) - FMT_MAKE_VALUE_(CStringRef, string.value, CSTRING, value.c_str()) - -#define FMT_MAKE_WSTR_VALUE(Type, TYPE) \ - MakeValue(typename WCharHelper::Supported value) { \ - set_string(value); \ - } \ - static uint64_t type(Type) { return Arg::TYPE; } - - FMT_MAKE_WSTR_VALUE(wchar_t *, WSTRING) - FMT_MAKE_WSTR_VALUE(const wchar_t *, WSTRING) - FMT_MAKE_WSTR_VALUE(const std::wstring &, WSTRING) - FMT_MAKE_WSTR_VALUE(WStringRef, WSTRING) - - FMT_MAKE_VALUE(void *, pointer, POINTER) - FMT_MAKE_VALUE(const void *, pointer, POINTER) - - template - MakeValue(const T &value, - typename EnableIf::value>::value, int>::type = 0) { - custom.value = &value; - custom.format = &format_custom_arg; - } - - template - static typename EnableIf::value>::value, uint64_t>::type - type(const T &) { - return Arg::CUSTOM; - } - - // Additional template param `Char_` is needed here because make_type always - // uses char. - template - MakeValue(const NamedArg &value) { pointer = &value; } - template - MakeValue(const NamedArgWithType &value) { pointer = &value; } - - template - static uint64_t type(const NamedArg &) { return Arg::NAMED_ARG; } - template - static uint64_t type(const NamedArgWithType &) { return Arg::NAMED_ARG; } -}; - -template -class MakeArg : public Arg { -public: - MakeArg() { - type = Arg::NONE; - } - - template - MakeArg(const T &value) - : Arg(MakeValue(value)) { - type = static_cast(MakeValue::type(value)); - } -}; - -template -struct NamedArg : Arg { - BasicStringRef name; - - template - NamedArg(BasicStringRef argname, const T &value) - : Arg(MakeArg< BasicFormatter >(value)), name(argname) {} -}; - -template -struct NamedArgWithType : NamedArg { - NamedArgWithType(BasicStringRef argname, const T &value) - : NamedArg(argname, value) {} -}; - -class RuntimeError : public std::runtime_error { - protected: - RuntimeError() : std::runtime_error("") {} - RuntimeError(const RuntimeError &rerr) : std::runtime_error(rerr) {} - FMT_API ~RuntimeError() FMT_DTOR_NOEXCEPT; -}; - -template -class ArgMap; -} // namespace internal - -/** An argument list. */ -class ArgList { - private: - // To reduce compiled code size per formatting function call, types of first - // MAX_PACKED_ARGS arguments are passed in the types_ field. - uint64_t types_; - union { - // If the number of arguments is less than MAX_PACKED_ARGS, the argument - // values are stored in values_, otherwise they are stored in args_. - // This is done to reduce compiled code size as storing larger objects - // may require more code (at least on x86-64) even if the same amount of - // data is actually copied to stack. It saves ~10% on the bloat test. - const internal::Value *values_; - const internal::Arg *args_; - }; - - internal::Arg::Type type(unsigned index) const { - return type(types_, index); - } - - template - friend class internal::ArgMap; - - public: - // Maximum number of arguments with packed types. - enum { MAX_PACKED_ARGS = 16 }; - - ArgList() : types_(0) {} - - ArgList(ULongLong types, const internal::Value *values) - : types_(types), values_(values) {} - ArgList(ULongLong types, const internal::Arg *args) - : types_(types), args_(args) {} - - uint64_t types() const { return types_; } - - /** Returns the argument at specified index. */ - internal::Arg operator[](unsigned index) const { - using internal::Arg; - Arg arg; - bool use_values = type(MAX_PACKED_ARGS - 1) == Arg::NONE; - if (index < MAX_PACKED_ARGS) { - Arg::Type arg_type = type(index); - internal::Value &val = arg; - if (arg_type != Arg::NONE) - val = use_values ? values_[index] : args_[index]; - arg.type = arg_type; - return arg; - } - if (use_values) { - // The index is greater than the number of arguments that can be stored - // in values, so return a "none" argument. - arg.type = Arg::NONE; - return arg; - } - for (unsigned i = MAX_PACKED_ARGS; i <= index; ++i) { - if (args_[i].type == Arg::NONE) - return args_[i]; - } - return args_[index]; - } - - static internal::Arg::Type type(uint64_t types, unsigned index) { - unsigned shift = index * 4; - uint64_t mask = 0xf; - return static_cast( - (types & (mask << shift)) >> shift); - } -}; - -#define FMT_DISPATCH(call) static_cast(this)->call - -/** - \rst - An argument visitor based on the `curiously recurring template pattern - `_. - - To use `~fmt::ArgVisitor` define a subclass that implements some or all of the - visit methods with the same signatures as the methods in `~fmt::ArgVisitor`, - for example, `~fmt::ArgVisitor::visit_int()`. - Pass the subclass as the *Impl* template parameter. Then calling - `~fmt::ArgVisitor::visit` for some argument will dispatch to a visit method - specific to the argument type. For example, if the argument type is - ``double`` then the `~fmt::ArgVisitor::visit_double()` method of a subclass - will be called. If the subclass doesn't contain a method with this signature, - then a corresponding method of `~fmt::ArgVisitor` will be called. - - **Example**:: - - class MyArgVisitor : public fmt::ArgVisitor { - public: - void visit_int(int value) { fmt::print("{}", value); } - void visit_double(double value) { fmt::print("{}", value ); } - }; - \endrst - */ -template -class ArgVisitor { - private: - typedef internal::Arg Arg; - - public: - void report_unhandled_arg() {} - - Result visit_unhandled_arg() { - FMT_DISPATCH(report_unhandled_arg()); - return Result(); - } - - /** Visits an ``int`` argument. **/ - Result visit_int(int value) { - return FMT_DISPATCH(visit_any_int(value)); - } - - /** Visits a ``long long`` argument. **/ - Result visit_long_long(LongLong value) { - return FMT_DISPATCH(visit_any_int(value)); - } - - /** Visits an ``unsigned`` argument. **/ - Result visit_uint(unsigned value) { - return FMT_DISPATCH(visit_any_int(value)); - } - - /** Visits an ``unsigned long long`` argument. **/ - Result visit_ulong_long(ULongLong value) { - return FMT_DISPATCH(visit_any_int(value)); - } - - /** Visits a ``bool`` argument. **/ - Result visit_bool(bool value) { - return FMT_DISPATCH(visit_any_int(value)); - } - - /** Visits a ``char`` or ``wchar_t`` argument. **/ - Result visit_char(int value) { - return FMT_DISPATCH(visit_any_int(value)); - } - - /** Visits an argument of any integral type. **/ - template - Result visit_any_int(T) { - return FMT_DISPATCH(visit_unhandled_arg()); - } - - /** Visits a ``double`` argument. **/ - Result visit_double(double value) { - return FMT_DISPATCH(visit_any_double(value)); - } - - /** Visits a ``long double`` argument. **/ - Result visit_long_double(long double value) { - return FMT_DISPATCH(visit_any_double(value)); - } - - /** Visits a ``double`` or ``long double`` argument. **/ - template - Result visit_any_double(T) { - return FMT_DISPATCH(visit_unhandled_arg()); - } - - /** Visits a null-terminated C string (``const char *``) argument. **/ - Result visit_cstring(const char *) { - return FMT_DISPATCH(visit_unhandled_arg()); - } - - /** Visits a string argument. **/ - Result visit_string(Arg::StringValue) { - return FMT_DISPATCH(visit_unhandled_arg()); - } - - /** Visits a wide string argument. **/ - Result visit_wstring(Arg::StringValue) { - return FMT_DISPATCH(visit_unhandled_arg()); - } - - /** Visits a pointer argument. **/ - Result visit_pointer(const void *) { - return FMT_DISPATCH(visit_unhandled_arg()); - } - - /** Visits an argument of a custom (user-defined) type. **/ - Result visit_custom(Arg::CustomValue) { - return FMT_DISPATCH(visit_unhandled_arg()); - } - - /** - \rst - Visits an argument dispatching to the appropriate visit method based on - the argument type. For example, if the argument type is ``double`` then - the `~fmt::ArgVisitor::visit_double()` method of the *Impl* class will be - called. - \endrst - */ - Result visit(const Arg &arg) { - switch (arg.type) { - case Arg::NONE: - case Arg::NAMED_ARG: - FMT_ASSERT(false, "invalid argument type"); - break; - case Arg::INT: - return FMT_DISPATCH(visit_int(arg.int_value)); - case Arg::UINT: - return FMT_DISPATCH(visit_uint(arg.uint_value)); - case Arg::LONG_LONG: - return FMT_DISPATCH(visit_long_long(arg.long_long_value)); - case Arg::ULONG_LONG: - return FMT_DISPATCH(visit_ulong_long(arg.ulong_long_value)); - case Arg::BOOL: - return FMT_DISPATCH(visit_bool(arg.int_value != 0)); - case Arg::CHAR: - return FMT_DISPATCH(visit_char(arg.int_value)); - case Arg::DOUBLE: - return FMT_DISPATCH(visit_double(arg.double_value)); - case Arg::LONG_DOUBLE: - return FMT_DISPATCH(visit_long_double(arg.long_double_value)); - case Arg::CSTRING: - return FMT_DISPATCH(visit_cstring(arg.string.value)); - case Arg::STRING: - return FMT_DISPATCH(visit_string(arg.string)); - case Arg::WSTRING: - return FMT_DISPATCH(visit_wstring(arg.wstring)); - case Arg::POINTER: - return FMT_DISPATCH(visit_pointer(arg.pointer)); - case Arg::CUSTOM: - return FMT_DISPATCH(visit_custom(arg.custom)); - } - return Result(); - } -}; - -enum Alignment { - ALIGN_DEFAULT, ALIGN_LEFT, ALIGN_RIGHT, ALIGN_CENTER, ALIGN_NUMERIC -}; - -// Flags. -enum { - SIGN_FLAG = 1, PLUS_FLAG = 2, MINUS_FLAG = 4, HASH_FLAG = 8, - CHAR_FLAG = 0x10 // Argument has char type - used in error reporting. -}; - -// An empty format specifier. -struct EmptySpec {}; - -// A type specifier. -template -struct TypeSpec : EmptySpec { - Alignment align() const { return ALIGN_DEFAULT; } - unsigned width() const { return 0; } - int precision() const { return -1; } - bool flag(unsigned) const { return false; } - char type() const { return TYPE; } - char type_prefix() const { return TYPE; } - char fill() const { return ' '; } -}; - -// A width specifier. -struct WidthSpec { - unsigned width_; - // Fill is always wchar_t and cast to char if necessary to avoid having - // two specialization of WidthSpec and its subclasses. - wchar_t fill_; - - WidthSpec(unsigned width, wchar_t fill) : width_(width), fill_(fill) {} - - unsigned width() const { return width_; } - wchar_t fill() const { return fill_; } -}; - -// An alignment specifier. -struct AlignSpec : WidthSpec { - Alignment align_; - - AlignSpec(unsigned width, wchar_t fill, Alignment align = ALIGN_DEFAULT) - : WidthSpec(width, fill), align_(align) {} - - Alignment align() const { return align_; } - - int precision() const { return -1; } -}; - -// An alignment and type specifier. -template -struct AlignTypeSpec : AlignSpec { - AlignTypeSpec(unsigned width, wchar_t fill) : AlignSpec(width, fill) {} - - bool flag(unsigned) const { return false; } - char type() const { return TYPE; } - char type_prefix() const { return TYPE; } -}; - -// A full format specifier. -struct FormatSpec : AlignSpec { - unsigned flags_; - int precision_; - char type_; - - FormatSpec( - unsigned width = 0, char type = 0, wchar_t fill = ' ') - : AlignSpec(width, fill), flags_(0), precision_(-1), type_(type) {} - - bool flag(unsigned f) const { return (flags_ & f) != 0; } - int precision() const { return precision_; } - char type() const { return type_; } - char type_prefix() const { return type_; } -}; - -// An integer format specifier. -template , typename Char = char> -class IntFormatSpec : public SpecT { - private: - T value_; - - public: - IntFormatSpec(T val, const SpecT &spec = SpecT()) - : SpecT(spec), value_(val) {} - - T value() const { return value_; } -}; - -// A string format specifier. -template -class StrFormatSpec : public AlignSpec { - private: - const Char *str_; - - public: - template - StrFormatSpec(const Char *str, unsigned width, FillChar fill) - : AlignSpec(width, fill), str_(str) { - internal::CharTraits::convert(FillChar()); - } - - const Char *str() const { return str_; } -}; - -/** - Returns an integer format specifier to format the value in base 2. - */ -IntFormatSpec > bin(int value); - -/** - Returns an integer format specifier to format the value in base 8. - */ -IntFormatSpec > oct(int value); - -/** - Returns an integer format specifier to format the value in base 16 using - lower-case letters for the digits above 9. - */ -IntFormatSpec > hex(int value); - -/** - Returns an integer formatter format specifier to format in base 16 using - upper-case letters for the digits above 9. - */ -IntFormatSpec > hexu(int value); - -/** - \rst - Returns an integer format specifier to pad the formatted argument with the - fill character to the specified width using the default (right) numeric - alignment. - - **Example**:: - - MemoryWriter out; - out << pad(hex(0xcafe), 8, '0'); - // out.str() == "0000cafe" - - \endrst - */ -template -IntFormatSpec, Char> pad( - int value, unsigned width, Char fill = ' '); - -#define FMT_DEFINE_INT_FORMATTERS(TYPE) \ -inline IntFormatSpec > bin(TYPE value) { \ - return IntFormatSpec >(value, TypeSpec<'b'>()); \ -} \ - \ -inline IntFormatSpec > oct(TYPE value) { \ - return IntFormatSpec >(value, TypeSpec<'o'>()); \ -} \ - \ -inline IntFormatSpec > hex(TYPE value) { \ - return IntFormatSpec >(value, TypeSpec<'x'>()); \ -} \ - \ -inline IntFormatSpec > hexu(TYPE value) { \ - return IntFormatSpec >(value, TypeSpec<'X'>()); \ -} \ - \ -template \ -inline IntFormatSpec > pad( \ - IntFormatSpec > f, unsigned width) { \ - return IntFormatSpec >( \ - f.value(), AlignTypeSpec(width, ' ')); \ -} \ - \ -/* For compatibility with older compilers we provide two overloads for pad, */ \ -/* one that takes a fill character and one that doesn't. In the future this */ \ -/* can be replaced with one overload making the template argument Char */ \ -/* default to char (C++11). */ \ -template \ -inline IntFormatSpec, Char> pad( \ - IntFormatSpec, Char> f, \ - unsigned width, Char fill) { \ - return IntFormatSpec, Char>( \ - f.value(), AlignTypeSpec(width, fill)); \ -} \ - \ -inline IntFormatSpec > pad( \ - TYPE value, unsigned width) { \ - return IntFormatSpec >( \ - value, AlignTypeSpec<0>(width, ' ')); \ -} \ - \ -template \ -inline IntFormatSpec, Char> pad( \ - TYPE value, unsigned width, Char fill) { \ - return IntFormatSpec, Char>( \ - value, AlignTypeSpec<0>(width, fill)); \ -} - -FMT_DEFINE_INT_FORMATTERS(int) -FMT_DEFINE_INT_FORMATTERS(long) -FMT_DEFINE_INT_FORMATTERS(unsigned) -FMT_DEFINE_INT_FORMATTERS(unsigned long) -FMT_DEFINE_INT_FORMATTERS(LongLong) -FMT_DEFINE_INT_FORMATTERS(ULongLong) - -/** - \rst - Returns a string formatter that pads the formatted argument with the fill - character to the specified width using the default (left) string alignment. - - **Example**:: - - std::string s = str(MemoryWriter() << pad("abc", 8)); - // s == "abc " - - \endrst - */ -template -inline StrFormatSpec pad( - const Char *str, unsigned width, Char fill = ' ') { - return StrFormatSpec(str, width, fill); -} - -inline StrFormatSpec pad( - const wchar_t *str, unsigned width, char fill = ' ') { - return StrFormatSpec(str, width, fill); -} - -namespace internal { - -template -class ArgMap { - private: - typedef std::vector< - std::pair, internal::Arg> > MapType; - typedef typename MapType::value_type Pair; - - MapType map_; - - public: - FMT_API void init(const ArgList &args); - - const internal::Arg *find(const fmt::BasicStringRef &name) const { - // The list is unsorted, so just return the first matching name. - for (typename MapType::const_iterator it = map_.begin(), end = map_.end(); - it != end; ++it) { - if (it->first == name) - return &it->second; - } - return FMT_NULL; - } -}; - -template -class ArgFormatterBase : public ArgVisitor { - private: - BasicWriter &writer_; - Spec &spec_; - - FMT_DISALLOW_COPY_AND_ASSIGN(ArgFormatterBase); - - void write_pointer(const void *p) { - spec_.flags_ = HASH_FLAG; - spec_.type_ = 'x'; - writer_.write_int(reinterpret_cast(p), spec_); - } - - // workaround MSVC two-phase lookup issue - typedef internal::Arg Arg; - - protected: - BasicWriter &writer() { return writer_; } - Spec &spec() { return spec_; } - - void write(bool value) { - const char *str_value = value ? "true" : "false"; - Arg::StringValue str = { str_value, std::strlen(str_value) }; - writer_.write_str(str, spec_); - } - - void write(const char *value) { - Arg::StringValue str = {value, value ? std::strlen(value) : 0}; - writer_.write_str(str, spec_); - } - - public: - typedef Spec SpecType; - - ArgFormatterBase(BasicWriter &w, Spec &s) - : writer_(w), spec_(s) {} - - template - void visit_any_int(T value) { writer_.write_int(value, spec_); } - - template - void visit_any_double(T value) { writer_.write_double(value, spec_); } - - void visit_bool(bool value) { - if (spec_.type_) { - visit_any_int(value); - return; - } - write(value); - } - - void visit_char(int value) { - if (spec_.type_ && spec_.type_ != 'c') { - spec_.flags_ |= CHAR_FLAG; - writer_.write_int(value, spec_); - return; - } - if (spec_.align_ == ALIGN_NUMERIC || spec_.flags_ != 0) - FMT_THROW(FormatError("invalid format specifier for char")); - typedef typename BasicWriter::CharPtr CharPtr; - Char fill = internal::CharTraits::cast(spec_.fill()); - CharPtr out = CharPtr(); - const unsigned CHAR_SIZE = 1; - if (spec_.width_ > CHAR_SIZE) { - out = writer_.grow_buffer(spec_.width_); - if (spec_.align_ == ALIGN_RIGHT) { - std::uninitialized_fill_n(out, spec_.width_ - CHAR_SIZE, fill); - out += spec_.width_ - CHAR_SIZE; - } else if (spec_.align_ == ALIGN_CENTER) { - out = writer_.fill_padding(out, spec_.width_, - internal::const_check(CHAR_SIZE), fill); - } else { - std::uninitialized_fill_n(out + CHAR_SIZE, - spec_.width_ - CHAR_SIZE, fill); - } - } else { - out = writer_.grow_buffer(CHAR_SIZE); - } - *out = internal::CharTraits::cast(value); - } - - void visit_cstring(const char *value) { - if (spec_.type_ == 'p') - return write_pointer(value); - write(value); - } - - // Qualification with "internal" here and below is a workaround for nvcc. - void visit_string(internal::Arg::StringValue value) { - writer_.write_str(value, spec_); - } - - using ArgVisitor::visit_wstring; - - void visit_wstring(internal::Arg::StringValue value) { - writer_.write_str(value, spec_); - } - - void visit_pointer(const void *value) { - if (spec_.type_ && spec_.type_ != 'p') - report_unknown_type(spec_.type_, "pointer"); - write_pointer(value); - } -}; - -class FormatterBase { - private: - ArgList args_; - int next_arg_index_; - - // Returns the argument with specified index. - FMT_API Arg do_get_arg(unsigned arg_index, const char *&error); - - protected: - const ArgList &args() const { return args_; } - - explicit FormatterBase(const ArgList &args) { - args_ = args; - next_arg_index_ = 0; - } - - // Returns the next argument. - Arg next_arg(const char *&error) { - if (next_arg_index_ >= 0) - return do_get_arg(internal::to_unsigned(next_arg_index_++), error); - error = "cannot switch from manual to automatic argument indexing"; - return Arg(); - } - - // Checks if manual indexing is used and returns the argument with - // specified index. - Arg get_arg(unsigned arg_index, const char *&error) { - return check_no_auto_index(error) ? do_get_arg(arg_index, error) : Arg(); - } - - bool check_no_auto_index(const char *&error) { - if (next_arg_index_ > 0) { - error = "cannot switch from automatic to manual argument indexing"; - return false; - } - next_arg_index_ = -1; - return true; - } - - template - void write(BasicWriter &w, const Char *start, const Char *end) { - if (start != end) - w << BasicStringRef(start, internal::to_unsigned(end - start)); - } -}; -} // namespace internal - -/** - \rst - An argument formatter based on the `curiously recurring template pattern - `_. - - To use `~fmt::BasicArgFormatter` define a subclass that implements some or - all of the visit methods with the same signatures as the methods in - `~fmt::ArgVisitor`, for example, `~fmt::ArgVisitor::visit_int()`. - Pass the subclass as the *Impl* template parameter. When a formatting - function processes an argument, it will dispatch to a visit method - specific to the argument type. For example, if the argument type is - ``double`` then the `~fmt::ArgVisitor::visit_double()` method of a subclass - will be called. If the subclass doesn't contain a method with this signature, - then a corresponding method of `~fmt::BasicArgFormatter` or its superclass - will be called. - \endrst - */ -template -class BasicArgFormatter : public internal::ArgFormatterBase { - private: - BasicFormatter &formatter_; - const Char *format_; - - public: - /** - \rst - Constructs an argument formatter object. - *formatter* is a reference to the main formatter object, *spec* contains - format specifier information for standard argument types, and *fmt* points - to the part of the format string being parsed for custom argument types. - \endrst - */ - BasicArgFormatter(BasicFormatter &formatter, - Spec &spec, const Char *fmt) - : internal::ArgFormatterBase(formatter.writer(), spec), - formatter_(formatter), format_(fmt) {} - - /** Formats an argument of a custom (user-defined) type. */ - void visit_custom(internal::Arg::CustomValue c) { - c.format(&formatter_, c.value, &format_); - } -}; - -/** The default argument formatter. */ -template -class ArgFormatter : - public BasicArgFormatter, Char, FormatSpec> { - public: - /** Constructs an argument formatter object. */ - ArgFormatter(BasicFormatter &formatter, - FormatSpec &spec, const Char *fmt) - : BasicArgFormatter, - Char, FormatSpec>(formatter, spec, fmt) {} -}; - -/** This template formats data and writes the output to a writer. */ -template -class BasicFormatter : private internal::FormatterBase { - public: - /** The character type for the output. */ - typedef CharType Char; - - private: - BasicWriter &writer_; - internal::ArgMap map_; - - FMT_DISALLOW_COPY_AND_ASSIGN(BasicFormatter); - - using internal::FormatterBase::get_arg; - - // Checks if manual indexing is used and returns the argument with - // specified name. - internal::Arg get_arg(BasicStringRef arg_name, const char *&error); - - // Parses argument index and returns corresponding argument. - internal::Arg parse_arg_index(const Char *&s); - - // Parses argument name and returns corresponding argument. - internal::Arg parse_arg_name(const Char *&s); - - public: - /** - \rst - Constructs a ``BasicFormatter`` object. References to the arguments and - the writer are stored in the formatter object so make sure they have - appropriate lifetimes. - \endrst - */ - BasicFormatter(const ArgList &args, BasicWriter &w) - : internal::FormatterBase(args), writer_(w) {} - - /** Returns a reference to the writer associated with this formatter. */ - BasicWriter &writer() { return writer_; } - - /** Formats stored arguments and writes the output to the writer. */ - void format(BasicCStringRef format_str); - - // Formats a single argument and advances format_str, a format string pointer. - const Char *format(const Char *&format_str, const internal::Arg &arg); -}; - -// Generates a comma-separated list with results of applying f to -// numbers 0..n-1. -# define FMT_GEN(n, f) FMT_GEN##n(f) -# define FMT_GEN1(f) f(0) -# define FMT_GEN2(f) FMT_GEN1(f), f(1) -# define FMT_GEN3(f) FMT_GEN2(f), f(2) -# define FMT_GEN4(f) FMT_GEN3(f), f(3) -# define FMT_GEN5(f) FMT_GEN4(f), f(4) -# define FMT_GEN6(f) FMT_GEN5(f), f(5) -# define FMT_GEN7(f) FMT_GEN6(f), f(6) -# define FMT_GEN8(f) FMT_GEN7(f), f(7) -# define FMT_GEN9(f) FMT_GEN8(f), f(8) -# define FMT_GEN10(f) FMT_GEN9(f), f(9) -# define FMT_GEN11(f) FMT_GEN10(f), f(10) -# define FMT_GEN12(f) FMT_GEN11(f), f(11) -# define FMT_GEN13(f) FMT_GEN12(f), f(12) -# define FMT_GEN14(f) FMT_GEN13(f), f(13) -# define FMT_GEN15(f) FMT_GEN14(f), f(14) - -namespace internal { -inline uint64_t make_type() { return 0; } - -template -inline uint64_t make_type(const T &arg) { - return MakeValue< BasicFormatter >::type(arg); -} - -template -struct ArgArray; - -template -struct ArgArray { - typedef Value Type[N > 0 ? N : 1]; - - template - static Value make(const T &value) { -#ifdef __clang__ - Value result = MakeValue(value); - // Workaround a bug in Apple LLVM version 4.2 (clang-425.0.28) of clang: - // https://github.com/fmtlib/fmt/issues/276 - (void)result.custom.format; - return result; -#else - return MakeValue(value); -#endif - } -}; - -template -struct ArgArray { - typedef Arg Type[N + 1]; // +1 for the list end Arg::NONE - - template - static Arg make(const T &value) { return MakeArg(value); } -}; - -#if FMT_USE_VARIADIC_TEMPLATES -template -inline uint64_t make_type(const Arg &first, const Args & ... tail) { - return make_type(first) | (make_type(tail...) << 4); -} - -#else - -struct ArgType { - uint64_t type; - - ArgType() : type(0) {} - - template - ArgType(const T &arg) : type(make_type(arg)) {} -}; - -# define FMT_ARG_TYPE_DEFAULT(n) ArgType t##n = ArgType() - -inline uint64_t make_type(FMT_GEN15(FMT_ARG_TYPE_DEFAULT)) { - return t0.type | (t1.type << 4) | (t2.type << 8) | (t3.type << 12) | - (t4.type << 16) | (t5.type << 20) | (t6.type << 24) | (t7.type << 28) | - (t8.type << 32) | (t9.type << 36) | (t10.type << 40) | (t11.type << 44) | - (t12.type << 48) | (t13.type << 52) | (t14.type << 56); -} -#endif -} // namespace internal - -# define FMT_MAKE_TEMPLATE_ARG(n) typename T##n -# define FMT_MAKE_ARG_TYPE(n) T##n -# define FMT_MAKE_ARG(n) const T##n &v##n -# define FMT_ASSIGN_char(n) \ - arr[n] = fmt::internal::MakeValue< fmt::BasicFormatter >(v##n) -# define FMT_ASSIGN_wchar_t(n) \ - arr[n] = fmt::internal::MakeValue< fmt::BasicFormatter >(v##n) - -#if FMT_USE_VARIADIC_TEMPLATES -// Defines a variadic function returning void. -# define FMT_VARIADIC_VOID(func, arg_type) \ - template \ - void func(arg_type arg0, const Args & ... args) { \ - typedef fmt::internal::ArgArray ArgArray; \ - typename ArgArray::Type array{ \ - ArgArray::template make >(args)...}; \ - func(arg0, fmt::ArgList(fmt::internal::make_type(args...), array)); \ - } - -// Defines a variadic constructor. -# define FMT_VARIADIC_CTOR(ctor, func, arg0_type, arg1_type) \ - template \ - ctor(arg0_type arg0, arg1_type arg1, const Args & ... args) { \ - typedef fmt::internal::ArgArray ArgArray; \ - typename ArgArray::Type array{ \ - ArgArray::template make >(args)...}; \ - func(arg0, arg1, fmt::ArgList(fmt::internal::make_type(args...), array)); \ - } - -#else - -# define FMT_MAKE_REF(n) \ - fmt::internal::MakeValue< fmt::BasicFormatter >(v##n) -# define FMT_MAKE_REF2(n) v##n - -// Defines a wrapper for a function taking one argument of type arg_type -// and n additional arguments of arbitrary types. -# define FMT_WRAP1(func, arg_type, n) \ - template \ - inline void func(arg_type arg1, FMT_GEN(n, FMT_MAKE_ARG)) { \ - const fmt::internal::ArgArray::Type array = {FMT_GEN(n, FMT_MAKE_REF)}; \ - func(arg1, fmt::ArgList( \ - fmt::internal::make_type(FMT_GEN(n, FMT_MAKE_REF2)), array)); \ - } - -// Emulates a variadic function returning void on a pre-C++11 compiler. -# define FMT_VARIADIC_VOID(func, arg_type) \ - inline void func(arg_type arg) { func(arg, fmt::ArgList()); } \ - FMT_WRAP1(func, arg_type, 1) FMT_WRAP1(func, arg_type, 2) \ - FMT_WRAP1(func, arg_type, 3) FMT_WRAP1(func, arg_type, 4) \ - FMT_WRAP1(func, arg_type, 5) FMT_WRAP1(func, arg_type, 6) \ - FMT_WRAP1(func, arg_type, 7) FMT_WRAP1(func, arg_type, 8) \ - FMT_WRAP1(func, arg_type, 9) FMT_WRAP1(func, arg_type, 10) - -# define FMT_CTOR(ctor, func, arg0_type, arg1_type, n) \ - template \ - ctor(arg0_type arg0, arg1_type arg1, FMT_GEN(n, FMT_MAKE_ARG)) { \ - const fmt::internal::ArgArray::Type array = {FMT_GEN(n, FMT_MAKE_REF)}; \ - func(arg0, arg1, fmt::ArgList( \ - fmt::internal::make_type(FMT_GEN(n, FMT_MAKE_REF2)), array)); \ - } - -// Emulates a variadic constructor on a pre-C++11 compiler. -# define FMT_VARIADIC_CTOR(ctor, func, arg0_type, arg1_type) \ - FMT_CTOR(ctor, func, arg0_type, arg1_type, 1) \ - FMT_CTOR(ctor, func, arg0_type, arg1_type, 2) \ - FMT_CTOR(ctor, func, arg0_type, arg1_type, 3) \ - FMT_CTOR(ctor, func, arg0_type, arg1_type, 4) \ - FMT_CTOR(ctor, func, arg0_type, arg1_type, 5) \ - FMT_CTOR(ctor, func, arg0_type, arg1_type, 6) \ - FMT_CTOR(ctor, func, arg0_type, arg1_type, 7) \ - FMT_CTOR(ctor, func, arg0_type, arg1_type, 8) \ - FMT_CTOR(ctor, func, arg0_type, arg1_type, 9) \ - FMT_CTOR(ctor, func, arg0_type, arg1_type, 10) -#endif - -// Generates a comma-separated list with results of applying f to pairs -// (argument, index). -#define FMT_FOR_EACH1(f, x0) f(x0, 0) -#define FMT_FOR_EACH2(f, x0, x1) \ - FMT_FOR_EACH1(f, x0), f(x1, 1) -#define FMT_FOR_EACH3(f, x0, x1, x2) \ - FMT_FOR_EACH2(f, x0 ,x1), f(x2, 2) -#define FMT_FOR_EACH4(f, x0, x1, x2, x3) \ - FMT_FOR_EACH3(f, x0, x1, x2), f(x3, 3) -#define FMT_FOR_EACH5(f, x0, x1, x2, x3, x4) \ - FMT_FOR_EACH4(f, x0, x1, x2, x3), f(x4, 4) -#define FMT_FOR_EACH6(f, x0, x1, x2, x3, x4, x5) \ - FMT_FOR_EACH5(f, x0, x1, x2, x3, x4), f(x5, 5) -#define FMT_FOR_EACH7(f, x0, x1, x2, x3, x4, x5, x6) \ - FMT_FOR_EACH6(f, x0, x1, x2, x3, x4, x5), f(x6, 6) -#define FMT_FOR_EACH8(f, x0, x1, x2, x3, x4, x5, x6, x7) \ - FMT_FOR_EACH7(f, x0, x1, x2, x3, x4, x5, x6), f(x7, 7) -#define FMT_FOR_EACH9(f, x0, x1, x2, x3, x4, x5, x6, x7, x8) \ - FMT_FOR_EACH8(f, x0, x1, x2, x3, x4, x5, x6, x7), f(x8, 8) -#define FMT_FOR_EACH10(f, x0, x1, x2, x3, x4, x5, x6, x7, x8, x9) \ - FMT_FOR_EACH9(f, x0, x1, x2, x3, x4, x5, x6, x7, x8), f(x9, 9) - -/** - An error returned by an operating system or a language runtime, - for example a file opening error. -*/ -class SystemError : public internal::RuntimeError { - private: - FMT_API void init(int err_code, CStringRef format_str, ArgList args); - - protected: - int error_code_; - - typedef char Char; // For FMT_VARIADIC_CTOR. - - SystemError() {} - - public: - /** - \rst - Constructs a :class:`fmt::SystemError` object with a description - formatted with `fmt::format_system_error`. *message* and additional - arguments passed into the constructor are formatted similarly to - `fmt::format`. - - **Example**:: - - // This throws a SystemError with the description - // cannot open file 'madeup': No such file or directory - // or similar (system message may vary). - const char *filename = "madeup"; - std::FILE *file = std::fopen(filename, "r"); - if (!file) - throw fmt::SystemError(errno, "cannot open file '{}'", filename); - \endrst - */ - SystemError(int error_code, CStringRef message) { - init(error_code, message, ArgList()); - } - FMT_DEFAULTED_COPY_CTOR(SystemError) - FMT_VARIADIC_CTOR(SystemError, init, int, CStringRef) - - FMT_API ~SystemError() FMT_DTOR_NOEXCEPT; - - int error_code() const { return error_code_; } -}; - -/** - \rst - Formats an error returned by an operating system or a language runtime, - for example a file opening error, and writes it to *out* in the following - form: - - .. parsed-literal:: - **: ** - - where ** is the passed message and ** is - the system message corresponding to the error code. - *error_code* is a system error code as given by ``errno``. - If *error_code* is not a valid error code such as -1, the system message - may look like "Unknown error -1" and is platform-dependent. - \endrst - */ -FMT_API void format_system_error(fmt::Writer &out, int error_code, - fmt::StringRef message) FMT_NOEXCEPT; - -/** - \rst - This template provides operations for formatting and writing data into - a character stream. The output is stored in a buffer provided by a subclass - such as :class:`fmt::BasicMemoryWriter`. - - You can use one of the following typedefs for common character types: - - +---------+----------------------+ - | Type | Definition | - +=========+======================+ - | Writer | BasicWriter | - +---------+----------------------+ - | WWriter | BasicWriter | - +---------+----------------------+ - - \endrst - */ -template -class BasicWriter { - private: - // Output buffer. - Buffer &buffer_; - - FMT_DISALLOW_COPY_AND_ASSIGN(BasicWriter); - - typedef typename internal::CharTraits::CharPtr CharPtr; - -#if FMT_SECURE_SCL - // Returns pointer value. - static Char *get(CharPtr p) { return p.base(); } -#else - static Char *get(Char *p) { return p; } -#endif - - // Fills the padding around the content and returns the pointer to the - // content area. - static CharPtr fill_padding(CharPtr buffer, - unsigned total_size, std::size_t content_size, wchar_t fill); - - // Grows the buffer by n characters and returns a pointer to the newly - // allocated area. - CharPtr grow_buffer(std::size_t n) { - std::size_t size = buffer_.size(); - buffer_.resize(size + n); - return internal::make_ptr(&buffer_[size], n); - } - - // Writes an unsigned decimal integer. - template - Char *write_unsigned_decimal(UInt value, unsigned prefix_size = 0) { - unsigned num_digits = internal::count_digits(value); - Char *ptr = get(grow_buffer(prefix_size + num_digits)); - internal::format_decimal(ptr + prefix_size, value, num_digits); - return ptr; - } - - // Writes a decimal integer. - template - void write_decimal(Int value) { - typedef typename internal::IntTraits::MainType MainType; - MainType abs_value = static_cast(value); - if (internal::is_negative(value)) { - abs_value = 0 - abs_value; - *write_unsigned_decimal(abs_value, 1) = '-'; - } else { - write_unsigned_decimal(abs_value, 0); - } - } - - // Prepare a buffer for integer formatting. - CharPtr prepare_int_buffer(unsigned num_digits, - const EmptySpec &, const char *prefix, unsigned prefix_size) { - unsigned size = prefix_size + num_digits; - CharPtr p = grow_buffer(size); - std::uninitialized_copy(prefix, prefix + prefix_size, p); - return p + size - 1; - } - - template - CharPtr prepare_int_buffer(unsigned num_digits, - const Spec &spec, const char *prefix, unsigned prefix_size); - - // Formats an integer. - template - void write_int(T value, Spec spec); - - // Formats a floating-point number (double or long double). - template - void write_double(T value, const Spec &spec); - - // Writes a formatted string. - template - CharPtr write_str(const StrChar *s, std::size_t size, const AlignSpec &spec); - - template - void write_str(const internal::Arg::StringValue &str, - const Spec &spec); - - // This following methods are private to disallow writing wide characters - // and strings to a char stream. If you want to print a wide string as a - // pointer as std::ostream does, cast it to const void*. - // Do not implement! - void operator<<(typename internal::WCharHelper::Unsupported); - void operator<<( - typename internal::WCharHelper::Unsupported); - - // Appends floating-point length specifier to the format string. - // The second argument is only used for overload resolution. - void append_float_length(Char *&format_ptr, long double) { - *format_ptr++ = 'L'; - } - - template - void append_float_length(Char *&, T) {} - - template - friend class internal::ArgFormatterBase; - - template - friend class BasicPrintfArgFormatter; - - protected: - /** - Constructs a ``BasicWriter`` object. - */ - explicit BasicWriter(Buffer &b) : buffer_(b) {} - - public: - /** - \rst - Destroys a ``BasicWriter`` object. - \endrst - */ - virtual ~BasicWriter() {} - - /** - Returns the total number of characters written. - */ - std::size_t size() const { return buffer_.size(); } - - /** - Returns a pointer to the output buffer content. No terminating null - character is appended. - */ - const Char *data() const FMT_NOEXCEPT { return &buffer_[0]; } - - /** - Returns a pointer to the output buffer content with terminating null - character appended. - */ - const Char *c_str() const { - std::size_t size = buffer_.size(); - buffer_.reserve(size + 1); - buffer_[size] = '\0'; - return &buffer_[0]; - } - - /** - \rst - Returns the content of the output buffer as an `std::string`. - \endrst - */ - std::basic_string str() const { - return std::basic_string(&buffer_[0], buffer_.size()); - } - - /** - \rst - Writes formatted data. - - *args* is an argument list representing arbitrary arguments. - - **Example**:: - - MemoryWriter out; - out.write("Current point:\n"); - out.write("({:+f}, {:+f})", -3.14, 3.14); - - This will write the following output to the ``out`` object: - - .. code-block:: none - - Current point: - (-3.140000, +3.140000) - - The output can be accessed using :func:`data()`, :func:`c_str` or - :func:`str` methods. - - See also :ref:`syntax`. - \endrst - */ - void write(BasicCStringRef format, ArgList args) { - BasicFormatter(args, *this).format(format); - } - FMT_VARIADIC_VOID(write, BasicCStringRef) - - BasicWriter &operator<<(int value) { - write_decimal(value); - return *this; - } - BasicWriter &operator<<(unsigned value) { - return *this << IntFormatSpec(value); - } - BasicWriter &operator<<(long value) { - write_decimal(value); - return *this; - } - BasicWriter &operator<<(unsigned long value) { - return *this << IntFormatSpec(value); - } - BasicWriter &operator<<(LongLong value) { - write_decimal(value); - return *this; - } - - /** - \rst - Formats *value* and writes it to the stream. - \endrst - */ - BasicWriter &operator<<(ULongLong value) { - return *this << IntFormatSpec(value); - } - - BasicWriter &operator<<(double value) { - write_double(value, FormatSpec()); - return *this; - } - - /** - \rst - Formats *value* using the general format for floating-point numbers - (``'g'``) and writes it to the stream. - \endrst - */ - BasicWriter &operator<<(long double value) { - write_double(value, FormatSpec()); - return *this; - } - - /** - Writes a character to the stream. - */ - BasicWriter &operator<<(char value) { - buffer_.push_back(value); - return *this; - } - - BasicWriter &operator<<( - typename internal::WCharHelper::Supported value) { - buffer_.push_back(value); - return *this; - } - - /** - \rst - Writes *value* to the stream. - \endrst - */ - BasicWriter &operator<<(fmt::BasicStringRef value) { - const Char *str = value.data(); - buffer_.append(str, str + value.size()); - return *this; - } - - BasicWriter &operator<<( - typename internal::WCharHelper::Supported value) { - const char *str = value.data(); - buffer_.append(str, str + value.size()); - return *this; - } - - template - BasicWriter &operator<<(IntFormatSpec spec) { - internal::CharTraits::convert(FillChar()); - write_int(spec.value(), spec); - return *this; - } - - template - BasicWriter &operator<<(const StrFormatSpec &spec) { - const StrChar *s = spec.str(); - write_str(s, std::char_traits::length(s), spec); - return *this; - } - - void clear() FMT_NOEXCEPT { buffer_.clear(); } - - Buffer &buffer() FMT_NOEXCEPT { return buffer_; } -}; - -template -template -typename BasicWriter::CharPtr BasicWriter::write_str( - const StrChar *s, std::size_t size, const AlignSpec &spec) { - CharPtr out = CharPtr(); - if (spec.width() > size) { - out = grow_buffer(spec.width()); - Char fill = internal::CharTraits::cast(spec.fill()); - if (spec.align() == ALIGN_RIGHT) { - std::uninitialized_fill_n(out, spec.width() - size, fill); - out += spec.width() - size; - } else if (spec.align() == ALIGN_CENTER) { - out = fill_padding(out, spec.width(), size, fill); - } else { - std::uninitialized_fill_n(out + size, spec.width() - size, fill); - } - } else { - out = grow_buffer(size); - } - std::uninitialized_copy(s, s + size, out); - return out; -} - -template -template -void BasicWriter::write_str( - const internal::Arg::StringValue &s, const Spec &spec) { - // Check if StrChar is convertible to Char. - internal::CharTraits::convert(StrChar()); - if (spec.type_ && spec.type_ != 's') - internal::report_unknown_type(spec.type_, "string"); - const StrChar *str_value = s.value; - std::size_t str_size = s.size; - if (str_size == 0) { - if (!str_value) { - FMT_THROW(FormatError("string pointer is null")); - } - } - std::size_t precision = static_cast(spec.precision_); - if (spec.precision_ >= 0 && precision < str_size) - str_size = precision; - write_str(str_value, str_size, spec); -} - -template -typename BasicWriter::CharPtr - BasicWriter::fill_padding( - CharPtr buffer, unsigned total_size, - std::size_t content_size, wchar_t fill) { - std::size_t padding = total_size - content_size; - std::size_t left_padding = padding / 2; - Char fill_char = internal::CharTraits::cast(fill); - std::uninitialized_fill_n(buffer, left_padding, fill_char); - buffer += left_padding; - CharPtr content = buffer; - std::uninitialized_fill_n(buffer + content_size, - padding - left_padding, fill_char); - return content; -} - -template -template -typename BasicWriter::CharPtr - BasicWriter::prepare_int_buffer( - unsigned num_digits, const Spec &spec, - const char *prefix, unsigned prefix_size) { - unsigned width = spec.width(); - Alignment align = spec.align(); - Char fill = internal::CharTraits::cast(spec.fill()); - if (spec.precision() > static_cast(num_digits)) { - // Octal prefix '0' is counted as a digit, so ignore it if precision - // is specified. - if (prefix_size > 0 && prefix[prefix_size - 1] == '0') - --prefix_size; - unsigned number_size = - prefix_size + internal::to_unsigned(spec.precision()); - AlignSpec subspec(number_size, '0', ALIGN_NUMERIC); - if (number_size >= width) - return prepare_int_buffer(num_digits, subspec, prefix, prefix_size); - buffer_.reserve(width); - unsigned fill_size = width - number_size; - if (align != ALIGN_LEFT) { - CharPtr p = grow_buffer(fill_size); - std::uninitialized_fill(p, p + fill_size, fill); - } - CharPtr result = prepare_int_buffer( - num_digits, subspec, prefix, prefix_size); - if (align == ALIGN_LEFT) { - CharPtr p = grow_buffer(fill_size); - std::uninitialized_fill(p, p + fill_size, fill); - } - return result; - } - unsigned size = prefix_size + num_digits; - if (width <= size) { - CharPtr p = grow_buffer(size); - std::uninitialized_copy(prefix, prefix + prefix_size, p); - return p + size - 1; - } - CharPtr p = grow_buffer(width); - CharPtr end = p + width; - if (align == ALIGN_LEFT) { - std::uninitialized_copy(prefix, prefix + prefix_size, p); - p += size; - std::uninitialized_fill(p, end, fill); - } else if (align == ALIGN_CENTER) { - p = fill_padding(p, width, size, fill); - std::uninitialized_copy(prefix, prefix + prefix_size, p); - p += size; - } else { - if (align == ALIGN_NUMERIC) { - if (prefix_size != 0) { - p = std::uninitialized_copy(prefix, prefix + prefix_size, p); - size -= prefix_size; - } - } else { - std::uninitialized_copy(prefix, prefix + prefix_size, end - size); - } - std::uninitialized_fill(p, end - size, fill); - p = end; - } - return p - 1; -} - -template -template -void BasicWriter::write_int(T value, Spec spec) { - unsigned prefix_size = 0; - typedef typename internal::IntTraits::MainType UnsignedType; - UnsignedType abs_value = static_cast(value); - char prefix[4] = ""; - if (internal::is_negative(value)) { - prefix[0] = '-'; - ++prefix_size; - abs_value = 0 - abs_value; - } else if (spec.flag(SIGN_FLAG)) { - prefix[0] = spec.flag(PLUS_FLAG) ? '+' : ' '; - ++prefix_size; - } - switch (spec.type()) { - case 0: case 'd': { - unsigned num_digits = internal::count_digits(abs_value); - CharPtr p = prepare_int_buffer(num_digits, spec, prefix, prefix_size) + 1; - internal::format_decimal(get(p), abs_value, 0); - break; - } - case 'x': case 'X': { - UnsignedType n = abs_value; - if (spec.flag(HASH_FLAG)) { - prefix[prefix_size++] = '0'; - prefix[prefix_size++] = spec.type_prefix(); - } - unsigned num_digits = 0; - do { - ++num_digits; - } while ((n >>= 4) != 0); - Char *p = get(prepare_int_buffer( - num_digits, spec, prefix, prefix_size)); - n = abs_value; - const char *digits = spec.type() == 'x' ? - "0123456789abcdef" : "0123456789ABCDEF"; - do { - *p-- = digits[n & 0xf]; - } while ((n >>= 4) != 0); - break; - } - case 'b': case 'B': { - UnsignedType n = abs_value; - if (spec.flag(HASH_FLAG)) { - prefix[prefix_size++] = '0'; - prefix[prefix_size++] = spec.type_prefix(); - } - unsigned num_digits = 0; - do { - ++num_digits; - } while ((n >>= 1) != 0); - Char *p = get(prepare_int_buffer(num_digits, spec, prefix, prefix_size)); - n = abs_value; - do { - *p-- = static_cast('0' + (n & 1)); - } while ((n >>= 1) != 0); - break; - } - case 'o': { - UnsignedType n = abs_value; - if (spec.flag(HASH_FLAG)) - prefix[prefix_size++] = '0'; - unsigned num_digits = 0; - do { - ++num_digits; - } while ((n >>= 3) != 0); - Char *p = get(prepare_int_buffer(num_digits, spec, prefix, prefix_size)); - n = abs_value; - do { - *p-- = static_cast('0' + (n & 7)); - } while ((n >>= 3) != 0); - break; - } - case 'n': { - unsigned num_digits = internal::count_digits(abs_value); - fmt::StringRef sep = ""; -#if !(defined(ANDROID) || defined(__ANDROID__)) - sep = internal::thousands_sep(std::localeconv()); -#endif - unsigned size = static_cast( - num_digits + sep.size() * ((num_digits - 1) / 3)); - CharPtr p = prepare_int_buffer(size, spec, prefix, prefix_size) + 1; - internal::format_decimal(get(p), abs_value, 0, internal::ThousandsSep(sep)); - break; - } - default: - internal::report_unknown_type( - spec.type(), spec.flag(CHAR_FLAG) ? "char" : "integer"); - break; - } -} - -template -template -void BasicWriter::write_double(T value, const Spec &spec) { - // Check type. - char type = spec.type(); - bool upper = false; - switch (type) { - case 0: - type = 'g'; - break; - case 'e': case 'f': case 'g': case 'a': - break; - case 'F': -#if FMT_MSC_VER - // MSVC's printf doesn't support 'F'. - type = 'f'; -#endif - // Fall through. - case 'E': case 'G': case 'A': - upper = true; - break; - default: - internal::report_unknown_type(type, "double"); - break; - } - - char sign = 0; - // Use isnegative instead of value < 0 because the latter is always - // false for NaN. - if (internal::FPUtil::isnegative(static_cast(value))) { - sign = '-'; - value = -value; - } else if (spec.flag(SIGN_FLAG)) { - sign = spec.flag(PLUS_FLAG) ? '+' : ' '; - } - - if (internal::FPUtil::isnotanumber(value)) { - // Format NaN ourselves because sprintf's output is not consistent - // across platforms. - std::size_t nan_size = 4; - const char *nan = upper ? " NAN" : " nan"; - if (!sign) { - --nan_size; - ++nan; - } - CharPtr out = write_str(nan, nan_size, spec); - if (sign) - *out = sign; - return; - } - - if (internal::FPUtil::isinfinity(value)) { - // Format infinity ourselves because sprintf's output is not consistent - // across platforms. - std::size_t inf_size = 4; - const char *inf = upper ? " INF" : " inf"; - if (!sign) { - --inf_size; - ++inf; - } - CharPtr out = write_str(inf, inf_size, spec); - if (sign) - *out = sign; - return; - } - - std::size_t offset = buffer_.size(); - unsigned width = spec.width(); - if (sign) { - buffer_.reserve(buffer_.size() + (width > 1u ? width : 1u)); - if (width > 0) - --width; - ++offset; - } - - // Build format string. - enum { MAX_FORMAT_SIZE = 10}; // longest format: %#-*.*Lg - Char format[MAX_FORMAT_SIZE]; - Char *format_ptr = format; - *format_ptr++ = '%'; - unsigned width_for_sprintf = width; - if (spec.flag(HASH_FLAG)) - *format_ptr++ = '#'; - if (spec.align() == ALIGN_CENTER) { - width_for_sprintf = 0; - } else { - if (spec.align() == ALIGN_LEFT) - *format_ptr++ = '-'; - if (width != 0) - *format_ptr++ = '*'; - } - if (spec.precision() >= 0) { - *format_ptr++ = '.'; - *format_ptr++ = '*'; - } - - append_float_length(format_ptr, value); - *format_ptr++ = type; - *format_ptr = '\0'; - - // Format using snprintf. - Char fill = internal::CharTraits::cast(spec.fill()); - unsigned n = 0; - Char *start = FMT_NULL; - for (;;) { - std::size_t buffer_size = buffer_.capacity() - offset; -#if FMT_MSC_VER - // MSVC's vsnprintf_s doesn't work with zero size, so reserve - // space for at least one extra character to make the size non-zero. - // Note that the buffer's capacity will increase by more than 1. - if (buffer_size == 0) { - buffer_.reserve(offset + 1); - buffer_size = buffer_.capacity() - offset; - } -#endif - start = &buffer_[offset]; - int result = internal::CharTraits::format_float( - start, buffer_size, format, width_for_sprintf, spec.precision(), value); - if (result >= 0) { - n = internal::to_unsigned(result); - if (offset + n < buffer_.capacity()) - break; // The buffer is large enough - continue with formatting. - buffer_.reserve(offset + n + 1); - } else { - // If result is negative we ask to increase the capacity by at least 1, - // but as std::vector, the buffer grows exponentially. - buffer_.reserve(buffer_.capacity() + 1); - } - } - if (sign) { - if ((spec.align() != ALIGN_RIGHT && spec.align() != ALIGN_DEFAULT) || - *start != ' ') { - *(start - 1) = sign; - sign = 0; - } else { - *(start - 1) = fill; - } - ++n; - } - if (spec.align() == ALIGN_CENTER && spec.width() > n) { - width = spec.width(); - CharPtr p = grow_buffer(width); - std::memmove(get(p) + (width - n) / 2, get(p), n * sizeof(Char)); - fill_padding(p, spec.width(), n, fill); - return; - } - if (spec.fill() != ' ' || sign) { - while (*start == ' ') - *start++ = fill; - if (sign) - *(start - 1) = sign; - } - grow_buffer(n); -} - -/** - \rst - This class template provides operations for formatting and writing data - into a character stream. The output is stored in a memory buffer that grows - dynamically. - - You can use one of the following typedefs for common character types - and the standard allocator: - - +---------------+-----------------------------------------------------+ - | Type | Definition | - +===============+=====================================================+ - | MemoryWriter | BasicMemoryWriter> | - +---------------+-----------------------------------------------------+ - | WMemoryWriter | BasicMemoryWriter> | - +---------------+-----------------------------------------------------+ - - **Example**:: - - MemoryWriter out; - out << "The answer is " << 42 << "\n"; - out.write("({:+f}, {:+f})", -3.14, 3.14); - - This will write the following output to the ``out`` object: - - .. code-block:: none - - The answer is 42 - (-3.140000, +3.140000) - - The output can be converted to an ``std::string`` with ``out.str()`` or - accessed as a C string with ``out.c_str()``. - \endrst - */ -template > -class BasicMemoryWriter : public BasicWriter { - private: - internal::MemoryBuffer buffer_; - - public: - explicit BasicMemoryWriter(const Allocator& alloc = Allocator()) - : BasicWriter(buffer_), buffer_(alloc) {} - -#if FMT_USE_RVALUE_REFERENCES - /** - \rst - Constructs a :class:`fmt::BasicMemoryWriter` object moving the content - of the other object to it. - \endrst - */ - BasicMemoryWriter(BasicMemoryWriter &&other) - : BasicWriter(buffer_), buffer_(std::move(other.buffer_)) { - } - - /** - \rst - Moves the content of the other ``BasicMemoryWriter`` object to this one. - \endrst - */ - BasicMemoryWriter &operator=(BasicMemoryWriter &&other) { - buffer_ = std::move(other.buffer_); - return *this; - } -#endif -}; - -typedef BasicMemoryWriter MemoryWriter; -typedef BasicMemoryWriter WMemoryWriter; - -/** - \rst - This class template provides operations for formatting and writing data - into a fixed-size array. For writing into a dynamically growing buffer - use :class:`fmt::BasicMemoryWriter`. - - Any write method will throw ``std::runtime_error`` if the output doesn't fit - into the array. - - You can use one of the following typedefs for common character types: - - +--------------+---------------------------+ - | Type | Definition | - +==============+===========================+ - | ArrayWriter | BasicArrayWriter | - +--------------+---------------------------+ - | WArrayWriter | BasicArrayWriter | - +--------------+---------------------------+ - \endrst - */ -template -class BasicArrayWriter : public BasicWriter { - private: - internal::FixedBuffer buffer_; - - public: - /** - \rst - Constructs a :class:`fmt::BasicArrayWriter` object for *array* of the - given size. - \endrst - */ - BasicArrayWriter(Char *array, std::size_t size) - : BasicWriter(buffer_), buffer_(array, size) {} - - /** - \rst - Constructs a :class:`fmt::BasicArrayWriter` object for *array* of the - size known at compile time. - \endrst - */ - template - explicit BasicArrayWriter(Char (&array)[SIZE]) - : BasicWriter(buffer_), buffer_(array, SIZE) {} -}; - -typedef BasicArrayWriter ArrayWriter; -typedef BasicArrayWriter WArrayWriter; - -// Reports a system error without throwing an exception. -// Can be used to report errors from destructors. -FMT_API void report_system_error(int error_code, - StringRef message) FMT_NOEXCEPT; - -#if FMT_USE_WINDOWS_H - -/** A Windows error. */ -class WindowsError : public SystemError { - private: - FMT_API void init(int error_code, CStringRef format_str, ArgList args); - - public: - /** - \rst - Constructs a :class:`fmt::WindowsError` object with the description - of the form - - .. parsed-literal:: - **: ** - - where ** is the formatted message and ** is the - system message corresponding to the error code. - *error_code* is a Windows error code as given by ``GetLastError``. - If *error_code* is not a valid error code such as -1, the system message - will look like "error -1". - - **Example**:: - - // This throws a WindowsError with the description - // cannot open file 'madeup': The system cannot find the file specified. - // or similar (system message may vary). - const char *filename = "madeup"; - LPOFSTRUCT of = LPOFSTRUCT(); - HFILE file = OpenFile(filename, &of, OF_READ); - if (file == HFILE_ERROR) { - throw fmt::WindowsError(GetLastError(), - "cannot open file '{}'", filename); - } - \endrst - */ - WindowsError(int error_code, CStringRef message) { - init(error_code, message, ArgList()); - } - FMT_VARIADIC_CTOR(WindowsError, init, int, CStringRef) -}; - -// Reports a Windows error without throwing an exception. -// Can be used to report errors from destructors. -FMT_API void report_windows_error(int error_code, - StringRef message) FMT_NOEXCEPT; - -#endif - -enum Color { BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE }; - -/** - Formats a string and prints it to stdout using ANSI escape sequences - to specify color (experimental). - Example: - print_colored(fmt::RED, "Elapsed time: {0:.2f} seconds", 1.23); - */ -FMT_API void print_colored(Color c, CStringRef format, ArgList args); - -/** - \rst - Formats arguments and returns the result as a string. - - **Example**:: - - std::string message = format("The answer is {}", 42); - \endrst -*/ -inline std::string format(CStringRef format_str, ArgList args) { - MemoryWriter w; - w.write(format_str, args); - return w.str(); -} - -inline std::wstring format(WCStringRef format_str, ArgList args) { - WMemoryWriter w; - w.write(format_str, args); - return w.str(); -} - -/** - \rst - Prints formatted data to the file *f*. - - **Example**:: - - print(stderr, "Don't {}!", "panic"); - \endrst - */ -FMT_API void print(std::FILE *f, CStringRef format_str, ArgList args); - -/** - \rst - Prints formatted data to ``stdout``. - - **Example**:: - - print("Elapsed time: {0:.2f} seconds", 1.23); - \endrst - */ -FMT_API void print(CStringRef format_str, ArgList args); - -/** - Fast integer formatter. - */ -class FormatInt { - private: - // Buffer should be large enough to hold all digits (digits10 + 1), - // a sign and a null character. - enum {BUFFER_SIZE = std::numeric_limits::digits10 + 3}; - mutable char buffer_[BUFFER_SIZE]; - char *str_; - - // Formats value in reverse and returns the number of digits. - char *format_decimal(ULongLong value) { - char *buffer_end = buffer_ + BUFFER_SIZE - 1; - while (value >= 100) { - // Integer division is slow so do it for a group of two digits instead - // of for every digit. The idea comes from the talk by Alexandrescu - // "Three Optimization Tips for C++". See speed-test for a comparison. - unsigned index = static_cast((value % 100) * 2); - value /= 100; - *--buffer_end = internal::Data::DIGITS[index + 1]; - *--buffer_end = internal::Data::DIGITS[index]; - } - if (value < 10) { - *--buffer_end = static_cast('0' + value); - return buffer_end; - } - unsigned index = static_cast(value * 2); - *--buffer_end = internal::Data::DIGITS[index + 1]; - *--buffer_end = internal::Data::DIGITS[index]; - return buffer_end; - } - - void FormatSigned(LongLong value) { - ULongLong abs_value = static_cast(value); - bool negative = value < 0; - if (negative) - abs_value = 0 - abs_value; - str_ = format_decimal(abs_value); - if (negative) - *--str_ = '-'; - } - - public: - explicit FormatInt(int value) { FormatSigned(value); } - explicit FormatInt(long value) { FormatSigned(value); } - explicit FormatInt(LongLong value) { FormatSigned(value); } - explicit FormatInt(unsigned value) : str_(format_decimal(value)) {} - explicit FormatInt(unsigned long value) : str_(format_decimal(value)) {} - explicit FormatInt(ULongLong value) : str_(format_decimal(value)) {} - - /** Returns the number of characters written to the output buffer. */ - std::size_t size() const { - return internal::to_unsigned(buffer_ - str_ + BUFFER_SIZE - 1); - } - - /** - Returns a pointer to the output buffer content. No terminating null - character is appended. - */ - const char *data() const { return str_; } - - /** - Returns a pointer to the output buffer content with terminating null - character appended. - */ - const char *c_str() const { - buffer_[BUFFER_SIZE - 1] = '\0'; - return str_; - } - - /** - \rst - Returns the content of the output buffer as an ``std::string``. - \endrst - */ - std::string str() const { return std::string(str_, size()); } -}; - -// Formats a decimal integer value writing into buffer and returns -// a pointer to the end of the formatted string. This function doesn't -// write a terminating null character. -template -inline void format_decimal(char *&buffer, T value) { - typedef typename internal::IntTraits::MainType MainType; - MainType abs_value = static_cast(value); - if (internal::is_negative(value)) { - *buffer++ = '-'; - abs_value = 0 - abs_value; - } - if (abs_value < 100) { - if (abs_value < 10) { - *buffer++ = static_cast('0' + abs_value); - return; - } - unsigned index = static_cast(abs_value * 2); - *buffer++ = internal::Data::DIGITS[index]; - *buffer++ = internal::Data::DIGITS[index + 1]; - return; - } - unsigned num_digits = internal::count_digits(abs_value); - internal::format_decimal(buffer, abs_value, num_digits); - buffer += num_digits; -} - -/** - \rst - Returns a named argument for formatting functions. - - **Example**:: - - print("Elapsed time: {s:.2f} seconds", arg("s", 1.23)); - - \endrst - */ -template -inline internal::NamedArgWithType arg(StringRef name, const T &arg) { - return internal::NamedArgWithType(name, arg); -} - -template -inline internal::NamedArgWithType arg(WStringRef name, const T &arg) { - return internal::NamedArgWithType(name, arg); -} - -// The following two functions are deleted intentionally to disable -// nested named arguments as in ``format("{}", arg("a", arg("b", 42)))``. -template -void arg(StringRef, const internal::NamedArg&) FMT_DELETED_OR_UNDEFINED; -template -void arg(WStringRef, const internal::NamedArg&) FMT_DELETED_OR_UNDEFINED; -} - -#if FMT_GCC_VERSION -// Use the system_header pragma to suppress warnings about variadic macros -// because suppressing -Wvariadic-macros with the diagnostic pragma doesn't -// work. It is used at the end because we want to suppress as little warnings -// as possible. -# pragma GCC system_header -#endif - -// This is used to work around VC++ bugs in handling variadic macros. -#define FMT_EXPAND(args) args - -// Returns the number of arguments. -// Based on https://groups.google.com/forum/#!topic/comp.std.c/d-6Mj5Lko_s. -#define FMT_NARG(...) FMT_NARG_(__VA_ARGS__, FMT_RSEQ_N()) -#define FMT_NARG_(...) FMT_EXPAND(FMT_ARG_N(__VA_ARGS__)) -#define FMT_ARG_N(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, N, ...) N -#define FMT_RSEQ_N() 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 - -#define FMT_FOR_EACH_(N, f, ...) \ - FMT_EXPAND(FMT_CONCAT(FMT_FOR_EACH, N)(f, __VA_ARGS__)) -#define FMT_FOR_EACH(f, ...) \ - FMT_EXPAND(FMT_FOR_EACH_(FMT_NARG(__VA_ARGS__), f, __VA_ARGS__)) - -#define FMT_ADD_ARG_NAME(type, index) type arg##index -#define FMT_GET_ARG_NAME(type, index) arg##index - -#if FMT_USE_VARIADIC_TEMPLATES -# define FMT_VARIADIC_(Char, ReturnType, func, call, ...) \ - template \ - ReturnType func(FMT_FOR_EACH(FMT_ADD_ARG_NAME, __VA_ARGS__), \ - const Args & ... args) { \ - typedef fmt::internal::ArgArray ArgArray; \ - typename ArgArray::Type array{ \ - ArgArray::template make >(args)...}; \ - call(FMT_FOR_EACH(FMT_GET_ARG_NAME, __VA_ARGS__), \ - fmt::ArgList(fmt::internal::make_type(args...), array)); \ - } -#else -// Defines a wrapper for a function taking __VA_ARGS__ arguments -// and n additional arguments of arbitrary types. -# define FMT_WRAP(Char, ReturnType, func, call, n, ...) \ - template \ - inline ReturnType func(FMT_FOR_EACH(FMT_ADD_ARG_NAME, __VA_ARGS__), \ - FMT_GEN(n, FMT_MAKE_ARG)) { \ - fmt::internal::ArgArray::Type arr; \ - FMT_GEN(n, FMT_ASSIGN_##Char); \ - call(FMT_FOR_EACH(FMT_GET_ARG_NAME, __VA_ARGS__), fmt::ArgList( \ - fmt::internal::make_type(FMT_GEN(n, FMT_MAKE_REF2)), arr)); \ - } - -# define FMT_VARIADIC_(Char, ReturnType, func, call, ...) \ - inline ReturnType func(FMT_FOR_EACH(FMT_ADD_ARG_NAME, __VA_ARGS__)) { \ - call(FMT_FOR_EACH(FMT_GET_ARG_NAME, __VA_ARGS__), fmt::ArgList()); \ - } \ - FMT_WRAP(Char, ReturnType, func, call, 1, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 2, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 3, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 4, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 5, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 6, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 7, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 8, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 9, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 10, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 11, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 12, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 13, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 14, __VA_ARGS__) \ - FMT_WRAP(Char, ReturnType, func, call, 15, __VA_ARGS__) -#endif // FMT_USE_VARIADIC_TEMPLATES - -/** - \rst - Defines a variadic function with the specified return type, function name - and argument types passed as variable arguments to this macro. - - **Example**:: - - void print_error(const char *file, int line, const char *format, - fmt::ArgList args) { - fmt::print("{}: {}: ", file, line); - fmt::print(format, args); - } - FMT_VARIADIC(void, print_error, const char *, int, const char *) - - ``FMT_VARIADIC`` is used for compatibility with legacy C++ compilers that - don't implement variadic templates. You don't have to use this macro if - you don't need legacy compiler support and can use variadic templates - directly:: - - template - void print_error(const char *file, int line, const char *format, - const Args & ... args) { - fmt::print("{}: {}: ", file, line); - fmt::print(format, args...); - } - \endrst - */ -#define FMT_VARIADIC(ReturnType, func, ...) \ - FMT_VARIADIC_(char, ReturnType, func, return func, __VA_ARGS__) - -#define FMT_VARIADIC_W(ReturnType, func, ...) \ - FMT_VARIADIC_(wchar_t, ReturnType, func, return func, __VA_ARGS__) - -#define FMT_CAPTURE_ARG_(id, index) ::fmt::arg(#id, id) - -#define FMT_CAPTURE_ARG_W_(id, index) ::fmt::arg(L###id, id) - -/** - \rst - Convenient macro to capture the arguments' names and values into several - ``fmt::arg(name, value)``. - - **Example**:: - - int x = 1, y = 2; - print("point: ({x}, {y})", FMT_CAPTURE(x, y)); - // same as: - // print("point: ({x}, {y})", arg("x", x), arg("y", y)); - - \endrst - */ -#define FMT_CAPTURE(...) FMT_FOR_EACH(FMT_CAPTURE_ARG_, __VA_ARGS__) - -#define FMT_CAPTURE_W(...) FMT_FOR_EACH(FMT_CAPTURE_ARG_W_, __VA_ARGS__) - -namespace fmt { -FMT_VARIADIC(std::string, format, CStringRef) -FMT_VARIADIC_W(std::wstring, format, WCStringRef) -FMT_VARIADIC(void, print, CStringRef) -FMT_VARIADIC(void, print, std::FILE *, CStringRef) -FMT_VARIADIC(void, print_colored, Color, CStringRef) - -namespace internal { -template -inline bool is_name_start(Char c) { - return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || '_' == c; -} - -// Parses an unsigned integer advancing s to the end of the parsed input. -// This function assumes that the first character of s is a digit. -template -unsigned parse_nonnegative_int(const Char *&s) { - assert('0' <= *s && *s <= '9'); - unsigned value = 0; - do { - unsigned new_value = value * 10 + (*s++ - '0'); - // Check if value wrapped around. - if (new_value < value) { - value = (std::numeric_limits::max)(); - break; - } - value = new_value; - } while ('0' <= *s && *s <= '9'); - // Convert to unsigned to prevent a warning. - unsigned max_int = (std::numeric_limits::max)(); - if (value > max_int) - FMT_THROW(FormatError("number is too big")); - return value; -} - -inline void require_numeric_argument(const Arg &arg, char spec) { - if (arg.type > Arg::LAST_NUMERIC_TYPE) { - std::string message = - fmt::format("format specifier '{}' requires numeric argument", spec); - FMT_THROW(fmt::FormatError(message)); - } -} - -template -void check_sign(const Char *&s, const Arg &arg) { - char sign = static_cast(*s); - require_numeric_argument(arg, sign); - if (arg.type == Arg::UINT || arg.type == Arg::ULONG_LONG) { - FMT_THROW(FormatError(fmt::format( - "format specifier '{}' requires signed argument", sign))); - } - ++s; -} -} // namespace internal - -template -inline internal::Arg BasicFormatter::get_arg( - BasicStringRef arg_name, const char *&error) { - if (check_no_auto_index(error)) { - map_.init(args()); - const internal::Arg *arg = map_.find(arg_name); - if (arg) - return *arg; - error = "argument not found"; - } - return internal::Arg(); -} - -template -inline internal::Arg BasicFormatter::parse_arg_index(const Char *&s) { - const char *error = FMT_NULL; - internal::Arg arg = *s < '0' || *s > '9' ? - next_arg(error) : get_arg(internal::parse_nonnegative_int(s), error); - if (error) { - FMT_THROW(FormatError( - *s != '}' && *s != ':' ? "invalid format string" : error)); - } - return arg; -} - -template -inline internal::Arg BasicFormatter::parse_arg_name(const Char *&s) { - assert(internal::is_name_start(*s)); - const Char *start = s; - Char c; - do { - c = *++s; - } while (internal::is_name_start(c) || ('0' <= c && c <= '9')); - const char *error = FMT_NULL; - internal::Arg arg = get_arg(BasicStringRef(start, s - start), error); - if (error) - FMT_THROW(FormatError(error)); - return arg; -} - -template -const Char *BasicFormatter::format( - const Char *&format_str, const internal::Arg &arg) { - using internal::Arg; - const Char *s = format_str; - typename ArgFormatter::SpecType spec; - if (*s == ':') { - if (arg.type == Arg::CUSTOM) { - arg.custom.format(this, arg.custom.value, &s); - return s; - } - ++s; - // Parse fill and alignment. - if (Char c = *s) { - const Char *p = s + 1; - spec.align_ = ALIGN_DEFAULT; - do { - switch (*p) { - case '<': - spec.align_ = ALIGN_LEFT; - break; - case '>': - spec.align_ = ALIGN_RIGHT; - break; - case '=': - spec.align_ = ALIGN_NUMERIC; - break; - case '^': - spec.align_ = ALIGN_CENTER; - break; - } - if (spec.align_ != ALIGN_DEFAULT) { - if (p != s) { - if (c == '}') break; - if (c == '{') - FMT_THROW(FormatError("invalid fill character '{'")); - s += 2; - spec.fill_ = c; - } else ++s; - if (spec.align_ == ALIGN_NUMERIC) - require_numeric_argument(arg, '='); - break; - } - } while (--p >= s); - } - - // Parse sign. - switch (*s) { - case '+': - check_sign(s, arg); - spec.flags_ |= SIGN_FLAG | PLUS_FLAG; - break; - case '-': - check_sign(s, arg); - spec.flags_ |= MINUS_FLAG; - break; - case ' ': - check_sign(s, arg); - spec.flags_ |= SIGN_FLAG; - break; - } - - if (*s == '#') { - require_numeric_argument(arg, '#'); - spec.flags_ |= HASH_FLAG; - ++s; - } - - // Parse zero flag. - if (*s == '0') { - require_numeric_argument(arg, '0'); - spec.align_ = ALIGN_NUMERIC; - spec.fill_ = '0'; - ++s; - } - - // Parse width. - if ('0' <= *s && *s <= '9') { - spec.width_ = internal::parse_nonnegative_int(s); - } else if (*s == '{') { - ++s; - Arg width_arg = internal::is_name_start(*s) ? - parse_arg_name(s) : parse_arg_index(s); - if (*s++ != '}') - FMT_THROW(FormatError("invalid format string")); - ULongLong value = 0; - switch (width_arg.type) { - case Arg::INT: - if (width_arg.int_value < 0) - FMT_THROW(FormatError("negative width")); - value = width_arg.int_value; - break; - case Arg::UINT: - value = width_arg.uint_value; - break; - case Arg::LONG_LONG: - if (width_arg.long_long_value < 0) - FMT_THROW(FormatError("negative width")); - value = width_arg.long_long_value; - break; - case Arg::ULONG_LONG: - value = width_arg.ulong_long_value; - break; - default: - FMT_THROW(FormatError("width is not integer")); - } - if (value > (std::numeric_limits::max)()) - FMT_THROW(FormatError("number is too big")); - spec.width_ = static_cast(value); - } - - // Parse precision. - if (*s == '.') { - ++s; - spec.precision_ = 0; - if ('0' <= *s && *s <= '9') { - spec.precision_ = internal::parse_nonnegative_int(s); - } else if (*s == '{') { - ++s; - Arg precision_arg = internal::is_name_start(*s) ? - parse_arg_name(s) : parse_arg_index(s); - if (*s++ != '}') - FMT_THROW(FormatError("invalid format string")); - ULongLong value = 0; - switch (precision_arg.type) { - case Arg::INT: - if (precision_arg.int_value < 0) - FMT_THROW(FormatError("negative precision")); - value = precision_arg.int_value; - break; - case Arg::UINT: - value = precision_arg.uint_value; - break; - case Arg::LONG_LONG: - if (precision_arg.long_long_value < 0) - FMT_THROW(FormatError("negative precision")); - value = precision_arg.long_long_value; - break; - case Arg::ULONG_LONG: - value = precision_arg.ulong_long_value; - break; - default: - FMT_THROW(FormatError("precision is not integer")); - } - if (value > (std::numeric_limits::max)()) - FMT_THROW(FormatError("number is too big")); - spec.precision_ = static_cast(value); - } else { - FMT_THROW(FormatError("missing precision specifier")); - } - if (arg.type <= Arg::LAST_INTEGER_TYPE || arg.type == Arg::POINTER) { - FMT_THROW(FormatError( - fmt::format("precision not allowed in {} format specifier", - arg.type == Arg::POINTER ? "pointer" : "integer"))); - } - } - - // Parse type. - if (*s != '}' && *s) - spec.type_ = static_cast(*s++); - } - - if (*s++ != '}') - FMT_THROW(FormatError("missing '}' in format string")); - - // Format argument. - ArgFormatter(*this, spec, s - 1).visit(arg); - return s; -} - -template -void BasicFormatter::format(BasicCStringRef format_str) { - const Char *s = format_str.c_str(); - const Char *start = s; - while (*s) { - Char c = *s++; - if (c != '{' && c != '}') continue; - if (*s == c) { - write(writer_, start, s); - start = ++s; - continue; - } - if (c == '}') - FMT_THROW(FormatError("unmatched '}' in format string")); - write(writer_, start, s - 1); - internal::Arg arg = internal::is_name_start(*s) ? - parse_arg_name(s) : parse_arg_index(s); - start = s = format(s, arg); - } - write(writer_, start, s); -} - -template -struct ArgJoin { - It first; - It last; - BasicCStringRef sep; - - ArgJoin(It first, It last, const BasicCStringRef& sep) : - first(first), - last(last), - sep(sep) {} -}; - -template -ArgJoin join(It first, It last, const BasicCStringRef& sep) { - return ArgJoin(first, last, sep); -} - -template -ArgJoin join(It first, It last, const BasicCStringRef& sep) { - return ArgJoin(first, last, sep); -} - -#if FMT_HAS_GXX_CXX11 -template -auto join(const Range& range, const BasicCStringRef& sep) - -> ArgJoin { - return join(std::begin(range), std::end(range), sep); -} - -template -auto join(const Range& range, const BasicCStringRef& sep) - -> ArgJoin { - return join(std::begin(range), std::end(range), sep); -} -#endif - -template -void format_arg(fmt::BasicFormatter &f, - const Char *&format_str, const ArgJoin& e) { - const Char* end = format_str; - if (*end == ':') - ++end; - while (*end && *end != '}') - ++end; - if (*end != '}') - FMT_THROW(FormatError("missing '}' in format string")); - - It it = e.first; - if (it != e.last) { - const Char* save = format_str; - f.format(format_str, internal::MakeArg >(*it++)); - while (it != e.last) { - f.writer().write(e.sep); - format_str = save; - f.format(format_str, internal::MakeArg >(*it++)); - } - } - format_str = end + 1; -} -} // namespace fmt - -#if FMT_USE_USER_DEFINED_LITERALS -namespace fmt { -namespace internal { - -template -struct UdlFormat { - const Char *str; - - template - auto operator()(Args && ... args) const - -> decltype(format(str, std::forward(args)...)) { - return format(str, std::forward(args)...); - } -}; - -template -struct UdlArg { - const Char *str; - - template - NamedArgWithType operator=(T &&value) const { - return {str, std::forward(value)}; - } -}; - -} // namespace internal - -inline namespace literals { - -/** - \rst - C++11 literal equivalent of :func:`fmt::format`. - - **Example**:: - - using namespace fmt::literals; - std::string message = "The answer is {}"_format(42); - \endrst - */ -inline internal::UdlFormat -operator"" _format(const char *s, std::size_t) { return {s}; } -inline internal::UdlFormat -operator"" _format(const wchar_t *s, std::size_t) { return {s}; } - -/** - \rst - C++11 literal equivalent of :func:`fmt::arg`. - - **Example**:: - - using namespace fmt::literals; - print("Elapsed time: {s:.2f} seconds", "s"_a=1.23); - \endrst - */ -inline internal::UdlArg -operator"" _a(const char *s, std::size_t) { return {s}; } -inline internal::UdlArg -operator"" _a(const wchar_t *s, std::size_t) { return {s}; } - -} // inline namespace literals -} // namespace fmt -#endif // FMT_USE_USER_DEFINED_LITERALS - -// Restore warnings. -#if FMT_GCC_VERSION >= 406 -# pragma GCC diagnostic pop -#endif - -#if defined(__clang__) && !defined(FMT_ICC_VERSION) -# pragma clang diagnostic pop -#endif - -#ifdef FMT_HEADER_ONLY -# define FMT_FUNC inline -# include "format.cc" -#else -# define FMT_FUNC -#endif - -#endif // FMT_FORMAT_H_ diff --git a/resources/licenses/fmt_bsd2.txt b/resources/licenses/fmt_bsd2.txt deleted file mode 100644 index eb6be6503..000000000 --- a/resources/licenses/fmt_bsd2.txt +++ /dev/null @@ -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. diff --git a/resources/resources_autogenerated.qrc b/resources/resources_autogenerated.qrc index 0ba21f2fc..dbb639bbc 100644 --- a/resources/resources_autogenerated.qrc +++ b/resources/resources_autogenerated.qrc @@ -40,7 +40,6 @@ icon.png licenses/boost_boost.txt licenses/emoji-data-source.txt - licenses/fmt_bsd2.txt licenses/libcommuni_BSD3.txt licenses/openssl.txt licenses/pajlada_settings.txt diff --git a/src/Application.cpp b/src/Application.cpp index 776fef603..fe0aab349 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -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( diff --git a/src/PrecompiledHeader.hpp b/src/PrecompiledHeader.hpp index ab5c04144..cec2e2ff5 100644 --- a/src/PrecompiledHeader.hpp +++ b/src/PrecompiledHeader.hpp @@ -1,7 +1,6 @@ #pragma once #ifdef __cplusplus -# include # include # include # include diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index e1c5ba0d6..5616a2633 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -1,7 +1,6 @@ #include "common/Channel.hpp" #include "Application.hpp" -#include "debug/Log.hpp" #include "messages/Message.hpp" #include "messages/MessageBuilder.hpp" #include "singletons/Emotes.hpp" diff --git a/src/common/CompletionModel.cpp b/src/common/CompletionModel.cpp index cc988122b..4866e8a24 100644 --- a/src/common/CompletionModel.cpp +++ b/src/common/CompletionModel.cpp @@ -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" diff --git a/src/common/DownloadManager.cpp b/src/common/DownloadManager.cpp index a579d37a1..6a239370a 100644 --- a/src/common/DownloadManager.cpp +++ b/src/common/DownloadManager.cpp @@ -1,6 +1,5 @@ #include "DownloadManager.hpp" -#include "debug/Log.hpp" #include "singletons/Paths.hpp" #include @@ -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) diff --git a/src/common/NetworkPrivate.cpp b/src/common/NetworkPrivate.cpp index b1333cc6e..e135d910b 100644 --- a/src/common/NetworkPrivate.cpp +++ b/src/common/NetworkPrivate.cpp @@ -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 &data) if (reply == nullptr) { - log("Unhandled request type"); + qDebug() << "Unhandled request type"; return; } @@ -132,7 +131,7 @@ void loadUncached(const std::shared_ptr &data) { QObject::connect( data->timer_, &QTimer::timeout, worker, [reply, data]() { - log("Aborted!"); + qDebug() << "Aborted!"; reply->abort(); if (data->onError_) { diff --git a/src/common/NetworkRequest.cpp b/src/common/NetworkRequest.cpp index f0019bcbd..c4d3fb29a 100644 --- a/src/common/NetworkRequest.cpp +++ b/src/common/NetworkRequest.cpp @@ -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" diff --git a/src/common/NetworkResult.cpp b/src/common/NetworkResult.cpp index 17b12df07..feaf40bc5 100644 --- a/src/common/NetworkResult.cpp +++ b/src/common/NetworkResult.cpp @@ -1,7 +1,5 @@ #include "common/NetworkResult.hpp" -#include "debug/Log.hpp" - #include #include #include @@ -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; } diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index cccdf63a1..0845f3288 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -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" diff --git a/src/controllers/notifications/NotificationController.cpp b/src/controllers/notifications/NotificationController.cpp index 1c0b7ecc7..b9777c802 100644 --- a/src/controllers/notifications/NotificationController.cpp +++ b/src/controllers/notifications/NotificationController.cpp @@ -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; } diff --git a/src/debug/Benchmark.cpp b/src/debug/Benchmark.cpp index 6541545c4..12475f06d 100644 --- a/src/debug/Benchmark.cpp +++ b/src/debug/Benchmark.cpp @@ -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() diff --git a/src/debug/Benchmark.hpp b/src/debug/Benchmark.hpp index 065048e8f..b29dce802 100644 --- a/src/debug/Benchmark.hpp +++ b/src/debug/Benchmark.hpp @@ -1,7 +1,6 @@ #pragma once -#include "debug/Log.hpp" - +#include #include #include diff --git a/src/debug/Log.hpp b/src/debug/Log.hpp deleted file mode 100644 index 31536781a..000000000 --- a/src/debug/Log.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "util/Helpers.hpp" - -#include -#include - -namespace chatterino { - -template -inline void log(const std::string &formatString, Args &&... args) -{ - qDebug().noquote() << QTime::currentTime().toString("hh:mm:ss.zzz") - << fS(formatString, std::forward(args)...).c_str(); -} - -template -inline void log(const char *formatString, Args &&... args) -{ - log(std::string(formatString), std::forward(args)...); -} - -template -inline void log(const QString &formatString, Args &&... args) -{ - log(formatString.toStdString(), std::forward(args)...); -} - -} // namespace chatterino diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp index 558ffea24..b42223012 100644 --- a/src/messages/Image.cpp +++ b/src/messages/Image.cpp @@ -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; diff --git a/src/providers/bttv/BttvEmotes.cpp b/src/providers/bttv/BttvEmotes.cpp index 36481dd7c..d0c4c2a19 100644 --- a/src/providers/bttv/BttvEmotes.cpp +++ b/src/providers/bttv/BttvEmotes.cpp @@ -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" diff --git a/src/providers/emoji/Emojis.cpp b/src/providers/emoji/Emojis.cpp index f510d697f..670793e7c 100644 --- a/src/providers/emoji/Emojis.cpp +++ b/src/providers/emoji/Emojis.cpp @@ -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; } diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index 5f2906145..29dbac52d 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -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 emoteCallback, std::function)> 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(); diff --git a/src/providers/irc/AbstractIrcServer.cpp b/src/providers/irc/AbstractIrcServer.cpp index 4ca3fb04a..0a3019e5c 100644 --- a/src/providers/irc/AbstractIrcServer.cpp +++ b/src/providers/irc/AbstractIrcServer.cpp @@ -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,23 +158,24 @@ ChannelPtr AbstractIrcServer::getOrAddChannel(const QString &dirtyChannelName) } this->channels.insert(channelName, chan); - this->connections_.emplace_back(chan->destroyed.connect([this, - channelName] { - // fourtf: issues when the server itself is destroyed + this->connections_.emplace_back( + chan->destroyed.connect([this, channelName] { + // fourtf: issues when the server itself is destroyed - log("[AbstractIrcServer::addChannel] {} was destroyed", channelName); - this->channels.remove(channelName); + qDebug() << "[AbstractIrcServer::addChannel]" << channelName + << "was destroyed"; + this->channels.remove(channelName); - if (this->readConnection_) - { - this->readConnection_->sendRaw("PART #" + channelName); - } + if (this->readConnection_) + { + this->readConnection_->sendRaw("PART #" + channelName); + } - if (this->writeConnection_ && this->hasSeparateWriteConnection()) - { - this->writeConnection_->sendRaw("PART #" + channelName); - } - })); + if (this->writeConnection_ && this->hasSeparateWriteConnection()) + { + this->writeConnection_->sendRaw("PART #" + channelName); + } + })); // join irc channel { diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index a3c5e3c78..1e3aae415 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -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" @@ -235,9 +234,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 +298,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 +353,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 +565,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; } diff --git a/src/providers/twitch/PartialTwitchUser.cpp b/src/providers/twitch/PartialTwitchUser.cpp index d482311f6..631f41fc6 100644 --- a/src/providers/twitch/PartialTwitchUser.cpp +++ b/src/providers/twitch/PartialTwitchUser.cpp @@ -2,7 +2,6 @@ #include "common/Common.hpp" #include "common/NetworkRequest.hpp" -#include "debug/Log.hpp" #include "providers/twitch/TwitchCommon.hpp" #include @@ -39,31 +38,30 @@ void PartialTwitchUser::getId(std::function successCallback, 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"; 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"; 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"; 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"; return Failure; } successCallback(id.toString()); diff --git a/src/providers/twitch/PubsubClient.cpp b/src/providers/twitch/PubsubClient.cpp index 0f8ad1d22..9cf57c28f 100644 --- a/src/providers/twitch/PubsubClient.cpp +++ b/src/providers/twitch/PubsubClient.cpp @@ -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 @@ -77,14 +77,14 @@ namespace detail { return true; } - void PubSubClient::unlistenPrefix(const std::string &prefix) + void PubSubClient::unlistenPrefix(const QString &prefix) { - std::vector topics; + std::vector 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 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 topics({"whispers." + userID}); + std::vector 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 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 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(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 diff --git a/src/providers/twitch/PubsubClient.hpp b/src/providers/twitch/PubsubClient.hpp index 132cd35a5..0400683e2 100644 --- a/src/providers/twitch/PubsubClient.hpp +++ b/src/providers/twitch/PubsubClient.hpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include #include @@ -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> requests; private: - void listenToTopic(const std::string &topic, + void listenToTopic(const QString &topic, std::shared_ptr account); void listen(rapidjson::Document &&msg); bool tryListen(rapidjson::Document &msg); - bool isListeningToTopic(const std::string &topic); + bool isListeningToTopic(const QString &topic); void addClient(); diff --git a/src/providers/twitch/PubsubHelpers.cpp b/src/providers/twitch/PubsubHelpers.cpp index b30c47c0f..d2e26ce13 100644 --- a/src/providers/twitch/PubsubHelpers.cpp +++ b/src/providers/twitch/PubsubHelpers.cpp @@ -46,9 +46,8 @@ bool getTargetUser(const rapidjson::Value &data, ActionUser &user) return rj::getSafe(data, "target_user_id", user.id); } -rapidjson::Document createListenMessage( - const std::vector &topicsVec, - std::shared_ptr account) +rapidjson::Document createListenMessage(const std::vector &topicsVec, + std::shared_ptr account) { rapidjson::Document msg(rapidjson::kObjectType); auto &a = msg.GetAllocator(); @@ -75,8 +74,7 @@ rapidjson::Document createListenMessage( return msg; } -rapidjson::Document createUnlistenMessage( - const std::vector &topicsVec) +rapidjson::Document createUnlistenMessage(const std::vector &topicsVec) { rapidjson::Document msg(rapidjson::kObjectType); auto &a = msg.GetAllocator(); diff --git a/src/providers/twitch/PubsubHelpers.hpp b/src/providers/twitch/PubsubHelpers.hpp index ad8c52130..3c784697a 100644 --- a/src/providers/twitch/PubsubHelpers.hpp +++ b/src/providers/twitch/PubsubHelpers.hpp @@ -3,7 +3,6 @@ #include #include #include -#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 &topicsVec, - std::shared_ptr account); +rapidjson::Document createListenMessage(const std::vector &topicsVec, + std::shared_ptr account); rapidjson::Document createUnlistenMessage( - const std::vector &topicsVec); + const std::vector &topicsVec); // Create timer using given ioService template @@ -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 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; } diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index 2556c35e0..b5930529e 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -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 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; } @@ -489,7 +490,7 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr emoteSet) { if (!emoteSet) { - log("null emote set sent"); + qDebug() << "null emote set sent"; return; } @@ -505,7 +506,8 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr 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 +529,7 @@ void TwitchAccount::loadEmoteSetData(std::shared_ptr 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(); diff --git a/src/providers/twitch/TwitchAccountManager.cpp b/src/providers/twitch/TwitchAccountManager.cpp index b37a67da2..b772f2911 100644 --- a/src/providers/twitch/TwitchAccountManager.cpp +++ b/src/providers/twitch/TwitchAccountManager.cpp @@ -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) diff --git a/src/providers/twitch/TwitchApi.cpp b/src/providers/twitch/TwitchApi.cpp index d91fc3701..6c3b6b6ee 100644 --- a/src/providers/twitch/TwitchApi.cpp +++ b/src/providers/twitch/TwitchApi.cpp @@ -2,7 +2,6 @@ #include "common/Common.hpp" #include "common/NetworkRequest.hpp" -#include "debug/Log.hpp" #include "providers/twitch/TwitchCommon.hpp" #include @@ -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; } diff --git a/src/providers/twitch/TwitchBadges.cpp b/src/providers/twitch/TwitchBadges.cpp index cc9022a4d..b41762bc6 100644 --- a/src/providers/twitch/TwitchBadges.cpp +++ b/src/providers/twitch/TwitchBadges.cpp @@ -7,7 +7,6 @@ #include "common/NetworkRequest.hpp" #include "common/Outcome.hpp" -#include "debug/Log.hpp" #include "messages/Emote.hpp" namespace chatterino { diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 27fe8bd3e..df1582422 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -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; } @@ -836,7 +837,7 @@ boost::optional 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) { diff --git a/src/providers/twitch/TwitchEmotes.cpp b/src/providers/twitch/TwitchEmotes.cpp index 8b3f5498f..e7a3bea06 100644 --- a/src/providers/twitch/TwitchEmotes.cpp +++ b/src/providers/twitch/TwitchEmotes.cpp @@ -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" diff --git a/src/providers/twitch/TwitchHelpers.cpp b/src/providers/twitch/TwitchHelpers.cpp index 4189cea0c..5c71c4cd2 100644 --- a/src/providers/twitch/TwitchHelpers.cpp +++ b/src/providers/twitch/TwitchHelpers.cpp @@ -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; } diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index b781061e7..839590150 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -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" @@ -90,8 +89,6 @@ namespace { QStringList parts = badgeInfo.split('/'); if (parts.size() != 2) { - log("Skipping badge-info because it split weird: {}", - badgeInfo); continue; } @@ -110,7 +107,6 @@ namespace { QStringList parts = badge.split('/'); if (parts.size() != 2) { - log("Skipping badge because it split weird: {}", badge); continue; } @@ -159,8 +155,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 +186,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; } } @@ -427,8 +423,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(emoteImage, MessageElementFlag::TwitchEmote); @@ -771,7 +767,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> v( @@ -809,7 +805,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{ startIndex + pos, emote.second, emote.first}); @@ -872,7 +868,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 +937,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( @@ -991,8 +987,8 @@ void TwitchMessageBuilder::parseHighlights() { continue; } - log("Highlight because user {} sent a message", - this->ircMessage->nick()); + qDebug() << "Highlight because user" << this->ircMessage->nick() + << "sent a message"; if (!this->highlightVisual_) { this->highlightVisual_ = true; @@ -1044,8 +1040,8 @@ void TwitchMessageBuilder::parseHighlights() continue; } - log("Highlight because {} matches {}", this->originalMessage_, - highlight.getPattern()); + qDebug() << "Highlight because" << this->originalMessage_ << "matches" + << highlight.getPattern(); if (!this->highlightVisual_) { this->highlightVisual_ = true; @@ -1130,7 +1126,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 +1212,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; diff --git a/src/singletons/Logging.cpp b/src/singletons/Logging.cpp index 0d4d59ed2..d78241f0f 100644 --- a/src/singletons/Logging.cpp +++ b/src/singletons/Logging.cpp @@ -1,7 +1,6 @@ #include "singletons/Logging.hpp" #include "Application.hpp" -#include "debug/Log.hpp" #include "singletons/Paths.hpp" #include "singletons/Settings.hpp" diff --git a/src/singletons/Settings.cpp b/src/singletons/Settings.cpp index 94d13b68a..30ba15ec3 100644 --- a/src/singletons/Settings.cpp +++ b/src/singletons/Settings.cpp @@ -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" diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 65bc8f441..bddb948f7 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -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; diff --git a/src/singletons/helper/LoggingChannel.cpp b/src/singletons/helper/LoggingChannel.cpp index 82230edcc..57b7dd590 100644 --- a/src/singletons/helper/LoggingChannel.cpp +++ b/src/singletons/helper/LoggingChannel.cpp @@ -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); diff --git a/src/util/Helpers.hpp b/src/util/Helpers.hpp index cb37a1825..5aefe29a8 100644 --- a/src/util/Helpers.hpp +++ b/src/util/Helpers.hpp @@ -1,16 +1,9 @@ #pragma once -#include #include namespace chatterino { -template -auto fS(Args &&... args) -{ - return fmt::format(std::forward(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 &f, const char *&, const QString &v) -{ - f.writer().write("{}", v.toStdString()); -} - -} // namespace fmt diff --git a/src/util/IncognitoBrowser.cpp b/src/util/IncognitoBrowser.cpp index bacc2d46b..306a2bf46 100644 --- a/src/util/IncognitoBrowser.cpp +++ b/src/util/IncognitoBrowser.cpp @@ -5,8 +5,6 @@ #include #include -#include "debug/Log.hpp" - namespace chatterino { namespace { #ifdef Q_OS_WIN diff --git a/src/util/StreamLink.cpp b/src/util/StreamLink.cpp index 9c70adb27..c9fdd0e20 100644 --- a/src/util/StreamLink.cpp +++ b/src/util/StreamLink.cpp @@ -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()); diff --git a/src/widgets/AccountSwitchPopup.cpp b/src/widgets/AccountSwitchPopup.cpp index 9857e9269..15f5ab4d1 100644 --- a/src/widgets/AccountSwitchPopup.cpp +++ b/src/widgets/AccountSwitchPopup.cpp @@ -1,5 +1,4 @@ #include "widgets/AccountSwitchPopup.hpp" -#include "debug/Log.hpp" #include "widgets/dialogs/SettingsDialog.hpp" #include diff --git a/src/widgets/BaseWidget.cpp b/src/widgets/BaseWidget.cpp index c4fb5304e..14c78040d 100644 --- a/src/widgets/BaseWidget.cpp +++ b/src/widgets/BaseWidget.cpp @@ -2,7 +2,6 @@ #include "BaseSettings.hpp" #include "BaseTheme.hpp" -#include "debug/Log.hpp" #include "widgets/BaseWindow.hpp" #include diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index 7e80818c9..b1e939db1 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -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; } } diff --git a/src/widgets/Notebook.cpp b/src/widgets/Notebook.cpp index ca81a9353..b0fa87e68 100644 --- a/src/widgets/Notebook.cpp +++ b/src/widgets/Notebook.cpp @@ -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" diff --git a/src/widgets/dialogs/LoginDialog.cpp b/src/widgets/dialogs/LoginDialog.cpp index 9fb70d296..f96c86342 100644 --- a/src/widgets/dialogs/LoginDialog.cpp +++ b/src/widgets/dialogs/LoginDialog.cpp @@ -65,7 +65,7 @@ namespace { // messageBox.setIcon(QMessageBox::Information); // messageBox.setText("Successfully logged in with user " + // qS(username) + "!"); - auto basePath = fS("/accounts/uid{}", userID); + std::string basePath = "/accounts/uid" + userID.toStdString(); pajlada::Settings::Setting::set(basePath + "/username", username); pajlada::Settings::Setting::set(basePath + "/userID", userID); diff --git a/src/widgets/dialogs/LogsPopup.cpp b/src/widgets/dialogs/LogsPopup.cpp index b5d030037..695b4e567 100644 --- a/src/widgets/dialogs/LogsPopup.cpp +++ b/src/widgets/dialogs/LogsPopup.cpp @@ -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" @@ -70,7 +69,7 @@ void LogsPopup::getLogs() 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 &messages) diff --git a/src/widgets/dialogs/QualityPopup.cpp b/src/widgets/dialogs/QualityPopup.cpp index 322741ab8..00feffa64 100644 --- a/src/widgets/dialogs/QualityPopup.cpp +++ b/src/widgets/dialogs/QualityPopup.cpp @@ -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(); diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 3dc61eed9..4ed079fd7 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -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" @@ -740,9 +739,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; } diff --git a/src/widgets/helper/NotebookTab.cpp b/src/widgets/helper/NotebookTab.cpp index f131bd4c0..e82424ecd 100644 --- a/src/widgets/helper/NotebookTab.cpp +++ b/src/widgets/helper/NotebookTab.cpp @@ -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" diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index 0814fc669..5a7dcf534 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -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; } diff --git a/src/widgets/settingspages/HighlightingPage.cpp b/src/widgets/settingspages/HighlightingPage.cpp index d7a7c1d4d..f5ec67d25 100644 --- a/src/widgets/settingspages/HighlightingPage.cpp +++ b/src/widgets/settingspages/HighlightingPage.cpp @@ -5,7 +5,6 @@ #include "controllers/highlights/HighlightController.hpp" #include "controllers/highlights/HighlightModel.hpp" #include "controllers/highlights/UserHighlightModel.hpp" -#include "debug/Log.hpp" #include "singletons/Settings.hpp" #include "util/LayoutCreator.hpp" #include "util/StandardItemHelper.hpp" diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 729eb8251..0a93b4888 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -3,7 +3,6 @@ #include "common/Common.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" @@ -530,7 +529,7 @@ void Split::openInStreamlink() } catch (const Exception &ex) { - log("Error in doOpenStreamlink: {}", ex.what()); + qDebug() << "Error in doOpenStreamlink:" << ex.what(); } } From 90296a2d854501e2de3eb91fd9f14a4072103efb Mon Sep 17 00:00:00 2001 From: pajlada Date: Fri, 3 Jan 2020 20:55:13 +0100 Subject: [PATCH 02/22] Expect a PONG response to our PINGs (#1476) If no PONG was received, force a reconnection Fixes #1164 --- src/providers/irc/IrcConnection2.cpp | 29 +++++++++++++++++++++++++++- src/providers/irc/IrcConnection2.hpp | 3 +++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/providers/irc/IrcConnection2.cpp b/src/providers/irc/IrcConnection2.cpp index 0ba639247..08ff718bc 100644 --- a/src/providers/irc/IrcConnection2.cpp +++ b/src/providers/irc/IrcConnection2.cpp @@ -1,7 +1,16 @@ #include "IrcConnection2.hpp" +#include "common/Version.hpp" + namespace chatterino { +namespace { + + const auto payload = + QString("chatterino/%1").arg(Version::instance().version()); + +} // namespace + IrcConnection::IrcConnection(QObject *parent) : Communi::IrcConnection(parent) { @@ -11,10 +20,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 +46,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; diff --git a/src/providers/irc/IrcConnection2.hpp b/src/providers/irc/IrcConnection2.hpp index 70aa9f670..65f1e1ea0 100644 --- a/src/providers/irc/IrcConnection2.hpp +++ b/src/providers/irc/IrcConnection2.hpp @@ -18,6 +18,9 @@ private: QTimer pingTimer_; QTimer reconnectTimer_; std::atomic 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 waitingForPong_{false}; }; } // namespace chatterino From 7f4b7564da8a05f46f4b9c00b04a147efcee816f Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Fri, 3 Jan 2020 21:08:27 +0100 Subject: [PATCH 03/22] Fix a windows-only occurence of log() --- src/util/IncognitoBrowser.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/IncognitoBrowser.cpp b/src/util/IncognitoBrowser.cpp index 306a2bf46..216c6f8c5 100644 --- a/src/util/IncognitoBrowser.cpp +++ b/src/util/IncognitoBrowser.cpp @@ -61,7 +61,7 @@ namespace { if (command.isNull()) return QString(); - log(command); + qDebug() << command; // inject switch to enable private browsing command = injectPrivateSwitch(command); From a6c91afde46e27c0c67f59274d34c62912aadbc0 Mon Sep 17 00:00:00 2001 From: DatGuy1 Date: Fri, 3 Jan 2020 22:11:00 +0200 Subject: [PATCH 04/22] Remove support for CBenni's Logviewer (#1458) --- src/widgets/dialogs/LogsPopup.cpp | 57 ++----------------------------- src/widgets/dialogs/LogsPopup.hpp | 1 - 2 files changed, 3 insertions(+), 55 deletions(-) diff --git a/src/widgets/dialogs/LogsPopup.cpp b/src/widgets/dialogs/LogsPopup.cpp index 695b4e567..18e1f1a04 100644 --- a/src/widgets/dialogs/LogsPopup.cpp +++ b/src/widgets/dialogs/LogsPopup.cpp @@ -54,7 +54,7 @@ void LogsPopup::getLogs() dynamic_cast(this->channel_.get())) { this->channelName_ = twitchChannel->getName(); - this->getLogviewerLogs(twitchChannel->roomId()); + this->getOverrustleLogs(); return; } @@ -62,10 +62,7 @@ void LogsPopup::getLogs() if (!this->channelName_.isEmpty()) { - PartialTwitchUser::byName(this->channelName_) - .getId( - [=](const QString &roomID) { this->getLogviewerLogs(roomID); }, - this); + this->getOverrustleLogs(); return; } @@ -80,57 +77,9 @@ void LogsPopup::setMessages(std::vector &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 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(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_); diff --git a/src/widgets/dialogs/LogsPopup.hpp b/src/widgets/dialogs/LogsPopup.hpp index 66bf19acc..ab29ae431 100644 --- a/src/widgets/dialogs/LogsPopup.hpp +++ b/src/widgets/dialogs/LogsPopup.hpp @@ -26,7 +26,6 @@ private: void setMessages(std::vector &messages); void getOverrustleLogs(); - void getLogviewerLogs(const QString &roomID); }; } // namespace chatterino From 1eaccd9e1f4af9ffc8f2fd185400b7e3d77bc481 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 4 Jan 2020 04:21:19 +0100 Subject: [PATCH 05/22] Export TRAVIS_TAG alongside GIT_TAG for Travis CI builds --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 5e6041b24..d20089fa6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: From 265bab1ce64eba9238f3a7e37661207b013ef4b2 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 5 Jan 2020 09:45:10 +0100 Subject: [PATCH 06/22] Add the ability to tab through the emote menu (#1483) Fixes #1478 --- src/widgets/dialogs/EmotePopup.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index 6aebd2149..00beb11c3 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -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" @@ -131,6 +132,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) From 81b79e14b5144126bdc871b8eb6c216766d4ddb1 Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Sun, 12 Jan 2020 10:06:01 +0100 Subject: [PATCH 07/22] Fix a crash in TwitchChannel::refreshCheerEmotes If you closed down the split right after the refreshCheerEmotes call was made, then refreshCheerEmotes used this after it was free'd --- src/providers/twitch/TwitchChannel.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index df1582422..d305f0c43 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -744,6 +744,12 @@ void TwitchChannel::refreshCheerEmotes() NetworkRequest::twitchRequest(url) .onSuccess([this, weak = weakOf(this)](auto result) -> Outcome { + auto shared = weak.lock(); + if (!shared) + { + return Failure; + } + auto cheerEmoteSets = ParseCheermoteSets(result.parseRapidJson()); std::vector emoteSets; From 72f34f5febb83579a70acada75e9e1f2016bfd24 Mon Sep 17 00:00:00 2001 From: apa420 <17131426+apa420@users.noreply.github.com> Date: Sun, 12 Jan 2020 17:38:55 +0000 Subject: [PATCH 08/22] Update README.md (#1498) Fixes #1497 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 519fa4da6..a0d5d99ed 100644 --- a/README.md +++ b/README.md @@ -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: http://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 From 476825dc35c34e72729d07fa5661c53fd9db60d8 Mon Sep 17 00:00:00 2001 From: fourtf Date: Sun, 12 Jan 2020 18:41:02 +0100 Subject: [PATCH 09/22] http -> https in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0d5d99ed..a0fc71be7 100644 --- a/README.md +++ b/README.md @@ -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/9.0.0/LLVM-9.0.0-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 From f87e008679646aaabbaa887f3430f7c3847e4a16 Mon Sep 17 00:00:00 2001 From: Edgar Date: Thu, 23 Jan 2020 09:21:01 +0100 Subject: [PATCH 10/22] :construction_worker: Cache conan packages (#1487) --- .github/workflows/build.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eea852f4b..7fd328342 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 From 809b63bb5e335d1aed7bd9fa7440e994e2836f51 Mon Sep 17 00:00:00 2001 From: Marcin Moskal Date: Fri, 24 Jan 2020 21:30:35 +0100 Subject: [PATCH 11/22] Change CHATTERINO2_TWITCH_SERVER_PORT env default to 443 (#1513) --- src/common/Env.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/Env.cpp b/src/common/Env.cpp index 908e25f4b..1fa819c3d 100644 --- a/src/common/Env.cpp +++ b/src/common/Env.cpp @@ -59,7 +59,7 @@ Env::Env() "https://braize.pajlada.com/chatterino/twitchemotes/set/%1/")) , 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)) { } From 00414eb7799e979a892d1e47555c3023b9ba2742 Mon Sep 17 00:00:00 2001 From: Alexey Kutepov Date: Sat, 25 Jan 2020 03:36:51 +0700 Subject: [PATCH 12/22] Synchronize Clipboard with Primary Selection on Linux when copying (#1502) * Introduce crossPlatformCopy() It sets the text of the clipboard and also syncs it with the selection clipboard if it is supported. Such behaviour is pretty common for X11 application on Unix-like Operating Systems. * Fix clang-format remarks * Fix weird clang-format config discrepancy between my machine and CI * Remove clipboard argument from crossPlatformCopy * Fix clang-format remarks --- chatterino.pro | 2 ++ src/util/Clipboard.cpp | 16 ++++++++++++++++ src/util/Clipboard.hpp | 9 +++++++++ src/widgets/helper/ChannelView.cpp | 28 ++++++++++++---------------- src/widgets/splits/Split.cpp | 3 ++- 5 files changed, 41 insertions(+), 17 deletions(-) create mode 100644 src/util/Clipboard.cpp create mode 100644 src/util/Clipboard.hpp diff --git a/chatterino.pro b/chatterino.pro index 79d36ab60..23c6cc5ab 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -210,6 +210,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 \ @@ -412,6 +413,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 \ diff --git a/src/util/Clipboard.cpp b/src/util/Clipboard.cpp new file mode 100644 index 000000000..c71014476 --- /dev/null +++ b/src/util/Clipboard.cpp @@ -0,0 +1,16 @@ +#include "util/Clipboard.hpp" +#include + +namespace chatterino { + +void crossPlatformCopy(const QString &text) +{ + auto clipboard = QApplication::clipboard(); + clipboard->setText(text); + if (clipboard->supportsSelection()) + { + clipboard->setText(text, QClipboard::Selection); + } +} + +} // namespace chatterino diff --git a/src/util/Clipboard.hpp b/src/util/Clipboard.hpp new file mode 100644 index 000000000..ff23a1f2d --- /dev/null +++ b/src/util/Clipboard.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace chatterino { + +void crossPlatformCopy(const QString &text); + +} // namespace chatterino diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 4ed079fd7..3fe02bfbe 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -28,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" @@ -64,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)); @@ -84,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)); // @@ -124,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); @@ -1552,8 +1550,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(); } @@ -1561,9 +1558,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] { @@ -1571,14 +1567,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. diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 0a93b4888..6b285770b 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -10,6 +10,7 @@ #include "singletons/Settings.hpp" #include "singletons/Theme.hpp" #include "singletons/WindowManager.hpp" +#include "util/Clipboard.hpp" #include "util/Shortcut.hpp" #include "util/StreamLink.hpp" #include "widgets/Notebook.hpp" @@ -659,7 +660,7 @@ void Split::openSubPage() void Split::copyToClipboard() { - QApplication::clipboard()->setText(this->view_->getSelectedText()); + crossPlatformCopy(this->view_->getSelectedText()); } void Split::showSearch() From 5957551d0667f82b0a078bfc80e578e686adf56d Mon Sep 17 00:00:00 2001 From: Leon Richardt Date: Sat, 25 Jan 2020 11:03:10 +0100 Subject: [PATCH 13/22] Better Highlights (#1320) * Support for user-defined sounds and colors * Make color & sound columns selectable * Add custom row for subscription highlights * Add subscriptions to custom highlights and centrally manage highlight colors * Dynamically update message highlight colors --- chatterino.pro | 9 + .../highlights/HighlightBlacklistModel.cpp | 9 +- .../highlights/HighlightBlacklistModel.hpp | 6 + src/controllers/highlights/HighlightModel.cpp | 169 ++++++-- src/controllers/highlights/HighlightModel.hpp | 12 + .../highlights/HighlightPhrase.cpp | 113 ++++++ .../highlights/HighlightPhrase.hpp | 133 ++++--- .../highlights/UserHighlightModel.cpp | 38 +- src/messages/Message.cpp | 11 +- src/messages/Message.hpp | 1 + src/messages/layouts/MessageLayout.cpp | 43 +- src/providers/colors/ColorProvider.cpp | 124 ++++++ src/providers/colors/ColorProvider.hpp | 53 +++ src/providers/twitch/TwitchMessageBuilder.cpp | 133 ++++++- src/providers/twitch/TwitchMessageBuilder.hpp | 2 + src/singletons/Settings.hpp | 23 +- src/singletons/Theme.cpp | 8 +- src/util/StandardItemHelper.hpp | 13 + src/widgets/Scrollbar.cpp | 13 +- src/widgets/dialogs/ColorPickerDialog.cpp | 367 ++++++++++++++++++ src/widgets/dialogs/ColorPickerDialog.hpp | 108 ++++++ src/widgets/helper/ColorButton.cpp | 23 ++ src/widgets/helper/ColorButton.hpp | 18 + src/widgets/helper/QColorPicker.cpp | 290 ++++++++++++++ src/widgets/helper/QColorPicker.hpp | 130 +++++++ src/widgets/helper/ScrollbarHighlight.cpp | 11 +- src/widgets/helper/ScrollbarHighlight.hpp | 16 +- .../settingspages/HighlightingPage.cpp | 130 ++++++- .../settingspages/HighlightingPage.hpp | 3 + 29 files changed, 1822 insertions(+), 187 deletions(-) create mode 100644 src/controllers/highlights/HighlightPhrase.cpp create mode 100644 src/providers/colors/ColorProvider.cpp create mode 100644 src/providers/colors/ColorProvider.hpp create mode 100644 src/widgets/dialogs/ColorPickerDialog.cpp create mode 100644 src/widgets/dialogs/ColorPickerDialog.hpp create mode 100644 src/widgets/helper/ColorButton.cpp create mode 100644 src/widgets/helper/ColorButton.hpp create mode 100644 src/widgets/helper/QColorPicker.cpp create mode 100644 src/widgets/helper/QColorPicker.hpp diff --git a/chatterino.pro b/chatterino.pro index 23c6cc5ab..d569991fa 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -133,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 \ @@ -166,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 \ @@ -228,6 +230,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 \ @@ -243,12 +246,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 \ @@ -366,6 +371,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 \ @@ -450,6 +456,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 \ @@ -465,6 +472,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 \ @@ -473,6 +481,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 \ diff --git a/src/controllers/highlights/HighlightBlacklistModel.cpp b/src/controllers/highlights/HighlightBlacklistModel.cpp index 47d095a2d..0ff68bc0b 100644 --- a/src/controllers/highlights/HighlightBlacklistModel.cpp +++ b/src/controllers/highlights/HighlightBlacklistModel.cpp @@ -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 &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 diff --git a/src/controllers/highlights/HighlightBlacklistModel.hpp b/src/controllers/highlights/HighlightBlacklistModel.hpp index d073425a5..c3420b066 100644 --- a/src/controllers/highlights/HighlightBlacklistModel.hpp +++ b/src/controllers/highlights/HighlightBlacklistModel.hpp @@ -13,6 +13,12 @@ class HighlightBlacklistModel : public SignalVectorModel { explicit HighlightBlacklistModel(QObject *parent); +public: + enum Column { + Pattern = 0, + UseRegex = 1, + }; + protected: // turn a vector item into a model row virtual HighlightBlacklistUser getItemFromRow( diff --git a/src/controllers/highlights/HighlightModel.cpp b/src/controllers/highlights/HighlightModel.cpp index 85950dd07..05f37bf78 100644 --- a/src/controllers/highlights/HighlightModel.cpp +++ b/src/controllers/highlights/HighlightModel.cpp @@ -8,7 +8,7 @@ namespace chatterino { // commandmodel HighlightModel::HighlightModel(QObject *parent) - : SignalVectorModel(5, parent) + : SignalVectorModel(7, parent) { } @@ -16,52 +16,103 @@ HighlightModel::HighlightModel(QObject *parent) HighlightPhrase HighlightModel::getItemFromRow( std::vector &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(); - 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 &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 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 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 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 &row, @@ -70,7 +121,7 @@ void HighlightModel::customRowSetData(const std::vector &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 &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 &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 &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().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; } diff --git a/src/controllers/highlights/HighlightModel.hpp b/src/controllers/highlights/HighlightModel.hpp index 40796c65e..13b85bca1 100644 --- a/src/controllers/highlights/HighlightModel.hpp +++ b/src/controllers/highlights/HighlightModel.hpp @@ -13,6 +13,18 @@ class HighlightModel : public SignalVectorModel { 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( diff --git a/src/controllers/highlights/HighlightPhrase.cpp b/src/controllers/highlights/HighlightPhrase.cpp new file mode 100644 index 000000000..d310a921d --- /dev/null +++ b/src/controllers/highlights/HighlightPhrase.cpp @@ -0,0 +1,113 @@ +#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_); +} + +/** + * @brief Create a new HighlightPhrase. + * + * Use this constructor when updating an existing HighlightPhrase's 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(color); +} + +/** + * @brief Create a new HighlightPhrase. + * + * Use this constructor when creating a new HighlightPhrase. + */ +HighlightPhrase::HighlightPhrase(const QString &pattern, bool hasAlert, + bool hasSound, bool isRegex, + bool isCaseSensitive, const QString &soundUrl, + std::shared_ptr 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 HighlightPhrase::getColor() const +{ + return this->color_; +} + +} // namespace chatterino diff --git a/src/controllers/highlights/HighlightPhrase.hpp b/src/controllers/highlights/HighlightPhrase.hpp index e1877bc42..6121fc0fb 100644 --- a/src/controllers/highlights/HighlightPhrase.hpp +++ b/src/controllers/highlights/HighlightPhrase.hpp @@ -1,5 +1,6 @@ #pragma once +#include "providers/colors/ColorProvider.hpp" #include "util/RapidJsonSerializeQString.hpp" #include "util/RapidjsonHelpers.hpp" @@ -12,70 +13,64 @@ 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)) - { - } + 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_; - } + HighlightPhrase(const QString &pattern, bool hasAlert, bool hasSound, + bool isRegex, bool isCaseSensitive, const QString &soundUrl, + std::shared_ptr 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 getColor() const; private: QString pattern_; - bool alert_; - bool sound_; + bool hasAlert_; + bool hasSound_; bool isRegex_; - bool caseSensitive_; + bool isCaseSensitive_; + QUrl soundUrl_; + std::shared_ptr color_; QRegularExpression regex_; }; + } // namespace chatterino namespace pajlada { @@ -88,10 +83,13 @@ struct Serialize { 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 +102,30 @@ struct Deserialize { 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); } }; diff --git a/src/controllers/highlights/UserHighlightModel.cpp b/src/controllers/highlights/UserHighlightModel.cpp index 5e3ed12c5..e12d7e052 100644 --- a/src/controllers/highlights/UserHighlightModel.cpp +++ b/src/controllers/highlights/UserHighlightModel.cpp @@ -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(5, parent) + : SignalVectorModel(7, parent) { } @@ -16,24 +17,37 @@ UserHighlightModel::UserHighlightModel(QObject *parent) HighlightPhrase UserHighlightModel::getItemFromRow( std::vector &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(); + + 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 &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 diff --git a/src/messages/Message.cpp b/src/messages/Message.cpp index 2696d1098..218fa3a73 100644 --- a/src/messages/Message.cpp +++ b/src/messages/Message.cpp @@ -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(); } diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index 47af81abf..938ee6428 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -55,6 +55,7 @@ struct Message : boost::noncopyable { QString displayName; QString localizedName; QString timeoutUser; + std::shared_ptr highlightColor; uint32_t count = 1; std::vector> elements; diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index bf71c0f8c..03097c3d1 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -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()) @@ -249,21 +262,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)) { diff --git a/src/providers/colors/ColorProvider.cpp b/src/providers/colors/ColorProvider.cpp new file mode 100644 index 000000000..492689aa0 --- /dev/null +++ b/src/providers/colors/ColorProvider.cpp @@ -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 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 ColorProvider::recentColors() const +{ + QSet 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 &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(customColor)}); + } + else + { + this->typeColorMap_.insert( + {ColorType::SelfHighlight, + std::make_shared(backgrounds.highlighted)}); + } + + customColor = getSettings()->subHighlightColor; + if (QColor(customColor).isValid()) + { + this->typeColorMap_.insert( + {ColorType::Subscription, std::make_shared(customColor)}); + } + else + { + this->typeColorMap_.insert( + {ColorType::Subscription, + std::make_shared(backgrounds.subscription)}); + } + + customColor = getSettings()->whisperHighlightColor; + if (QColor(customColor).isValid()) + { + this->typeColorMap_.insert( + {ColorType::Whisper, std::make_shared(customColor)}); + } + else + { + this->typeColorMap_.insert( + {ColorType::Whisper, + std::make_shared(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 diff --git a/src/providers/colors/ColorProvider.hpp b/src/providers/colors/ColorProvider.hpp new file mode 100644 index 000000000..e038b17f2 --- /dev/null +++ b/src/providers/colors/ColorProvider.hpp @@ -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 color(ColorType type) const; + + void updateColor(ColorType type, QColor color); + + /** + * @brief Return a set of recently used colors used anywhere in Chatterino. + */ + QSet recentColors() const; + + /** + * @brief Return a vector of colors that are good defaults for use + * throughout the program. + */ + const std::vector &defaultColors() const; + +private: + ColorProvider(); + + void initTypeColorMap(); + void initDefaultColors(); + + std::unordered_map> typeColorMap_; + std::vector 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)); +} diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 839590150..a0142cf64 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -64,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 { @@ -233,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(); @@ -967,6 +980,43 @@ 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(); + } + } + + if (!this->highlightVisual_) + { + this->highlightVisual_ = true; + 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(); @@ -993,23 +1043,33 @@ void TwitchMessageBuilder::parseHighlights() { this->highlightVisual_ = true; this->message().flags.set(MessageFlag::Highlighted); + this->message().highlightColor = userHighlight.getColor(); } - if (userHighlight.getAlert()) + 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; + // Usernames "beat" highlight phrases: Once a username highlight + // has been applied, no further highlight phrases will be checked + return; } } @@ -1028,7 +1088,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)); } @@ -1046,23 +1108,36 @@ void TwitchMessageBuilder::parseHighlights() { this->highlightVisual_ = true; this->message().flags.set(MessageFlag::Highlighted); + this->message().highlightColor = highlight.getColor(); } - if (highlight.getAlert()) + 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 the first highlight has been set. If a message would + // trigger multiple highlights, only the first one from the list + // will be applied. break; } } @@ -1077,6 +1152,24 @@ void TwitchMessageBuilder::parseHighlights() 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(); + } + } + if (!this->highlightVisual_) + { + this->highlightVisual_ = true; + this->message().flags.set(MessageFlag::Highlighted); + this->message().highlightColor = + ColorProvider::instance().color(ColorType::Whisper); } } } diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp index e85fce950..b8f5613f4 100644 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ b/src/providers/twitch/TwitchMessageBuilder.hpp @@ -95,6 +95,8 @@ private: bool highlightVisual_ = false; bool highlightAlert_ = false; bool highlightSound_ = false; + + QUrl highlightSoundUrl_; }; } // namespace chatterino diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index acdb495d1..050c9db98 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -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}; diff --git a/src/singletons/Theme.cpp b/src/singletons/Theme.cpp index 4fb1fdd2a..1876126e2 100644 --- a/src/singletons/Theme.cpp +++ b/src/singletons/Theme.cpp @@ -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; diff --git a/src/util/StandardItemHelper.hpp b/src/util/StandardItemHelper.hpp index 7b120e4d7..4b7ba52b9 100644 --- a/src/util/StandardItemHelper.hpp +++ b/src/util/StandardItemHelper.hpp @@ -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(); diff --git a/src/widgets/Scrollbar.cpp b/src/widgets/Scrollbar.cpp index 30253e484..9af6c307e 100644 --- a/src/widgets/Scrollbar.cpp +++ b/src/widgets/Scrollbar.cpp @@ -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()) { diff --git a/src/widgets/dialogs/ColorPickerDialog.cpp b/src/widgets/dialogs/ColorPickerDialog.cpp new file mode 100644 index 000000000..669edf4f3 --- /dev/null +++ b/src/widgets/dialogs/ColorPickerDialog.cpp @@ -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 layoutWidget(this->getLayoutContainer()); + auto layout = layoutWidget.setLayoutType().withoutMargin(); + + // This hosts the business logic: color picker and predefined colors + LayoutCreator contentCreator(new QWidget()); + auto contents = contentCreator.setLayoutType(); + + // This hosts the predefined colors (and also the currently selected color) + LayoutCreator predefCreator(new QWidget()); + auto predef = predefCreator.setLayoutType(); + + // Recently used colors + { + LayoutCreator gridCreator(new QWidget()); + this->initRecentColors(gridCreator); + + predef.append(gridCreator.getElement()); + } + + // Default colors + { + LayoutCreator gridCreator(new QWidget()); + this->initDefaultColors(gridCreator); + + predef.append(gridCreator.getElement()); + } + + // Currently selected color + { + LayoutCreator curColorCreator(new QWidget()); + auto curColor = curColorCreator.setLayoutType(); + curColor.emplace("Selected:").assign(&this->ui_.selected.label); + curColor.emplace(initial).assign( + &this->ui_.selected.color); + + predef.append(curColor.getElement()); + } + + contents.append(predef.getElement()); + + // Color picker + { + LayoutCreator obj(new QWidget()); + auto vbox = obj.setLayoutType(); + + // The actual color picker + { + LayoutCreator cpCreator(new QWidget()); + this->initColorPicker(cpCreator); + + vbox.append(cpCreator.getElement()); + } + + // Spin boxes + { + LayoutCreator sbCreator(new QWidget()); + this->initSpinBoxes(sbCreator); + + vbox.append(sbCreator.getElement()); + } + + // HTML color + { + LayoutCreator htmlCreator(new QWidget()); + this->initHtmlColor(htmlCreator); + + vbox.append(htmlCreator.getElement()); + } + + contents.append(obj.getElement()); + } + + layout.append(contents.getElement()); + + // Dialog buttons + auto buttons = + layout.emplace().emplace(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 &creator) +{ + auto grid = creator.setLayoutType(); + + 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 &creator) +{ + auto grid = creator.setLayoutType(); + + 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 &creator) +{ + auto cpPanel = creator.setLayoutType(); + + /* + * 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 &creator) +{ + auto spinBoxes = creator.setLayoutType(); + + 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::of(&QSpinBox::valueChanged), [=](int value) { + this->selectColor(QColor(red->value(), green->value(), + blue->value(), alpha->value()), + false); + }); + } +} + +void ColorPickerDialog::initHtmlColor(LayoutCreator &creator) +{ + auto html = creator.setLayoutType(); + + // 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 diff --git a/src/widgets/dialogs/ColorPickerDialog.hpp b/src/widgets/dialogs/ColorPickerDialog.hpp new file mode 100644 index 000000000..5f5a48cc0 --- /dev/null +++ b/src/widgets/dialogs/ColorPickerDialog.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include "util/LayoutCreator.hpp" +#include "widgets/BasePopup.hpp" +#include "widgets/helper/ColorButton.hpp" +#include "widgets/helper/QColorPicker.hpp" + +#include + +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 colors; + } recent; + + struct { + QLabel *label; + std::vector colors; + } def; + + struct { + QLabel *label; + ColorButton *color; + } selected; + + struct { + QColorPicker *colorPicker; + QColorLuminancePicker *luminancePicker; + + std::array spinBoxLabels; + std::array 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 &creator); + void initDefaultColors(LayoutCreator &creator); + void initColorPicker(LayoutCreator &creator); + void initSpinBoxes(LayoutCreator &creator); + void initHtmlColor(LayoutCreator &creator); +}; +} // namespace chatterino diff --git a/src/widgets/helper/ColorButton.cpp b/src/widgets/helper/ColorButton.cpp new file mode 100644 index 000000000..afd21f832 --- /dev/null +++ b/src/widgets/helper/ColorButton.cpp @@ -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 diff --git a/src/widgets/helper/ColorButton.hpp b/src/widgets/helper/ColorButton.hpp new file mode 100644 index 000000000..596cb402a --- /dev/null +++ b/src/widgets/helper/ColorButton.hpp @@ -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 diff --git a/src/widgets/helper/QColorPicker.cpp b/src/widgets/helper/QColorPicker.cpp new file mode 100644 index 000000000..5b025ddcb --- /dev/null +++ b/src/widgets/helper/QColorPicker.cpp @@ -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 + +/* + * 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 diff --git a/src/widgets/helper/QColorPicker.hpp b/src/widgets/helper/QColorPicker.hpp new file mode 100644 index 000000000..83109b36b --- /dev/null +++ b/src/widgets/helper/QColorPicker.hpp @@ -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 + +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 diff --git a/src/widgets/helper/ScrollbarHighlight.cpp b/src/widgets/helper/ScrollbarHighlight.cpp index addfffafe..02aafff26 100644 --- a/src/widgets/helper/ScrollbarHighlight.cpp +++ b/src/widgets/helper/ScrollbarHighlight.cpp @@ -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()) , style_(Style::None) { } -ScrollbarHighlight::ScrollbarHighlight(Color color, Style style) +ScrollbarHighlight::ScrollbarHighlight(const std::shared_ptr 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 diff --git a/src/widgets/helper/ScrollbarHighlight.hpp b/src/widgets/helper/ScrollbarHighlight.hpp index e34cf913b..22fbc48f5 100644 --- a/src/widgets/helper/ScrollbarHighlight.hpp +++ b/src/widgets/helper/ScrollbarHighlight.hpp @@ -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 color, + Style style = Default); + + QColor getColor() const; Style getStyle() const; bool isNull() const; private: - Color color_; + std::shared_ptr color_; Style style_; }; diff --git a/src/widgets/settingspages/HighlightingPage.cpp b/src/widgets/settingspages/HighlightingPage.cpp index f5ec67d25..e1c018aea 100644 --- a/src/widgets/settingspages/HighlightingPage.cpp +++ b/src/widgets/settingspages/HighlightingPage.cpp @@ -6,9 +6,10 @@ #include "controllers/highlights/HighlightModel.hpp" #include "controllers/highlights/UserHighlightModel.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 #include @@ -45,8 +46,10 @@ HighlightingPage::HighlightingPage() // HIGHLIGHTS auto highlights = tabs.appendTab(new QVBoxLayout, "Messages"); { - highlights.emplace("Messages can be highlighted if " - "they match a certain pattern."); + highlights.emplace( + "Messages can be highlighted if they match a certain " + "pattern.\n" + "NOTE: User highlights will override phrase highlights."); EditableModelView *view = highlights @@ -56,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( @@ -71,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( - "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( @@ -89,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( @@ -107,8 +120,15 @@ 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); + }); } auto disabledUsers = @@ -147,17 +167,33 @@ HighlightingPage::HighlightingPage() // MISC auto customSound = layout.emplace().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("Select custom sound file"); - QObject::connect(selectFile.getElement(), &QPushButton::clicked, - this, [this] { - auto fileName = QFileDialog::getOpenFileName( - this, tr("Open Sound"), "", - tr("Audio Files (*.mp3 *.wav)")); - getSettings()->pathHighlightSound = fileName; - }); + customSound.emplace(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); + }); } layout.append(createCheckBox(ALWAYS_PLAY, @@ -171,4 +207,60 @@ 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(); + + 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); + + // For special highlights 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 diff --git a/src/widgets/settingspages/HighlightingPage.hpp b/src/widgets/settingspages/HighlightingPage.hpp index a60991bdc..031eece3f 100644 --- a/src/widgets/settingspages/HighlightingPage.hpp +++ b/src/widgets/settingspages/HighlightingPage.hpp @@ -1,5 +1,6 @@ #pragma once +#include "widgets/helper/EditableModelView.hpp" #include "widgets/settingspages/SettingsPage.hpp" #include @@ -17,6 +18,8 @@ public: private: QTimer disabledUsersChangedTimer_; + + void tableCellClicked(const QModelIndex &clicked, EditableModelView *view); }; } // namespace chatterino From 71337c4dbe5be0c5b6d07c88da84656ee534db8c Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Sat, 25 Jan 2020 11:28:10 +0100 Subject: [PATCH 14/22] Add missing include in ColorPickerDialog.hpp for MSVC2017 --- src/widgets/dialogs/ColorPickerDialog.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/widgets/dialogs/ColorPickerDialog.hpp b/src/widgets/dialogs/ColorPickerDialog.hpp index 5f5a48cc0..6f4f0c372 100644 --- a/src/widgets/dialogs/ColorPickerDialog.hpp +++ b/src/widgets/dialogs/ColorPickerDialog.hpp @@ -7,6 +7,8 @@ #include +#include + namespace chatterino { /** From 410de8226113c8f3a274d200f07602854d616f17 Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sat, 25 Jan 2020 12:59:31 +0100 Subject: [PATCH 15/22] Make a command that shows the Chatterino user card (/usercard) (#1375) * Make UserInfoPopup be able to show that fetching the information failed. --- .../commands/CommandController.cpp | 16 +++++++++++++++ src/providers/twitch/PartialTwitchUser.cpp | 13 +++++++++++- src/providers/twitch/PartialTwitchUser.hpp | 4 ++++ src/widgets/dialogs/UserInfoPopup.cpp | 20 ++++++++++++++++++- 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/controllers/commands/CommandController.cpp b/src/controllers/commands/CommandController.cpp index 0845f3288..a6150cfa9 100644 --- a/src/controllers/commands/CommandController.cpp +++ b/src/controllers/commands/CommandController.cpp @@ -17,6 +17,7 @@ #include "singletons/Theme.hpp" #include "util/CombinePath.hpp" #include "widgets/dialogs/LogsPopup.hpp" +#include "widgets/dialogs/UserInfoPopup.hpp" #include #include @@ -481,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 ""; + } } { diff --git a/src/providers/twitch/PartialTwitchUser.cpp b/src/providers/twitch/PartialTwitchUser.cpp index 631f41fc6..5407fbada 100644 --- a/src/providers/twitch/PartialTwitchUser.cpp +++ b/src/providers/twitch/PartialTwitchUser.cpp @@ -27,6 +27,13 @@ PartialTwitchUser PartialTwitchUser::byId(const QString &id) void PartialTwitchUser::getId(std::function successCallback, const QObject *caller) +{ + getId( + successCallback, [] {}, caller); +} +void PartialTwitchUser::getId(std::function successCallback, + std::function failureCallback, + const QObject *caller) { assert(!this->username_.isEmpty()); @@ -34,12 +41,13 @@ void PartialTwitchUser::getId(std::function 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()) { qDebug() << "API Error while getting user id, users is not an array"; + failureCallback(); return Failure; } @@ -48,12 +56,14 @@ void PartialTwitchUser::getId(std::function successCallback, { qDebug() << "API Error while getting user id, users array size " "is not 1"; + failureCallback(); return Failure; } if (!users[0].isObject()) { qDebug() << "API Error while getting user id, first user is " "not an object"; + failureCallback(); return Failure; } auto firstUser = users[0].toObject(); @@ -62,6 +72,7 @@ void PartialTwitchUser::getId(std::function successCallback, { qDebug() << "API Error: while getting user id, first user " "object `_id` key is not a string"; + failureCallback(); return Failure; } successCallback(id.toString()); diff --git a/src/providers/twitch/PartialTwitchUser.hpp b/src/providers/twitch/PartialTwitchUser.hpp index 36876dc26..6537bb279 100644 --- a/src/providers/twitch/PartialTwitchUser.hpp +++ b/src/providers/twitch/PartialTwitchUser.hpp @@ -21,6 +21,10 @@ public: void getId(std::function successCallback, const QObject *caller = nullptr); + + void getId(std::function successCallback, + std::function failureCallback, + const QObject *caller = nullptr); }; } // namespace chatterino diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index b0fc0071a..e530a5bff 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -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 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); From 4b1202437b610cec9f895d9cddde458e23e3d0c1 Mon Sep 17 00:00:00 2001 From: apa420 <17131426+apa420@users.noreply.github.com> Date: Sat, 25 Jan 2020 13:05:59 +0100 Subject: [PATCH 16/22] Sort emotes alphabetically in emote picker (#1499) --- src/providers/twitch/TwitchAccount.cpp | 4 ++++ src/widgets/dialogs/EmotePopup.cpp | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index b5930529e..13b4d4092 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -482,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); } }; diff --git a/src/widgets/dialogs/EmotePopup.cpp b/src/widgets/dialogs/EmotePopup.cpp index 00beb11c3..c000e3ec5 100644 --- a/src/widgets/dialogs/EmotePopup.cpp +++ b/src/widgets/dialogs/EmotePopup.cpp @@ -32,7 +32,14 @@ namespace { if (!map.empty()) { - for (const auto &emote : map) + std::vector> vec(map.begin(), + map.end()); + std::sort(vec.begin(), vec.end(), + [](const std::pair &l, + const std::pair &r) { + return l.first.string < r.first.string; + }); + for (const auto &emote : vec) { builder .emplace(emote.second, From a078d116d267b30f28e5ee739e3b99cba9b7f524 Mon Sep 17 00:00:00 2001 From: pajlada Date: Sat, 25 Jan 2020 14:33:38 +0100 Subject: [PATCH 17/22] Fix ping payload not breaking portable mode (#1516) This payload was initialized before main was called, so before the QApplication was initialized. This broke our portable checker Fixes #1481 --- src/providers/irc/IrcConnection2.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/providers/irc/IrcConnection2.cpp b/src/providers/irc/IrcConnection2.cpp index 08ff718bc..ed100149d 100644 --- a/src/providers/irc/IrcConnection2.cpp +++ b/src/providers/irc/IrcConnection2.cpp @@ -6,8 +6,7 @@ namespace chatterino { namespace { - const auto payload = - QString("chatterino/%1").arg(Version::instance().version()); + const auto payload = QString("chatterino/" CHATTERINO_VERSION); } // namespace From 93a6c55ed34614009dee95aa95d98bbab46cc4bb Mon Sep 17 00:00:00 2001 From: Leon Richardt Date: Sun, 26 Jan 2020 10:08:25 +0100 Subject: [PATCH 18/22] Fix subscription messages triggering split highlights (#1519) Since #1320, subscription messages are treated as highlights in order to allow customization. This caused subscription messages to highlight the split(s) the message was received in. This is not intended behavior. This commit fixes the issue by additionally checking if the `Subscription` flag is set on a highlighted message. --- src/widgets/helper/ChannelView.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 3fe02bfbe..c2542354d 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -648,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); } From bfee75ec5855f997c43abb96d77f711976242ecf Mon Sep 17 00:00:00 2001 From: Mm2PL Date: Sun, 26 Jan 2020 10:10:40 +0100 Subject: [PATCH 19/22] Show the toggle mod mode button when mod buttons are enabled. (#1518) * Fix #1288 Show the toggle mod mode button when mod buttons are enabled. * Automatic formatting ain't good enough i guess. --- src/widgets/splits/SplitHeader.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index d86df9b5c..79153b44c 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -577,10 +577,15 @@ void SplitHeader::updateModerationModeIcon() auto channel = this->split_->getChannel(); auto twitchChannel = dynamic_cast(channel.get()); - if (twitchChannel != nullptr && twitchChannel->hasModRights()) + if (twitchChannel != nullptr && + (twitchChannel->hasModRights() || moderationMode)) + { this->moderationButton_->show(); + } else + { this->moderationButton_->hide(); + } } void SplitHeader::paintEvent(QPaintEvent *) From 497ce2d2f27991136be08856fc79d9eacf8fbaec Mon Sep 17 00:00:00 2001 From: Leon Richardt Date: Mon, 27 Jan 2020 00:16:09 +0100 Subject: [PATCH 20/22] Better Highlights: Fix Unintentional Color Update (#1522) * HighlightPhrase: Fix wrong documentation * Use right constructor for new HighlightPhrases * Fix preset highlights changing unintentionally Prior to this commit, the callback for reacting to user input on the highlight table (namely, `HighlightingPage::tableCellClicked`) only checked for the row number in order to determine whether preset highlights (self highlights, whispers, and subscriptions) need to be updated. Hence, changing rows 0 through 2 in the "User Highlights" tab would also update the preset highlights. This commit adds a check to determine whether the callback was triggered by the "Messages" highlight tab, or not. --- .../highlights/HighlightPhrase.cpp | 10 ----- .../highlights/HighlightPhrase.hpp | 10 +++++ .../settingspages/HighlightingPage.cpp | 39 ++++++++++++------- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/controllers/highlights/HighlightPhrase.cpp b/src/controllers/highlights/HighlightPhrase.cpp index d310a921d..40942df3d 100644 --- a/src/controllers/highlights/HighlightPhrase.cpp +++ b/src/controllers/highlights/HighlightPhrase.cpp @@ -12,11 +12,6 @@ bool HighlightPhrase::operator==(const HighlightPhrase &other) const other.soundUrl_, other.color_); } -/** - * @brief Create a new HighlightPhrase. - * - * Use this constructor when updating an existing HighlightPhrase's color. - */ HighlightPhrase::HighlightPhrase(const QString &pattern, bool hasAlert, bool hasSound, bool isRegex, bool isCaseSensitive, const QString &soundUrl, @@ -36,11 +31,6 @@ HighlightPhrase::HighlightPhrase(const QString &pattern, bool hasAlert, this->color_ = std::make_shared(color); } -/** - * @brief Create a new HighlightPhrase. - * - * Use this constructor when creating a new HighlightPhrase. - */ HighlightPhrase::HighlightPhrase(const QString &pattern, bool hasAlert, bool hasSound, bool isRegex, bool isCaseSensitive, const QString &soundUrl, diff --git a/src/controllers/highlights/HighlightPhrase.hpp b/src/controllers/highlights/HighlightPhrase.hpp index 6121fc0fb..72849bb66 100644 --- a/src/controllers/highlights/HighlightPhrase.hpp +++ b/src/controllers/highlights/HighlightPhrase.hpp @@ -15,10 +15,20 @@ class HighlightPhrase public: bool operator==(const HighlightPhrase &other) const; + /** + * @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); + /** + * @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 color); diff --git a/src/widgets/settingspages/HighlightingPage.cpp b/src/widgets/settingspages/HighlightingPage.cpp index e1c018aea..46cdba3b6 100644 --- a/src/widgets/settingspages/HighlightingPage.cpp +++ b/src/widgets/settingspages/HighlightingPage.cpp @@ -121,7 +121,7 @@ HighlightingPage::HighlightingPage() getApp()->highlights->highlightedUsers.appendItem( HighlightPhrase{"highlighted user", true, false, false, false, "", - ColorProvider::instance().color( + *ColorProvider::instance().color( ColorType::SelfHighlight)}); }); @@ -243,20 +243,31 @@ void HighlightingPage::tableCellClicked(const QModelIndex &clicked, view->getModel()->setData(clicked, selected, Qt::DecorationRole); - // For special highlights we need to manually update the color map - auto instance = ColorProvider::instance(); - switch (clicked.row()) + // Hacky (?) way to figure out what tab the cell was clicked in + const bool fromMessages = (dynamic_cast( + view->getModel()) != nullptr); + + if (fromMessages) { - case 0: - instance.updateColor(ColorType::SelfHighlight, - selected); - break; - case 1: - instance.updateColor(ColorType::Whisper, selected); - break; - case 2: - instance.updateColor(ColorType::Subscription, selected); - break; + /* + * 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; + } } } }); From 1fd64be7f5a5ca78fcedc94fccde233359e7f962 Mon Sep 17 00:00:00 2001 From: hemirt <1310440+hemirt@users.noreply.github.com> Date: Sun, 2 Feb 2020 14:31:37 +0100 Subject: [PATCH 21/22] Makes it possible to hide one man spam (#1496) --- src/common/Channel.cpp | 1 + src/messages/Message.hpp | 1 + src/messages/layouts/MessageLayout.cpp | 6 ++ src/providers/twitch/IrcMessageHandler.cpp | 102 +++++++++++++++++- src/providers/twitch/IrcMessageHandler.hpp | 5 + src/singletons/Settings.hpp | 14 +++ src/widgets/Window.cpp | 5 + src/widgets/settingspages/GeneralPage.cpp | 27 +++++ .../settingspages/KeyboardSettingsPage.cpp | 4 + 9 files changed, 164 insertions(+), 1 deletion(-) diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index 5616a2633..43e88ee38 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -3,6 +3,7 @@ #include "Application.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" diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index 938ee6428..3b5bb97a8 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -33,6 +33,7 @@ enum class MessageFlag : uint32_t { Whisper = (1 << 16), HighlightedWhisper = (1 << 17), Debug = (1 << 18), + Similar = (1 << 19), }; using MessageFlags = FlagsEnum; diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index 03097c3d1..c048bd626 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -141,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); } diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 1e3aae415..7a456ecaf 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -21,6 +21,97 @@ namespace chatterino { +static float relativeSimilarity(const QString &str1, const QString &str2) +{ + // Longest Common Substring Problem + std::vector> tree(str1.size(), + std::vector(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 &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 parseBadges(QString badgesString) { QMap badges; @@ -133,7 +224,16 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *_message, } auto msg = builder.build(); - builder.triggerHighlights(); + + 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) diff --git a/src/providers/twitch/IrcMessageHandler.hpp b/src/providers/twitch/IrcMessageHandler.hpp index fe8321adf..9228054d8 100644 --- a/src/providers/twitch/IrcMessageHandler.hpp +++ b/src/providers/twitch/IrcMessageHandler.hpp @@ -1,6 +1,7 @@ #pragma once #include +#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 &messages); + static void setSimilarityFlags(MessagePtr message, ChannelPtr channel); + private: void addMessage(Communi::IrcMessage *message, const QString &target, const QString &content, TwitchIrcServer &server, diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 050c9db98..f0d1679d4 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -249,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(); }; diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 2824a8b38..c291be6c2 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -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() diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 0ffa1c8b5..4032db9e2 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -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( + "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( + "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( + "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); diff --git a/src/widgets/settingspages/KeyboardSettingsPage.cpp b/src/widgets/settingspages/KeyboardSettingsPage.cpp index 6b05cf945..2d2a809fd 100644 --- a/src/widgets/settingspages/KeyboardSettingsPage.cpp +++ b/src/widgets/settingspages/KeyboardSettingsPage.cpp @@ -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/..."), From b8953157ccc25e1ea2c8b6910f39a0694019d646 Mon Sep 17 00:00:00 2001 From: Leon Richardt Date: Sun, 2 Feb 2020 14:33:25 +0100 Subject: [PATCH 22/22] Better Highlights: Rework highlight parsing order (#1524) This commit is in response to #1523. Whispers are now only added to the `/mentions` tab if they also match a user name or phrase highlight. On a related note, the `highlightVisual_` member has been removed as it is no longer necessary. --- src/providers/twitch/TwitchMessageBuilder.cpp | 106 +++++++++--------- src/providers/twitch/TwitchMessageBuilder.hpp | 1 - 2 files changed, 51 insertions(+), 56 deletions(-) diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index a0142cf64..0d4680472 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -1004,13 +1004,9 @@ void TwitchMessageBuilder::parseHighlights() } } - if (!this->highlightVisual_) - { - this->highlightVisual_ = true; - this->message().flags.set(MessageFlag::Highlighted); - this->message().highlightColor = - ColorProvider::instance().color(ColorType::Subscription); - } + 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. @@ -1027,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 userHighlights = app->highlights->highlightedUsers.cloneVector(); @@ -1039,12 +1068,9 @@ void TwitchMessageBuilder::parseHighlights() } qDebug() << "Highlight because user" << this->ircMessage->nick() << "sent a message"; - if (!this->highlightVisual_) - { - this->highlightVisual_ = true; - this->message().flags.set(MessageFlag::Highlighted); - this->message().highlightColor = userHighlight.getColor(); - } + + this->message().flags.set(MessageFlag::Highlighted); + this->message().highlightColor = userHighlight.getColor(); if (userHighlight.hasAlert()) { @@ -1067,8 +1093,11 @@ void TwitchMessageBuilder::parseHighlights() if (this->highlightAlert_ && this->highlightSound_) { - // Usernames "beat" highlight phrases: Once a username highlight - // has been applied, no further highlight phrases will be checked + /* + * User name highlights "beat" highlight phrases: If a message has + * all attributes (color, taskbar flashing, sound) set, highlight + * phrases will not be checked. + */ return; } } @@ -1104,12 +1133,9 @@ void TwitchMessageBuilder::parseHighlights() qDebug() << "Highlight because" << this->originalMessage_ << "matches" << highlight.getPattern(); - if (!this->highlightVisual_) - { - this->highlightVisual_ = true; - this->message().flags.set(MessageFlag::Highlighted); - this->message().highlightColor = highlight.getColor(); - } + + this->message().flags.set(MessageFlag::Highlighted); + this->message().highlightColor = highlight.getColor(); if (highlight.hasAlert()) { @@ -1135,43 +1161,13 @@ void TwitchMessageBuilder::parseHighlights() if (this->highlightAlert_ && this->highlightSound_) { - // Break once the first highlight has been set. If a message would - // trigger multiple highlights, only the first one from the list - // will be applied. + /* + * 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; - - // Use custom sound if set, otherwise use fallback - if (!getSettings()->whisperHighlightSoundUrl.getValue().isEmpty()) - { - this->highlightSoundUrl_ = - QUrl(getSettings()->whisperHighlightSoundUrl.getValue()); - } - else - { - this->highlightSoundUrl_ = getFallbackHighlightSound(); - } - } - if (!this->highlightVisual_) - { - this->highlightVisual_ = true; - this->message().flags.set(MessageFlag::Highlighted); - this->message().highlightColor = - ColorProvider::instance().color(ColorType::Whisper); - } - } } void TwitchMessageBuilder::appendTwitchEmote( diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp index b8f5613f4..7066c3267 100644 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ b/src/providers/twitch/TwitchMessageBuilder.hpp @@ -92,7 +92,6 @@ private: const bool action_ = false; - bool highlightVisual_ = false; bool highlightAlert_ = false; bool highlightSound_ = false;