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
This commit is contained in:
Leon Richardt 2020-01-25 11:03:10 +01:00 committed by pajlada
parent 00414eb779
commit 5957551d06
29 changed files with 1822 additions and 187 deletions

View file

@ -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 \

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -55,6 +55,7 @@ struct Message : boost::noncopyable {
QString displayName;
QString localizedName;
QString timeoutUser;
std::shared_ptr<QColor> highlightColor;
uint32_t count = 1;
std::vector<std::unique_ptr<MessageElement>> elements;

View file

@ -25,6 +25,19 @@
namespace chatterino {
namespace {
QColor blendColors(const QColor &base, const QColor &apply)
{
const qreal &alpha = apply.alphaF();
QColor result;
result.setRgbF(base.redF() * (1 - alpha) + apply.redF() * alpha,
base.greenF() * (1 - alpha) + apply.greenF() * alpha,
base.blueF() * (1 - alpha) + apply.blueF() * alpha);
return result;
}
} // namespace
MessageLayout::MessageLayout(MessagePtr message)
: message_(message)
, container_(std::make_shared<MessageLayoutContainer>())
@ -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))
{

View file

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

View file

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

View file

@ -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);
}
}
}

View file

@ -95,6 +95,8 @@ private:
bool highlightVisual_ = false;
bool highlightAlert_ = false;
bool highlightSound_ = false;
QUrl highlightSoundUrl_;
};
} // namespace chatterino

View file

@ -146,18 +146,39 @@ public:
/// Highlighting
// BoolSetting enableHighlights = {"/highlighting/enabled", true};
BoolSetting customHighlightSound = {"/highlighting/useCustomSound", false};
BoolSetting enableSelfHighlight = {
"/highlighting/selfHighlight/nameIsHighlightKeyword", true};
BoolSetting enableSelfHighlightSound = {
"/highlighting/selfHighlight/enableSound", true};
BoolSetting enableSelfHighlightTaskbar = {
"/highlighting/selfHighlight/enableTaskbarFlashing", true};
QStringSetting selfHighlightSoundUrl = {
"/highlighting/selfHighlightSoundUrl", ""};
QStringSetting selfHighlightColor = {"/highlighting/selfHighlightColor",
""};
BoolSetting enableWhisperHighlight = {
"/highlighting/whisperHighlight/whispersHighlighted", true};
BoolSetting enableWhisperHighlightSound = {
"/highlighting/whisperHighlight/enableSound", false};
BoolSetting enableWhisperHighlightTaskbar = {
"/highlighting/whisperHighlight/enableTaskbarFlashing", false};
QStringSetting whisperHighlightSoundUrl = {
"/highlighting/whisperHighlightSoundUrl", ""};
QStringSetting whisperHighlightColor = {
"/highlighting/whisperHighlightColor", ""};
BoolSetting enableSubHighlight = {
"/highlighting/subHighlight/subsHighlighted", true};
BoolSetting enableSubHighlightSound = {
"/highlighting/subHighlight/enableSound", false};
BoolSetting enableSubHighlightTaskbar = {
"/highlighting/subHighlight/enableTaskbarFlashing", false};
QStringSetting subHighlightSoundUrl = {"/highlighting/subHighlightSoundUrl",
""};
QStringSetting subHighlightColor = {"/highlighting/subHighlightColor", ""};
QStringSetting highlightColor = {"/highlighting/color", ""};
BoolSetting longAlerts = {"/highlighting/alerts", false};
@ -168,7 +189,7 @@ public:
QStringSetting logPath = {"/logging/path", ""};
QStringSetting pathHighlightSound = {"/highlighting/highlightSoundPath",
"qrc:/sounds/ping2.wav"};
""};
BoolSetting highlightAlwaysPlaySound = {"/highlighting/alwaysPlaySound",
false};

View file

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

View file

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

View file

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

View file

@ -0,0 +1,367 @@
#include "widgets/dialogs/ColorPickerDialog.hpp"
#include "providers/colors/ColorProvider.hpp"
#include "singletons/Theme.hpp"
namespace chatterino {
ColorPickerDialog::ColorPickerDialog(const QColor &initial, QWidget *parent)
: BasePopup(BaseWindow::EnableCustomFrame, parent)
, color_()
, dialogConfirmed_(false)
{
// This hosts the "business logic" and the dialog button box
LayoutCreator<QWidget> layoutWidget(this->getLayoutContainer());
auto layout = layoutWidget.setLayoutType<QVBoxLayout>().withoutMargin();
// This hosts the business logic: color picker and predefined colors
LayoutCreator<QWidget> contentCreator(new QWidget());
auto contents = contentCreator.setLayoutType<QHBoxLayout>();
// This hosts the predefined colors (and also the currently selected color)
LayoutCreator<QWidget> predefCreator(new QWidget());
auto predef = predefCreator.setLayoutType<QVBoxLayout>();
// Recently used colors
{
LayoutCreator<QWidget> gridCreator(new QWidget());
this->initRecentColors(gridCreator);
predef.append(gridCreator.getElement());
}
// Default colors
{
LayoutCreator<QWidget> gridCreator(new QWidget());
this->initDefaultColors(gridCreator);
predef.append(gridCreator.getElement());
}
// Currently selected color
{
LayoutCreator<QWidget> curColorCreator(new QWidget());
auto curColor = curColorCreator.setLayoutType<QHBoxLayout>();
curColor.emplace<QLabel>("Selected:").assign(&this->ui_.selected.label);
curColor.emplace<ColorButton>(initial).assign(
&this->ui_.selected.color);
predef.append(curColor.getElement());
}
contents.append(predef.getElement());
// Color picker
{
LayoutCreator<QWidget> obj(new QWidget());
auto vbox = obj.setLayoutType<QVBoxLayout>();
// The actual color picker
{
LayoutCreator<QWidget> cpCreator(new QWidget());
this->initColorPicker(cpCreator);
vbox.append(cpCreator.getElement());
}
// Spin boxes
{
LayoutCreator<QWidget> sbCreator(new QWidget());
this->initSpinBoxes(sbCreator);
vbox.append(sbCreator.getElement());
}
// HTML color
{
LayoutCreator<QWidget> htmlCreator(new QWidget());
this->initHtmlColor(htmlCreator);
vbox.append(htmlCreator.getElement());
}
contents.append(obj.getElement());
}
layout.append(contents.getElement());
// Dialog buttons
auto buttons =
layout.emplace<QHBoxLayout>().emplace<QDialogButtonBox>(this);
{
auto *button_ok = buttons->addButton(QDialogButtonBox::Ok);
QObject::connect(button_ok, &QPushButton::clicked,
[=](bool) { this->ok(); });
auto *button_cancel = buttons->addButton(QDialogButtonBox::Cancel);
QObject::connect(button_cancel, &QAbstractButton::clicked,
[=](bool) { this->close(); });
}
this->themeChangedEvent();
this->selectColor(initial, false);
}
ColorPickerDialog::~ColorPickerDialog()
{
if (this->htmlColorValidator_)
{
this->htmlColorValidator_->deleteLater();
this->htmlColorValidator_ = nullptr;
}
}
QColor ColorPickerDialog::selectedColor() const
{
if (!this->dialogConfirmed_)
{
// If the Cancel button was clicked, return the invalid color
return QColor();
}
return this->color_;
}
void ColorPickerDialog::closeEvent(QCloseEvent *)
{
this->closed.invoke();
}
void ColorPickerDialog::themeChangedEvent()
{
BaseWindow::themeChangedEvent();
QString textCol = this->theme->splits.input.text.name(QColor::HexRgb);
QString bgCol = this->theme->splits.input.background.name(QColor::HexRgb);
// Labels
QString labelStyle = QString("color: %1;").arg(textCol);
this->ui_.recent.label->setStyleSheet(labelStyle);
this->ui_.def.label->setStyleSheet(labelStyle);
this->ui_.selected.label->setStyleSheet(labelStyle);
this->ui_.picker.htmlLabel->setStyleSheet(labelStyle);
for (auto spinBoxLabel : this->ui_.picker.spinBoxLabels)
{
spinBoxLabel->setStyleSheet(labelStyle);
}
this->ui_.picker.htmlEdit->setStyleSheet(
this->theme->splits.input.styleSheet);
// Styling spin boxes is too much effort
}
void ColorPickerDialog::selectColor(const QColor &color, bool fromColorPicker)
{
if (color == this->color_)
return;
this->color_ = color;
// Update UI elements
this->ui_.selected.color->setColor(this->color_);
/*
* Somewhat "ugly" hack to prevent feedback loop between widgets. Since
* this method is private, I'm okay with this being ugly.
*/
if (!fromColorPicker)
{
this->ui_.picker.colorPicker->setCol(this->color_.hslHue(),
this->color_.hslSaturation());
this->ui_.picker.luminancePicker->setCol(this->color_.hsvHue(),
this->color_.hsvSaturation(),
this->color_.value());
}
this->ui_.picker.spinBoxes[SpinBox::RED]->setValue(this->color_.red());
this->ui_.picker.spinBoxes[SpinBox::GREEN]->setValue(this->color_.green());
this->ui_.picker.spinBoxes[SpinBox::BLUE]->setValue(this->color_.blue());
this->ui_.picker.spinBoxes[SpinBox::ALPHA]->setValue(this->color_.alpha());
/*
* Here, we are intentionally using HexRgb instead of HexArgb. Most online
* sites (or other applications) will likely not include the alpha channel
* in their output.
*/
this->ui_.picker.htmlEdit->setText(this->color_.name(QColor::HexRgb));
}
void ColorPickerDialog::ok()
{
this->dialogConfirmed_ = true;
this->close();
}
void ColorPickerDialog::initRecentColors(LayoutCreator<QWidget> &creator)
{
auto grid = creator.setLayoutType<QGridLayout>();
auto label = this->ui_.recent.label = new QLabel("Recently used:");
grid->addWidget(label, 0, 0, 1, -1);
const auto recentColors = ColorProvider::instance().recentColors();
auto it = recentColors.begin();
size_t ind = 0;
while (it != recentColors.end() && ind < MAX_RECENT_COLORS)
{
this->ui_.recent.colors.push_back(new ColorButton(*it, this));
auto *button = this->ui_.recent.colors[ind];
const int rowInd = (ind / RECENT_COLORS_PER_ROW) + 1;
const int columnInd = ind % RECENT_COLORS_PER_ROW;
grid->addWidget(button, rowInd, columnInd);
QObject::connect(button, &QPushButton::clicked,
[=] { this->selectColor(button->color(), false); });
++it;
++ind;
}
auto spacer =
new QSpacerItem(40, 20, QSizePolicy::Minimum, QSizePolicy::Expanding);
grid->addItem(spacer, (ind / RECENT_COLORS_PER_ROW) + 2, 0, 1, 1,
Qt::AlignTop);
}
void ColorPickerDialog::initDefaultColors(LayoutCreator<QWidget> &creator)
{
auto grid = creator.setLayoutType<QGridLayout>();
auto label = this->ui_.def.label = new QLabel("Default colors:");
grid->addWidget(label, 0, 0, 1, -1);
const auto defaultColors = ColorProvider::instance().defaultColors();
auto it = defaultColors.begin();
size_t ind = 0;
while (it != defaultColors.end())
{
this->ui_.def.colors.push_back(new ColorButton(*it, this));
auto *button = this->ui_.def.colors[ind];
const int rowInd = (ind / DEFAULT_COLORS_PER_ROW) + 1;
const int columnInd = ind % DEFAULT_COLORS_PER_ROW;
grid->addWidget(button, rowInd, columnInd);
QObject::connect(button, &QPushButton::clicked,
[=] { this->selectColor(button->color(), false); });
++it;
++ind;
}
auto spacer =
new QSpacerItem(40, 20, QSizePolicy::Minimum, QSizePolicy::Expanding);
grid->addItem(spacer, (ind / DEFAULT_COLORS_PER_ROW) + 2, 0, 1, 1,
Qt::AlignTop);
}
void ColorPickerDialog::initColorPicker(LayoutCreator<QWidget> &creator)
{
auto cpPanel = creator.setLayoutType<QHBoxLayout>();
/*
* For some reason, LayoutCreator::emplace didn't work for these.
* (Or maybe I was too dense to make it work.)
* After trying to debug for 4 hours or so, I gave up and settled
* for this solution.
*/
auto *colorPicker = new QColorPicker(this);
this->ui_.picker.colorPicker = colorPicker;
auto *luminancePicker = new QColorLuminancePicker(this);
this->ui_.picker.luminancePicker = luminancePicker;
cpPanel.append(colorPicker);
cpPanel.append(luminancePicker);
QObject::connect(colorPicker, SIGNAL(newCol(int, int)), luminancePicker,
SLOT(setCol(int, int)));
QObject::connect(
luminancePicker, &QColorLuminancePicker::newHsv,
[=](int h, int s, int v) {
int alpha = this->ui_.picker.spinBoxes[SpinBox::ALPHA]->value();
this->selectColor(QColor::fromHsv(h, s, v, alpha), true);
});
}
void ColorPickerDialog::initSpinBoxes(LayoutCreator<QWidget> &creator)
{
auto spinBoxes = creator.setLayoutType<QGridLayout>();
auto *red = this->ui_.picker.spinBoxes[SpinBox::RED] =
new QColSpinBox(this);
auto *green = this->ui_.picker.spinBoxes[SpinBox::GREEN] =
new QColSpinBox(this);
auto *blue = this->ui_.picker.spinBoxes[SpinBox::BLUE] =
new QColSpinBox(this);
auto *alpha = this->ui_.picker.spinBoxes[SpinBox::ALPHA] =
new QColSpinBox(this);
// We need pointers to these for theme changes
auto *redLbl = this->ui_.picker.spinBoxLabels[SpinBox::RED] =
new QLabel("Red:");
auto *greenLbl = this->ui_.picker.spinBoxLabels[SpinBox::GREEN] =
new QLabel("Green:");
auto *blueLbl = this->ui_.picker.spinBoxLabels[SpinBox::BLUE] =
new QLabel("Blue:");
auto *alphaLbl = this->ui_.picker.spinBoxLabels[SpinBox::ALPHA] =
new QLabel("Alpha:");
spinBoxes->addWidget(redLbl, 0, 0);
spinBoxes->addWidget(red, 0, 1);
spinBoxes->addWidget(greenLbl, 1, 0);
spinBoxes->addWidget(green, 1, 1);
spinBoxes->addWidget(blueLbl, 2, 0);
spinBoxes->addWidget(blue, 2, 1);
spinBoxes->addWidget(alphaLbl, 3, 0);
spinBoxes->addWidget(alpha, 3, 1);
for (size_t i = 0; i < SpinBox::END; ++i)
{
QObject::connect(
this->ui_.picker.spinBoxes[i],
QOverload<int>::of(&QSpinBox::valueChanged), [=](int value) {
this->selectColor(QColor(red->value(), green->value(),
blue->value(), alpha->value()),
false);
});
}
}
void ColorPickerDialog::initHtmlColor(LayoutCreator<QWidget> &creator)
{
auto html = creator.setLayoutType<QGridLayout>();
// Copied from Qt source for QColorShower
static QRegularExpression regExp(
QStringLiteral("#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})"));
auto *validator = this->htmlColorValidator_ =
new QRegularExpressionValidator(regExp, this);
auto *htmlLabel = this->ui_.picker.htmlLabel = new QLabel("HTML:");
auto *htmlEdit = this->ui_.picker.htmlEdit = new QLineEdit(this);
htmlEdit->setValidator(validator);
html->addWidget(htmlLabel, 0, 0);
html->addWidget(htmlEdit, 0, 1);
QObject::connect(htmlEdit, &QLineEdit::textEdited,
[=](const QString &text) {
QColor col(text);
if (col.isValid())
this->selectColor(col, false);
});
}
} // namespace chatterino

View file

@ -0,0 +1,108 @@
#pragma once
#include "util/LayoutCreator.hpp"
#include "widgets/BasePopup.hpp"
#include "widgets/helper/ColorButton.hpp"
#include "widgets/helper/QColorPicker.hpp"
#include <pajlada/signals/signal.hpp>
namespace chatterino {
/**
* @brief A custom color picker dialog.
*
* This class exists because QColorPickerDialog did not suit our use case.
* This dialog provides buttons for recently used and default colors, as well
* as a color picker widget identical to the one used in QColorPickerDialog.
*/
class ColorPickerDialog : public BasePopup
{
public:
/**
* @brief Create a new color picker dialog that selects the initial color.
*
* You can connect to the ::closed signal of this instance to get notified
* when the dialog is closed.
*/
ColorPickerDialog(const QColor &initial, QWidget *parent = nullptr);
~ColorPickerDialog();
/**
* @brief Return the final color selected by the user.
*
* Note that this method will always return the invalid color if the dialog
* is still open, or if the dialog has not been confirmed.
*
* @return The color selected by the user, if the dialog was confirmed.
* The invalid color, if the dialog has not been confirmed.
*/
QColor selectedColor() const;
pajlada::Signals::NoArgSignal closed;
protected:
void closeEvent(QCloseEvent *);
void themeChangedEvent();
private:
struct {
struct {
QLabel *label;
std::vector<ColorButton *> colors;
} recent;
struct {
QLabel *label;
std::vector<ColorButton *> colors;
} def;
struct {
QLabel *label;
ColorButton *color;
} selected;
struct {
QColorPicker *colorPicker;
QColorLuminancePicker *luminancePicker;
std::array<QLabel *, 4> spinBoxLabels;
std::array<QColSpinBox *, 4> spinBoxes;
QLabel *htmlLabel;
QLineEdit *htmlEdit;
} picker;
} ui_;
enum SpinBox : size_t { RED = 0, GREEN = 1, BLUE = 2, ALPHA = 3, END };
static const size_t MAX_RECENT_COLORS = 10;
static const size_t RECENT_COLORS_PER_ROW = 5;
static const size_t DEFAULT_COLORS_PER_ROW = 5;
QColor color_;
bool dialogConfirmed_;
QRegularExpressionValidator *htmlColorValidator_{};
/**
* @brief Update the currently selected color.
*
* @param color Color to update to.
* @param fromColorPicker Whether the color update has been triggered by
* one of the color picker widgets. This is needed
* to prevent weird widget behavior.
*/
void selectColor(const QColor &color, bool fromColorPicker);
/// Called when the dialog is confirmed.
void ok();
// Helper methods for initializing UI elements
void initRecentColors(LayoutCreator<QWidget> &creator);
void initDefaultColors(LayoutCreator<QWidget> &creator);
void initColorPicker(LayoutCreator<QWidget> &creator);
void initSpinBoxes(LayoutCreator<QWidget> &creator);
void initHtmlColor(LayoutCreator<QWidget> &creator);
};
} // namespace chatterino

View file

@ -0,0 +1,23 @@
#include "widgets/helper/ColorButton.hpp"
namespace chatterino {
ColorButton::ColorButton(const QColor &color, QWidget *parent)
: QPushButton(parent)
, color_(color)
{
this->setColor(color_);
}
const QColor &ColorButton::color() const
{
return this->color_;
}
void ColorButton::setColor(QColor color)
{
this->color_ = color;
this->setStyleSheet("background-color: " + color.name(QColor::HexArgb));
}
} // namespace chatterino

View file

@ -0,0 +1,18 @@
#pragma once
namespace chatterino {
class ColorButton : public QPushButton
{
public:
ColorButton(const QColor &color, QWidget *parent = nullptr);
const QColor &color() const;
void setColor(QColor color);
private:
QColor color_;
};
} // namespace chatterino

View file

@ -0,0 +1,290 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWidgets module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "widgets/helper/QColorPicker.hpp"
#include <qdrawutil.h>
/*
* These classes are literally copied from the Qt source.
* Unfortunately, they are private to the QColorDialog class so we cannot use
* them directly.
* If they become public at any point in the future, it should be possible to
* replace every include of this header with the respective includes for the
* QColorPicker, QColorLuminancePicker, and QColSpinBox classes.
*/
namespace chatterino {
int QColorLuminancePicker::y2val(int y)
{
int d = height() - 2 * coff - 1;
return 255 - (y - coff) * 255 / d;
}
int QColorLuminancePicker::val2y(int v)
{
int d = height() - 2 * coff - 1;
return coff + (255 - v) * d / 255;
}
QColorLuminancePicker::QColorLuminancePicker(QWidget *parent)
: QWidget(parent)
{
hue = 100;
val = 100;
sat = 100;
pix = 0;
setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
}
QColorLuminancePicker::~QColorLuminancePicker()
{
delete pix;
}
void QColorLuminancePicker::mouseMoveEvent(QMouseEvent *m)
{
setVal(y2val(m->y()));
}
void QColorLuminancePicker::mousePressEvent(QMouseEvent *m)
{
setVal(y2val(m->y()));
}
void QColorLuminancePicker::setVal(int v)
{
if (val == v)
return;
val = qMax(0, qMin(v, 255));
delete pix;
pix = 0;
repaint();
emit newHsv(hue, sat, val);
}
//receives from a hue,sat chooser and relays.
void QColorLuminancePicker::setCol(int h, int s)
{
setCol(h, s, val);
emit newHsv(h, s, val);
}
QSize QColorLuminancePicker::sizeHint() const
{
return QSize(LUMINANCE_PICKER_WIDTH, LUMINANCE_PICKER_HEIGHT);
}
void QColorLuminancePicker::paintEvent(QPaintEvent *)
{
int w = width() - 5;
QRect r(0, foff, w, height() - 2 * foff);
int wi = r.width() - 2;
int hi = r.height() - 2;
if (!pix || pix->height() != hi || pix->width() != wi)
{
delete pix;
QImage img(wi, hi, QImage::Format_RGB32);
int y;
uint *pixel = (uint *)img.scanLine(0);
for (y = 0; y < hi; y++)
{
uint *end = pixel + wi;
std::fill(pixel, end,
QColor::fromHsv(hue, sat, y2val(y + coff)).rgb());
pixel = end;
}
pix = new QPixmap(QPixmap::fromImage(img));
}
QPainter p(this);
p.drawPixmap(1, coff, *pix);
const QPalette &g = palette();
qDrawShadePanel(&p, r, g, true);
p.setPen(g.windowText().color());
p.setBrush(g.windowText());
QPolygon a;
int y = val2y(val);
a.setPoints(3, w, y, w + 5, y + 5, w + 5, y - 5);
p.eraseRect(w, 0, 5, height());
p.drawPolygon(a);
}
void QColorLuminancePicker::setCol(int h, int s, int v)
{
val = v;
hue = h;
sat = s;
delete pix;
pix = 0;
repaint();
}
QPoint QColorPicker::colPt()
{
QRect r = contentsRect();
return QPoint((360 - hue) * (r.width() - 1) / 360,
(255 - sat) * (r.height() - 1) / 255);
}
int QColorPicker::huePt(const QPoint &pt)
{
QRect r = contentsRect();
return 360 - pt.x() * 360 / (r.width() - 1);
}
int QColorPicker::satPt(const QPoint &pt)
{
QRect r = contentsRect();
return 255 - pt.y() * 255 / (r.height() - 1);
}
void QColorPicker::setCol(const QPoint &pt)
{
setCol(huePt(pt), satPt(pt));
}
QColorPicker::QColorPicker(QWidget *parent)
: QFrame(parent)
, crossVisible(true)
{
hue = 0;
sat = 0;
setCol(150, 255);
setAttribute(Qt::WA_NoSystemBackground);
setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
}
QColorPicker::~QColorPicker()
{
}
void QColorPicker::setCrossVisible(bool visible)
{
if (crossVisible != visible)
{
crossVisible = visible;
update();
}
}
QSize QColorPicker::sizeHint() const
{
return QSize(COLOR_PICKER_WIDTH, COLOR_PICKER_HEIGHT);
}
void QColorPicker::setCol(int h, int s)
{
int nhue = qMin(qMax(0, h), 359);
int nsat = qMin(qMax(0, s), 255);
if (nhue == hue && nsat == sat)
return;
QRect r(colPt(), QSize(20, 20));
hue = nhue;
sat = nsat;
r = r.united(QRect(colPt(), QSize(20, 20)));
r.translate(contentsRect().x() - 9, contentsRect().y() - 9);
repaint(r);
}
void QColorPicker::mouseMoveEvent(QMouseEvent *m)
{
QPoint p = m->pos() - contentsRect().topLeft();
setCol(p);
emit newCol(hue, sat);
}
void QColorPicker::mousePressEvent(QMouseEvent *m)
{
QPoint p = m->pos() - contentsRect().topLeft();
setCol(p);
emit newCol(hue, sat);
}
void QColorPicker::paintEvent(QPaintEvent *)
{
QPainter p(this);
drawFrame(&p);
QRect r = contentsRect();
p.drawPixmap(r.topLeft(), pix);
if (crossVisible)
{
QPoint pt = colPt() + r.topLeft();
p.setPen(Qt::black);
p.fillRect(pt.x() - 9, pt.y(), 20, 2, Qt::black);
p.fillRect(pt.x(), pt.y() - 9, 2, 20, Qt::black);
}
}
void QColorPicker::resizeEvent(QResizeEvent *ev)
{
QFrame::resizeEvent(ev);
int w = width() - frameWidth() * 2;
int h = height() - frameWidth() * 2;
QImage img(w, h, QImage::Format_RGB32);
int x, y;
uint *pixel = (uint *)img.scanLine(0);
for (y = 0; y < h; y++)
{
const uint *end = pixel + w;
x = 0;
while (pixel < end)
{
QPoint p(x, y);
QColor c;
c.setHsv(huePt(p), satPt(p), 200);
*pixel = c.rgb();
++pixel;
++x;
}
}
pix = QPixmap::fromImage(img);
}
QColSpinBox::QColSpinBox(QWidget *parent)
: QSpinBox(parent)
{
this->setRange(0, 255);
}
void QColSpinBox::setValue(int i)
{
const QSignalBlocker blocker(this);
QSpinBox::setValue(i);
}
} // namespace chatterino

View file

@ -0,0 +1,130 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWidgets module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#pragma once
#include <QSpinBox>
namespace chatterino {
/*
* These classes are literally copied from the Qt source.
* Unfortunately, they are private to the QColorDialog class so we cannot use
* them directly.
* If they become public at any point in the future, it should be possible to
* replace every include of this header with the respective includes for the
* QColorPicker, QColorLuminancePicker, and QColSpinBox classes.
*/
class QColorPicker : public QFrame
{
Q_OBJECT
public:
QColorPicker(QWidget *parent);
~QColorPicker();
void setCrossVisible(bool visible);
public slots:
void setCol(int h, int s);
signals:
void newCol(int h, int s);
protected:
QSize sizeHint() const override;
void paintEvent(QPaintEvent *) override;
void mouseMoveEvent(QMouseEvent *) override;
void mousePressEvent(QMouseEvent *) override;
void resizeEvent(QResizeEvent *) override;
private:
int hue;
int sat;
QPoint colPt();
int huePt(const QPoint &pt);
int satPt(const QPoint &pt);
void setCol(const QPoint &pt);
QPixmap pix;
bool crossVisible;
};
static const int COLOR_PICKER_WIDTH = 220;
static const int COLOR_PICKER_HEIGHT = 200;
class QColorLuminancePicker : public QWidget
{
Q_OBJECT
public:
QColorLuminancePicker(QWidget *parent = 0);
~QColorLuminancePicker();
public slots:
void setCol(int h, int s, int v);
void setCol(int h, int s);
signals:
void newHsv(int h, int s, int v);
protected:
QSize sizeHint() const override;
void paintEvent(QPaintEvent *) override;
void mouseMoveEvent(QMouseEvent *) override;
void mousePressEvent(QMouseEvent *) override;
private:
enum { foff = 3, coff = 4 }; //frame and contents offset
int val;
int hue;
int sat;
int y2val(int y);
int val2y(int val);
void setVal(int v);
QPixmap *pix;
};
static const int LUMINANCE_PICKER_WIDTH = 25;
static const int LUMINANCE_PICKER_HEIGHT = COLOR_PICKER_HEIGHT;
class QColSpinBox : public QSpinBox
{
public:
QColSpinBox(QWidget *parent);
void setValue(int i);
};
} // namespace chatterino

View file

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

View file

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

View file

@ -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 <QFileDialog>
#include <QHeaderView>
@ -45,8 +46,10 @@ HighlightingPage::HighlightingPage()
// HIGHLIGHTS
auto highlights = tabs.appendTab(new QVBoxLayout, "Messages");
{
highlights.emplace<QLabel>("Messages can be highlighted if "
"they match a certain pattern.");
highlights.emplace<QLabel>(
"Messages can be highlighted if they match a certain "
"pattern.\n"
"NOTE: User highlights will override phrase highlights.");
EditableModelView *view =
highlights
@ -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<QLabel>(
"Messages from a certain user can be highlighted.");
"Messages from a certain user can be highlighted.\n"
"NOTE: User highlights will override phrase highlights.");
EditableModelView *view =
pingUsers
.emplace<EditableModelView>(
@ -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,7 +120,14 @@ HighlightingPage::HighlightingPage()
view->addButtonPressed.connect([] {
getApp()->highlights->highlightedUsers.appendItem(
HighlightPhrase{"highlighted user", true, false, false,
false});
false, "",
ColorProvider::instance().color(
ColorType::SelfHighlight)});
});
QObject::connect(view->getTableView(), &QTableView::clicked,
[this, view](const QModelIndex &clicked) {
this->tableCellClicked(clicked, view);
});
}
@ -147,16 +167,32 @@ HighlightingPage::HighlightingPage()
// MISC
auto customSound = layout.emplace<QHBoxLayout>().withoutMargin();
{
customSound.append(this->createCheckBox(
"Custom sound", getSettings()->customHighlightSound));
auto fallbackSound = customSound.append(this->createCheckBox(
"Fallback sound (played when no other sound is set)",
getSettings()->customHighlightSound));
auto getSelectFileText = [] {
const QString value = getSettings()->pathHighlightSound;
return value.isEmpty() ? "Select custom fallback sound"
: QUrl::fromLocalFile(value).fileName();
};
auto selectFile =
customSound.emplace<QPushButton>("Select custom sound file");
QObject::connect(selectFile.getElement(), &QPushButton::clicked,
this, [this] {
customSound.emplace<QPushButton>(getSelectFileText());
QObject::connect(
selectFile.getElement(), &QPushButton::clicked, this,
[=]() mutable {
auto fileName = QFileDialog::getOpenFileName(
this, tr("Open Sound"), "",
tr("Audio Files (*.mp3 *.wav)"));
getSettings()->pathHighlightSound = fileName;
selectFile.getElement()->setText(getSelectFileText());
// Set check box according to updated value
fallbackSound->setCheckState(
fileName.isEmpty() ? Qt::Unchecked : Qt::Checked);
});
}
@ -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<QColor>();
auto dialog = new ColorPickerDialog(initial, this);
dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->show();
dialog->closed.connect([=] {
QColor selected = dialog->selectedColor();
if (selected.isValid())
{
view->getModel()->setData(clicked, selected,
Qt::DecorationRole);
// 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

View file

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