Merge branch 'master' into apa-bits

This commit is contained in:
fourtf 2019-09-08 18:02:58 +02:00 committed by GitHub
commit 9f1eb654fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
94 changed files with 1178 additions and 1640 deletions

View file

@ -461,4 +461,4 @@ git_hash = $$str_member($$git_commit, 0, 8)
# https://stackoverflow.com/questions/3348711/add-a-define-to-qmake-with-a-value/18343449#18343449
DEFINES += CHATTERINO_GIT_COMMIT=\\\"$$git_commit\\\"
DEFINES += CHATTERINO_GIT_RELEASE=\\\"$$git_release\\\"
DEFINES += CHATTERINO_GIT_HASH=\\\"$$git_hash\\\"
DEFINES += CHATTERINO_GIT_HASH=\\\"$$git_hash\\\"

View file

@ -3,7 +3,7 @@ Below I have tried to list all environment variables that can be used to modify
### CHATTERINO2_RECENT_MESSAGES_URL
Used to change the URL that Chatterino2 uses when trying to load historic Twitch chat messages (if the setting is enabled).
Default value: `https://recent-messages.robotty.de/api/v2/recent-messages/%1?clearchatToNotice=true`
Default value: `https://recent-messages.robotty.de/api/v2/recent-messages/%1?clearchatToNotice=true` (an [open-source service](https://github.com/robotty/recent-messages) written and currently run by [@RAnders00](https://github.com/RAnders00))
Arguments:
- `%1` = Name of the Twitch channel

View file

@ -98,12 +98,12 @@ void AB_SETTINGS_CLASS::restoreSnapshot()
float AB_SETTINGS_CLASS::getClampedUiScale() const
{
return clamp<float>(this->uiScale.getValue(), 0.1, 10);
return clamp<float>(this->uiScale.getValue(), 0.2f, 10);
}
void AB_SETTINGS_CLASS::setClampedUiScale(float value)
{
this->uiScale.setValue(clamp<float>(value, 0.1, 10));
this->uiScale.setValue(clamp<float>(value, 0.2f, 10));
}
#ifndef AB_CUSTOM_SETTINGS

View file

@ -161,6 +161,8 @@ Fonts::FontData Fonts::createFontData(FontStyle type, float scale)
{FontStyle::Tiny, {8, "Monospace", false, QFont::Normal}},
{FontStyle::UiMedium,
{int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}},
{FontStyle::UiMediumBold,
{int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Bold}},
{FontStyle::UiTabs,
{int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}},
};

View file

@ -28,6 +28,7 @@ enum class FontStyle : uint8_t {
ChatVeryLarge,
UiMedium,
UiMediumBold,
UiTabs,
// don't remove this value

View file

@ -61,7 +61,12 @@ BaseWindow::BaseWindow(QWidget *parent, Flags _flags)
this->init();
getSettings()->uiScale.connect(
[this]() { postToThread([this] { this->updateScale(); }); },
[this]() {
postToThread([this] {
this->updateScale();
this->updateScale();
});
},
this->connections_);
this->updateScale();
@ -72,6 +77,30 @@ BaseWindow::BaseWindow(QWidget *parent, Flags _flags)
// QTimer::this->scaleChangedEvent(this->getScale());
this->resize(300, 150);
#ifdef USEWINSDK
this->useNextBounds_.setSingleShot(true);
QObject::connect(&this->useNextBounds_, &QTimer::timeout, this,
[this]() { this->currentBounds_ = this->nextBounds_; });
#endif
}
void BaseWindow::setInitialBounds(const QRect &bounds)
{
#ifdef USEWINSDK
this->initalBounds_ = bounds;
#else
this->setGeometry(bounds);
#endif
}
QRect BaseWindow::getBounds()
{
#ifdef USEWINSDK
return this->currentBounds_;
#else
return this->geometry();
#endif
}
float BaseWindow::scale() const
@ -574,6 +603,11 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message,
returnValue = this->handleSIZE(msg);
break;
case WM_MOVE:
returnValue = this->handleMOVE(msg);
*result = 0;
break;
case WM_NCHITTEST:
returnValue = this->handleNCHITTEST(msg, result);
break;
@ -704,12 +738,23 @@ bool BaseWindow::handleSHOWWINDOW(MSG *msg)
this->updateScale();
}
if (!this->shown_ && this->isVisible() && this->hasCustomWindowFrame())
if (!this->shown_ && this->isVisible())
{
this->shown_ = true;
if (this->hasCustomWindowFrame())
{
this->shown_ = true;
const MARGINS shadow = {8, 8, 8, 8};
DwmExtendFrameIntoClientArea(HWND(this->winId()), &shadow);
const MARGINS shadow = {8, 8, 8, 8};
DwmExtendFrameIntoClientArea(HWND(this->winId()), &shadow);
}
if (!this->initalBounds_.isNull())
{
::SetWindowPos(msg->hwnd, nullptr, this->initalBounds_.x(),
this->initalBounds_.y(), this->initalBounds_.width(),
this->initalBounds_.height(),
SWP_NOZORDER | SWP_NOACTIVATE);
this->currentBounds_ = this->initalBounds_;
}
}
this->calcButtonsSizes();
@ -770,6 +815,18 @@ bool BaseWindow::handleSIZE(MSG *msg)
{
this->ui_.windowLayout->setContentsMargins(0, 1, 0, 0);
}
this->isNotMinimizedOrMaximized_ = msg->wParam == SIZE_RESTORED;
if (this->isNotMinimizedOrMaximized_)
{
RECT rect;
::GetWindowRect(msg->hwnd, &rect);
this->currentBounds_ =
QRect(QPoint(rect.left, rect.top),
QPoint(rect.right - 1, rect.bottom - 1));
}
this->useNextBounds_.stop();
}
}
return false;
@ -778,6 +835,22 @@ bool BaseWindow::handleSIZE(MSG *msg)
#endif
}
bool BaseWindow::handleMOVE(MSG *msg)
{
#ifdef USEWINSDK
if (this->isNotMinimizedOrMaximized_)
{
RECT rect;
::GetWindowRect(msg->hwnd, &rect);
this->nextBounds_ = QRect(QPoint(rect.left, rect.top),
QPoint(rect.right - 1, rect.bottom - 1));
this->useNextBounds_.start(10);
}
#endif
return false;
}
bool BaseWindow::handleNCHITTEST(MSG *msg, long *result)
{
#ifdef USEWINSDK

View file

@ -34,6 +34,9 @@ public:
explicit BaseWindow(QWidget *parent = nullptr, Flags flags_ = None);
void setInitialBounds(const QRect &bounds);
QRect getBounds();
QWidget *getLayoutContainer();
bool hasCustomWindowFrame();
TitleBarButton *addTitleBarButton(const TitleBarButtonStyle &style,
@ -95,6 +98,7 @@ private:
bool handleSHOWWINDOW(MSG *msg);
bool handleNCCALCSIZE(MSG *msg, long *result);
bool handleSIZE(MSG *msg);
bool handleMOVE(MSG *msg);
bool handleNCHITTEST(MSG *msg, long *result);
bool enableCustomFrame_;
@ -116,6 +120,14 @@ private:
std::vector<Button *> buttons;
} ui_;
#ifdef USEWINSDK
QRect initalBounds_;
QRect currentBounds_;
QRect nextBounds_;
QTimer useNextBounds_;
bool isNotMinimizedOrMaximized_{};
#endif
pajlada::Signals::SignalHolder connections_;
std::vector<pajlada::Signals::ScopedConnection> managedConnections_;

View file

@ -100,6 +100,8 @@ void Label::paintEvent(QPaintEvent *)
? Qt::AlignLeft | Qt::AlignVCenter
: Qt::AlignCenter;
painter.setBrush(this->palette().windowText());
QTextOption option(alignment);
option.setWrapMode(QTextOption::NoWrap);
painter.drawText(textRect, this->text_, option);

View file

@ -38,14 +38,14 @@ const QPixmap &Button::getPixmap() const
return this->pixmap_;
}
void Button::setDim(bool value)
void Button::setDim(Dim value)
{
this->dimPixmap_ = value;
this->update();
}
bool Button::getDim() const
Button::Dim Button::getDim() const
{
return this->dimPixmap_;
}
@ -76,7 +76,12 @@ bool Button::getEnableMargin() const
qreal Button::getCurrentDimAmount() const
{
return this->dimPixmap_ && !this->mouseOver_ ? 0.7 : 1;
if (this->dimPixmap_ == Dim::None || this->mouseOver_)
return 1;
else if (this->dimPixmap_ == Dim::Some)
return 0.7;
else
return 0.15;
}
void Button::setBorderColor(const QColor &color)
@ -114,13 +119,13 @@ void Button::paintEvent(QPaintEvent *)
if (!this->pixmap_.isNull())
{
if (!this->mouseOver_ && this->dimPixmap_ && this->enabled_)
{
painter.setOpacity(this->getCurrentDimAmount());
}
painter.setOpacity(this->getCurrentDimAmount());
QRect rect = this->rect();
int s = this->enableMargin_ ? int(6 * this->scale()) : 0;
int margin = this->height() < 22 * this->scale() ? 3 : 6;
int s = this->enableMargin_ ? int(margin * this->scale()) : 0;
rect.moveLeft(s);
rect.setRight(rect.right() - s - s);

View file

@ -28,14 +28,16 @@ class Button : public BaseWidget
};
public:
enum class Dim { None, Some, Lots };
Button(BaseWidget *parent = nullptr);
void setMouseEffectColor(boost::optional<QColor> color);
void setPixmap(const QPixmap &pixmap_);
const QPixmap &getPixmap() const;
void setDim(bool value);
bool getDim() const;
void setDim(Dim value);
Dim getDim() const;
qreal getCurrentDimAmount() const;
void setEnable(bool value);
@ -76,7 +78,7 @@ private:
QColor borderColor_{};
QPixmap pixmap_{};
bool dimPixmap_{true};
Dim dimPixmap_{Dim::Some};
bool enableMargin_{true};
QPoint mousePos_{};
double hoverMultiplier_{0.0};

View file

@ -12,7 +12,8 @@ header_header = \
namespace chatterino {
class Resources2 : public Singleton {
class Resources2 : public Singleton
{
public:
Resources2();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,13 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<!-- Created using Krita: http://krita.org -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:krita="http://krita.org/namespaces/svg/krita"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
width="368.64pt"
height="368.64pt"
viewBox="0 0 368.64 368.64">
<defs/>
<path id="shape0" transform="translate(0.477297082745412, 59.3121166947556)" fill="#ffffff" fill-rule="evenodd" stroke="#000000" stroke-opacity="0" stroke-width="1" stroke-linecap="square" stroke-linejoin="bevel" d="M42.034 0.254558L43.0522 255.449L43.6262 256.827L44.1855 258.216L44.7441 259.606L45.3161 260.985L45.9154 262.341L46.5562 263.662L47.2523 264.937L48.018 266.154L48.867 267.3L49.8136 268.365L50.8717 269.336L52.0553 270.202L53.3785 270.951L54.8553 271.571L56.4997 272.05L58.3257 272.378L268.973 272.505L269.227 294.397L268.9 295.844L268.498 297.271L268.017 298.668L267.453 300.026L266.799 301.334L266.053 302.584L265.207 303.766L264.259 304.87L263.202 305.887L262.031 306.807L260.742 307.62L259.33 308.318L257.791 308.89L256.118 309.328L13.2689 309.328L11.4859 308.872L9.86178 308.304L8.38902 307.628L7.05996 306.85L5.86701 305.976L4.80256 305.011L3.85899 303.959L3.02871 302.828L2.3041 301.621L1.67756 300.345L1.14149 299.004L0.68828 297.605L0.310317 296.152L0 294.651L0.310203 14.5104L0.847067 13.1388L1.38231 11.7663L1.93671 10.4058L2.53105 9.0706L3.18609 7.77381L3.92263 6.52858L4.76143 5.34805L5.72327 4.24536L6.82893 3.23365L8.09918 2.32608L9.55481 1.53579L11.2166 0.87591L13.1053 0.3596L15.2417 0L42.034 0.254558"/><path id="shape1" transform="translate(61.0200045914134, 1.04007141771575)" fill="#f2f2f2" fill-rule="evenodd" d="M270.415 114.679L270.415 288.287L269.98 289.646L269.61 291.035L269.286 292.446L268.992 293.865L268.712 295.284L268.428 296.691L268.123 298.076L267.781 299.428L267.386 300.735L266.919 301.989L266.364 303.177L265.704 304.288L264.923 305.314L264.003 306.241L262.928 307.061L261.681 307.762L260.244 308.333L258.602 308.764L256.737 309.043L254.632 309.161L16.8749 309.798L14.0545 309.696L11.5994 309.354L9.48192 308.797L7.67441 308.049L6.14912 307.134L4.87835 306.076L3.8344 304.9L2.98956 303.63L2.31611 302.29L1.78637 300.905L1.37262 299.499L1.04715 298.095L0.78226 296.719L0.550247 295.394L0.323402 294.145L0.0740195 292.997L0 16.0599L0.288877 13.6625L0.719498 11.5587L1.28146 9.72701L1.96438 8.14583L2.75785 6.79365L3.65147 5.64893L4.63485 4.69013L5.69758 3.89572L6.82928 3.24416L8.01954 2.7139L9.25797 2.28343L10.5342 1.93118L11.8377 1.63564L13.1583 1.37525L14.4854 1.12849L15.8087 0.87381L17.1178 0.58968L18.4022 0.254558L138.172 0L138.045 97.4959L138.594 99.0696L139.153 100.628L139.731 102.16L140.334 103.656L140.97 105.104L141.646 106.495L142.369 107.817L143.147 109.06L143.987 110.213L144.896 111.266L145.883 112.208L146.954 113.028L148.116 113.716L149.377 114.261L150.744 114.653L152.225 114.88L153.827 114.933L191.897 114.85L249.265 114.725L270.415 114.679"/><path id="shape2" transform="translate(210.201627880834, 1.9091882641311)" fill="#ffffff" fill-rule="evenodd" d="M0.0636396 0L121.743 103.223L14.828 103.478C6.29306 101.355 0.80087 96.9823 0.0636396 89.0955C-0.0212132 88.5863 -0.0212132 58.8879 0.0636396 0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,004 B

View file

@ -0,0 +1,13 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<!-- Created using Krita: http://krita.org -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:krita="http://krita.org/namespaces/svg/krita"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
width="368.64pt"
height="368.64pt"
viewBox="0 0 368.64 368.64">
<defs/>
<path id="shape0" transform="translate(0.477297082745412, 59.3121166947556)" fill="#000000" fill-rule="evenodd" d="M42.034 0.254558L43.0522 255.449L43.6262 256.827L44.1855 258.216L44.7441 259.606L45.3161 260.985L45.9154 262.341L46.5562 263.662L47.2523 264.937L48.018 266.154L48.867 267.3L49.8136 268.365L50.8717 269.336L52.0553 270.202L53.3785 270.951L54.8553 271.571L56.4997 272.05L58.3257 272.378L268.973 272.505L269.227 294.397L268.9 295.844L268.498 297.271L268.017 298.668L267.453 300.026L266.799 301.334L266.053 302.584L265.207 303.766L264.259 304.87L263.202 305.887L262.031 306.807L260.742 307.62L259.33 308.318L257.791 308.89L256.118 309.328L13.2689 309.328L11.4859 308.872L9.86178 308.304L8.38902 307.628L7.05996 306.85L5.86701 305.976L4.80256 305.011L3.85899 303.959L3.02871 302.828L2.3041 301.621L1.67756 300.345L1.14149 299.004L0.68828 297.605L0.310317 296.152L0 294.651L0.310203 14.5104L0.847067 13.1388L1.38231 11.7663L1.93671 10.4058L2.53105 9.0706L3.18609 7.77381L3.92263 6.52858L4.76143 5.34805L5.72327 4.24536L6.82893 3.23365L8.09918 2.32608L9.55481 1.53579L11.2166 0.87591L13.1053 0.3596L15.2417 0L42.034 0.254558"/><path id="shape1" transform="translate(61.0200045914134, 1.04007141771575)" fill="#000000" fill-rule="evenodd" d="M270.415 114.679L270.415 288.287L269.98 289.646L269.61 291.035L269.286 292.446L268.992 293.865L268.712 295.284L268.428 296.691L268.123 298.076L267.781 299.428L267.386 300.735L266.919 301.989L266.364 303.177L265.704 304.288L264.923 305.314L264.003 306.241L262.928 307.061L261.681 307.762L260.244 308.333L258.602 308.764L256.737 309.043L254.632 309.161L16.8749 309.798L14.0545 309.696L11.5994 309.354L9.48192 308.797L7.67441 308.049L6.14912 307.134L4.87835 306.076L3.8344 304.9L2.98956 303.63L2.31611 302.29L1.78637 300.905L1.37262 299.499L1.04715 298.095L0.78226 296.719L0.550247 295.394L0.323402 294.145L0.0740195 292.997L0 16.0599L0.288877 13.6625L0.719498 11.5587L1.28146 9.72701L1.96438 8.14583L2.75785 6.79365L3.65147 5.64893L4.63485 4.69013L5.69758 3.89572L6.82928 3.24416L8.01954 2.7139L9.25797 2.28343L10.5342 1.93118L11.8377 1.63564L13.1583 1.37525L14.4854 1.12849L15.8087 0.87381L17.1178 0.58968L18.4022 0.254558L138.172 0L138.045 97.4959L138.594 99.0696L139.153 100.628L139.731 102.16L140.334 103.656L140.97 105.104L141.646 106.495L142.369 107.817L143.147 109.06L143.987 110.213L144.896 111.266L145.883 112.208L146.954 113.028L148.116 113.716L149.377 114.261L150.744 114.653L152.225 114.88L153.827 114.933L191.897 114.85L249.265 114.725L270.415 114.679"/><path id="shape2" transform="translate(210.201627880834, 1.9091882641311)" fill="#000000" fill-rule="evenodd" d="M0.0636396 0L121.743 103.223L14.828 103.478C6.29306 101.355 0.80087 96.9823 0.0636396 89.0955C-0.0212132 88.5863 -0.0212132 58.8879 0.0636396 0Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -18,10 +18,10 @@ def isNotIgnored(file):
return file.as_posix() not in ignored_files
all_files = list(filter(isNotIgnored, \
filter(Path.is_file, Path('.').glob('**/*'))))
image_files = list(filter(isNotIgnored, \
filter(Path.is_file, Path('.').glob('**/*.png'))))
all_files = sorted(list(filter(isNotIgnored, \
filter(Path.is_file, Path('.').glob('**/*')))))
image_files = sorted(list(filter(isNotIgnored, \
filter(Path.is_file, Path('.').glob('**/*.png')))))
with open('./resources_autogenerated.qrc', 'w') as out:
out.write(resources_header)

View file

@ -8,7 +8,7 @@ QCheckBox::indicator {
}
chatterino--ComboBox {
width: 100px;
width: 120px;
}
QScrollArea {
@ -27,19 +27,19 @@ QComboBox QFrame {
chatterino--SettingsPage {
background: #222;
border: 1px solid #555;
/*border: 1px solid #555;
border-left: none;*/
}
chatterino--PageHeader {
margin-bottom: 12px;
}
chatterino--TitleLabel {
font-family: "Segoe UI light";
font-size: 24px;
color: #4FC3F7;
}
chatterino--TitleLabel2 {
font-family: "Segoe UI light";
font-size: 24px;
color: #bbb;
margin-top: 16px;
}
chatterino--DescriptionLabel {

View file

@ -1,19 +1,16 @@
<RCC>
<qresource prefix="/"> <file>chatterino.icns</file>
<file>contributors.txt</file>
<file>emoji.json</file>
<file>emojidata.txt</file>
<file>error.png</file>
<file>icon.ico</file>
<file>icon.png</file>
<file>pajaDank.png</file>
<file>tlds.txt</file>
<qresource prefix="/"> <file>.gitignore</file>
<file>avatars/fourtf.png</file>
<file>avatars/pajlada.png</file>
<file>buttons/addSplit.png</file>
<file>buttons/addSplitDark.png</file>
<file>buttons/ban.png</file>
<file>buttons/banRed.png</file>
<file>buttons/copyDark.png</file>
<file>buttons/copyDark.svg</file>
<file>buttons/copyDarkTheme.png</file>
<file>buttons/copyLight.png</file>
<file>buttons/copyLight.svg</file>
<file>buttons/emote.svg</file>
<file>buttons/emoteDark.svg</file>
<file>buttons/menuDark.png</file>
@ -23,14 +20,24 @@
<file>buttons/modModeDisabled2.png</file>
<file>buttons/modModeEnabled.png</file>
<file>buttons/modModeEnabled2.png</file>
<file>buttons/search.png</file>
<file>buttons/timeout.png</file>
<file>buttons/trashCan.png</file>
<file>buttons/trashcan.svg</file>
<file>buttons/unban.png</file>
<file>buttons/unmod.png</file>
<file>buttons/update.png</file>
<file>buttons/updateError.png</file>
<file>chatterino.desktop</file>
<file>chatterino.icns</file>
<file>contributors.txt</file>
<file>emoji.json</file>
<file>emojidata.txt</file>
<file>error.png</file>
<file>examples/moving.gif</file>
<file>examples/splitting.gif</file>
<file>icon.ico</file>
<file>icon.png</file>
<file>licenses/boost_boost.txt</file>
<file>licenses/emoji-data-source.txt</file>
<file>licenses/fmt_bsd2.txt</file>
@ -41,6 +48,7 @@
<file>licenses/qt_lgpl-3.0.txt</file>
<file>licenses/rapidjson.txt</file>
<file>licenses/websocketpp.txt</file>
<file>pajaDank.png</file>
<file>qss/settings.qss</file>
<file>settings/about.svg</file>
<file>settings/aboutlogo.png</file>
@ -63,6 +71,7 @@
<file>split/move.png</file>
<file>split/right.png</file>
<file>split/up.png</file>
<file>tlds.txt</file>
<file>twitch/admin.png</file>
<file>twitch/automod.png</file>
<file>twitch/broadcaster.png</file>
@ -76,4 +85,4 @@
<file>twitch/verified.png</file>
<file>twitch/vip.png</file>
</qresource>
</RCC>
</RCC>

View file

@ -24,6 +24,7 @@
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "singletons/Toasts.hpp"
#include "singletons/Updates.hpp"
#include "singletons/WindowManager.hpp"
#include "util/IsBigEndian.hpp"
#include "util/PostToThread.hpp"
@ -99,6 +100,9 @@ int Application::run(QApplication &qtApp)
this->windows->getMainWindow().show();
getSettings()->betaUpdates.connect(
[] { Updates::getInstance().checkForUpdates(); }, false);
return qtApp.exec();
}

10
src/ForwardDecl.hpp Normal file
View file

@ -0,0 +1,10 @@
#pragma once
namespace chatterino {
class Channel;
class ChannelView;
using ChannelPtr = std::shared_ptr<Channel>;
struct Message;
using MessagePtr = std::shared_ptr<const Message>;
} // namespace chatterino

View file

@ -1,3 +1,5 @@
#pragma once
#ifdef __cplusplus
# include <fmt/format.h>
# include <irccommand.h>

View file

@ -10,6 +10,9 @@ Resources2::Resources2()
this->buttons.addSplitDark = QPixmap(":/buttons/addSplitDark.png");
this->buttons.ban = QPixmap(":/buttons/ban.png");
this->buttons.banRed = QPixmap(":/buttons/banRed.png");
this->buttons.copyDark = QPixmap(":/buttons/copyDark.png");
this->buttons.copyDarkTheme = QPixmap(":/buttons/copyDarkTheme.png");
this->buttons.copyLight = QPixmap(":/buttons/copyLight.png");
this->buttons.menuDark = QPixmap(":/buttons/menuDark.png");
this->buttons.menuLight = QPixmap(":/buttons/menuLight.png");
this->buttons.mod = QPixmap(":/buttons/mod.png");
@ -17,6 +20,7 @@ Resources2::Resources2()
this->buttons.modModeDisabled2 = QPixmap(":/buttons/modModeDisabled2.png");
this->buttons.modModeEnabled = QPixmap(":/buttons/modModeEnabled.png");
this->buttons.modModeEnabled2 = QPixmap(":/buttons/modModeEnabled2.png");
this->buttons.search = QPixmap(":/buttons/search.png");
this->buttons.timeout = QPixmap(":/buttons/timeout.png");
this->buttons.trashCan = QPixmap(":/buttons/trashCan.png");
this->buttons.unban = QPixmap(":/buttons/unban.png");
@ -46,4 +50,4 @@ Resources2::Resources2()
this->twitch.vip = QPixmap(":/twitch/vip.png");
}
} // namespace chatterino
} // namespace chatterino

View file

@ -17,6 +17,9 @@ public:
QPixmap addSplitDark;
QPixmap ban;
QPixmap banRed;
QPixmap copyDark;
QPixmap copyDarkTheme;
QPixmap copyLight;
QPixmap menuDark;
QPixmap menuLight;
QPixmap mod;
@ -24,6 +27,7 @@ public:
QPixmap modModeDisabled2;
QPixmap modModeEnabled;
QPixmap modModeEnabled2;
QPixmap search;
QPixmap timeout;
QPixmap trashCan;
QPixmap unban;
@ -60,4 +64,4 @@ public:
} twitch;
};
} // namespace chatterino
} // namespace chatterino

View file

@ -24,6 +24,17 @@ QJsonObject NetworkResult::parseJson() const
return jsonDoc.object();
}
QJsonArray NetworkResult::parseJsonArray() const
{
QJsonDocument jsonDoc(QJsonDocument::fromJson(this->data_));
if (jsonDoc.isNull())
{
return QJsonArray{};
}
return jsonDoc.array();
}
rapidjson::Document NetworkResult::parseRapidJson() const
{
rapidjson::Document ret(rapidjson::kObjectType);

View file

@ -10,7 +10,13 @@ class NetworkResult
public:
NetworkResult(const QByteArray &data);
/// Parses the result as json and returns the root as an object.
/// Returns empty object if parsing failed.
QJsonObject parseJson() const;
/// Parses the result as json and returns the root as an array.
/// Returns empty object if parsing failed.
QJsonArray parseJsonArray() const;
/// Parses the result as json and returns the document.
rapidjson::Document parseRapidJson() const;
const QByteArray &getData() const;

View file

@ -121,7 +121,7 @@ public:
return this->vector_;
}
const std::vector<TVectorItem> cloneVector() const
std::vector<TVectorItem> cloneVector() const
{
std::shared_lock lock(this->mutex_);

View file

@ -2,7 +2,7 @@
#include <QtGlobal>
#define CHATTERINO_VERSION "2.1.2"
#define CHATTERINO_VERSION "2.1.4-beta-1"
#if defined(Q_OS_WIN)
# define CHATTERINO_OS "win"

View file

@ -8,7 +8,7 @@ namespace chatterino {
// commandmodel
HighlightModel::HighlightModel(QObject *parent)
: SignalVectorModel<HighlightPhrase>(4, parent)
: SignalVectorModel<HighlightPhrase>(5, parent)
{
}
@ -16,12 +16,13 @@ HighlightModel::HighlightModel(QObject *parent)
HighlightPhrase HighlightModel::getItemFromRow(
std::vector<QStandardItem *> &row, const HighlightPhrase &original)
{
// key, alert, sound, regex
// key, alert, sound, regex, case-sensitivity
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[3]->data(Qt::CheckStateRole).toBool(),
row[4]->data(Qt::CheckStateRole).toBool()};
}
// turns a row in the model into a vector item
@ -32,6 +33,7 @@ void HighlightModel::getRowFromItem(const HighlightPhrase &item,
setBoolItem(row[1], item.getAlert());
setBoolItem(row[2], item.getSound());
setBoolItem(row[3], item.isRegex());
setBoolItem(row[4], item.isCaseSensitive());
}
void HighlightModel::afterInit()

View file

@ -15,21 +15,24 @@ public:
bool operator==(const HighlightPhrase &other) const
{
return std::tie(this->pattern_, this->sound_, this->alert_,
this->isRegex_) == std::tie(other.pattern_,
other.sound_, other.alert_,
other.isRegex_);
this->isRegex_, this->caseSensitive_) ==
std::tie(other.pattern_, other.sound_, other.alert_,
other.isRegex_, other.caseSensitive_);
}
HighlightPhrase(const QString &pattern, bool alert, bool sound,
bool isRegex)
bool isRegex, bool caseSensitive)
: pattern_(pattern)
, alert_(alert)
, sound_(sound)
, isRegex_(isRegex)
, regex_(isRegex_ ? pattern
: "\\b" + QRegularExpression::escape(pattern) + "\\b",
QRegularExpression::CaseInsensitiveOption |
QRegularExpression::UseUnicodePropertiesOption)
, caseSensitive_(caseSensitive)
, regex_(
isRegex_ ? pattern
: "\\b" + QRegularExpression::escape(pattern) + "\\b",
QRegularExpression::UseUnicodePropertiesOption |
(caseSensitive_ ? QRegularExpression::NoPatternOption
: QRegularExpression::CaseInsensitiveOption))
{
}
@ -60,11 +63,17 @@ public:
return this->isValid() && this->regex_.match(subject).hasMatch();
}
bool isCaseSensitive() const
{
return this->caseSensitive_;
}
private:
QString pattern_;
bool alert_;
bool sound_;
bool isRegex_;
bool caseSensitive_;
QRegularExpression regex_;
};
} // namespace chatterino
@ -82,6 +91,7 @@ struct Serialize<chatterino::HighlightPhrase> {
chatterino::rj::set(ret, "alert", value.getAlert(), a);
chatterino::rj::set(ret, "sound", value.getSound(), a);
chatterino::rj::set(ret, "regex", value.isRegex(), a);
chatterino::rj::set(ret, "case", value.isCaseSensitive(), a);
return ret;
}
@ -93,20 +103,24 @@ struct Deserialize<chatterino::HighlightPhrase> {
{
if (!value.IsObject())
{
return chatterino::HighlightPhrase(QString(), true, false, false);
return chatterino::HighlightPhrase(QString(), true, false, false,
false);
}
QString _pattern;
bool _alert = true;
bool _sound = false;
bool _isRegex = false;
bool _caseSensitive = false;
chatterino::rj::getSafe(value, "pattern", _pattern);
chatterino::rj::getSafe(value, "alert", _alert);
chatterino::rj::getSafe(value, "sound", _sound);
chatterino::rj::getSafe(value, "regex", _isRegex);
chatterino::rj::getSafe(value, "case", _caseSensitive);
return chatterino::HighlightPhrase(_pattern, _alert, _sound, _isRegex);
return chatterino::HighlightPhrase(_pattern, _alert, _sound,
_isRegex, _caseSensitive);
}
};

View file

@ -8,7 +8,7 @@ namespace chatterino {
// commandmodel
UserHighlightModel::UserHighlightModel(QObject *parent)
: SignalVectorModel<HighlightPhrase>(4, parent)
: SignalVectorModel<HighlightPhrase>(5, parent)
{
}
@ -21,7 +21,8 @@ HighlightPhrase UserHighlightModel::getItemFromRow(
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[3]->data(Qt::CheckStateRole).toBool(),
row[4]->data(Qt::CheckStateRole).toBool()};
}
// row into vector item
@ -32,6 +33,7 @@ void UserHighlightModel::getRowFromItem(const HighlightPhrase &item,
setBoolItem(row[1], item.getAlert());
setBoolItem(row[2], item.getSound());
setBoolItem(row[3], item.isRegex());
setBoolItem(row[4], item.isCaseSensitive());
}
} // namespace chatterino

View file

@ -22,7 +22,7 @@ public:
{
}
std::size_t size()
std::size_t size() const
{
return this->length_;
}

View file

@ -145,7 +145,7 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container,
QSize(int(container.getScale() * image->width() * emoteScale),
int(container.getScale() * image->height() * emoteScale));
container.addElement((new ImageLayoutElement(*this, image, size))
container.addElement(this->makeImageLayoutElement(image, size)
->setLink(this->getLink()));
}
else
@ -159,6 +159,27 @@ void EmoteElement::addToContainer(MessageLayoutContainer &container,
}
}
MessageLayoutElement *EmoteElement::makeImageLayoutElement(
const ImagePtr &image, const QSize &size)
{
return new ImageLayoutElement(*this, image, size);
}
// MOD BADGE
ModBadgeElement::ModBadgeElement(const EmotePtr &data,
MessageElementFlags flags_)
: EmoteElement(data, flags_)
{
}
MessageLayoutElement *ModBadgeElement::makeImageLayoutElement(
const ImagePtr &image, const QSize &size)
{
static const QColor modBadgeBackgroundColor("#34AE0A");
return new ImageWithBackgroundLayoutElement(*this, image, size,
modBadgeBackgroundColor);
}
// BADGE
BadgeElement::BadgeElement(const EmotePtr &emote, MessageElementFlags flags)
: MessageElement(flags)

View file

@ -17,6 +17,7 @@
namespace chatterino {
class Channel;
struct MessageLayoutContainer;
class MessageLayoutElement;
class Image;
using ImagePtr = std::shared_ptr<Image>;
@ -209,11 +210,26 @@ public:
MessageElementFlags flags_) override;
EmotePtr getEmote() const;
protected:
virtual MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
const QSize &size);
private:
std::unique_ptr<TextElement> textElement_;
EmotePtr emote_;
};
// Behaves like an emote element, except it creates a different image layout element that draws the mod badge background
class ModBadgeElement : public EmoteElement
{
public:
ModBadgeElement(const EmotePtr &data, MessageElementFlags flags_);
protected:
MessageLayoutElement *makeImageLayoutElement(const ImagePtr &image,
const QSize &size) override;
};
class BadgeElement : public MessageElement
{
public:

View file

@ -168,6 +168,33 @@ int ImageLayoutElement::getXFromIndex(int index)
}
}
//
// IMAGE WITH BACKGROUND
//
ImageWithBackgroundLayoutElement::ImageWithBackgroundLayoutElement(
MessageElement &creator, ImagePtr image, const QSize &size, QColor color)
: ImageLayoutElement(creator, image, size)
, color_(color)
{
}
void ImageWithBackgroundLayoutElement::paint(QPainter &painter)
{
if (this->image_ == nullptr)
{
return;
}
auto pixmap = this->image_->pixmapOrLoad();
if (pixmap && !this->image_->animated())
{
painter.fillRect(QRectF(this->getRect()), this->color_);
// fourtf: make it use qreal values
painter.drawPixmap(QRectF(this->getRect()), *pixmap, QRectF());
}
}
//
// TEXT
//

View file

@ -72,10 +72,22 @@ protected:
int getMouseOverIndex(const QPoint &abs) const override;
int getXFromIndex(int index) override;
private:
ImagePtr image_;
};
class ImageWithBackgroundLayoutElement : public ImageLayoutElement
{
public:
ImageWithBackgroundLayoutElement(MessageElement &creator, ImagePtr image,
const QSize &size, QColor color);
protected:
void paint(QPainter &painter) override;
private:
QColor color_;
};
// TEXT
class TextLayoutElement : public MessageLayoutElement
{

View file

@ -21,13 +21,25 @@ namespace {
return {urlTemplate.replace("{{id}}", id.string)
.replace("{{image}}", emoteScale)};
}
Url getEmoteLinkV3(const EmoteId &id, const QString &emoteScale)
{
static const QString urlTemplate(
"https://cdn.betterttv.net/emote/%1/%2");
return {urlTemplate.arg(id.string, emoteScale)};
}
EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id)
{
static std::unordered_map<EmoteId, std::weak_ptr<const Emote>> cache;
static std::mutex mutex;
return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id);
}
std::pair<Outcome, EmoteMap> parseGlobalEmotes(
const QJsonObject &jsonRoot, const EmoteMap &currentEmotes)
const QJsonArray &jsonEmotes, const EmoteMap &currentEmotes)
{
auto emotes = EmoteMap();
auto jsonEmotes = jsonRoot.value("emotes").toArray();
auto urlTemplate =
qS("https:") + jsonRoot.value("urlTemplate").toString();
for (auto jsonEmote : jsonEmotes)
{
@ -37,10 +49,9 @@ namespace {
auto emote = Emote(
{name,
ImageSet{
Image::fromUrl(getEmoteLink(urlTemplate, id, "1x"), 1),
Image::fromUrl(getEmoteLink(urlTemplate, id, "2x"), 0.5),
Image::fromUrl(getEmoteLink(urlTemplate, id, "3x"), 0.25)},
ImageSet{Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25)},
Tooltip{name.string + "<br />Global BetterTTV Emote"},
Url{"https://manage.betterttv.net/emotes/" + id.string}});
@ -50,38 +61,36 @@ namespace {
return {Success, std::move(emotes)};
}
EmotePtr cachedOrMake(Emote &&emote, const EmoteId &id)
{
static std::unordered_map<EmoteId, std::weak_ptr<const Emote>> cache;
static std::mutex mutex;
return cachedOrMakeEmotePtr(std::move(emote), cache, mutex, id);
}
std::pair<Outcome, EmoteMap> parseChannelEmotes(const QJsonObject &jsonRoot)
{
auto emotes = EmoteMap();
auto jsonEmotes = jsonRoot.value("emotes").toArray();
auto urlTemplate = "https:" + jsonRoot.value("urlTemplate").toString();
for (auto jsonEmote_ : jsonEmotes)
{
auto jsonEmote = jsonEmote_.toObject();
auto innerParse = [&jsonRoot, &emotes](const char *key) {
auto jsonEmotes = jsonRoot.value(key).toArray();
for (auto jsonEmote_ : jsonEmotes)
{
auto jsonEmote = jsonEmote_.toObject();
auto id = EmoteId{jsonEmote.value("id").toString()};
auto name = EmoteName{jsonEmote.value("code").toString()};
// emoteObject.value("imageType").toString();
auto id = EmoteId{jsonEmote.value("id").toString()};
auto name = EmoteName{jsonEmote.value("code").toString()};
// emoteObject.value("imageType").toString();
auto emote = Emote(
{name,
ImageSet{
Image::fromUrl(getEmoteLink(urlTemplate, id, "1x"), 1),
Image::fromUrl(getEmoteLink(urlTemplate, id, "2x"), 0.5),
Image::fromUrl(getEmoteLink(urlTemplate, id, "3x"), 0.25)},
Tooltip{name.string + "<br />Channel BetterTTV Emote"},
Url{"https://manage.betterttv.net/emotes/" + id.string}});
auto emote = Emote(
{name,
ImageSet{
Image::fromUrl(getEmoteLinkV3(id, "1x"), 1),
Image::fromUrl(getEmoteLinkV3(id, "2x"), 0.5),
Image::fromUrl(getEmoteLinkV3(id, "3x"), 0.25),
},
Tooltip{name.string + "<br />Channel BetterTTV Emote"},
Url{"https://manage.betterttv.net/emotes/" + id.string}});
emotes[name] = cachedOrMake(std::move(emote), id);
}
emotes[name] = cachedOrMake(std::move(emote), id);
}
};
innerParse("channelEmotes");
innerParse("sharedEmotes");
return {Success, std::move(emotes)};
}
@ -116,7 +125,7 @@ void BttvEmotes::loadEmotes()
.timeout(30000)
.onSuccess([this](auto result) -> Outcome {
auto emotes = this->global_.get();
auto pair = parseGlobalEmotes(result.parseJson(), *emotes);
auto pair = parseGlobalEmotes(result.parseJsonArray(), *emotes);
if (pair.first)
this->global_.set(
std::make_shared<EmoteMap>(std::move(pair.second)));
@ -125,10 +134,10 @@ void BttvEmotes::loadEmotes()
.execute();
}
void BttvEmotes::loadChannel(const QString &channelName,
void BttvEmotes::loadChannel(const QString &channelId,
std::function<void(EmoteMap &&)> callback)
{
NetworkRequest(QString(bttvChannelEmoteApiUrl) + channelName)
NetworkRequest(QString(bttvChannelEmoteApiUrl) + channelId)
.timeout(3000)
.onSuccess([callback = std::move(callback)](auto result) -> Outcome {
auto pair = parseChannelEmotes(result.parseJson());

View file

@ -14,9 +14,9 @@ class EmoteMap;
class BttvEmotes final
{
static constexpr const char *globalEmoteApiUrl =
"https://api.betterttv.net/2/emotes";
"https://api.betterttv.net/3/cached/emotes/global";
static constexpr const char *bttvChannelEmoteApiUrl =
"https://api.betterttv.net/2/channels/";
"https://api.betterttv.net/3/cached/users/twitch/";
public:
BttvEmotes();
@ -24,7 +24,7 @@ public:
std::shared_ptr<const EmoteMap> emotes() const;
boost::optional<EmotePtr> emote(const EmoteName &name) const;
void loadEmotes();
static void loadChannel(const QString &channelName,
static void loadChannel(const QString &channelId,
std::function<void(EmoteMap &&)> callback);
private:

View file

@ -79,7 +79,38 @@ namespace {
return {Success, std::move(emotes)};
}
std::pair<Outcome, EmoteMap> parseChannelEmotes(const QJsonObject &jsonRoot)
boost::optional<EmotePtr> parseModBadge(const QJsonObject &jsonRoot)
{
boost::optional<EmotePtr> modBadge;
auto room = jsonRoot.value("room").toObject();
auto modUrls = room.value("mod_urls").toObject();
if (!modUrls.isEmpty())
{
auto modBadge1x = getEmoteLink(modUrls, "1");
auto modBadge2x = getEmoteLink(modUrls, "2");
auto modBadge3x = getEmoteLink(modUrls, "4");
auto modBadgeImageSet = ImageSet{
Image::fromUrl(modBadge1x, 1),
modBadge2x.string.isEmpty() ? Image::getEmpty()
: Image::fromUrl(modBadge2x, 0.5),
modBadge3x.string.isEmpty() ? Image::getEmpty()
: Image::fromUrl(modBadge3x, 0.25),
};
modBadge = std::make_shared<Emote>(Emote{
{""},
modBadgeImageSet,
Tooltip{"Twitch Channel Moderator"},
modBadge1x,
});
}
return modBadge;
}
EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot)
{
auto jsonSets = jsonRoot.value("sets").toObject();
auto emotes = EmoteMap();
@ -110,7 +141,7 @@ namespace {
}
}
return {Success, std::move(emotes)};
return emotes;
}
} // namespace
@ -138,7 +169,7 @@ void FfzEmotes::loadEmotes()
QString url("https://api.frankerfacez.com/v1/set/global");
NetworkRequest(url)
.timeout(30000)
.onSuccess([this](auto result) -> Outcome {
auto emotes = this->emotes();
@ -151,21 +182,28 @@ void FfzEmotes::loadEmotes()
.execute();
}
void FfzEmotes::loadChannel(const QString &channelName,
std::function<void(EmoteMap &&)> callback)
void FfzEmotes::loadChannel(
const QString &channelId, std::function<void(EmoteMap &&)> emoteCallback,
std::function<void(boost::optional<EmotePtr>)> modBadgeCallback)
{
log("[FFZEmotes] Reload FFZ Channel Emotes for channel {}\n", channelName);
log("[FFZEmotes] Reload FFZ Channel Emotes for channel {}\n", channelId);
NetworkRequest("https://api.frankerfacez.com/v1/room/id/" + channelId)
NetworkRequest("https://api.frankerfacez.com/v1/room/" + channelName)
.timeout(20000)
.onSuccess([callback = std::move(callback)](auto result) -> Outcome {
auto pair = parseChannelEmotes(result.parseJson());
if (pair.first)
callback(std::move(pair.second));
return pair.first;
.onSuccess([emoteCallback = std::move(emoteCallback),
modBadgeCallback =
std::move(modBadgeCallback)](auto result) -> Outcome {
auto json = result.parseJson();
auto emoteMap = parseChannelEmotes(json);
auto modBadge = parseModBadge(json);
emoteCallback(std::move(emoteMap));
modBadgeCallback(std::move(modBadge));
return Success;
})
.onError([channelName](int result) {
.onError([channelId](int result) {
if (result == 203)
{
// User does not have any FFZ emotes
@ -176,12 +214,12 @@ void FfzEmotes::loadChannel(const QString &channelName,
{
// TODO: Auto retry in case of a timeout, with a delay
log("Fetching FFZ emotes for channel {} failed due to timeout",
channelName);
channelId);
return true;
}
log("Error fetching FFZ emotes for channel {}, error {}",
channelName, result);
log("Error fetching FFZ emotes for channel {}, error {}", channelId,
result);
return true;
})

View file

@ -22,8 +22,10 @@ public:
std::shared_ptr<const EmoteMap> emotes() const;
boost::optional<EmotePtr> emote(const EmoteName &name) const;
void loadEmotes();
static void loadChannel(const QString &channelName,
std::function<void(EmoteMap &&)> callback);
static void loadChannel(
const QString &channelId,
std::function<void(EmoteMap &&)> emoteCallback,
std::function<void(boost::optional<EmotePtr>)> modBadgeCallback);
private:
Atomic<std::shared_ptr<const EmoteMap>> global_;

View file

@ -1,63 +0,0 @@
#include "FfzModBadge.hpp"
#include <QBuffer>
#include <QImageReader>
#include <QJsonObject>
#include <QPainter>
#include <QString>
#include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "messages/Emote.hpp"
namespace chatterino {
FfzModBadge::FfzModBadge(const QString &channelName)
: channelName_(channelName)
{
}
void FfzModBadge::loadCustomModBadge()
{
static QString partialUrl("https://cdn.frankerfacez.com/room-badge/mod/");
QString url = partialUrl + channelName_ + "/1";
NetworkRequest(url)
.onSuccess([this, url](auto result) -> Outcome {
auto data = result.getData();
QBuffer buffer(const_cast<QByteArray *>(&data));
buffer.open(QIODevice::ReadOnly);
QImageReader reader(&buffer);
if (reader.imageCount() == 0)
return Failure;
QPixmap badgeOverlay = QPixmap::fromImageReader(&reader);
QPixmap badgePixmap(18, 18);
// the default mod badge green color
badgePixmap.fill(QColor("#34AE0A"));
QPainter painter(&badgePixmap);
QRectF rect(0, 0, 18, 18);
painter.drawPixmap(rect, badgeOverlay, rect);
auto emote = Emote{{""},
ImageSet{Image::fromPixmap(badgePixmap)},
Tooltip{"Twitch Channel Moderator"},
Url{url}};
this->badge_ = std::make_shared<Emote>(emote);
// getBadge.execute();
return Success;
})
.execute();
}
EmotePtr FfzModBadge::badge() const
{
return this->badge_;
}
} // namespace chatterino

View file

@ -1,25 +0,0 @@
#pragma once
#include <QString>
#include <boost/optional.hpp>
namespace chatterino {
struct Emote;
using EmotePtr = std::shared_ptr<const Emote>;
class FfzModBadge
{
public:
FfzModBadge(const QString &channelName);
void loadCustomModBadge();
EmotePtr badge() const;
private:
const QString channelName_;
EmotePtr badge_;
};
} // namespace chatterino

View file

@ -30,15 +30,18 @@ AbstractIrcServer::AbstractIrcServer()
this->readConnection_.reset(new IrcConnection);
this->readConnection_->moveToThread(QCoreApplication::instance()->thread());
QObject::connect(this->readConnection_.get(),
&Communi::IrcConnection::messageReceived,
[this](auto msg) { this->messageReceived(msg); });
QObject::connect(
this->readConnection_.get(), &Communi::IrcConnection::messageReceived,
[this](auto msg) { this->readConnectionMessageReceived(msg); });
QObject::connect(this->readConnection_.get(),
&Communi::IrcConnection::privateMessageReceived,
[this](auto msg) { this->privateMessageReceived(msg); });
QObject::connect(
this->readConnection_.get(), &Communi::IrcConnection::connected,
[this] { this->onConnected(this->readConnection_.get()); });
[this] { this->onReadConnected(this->readConnection_.get()); });
QObject::connect(
this->writeConnection_.get(), &Communi::IrcConnection::connected,
[this] { this->onWriteConnected(this->writeConnection_.get()); });
QObject::connect(this->readConnection_.get(),
&Communi::IrcConnection::disconnected,
[this] { this->onDisconnected(); });
@ -227,7 +230,7 @@ std::shared_ptr<Channel> AbstractIrcServer::getChannelOrEmpty(
return Channel::getEmpty();
}
void AbstractIrcServer::onConnected(IrcConnection *connection)
void AbstractIrcServer::onReadConnected(IrcConnection *connection)
{
std::lock_guard<std::mutex> lock(this->channelMutex);
@ -262,6 +265,10 @@ void AbstractIrcServer::onConnected(IrcConnection *connection)
this->falloffCounter_ = 1;
}
void AbstractIrcServer::onWriteConnected(IrcConnection *connection)
{
}
void AbstractIrcServer::onDisconnected()
{
std::lock_guard<std::mutex> lock(this->channelMutex);
@ -310,7 +317,7 @@ void AbstractIrcServer::addFakeMessage(const QString &data)
}
else
{
this->messageReceived(fakeMessage);
this->readConnectionMessageReceived(fakeMessage);
}
}
@ -319,7 +326,8 @@ void AbstractIrcServer::privateMessageReceived(
{
}
void AbstractIrcServer::messageReceived(Communi::IrcMessage *message)
void AbstractIrcServer::readConnectionMessageReceived(
Communi::IrcMessage *message)
{
}

View file

@ -49,10 +49,11 @@ protected:
const QString &channelName) = 0;
virtual void privateMessageReceived(Communi::IrcPrivateMessage *message);
virtual void messageReceived(Communi::IrcMessage *message);
virtual void readConnectionMessageReceived(Communi::IrcMessage *message);
virtual void writeConnectionMessageReceived(Communi::IrcMessage *message);
virtual void onConnected(IrcConnection *connection);
virtual void onReadConnected(IrcConnection *connection);
virtual void onWriteConnected(IrcConnection *connection);
virtual void onDisconnected();
virtual void onSocketError();

View file

@ -22,7 +22,7 @@ ChatroomChannel::ChatroomChannel(const QString &channelName,
}
}
void ChatroomChannel::refreshChannelEmotes()
void ChatroomChannel::refreshBTTVChannelEmotes()
{
if (this->chatroomOwnerId.isEmpty())
{
@ -36,17 +36,28 @@ void ChatroomChannel::refreshChannelEmotes()
this->bttvEmotes_.set(
std::make_shared<EmoteMap>(std::move(emoteMap)));
});
FfzEmotes::loadChannel(username, [this, weak](auto &&emoteMap) {
if (auto shared = weak.lock())
this->ffzEmotes_.set(
std::make_shared<EmoteMap>(std::move(emoteMap)));
});
if (auto shared = weak.lock())
{
this->chatroomOwnerName = username;
}
});
}
void ChatroomChannel::refreshFFZChannelEmotes()
{
if (this->chatroomOwnerId.isEmpty())
{
return;
}
FfzEmotes::loadChannel(
this->chatroomOwnerId,
[this](auto &&emoteMap) {
this->ffzEmotes_.set(
std::make_shared<EmoteMap>(std::move(emoteMap)));
},
[this](auto &&modBadge) {
this->ffzCustomModBadge_.set(std::move(modBadge));
});
}
const QString &ChatroomChannel::getDisplayName() const
{

View file

@ -13,7 +13,8 @@ protected:
explicit ChatroomChannel(const QString &channelName,
TwitchBadges &globalTwitchBadges,
BttvEmotes &globalBttv, FfzEmotes &globalFfz);
virtual void refreshChannelEmotes() override;
virtual void refreshBTTVChannelEmotes() override;
virtual void refreshFFZChannelEmotes() override;
virtual const QString &getDisplayName() const override;
QString chatroomOwnerId;

View file

@ -582,49 +582,6 @@ void IrcMessageHandler::handleNoticeMessage(Communi::IrcNoticeMessage *message)
}
}
void IrcMessageHandler::handleWriteConnectionNoticeMessage(
Communi::IrcNoticeMessage *message)
{
static std::unordered_set<std::string> readConnectionOnlyIDs{
"host_on",
"host_off",
"host_target_went_offline",
"emote_only_on",
"emote_only_off",
"slow_on",
"slow_off",
"subs_on",
"subs_off",
"r9k_on",
"r9k_off",
// Display for user who times someone out. This implies you're a
// moderator, at which point you will be connected to PubSub and receive
// a better message from there
"timeout_success",
"ban_success",
// Channel suspended notices
"msg_channel_suspended",
};
QVariant v = message->tag("msg-id");
if (v.isValid())
{
std::string msgID = v.toString().toStdString();
if (readConnectionOnlyIDs.find(msgID) != readConnectionOnlyIDs.end())
{
return;
}
log("Showing notice message from write connection with message id '{}'",
msgID);
}
this->handleNoticeMessage(message);
}
void IrcMessageHandler::handleJoinMessage(Communi::IrcMessage *message)
{
auto app = getApp();

View file

@ -46,8 +46,6 @@ public:
Communi::IrcNoticeMessage *message);
void handleNoticeMessage(Communi::IrcNoticeMessage *message);
void handleWriteConnectionNoticeMessage(Communi::IrcNoticeMessage *message);
void handleJoinMessage(Communi::IrcMessage *message);
void handlePartMessage(Communi::IrcMessage *message);

View file

@ -699,13 +699,13 @@ PubSub::PubSub()
};
this->moderationActionHandlers["denied_automod_message"] =
[this](const auto &data, const auto &roomID) {
[](const auto &data, const auto &roomID) {
// This message got denied by a moderator
// qDebug() << QString::fromStdString(rj::stringify(data));
};
this->moderationActionHandlers["approved_automod_message"] =
[this](const auto &data, const auto &roomID) {
[](const auto &data, const auto &roomID) {
// This message got approved by a moderator
// qDebug() << QString::fromStdString(rj::stringify(data));
};

View file

@ -87,7 +87,6 @@ TwitchChannel::TwitchChannel(const QString &name,
, globalFfz_(ffz)
, bttvEmotes_(std::make_shared<EmoteMap>())
, ffzEmotes_(std::make_shared<EmoteMap>())
, ffzCustomModBadge_(name)
, mod_(false)
{
log("[TwitchChannel:{}] Opened", name);
@ -113,6 +112,8 @@ TwitchChannel::TwitchChannel(const QString &name,
this->refreshLiveStatus();
this->refreshBadges();
this->refreshCheerEmotes();
this->refreshFFZChannelEmotes();
this->refreshBTTVChannelEmotes();
});
// timers
@ -135,9 +136,7 @@ TwitchChannel::TwitchChannel(const QString &name,
void TwitchChannel::initialize()
{
this->refreshChatters();
this->refreshChannelEmotes();
this->refreshBadges();
this->ffzCustomModBadge_.loadCustomModBadge();
}
bool TwitchChannel::isEmpty() const
@ -150,19 +149,30 @@ bool TwitchChannel::canSendMessage() const
return !this->isEmpty();
}
void TwitchChannel::refreshChannelEmotes()
void TwitchChannel::refreshBTTVChannelEmotes()
{
BttvEmotes::loadChannel(
this->getName(), [this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
this->roomId(), [this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
if (auto shared = weak.lock())
this->bttvEmotes_.set(
std::make_shared<EmoteMap>(std::move(emoteMap)));
});
}
void TwitchChannel::refreshFFZChannelEmotes()
{
FfzEmotes::loadChannel(
this->getName(), [this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
this->roomId(),
[this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
if (auto shared = weak.lock())
this->ffzEmotes_.set(
std::make_shared<EmoteMap>(std::move(emoteMap)));
},
[this, weak = weakOf<Channel>(this)](auto &&modBadge) {
if (auto shared = weak.lock())
{
this->ffzCustomModBadge_.set(std::move(modBadge));
}
});
}
@ -818,10 +828,7 @@ boost::optional<EmotePtr> TwitchChannel::twitchBadge(
boost::optional<EmotePtr> TwitchChannel::ffzCustomModBadge() const
{
if (auto badge = this->ffzCustomModBadge_.badge())
return badge;
return boost::none;
return this->ffzCustomModBadge_.get();
}
boost::optional<CheerEmote> TwitchChannel::cheerEmote(const QString &string)

View file

@ -6,7 +6,6 @@
#include "common/Outcome.hpp"
#include "common/UniqueAccess.hpp"
#include "common/UsernameSet.hpp"
#include "providers/ffz/FfzModBadge.hpp"
#include "providers/twitch/TwitchEmotes.hpp"
#include <rapidjson/document.h>
@ -85,7 +84,8 @@ public:
std::shared_ptr<const EmoteMap> bttvEmotes() const;
std::shared_ptr<const EmoteMap> ffzEmotes() const;
virtual void refreshChannelEmotes();
virtual void refreshBTTVChannelEmotes();
virtual void refreshFFZChannelEmotes();
// Badges
boost::optional<EmotePtr> ffzCustomModBadge() const;
@ -150,13 +150,13 @@ protected:
FfzEmotes &globalFfz_;
Atomic<std::shared_ptr<const EmoteMap>> bttvEmotes_;
Atomic<std::shared_ptr<const EmoteMap>> ffzEmotes_;
Atomic<boost::optional<EmotePtr>> ffzCustomModBadge_;
private:
// Badges
UniqueAccess<std::map<QString, std::map<QString, EmotePtr>>>
badgeSets_; // "subscribers": { "0": ... "3": ... "6": ...
UniqueAccess<std::vector<CheerEmoteSet>> cheerEmoteSets_;
FfzModBadge ffzCustomModBadge_;
bool mod_ = false;
bool vip_ = false;

View file

@ -952,7 +952,7 @@ void TwitchMessageBuilder::parseHighlights()
{
HighlightPhrase selfHighlight(
currentUsername, getSettings()->enableSelfHighlightTaskbar,
getSettings()->enableSelfHighlightSound, false);
getSettings()->enableSelfHighlightSound, false, false);
activeHighlights.emplace_back(std::move(selfHighlight));
}
@ -1171,7 +1171,7 @@ void TwitchMessageBuilder::appendTwitchBadges()
{
if (auto customModBadge = this->twitchChannel->ffzCustomModBadge())
{
this->emplace<BadgeElement>(
this->emplace<ModBadgeElement>(
customModBadge.get(),
MessageElementFlag::BadgeChannelAuthority)
->setTooltip((*customModBadge)->tooltip.string);

View file

@ -128,9 +128,8 @@ void TwitchServer::privateMessageReceived(Communi::IrcPrivateMessage *message)
IrcMessageHandler::getInstance().handlePrivMessage(message, *this);
}
void TwitchServer::messageReceived(Communi::IrcMessage *message)
void TwitchServer::readConnectionMessageReceived(Communi::IrcMessage *message)
{
// this->readConnection
if (message->type() == Communi::IrcMessage::Type::Private)
{
// We already have a handler for private messages
@ -141,39 +140,11 @@ void TwitchServer::messageReceived(Communi::IrcMessage *message)
auto &handler = IrcMessageHandler::getInstance();
if (command == "ROOMSTATE")
{
handler.handleRoomStateMessage(message);
}
else if (command == "CLEARCHAT")
{
handler.handleClearChatMessage(message);
}
else if (command == "CLEARMSG")
{
handler.handleClearMessageMessage(message);
}
else if (command == "USERSTATE")
{
handler.handleUserStateMessage(message);
}
else if (command == "WHISPER")
{
handler.handleWhisperMessage(message);
}
else if (command == "USERNOTICE")
{
handler.handleUserNoticeMessage(message, *this);
}
else if (command == "MODE")
// Below commands enabled through the twitch.tv/membership CAP REQ
if (command == "MODE")
{
handler.handleModeMessage(message);
}
else if (command == "NOTICE")
{
handler.handleNoticeMessage(
static_cast<Communi::IrcNoticeMessage *>(message));
}
else if (command == "JOIN")
{
handler.handleJoinMessage(message);
@ -186,24 +157,58 @@ void TwitchServer::messageReceived(Communi::IrcMessage *message)
void TwitchServer::writeConnectionMessageReceived(Communi::IrcMessage *message)
{
switch (message->type())
{
case Communi::IrcMessage::Type::Notice:
{
IrcMessageHandler::getInstance().handleWriteConnectionNoticeMessage(
static_cast<Communi::IrcNoticeMessage *>(message));
}
break;
const QString &command = message->command();
default:;
auto &handler = IrcMessageHandler::getInstance();
// Below commands enabled through the twitch.tv/commands CAP REQ
if (command == "USERSTATE")
{
handler.handleUserStateMessage(message);
}
else if (command == "WHISPER")
{
handler.handleWhisperMessage(message);
}
else if (command == "USERNOTICE")
{
handler.handleUserNoticeMessage(message, *this);
}
else if (command == "ROOMSTATE")
{
handler.handleRoomStateMessage(message);
}
else if (command == "CLEARCHAT")
{
handler.handleClearChatMessage(message);
}
else if (command == "CLEARMSG")
{
handler.handleClearMessageMessage(message);
}
else if (command == "NOTICE")
{
handler.handleNoticeMessage(
static_cast<Communi::IrcNoticeMessage *>(message));
}
}
void TwitchServer::onConnected(IrcConnection *connection)
void TwitchServer::onReadConnected(IrcConnection *connection)
{
// connection in thise case is the read connection
connection->sendRaw(
"CAP REQ :twitch.tv/tags twitch.tv/commands twitch.tv/membership");
AbstractIrcServer::onReadConnected(connection);
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
// twitch.tv/membership enables the JOIN/PART/MODE/NAMES commands. See https://dev.twitch.tv/docs/irc/membership/
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/membership");
}
void TwitchServer::onWriteConnected(IrcConnection *connection)
{
AbstractIrcServer::onWriteConnected(connection);
// twitch.tv/tags enables IRCv3 tags on messages. See https://dev.twitch.tv/docs/irc/tags/
// twitch.tv/commands enables a bunch of miscellaneous command capabilities. See https://dev.twitch.tv/docs/irc/commands/
connection->sendRaw("CAP REQ :twitch.tv/tags twitch.tv/commands");
}
std::shared_ptr<Channel> TwitchServer::getCustomChannel(

View file

@ -51,11 +51,13 @@ protected:
virtual void privateMessageReceived(
Communi::IrcPrivateMessage *message) override;
virtual void messageReceived(Communi::IrcMessage *message) override;
virtual void readConnectionMessageReceived(
Communi::IrcMessage *message) override;
virtual void writeConnectionMessageReceived(
Communi::IrcMessage *message) override;
virtual void onConnected(IrcConnection *connection) override;
virtual void onReadConnected(IrcConnection *connection) override;
virtual void onWriteConnected(IrcConnection *connection) override;
virtual std::shared_ptr<Channel> getCustomChannel(
const QString &channelname) override;

View file

@ -198,11 +198,13 @@ public:
QStringSetting streamlinkOpts = {"/external/streamlink/options", ""};
/// Misc
BoolSetting betaUpdates = {"/misc/beta", false};
IntSetting startUpNotification = {"/misc/startUpNotification", 0};
QStringSetting currentVersion = {"/misc/currentVersion", ""};
BoolSetting loadTwitchMessageHistoryOnConnect = {
"/misc/twitch/loadMessageHistoryOnConnect", true};
IntSetting emotesTooltipPreview = {"/misc/emotesTooltipPreview", 0};
BoolSetting openLinksIncognito = {"/misc/openLinksIncognito", 0};
QStringSetting cachePath = {"/cache/path", ""};

View file

@ -1,5 +1,6 @@
#include "Updates.hpp"
#include "Settings.hpp"
#include "common/NetworkRequest.hpp"
#include "common/Outcome.hpp"
#include "common/Version.hpp"
@ -13,6 +14,12 @@
#include <QProcess>
namespace chatterino {
namespace {
QString currentBranch()
{
return getSettings()->betaUpdates ? "beta" : "stable";
}
} // namespace
Updates::Updates()
: currentVersion_(CHATTERINO_VERSION)
@ -195,8 +202,8 @@ void Updates::installUpdates()
void Updates::checkForUpdates()
{
QString url =
"https://notitia.chatterino.com/version/chatterino/" CHATTERINO_OS
"/stable";
"https://notitia.chatterino.com/version/chatterino/" CHATTERINO_OS "/" +
currentBranch();
NetworkRequest(url)
.timeout(60000)

View file

@ -10,7 +10,7 @@
#include "singletons/Settings.hpp"
#include "singletons/Theme.hpp"
#include "util/Clamp.hpp"
#include "widgets/AccountSwitchPopupWidget.hpp"
#include "widgets/AccountSwitchPopup.hpp"
#include "widgets/Notebook.hpp"
#include "widgets/Window.hpp"
#include "widgets/dialogs/SettingsDialog.hpp"
@ -63,7 +63,7 @@ void WindowManager::showSettingsDialog(SettingsDialogPreference preference)
void WindowManager::showAccountSelectPopup(QPoint point)
{
// static QWidget *lastFocusedWidget = nullptr;
static AccountSwitchPopupWidget *w = new AccountSwitchPopupWidget();
static AccountSwitchPopup *w = new AccountSwitchPopup();
if (w->hasFocus())
{
@ -79,7 +79,7 @@ void WindowManager::showAccountSelectPopup(QPoint point)
w->refresh();
QPoint buttonPos = point;
w->move(buttonPos.x(), buttonPos.y());
w->move(buttonPos.x() - 30, buttonPos.y());
w->show();
w->setFocus();
@ -338,7 +338,7 @@ void WindowManager::initialize(Settings &settings, Paths &paths)
// Have to offset x by one because qt moves the window 1px too
// far to the left:w
window.setGeometry(x + 1, y, width, height);
window.setInitialBounds({x, y, width, height});
}
}
@ -465,10 +465,12 @@ void WindowManager::save()
}
// window geometry
window_obj.insert("x", window->x());
window_obj.insert("y", window->y());
window_obj.insert("width", window->width());
window_obj.insert("height", window->height());
auto rect = window->getBounds();
window_obj.insert("x", rect.x());
window_obj.insert("y", rect.y());
window_obj.insert("width", rect.width());
window_obj.insert("height", rect.height());
// window tabs
QJsonArray tabs_arr;

View file

@ -1,4 +1,4 @@
#include "widgets/AccountSwitchPopupWidget.hpp"
#include "widgets/AccountSwitchPopup.hpp"
#include "debug/Log.hpp"
#include "widgets/dialogs/SettingsDialog.hpp"
@ -10,10 +10,10 @@
namespace chatterino {
AccountSwitchPopupWidget::AccountSwitchPopupWidget(QWidget *parent)
: QWidget(parent)
AccountSwitchPopup::AccountSwitchPopup(QWidget *parent)
: BaseWindow(parent,
BaseWindow::Flags(BaseWindow::TopMost | BaseWindow::Frameless))
{
this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
#ifdef Q_OS_LINUX
this->setWindowFlag(Qt::Popup);
#endif
@ -38,22 +38,22 @@ AccountSwitchPopupWidget::AccountSwitchPopupWidget(QWidget *parent)
SettingsDialog::showDialog(SettingsDialogPreference::Accounts); //
});
this->setLayout(vbox);
this->getLayoutContainer()->setLayout(vbox);
// this->setStyleSheet("background: #333");
this->setScaleIndependantSize(200, 200);
}
void AccountSwitchPopupWidget::refresh()
void AccountSwitchPopup::refresh()
{
this->ui_.accountSwitchWidget->refresh();
}
void AccountSwitchPopupWidget::focusOutEvent(QFocusEvent *)
void AccountSwitchPopup::focusOutEvent(QFocusEvent *)
{
this->hide();
}
void AccountSwitchPopupWidget::paintEvent(QPaintEvent *)
void AccountSwitchPopup::paintEvent(QPaintEvent *)
{
QPainter painter(this);

View file

@ -1,17 +1,18 @@
#pragma once
#include "widgets/AccountSwitchWidget.hpp"
#include "widgets/BaseWindow.hpp"
#include <QWidget>
namespace chatterino {
class AccountSwitchPopupWidget : public QWidget
class AccountSwitchPopup : public BaseWindow
{
Q_OBJECT
public:
AccountSwitchPopupWidget(QWidget *parent = nullptr);
AccountSwitchPopup(QWidget *parent = nullptr);
void refresh();

View file

@ -10,7 +10,7 @@
#include "singletons/WindowManager.hpp"
#include "util/InitUpdateButton.hpp"
#include "util/Shortcut.hpp"
#include "widgets/AccountSwitchPopupWidget.hpp"
#include "widgets/AccountSwitchPopup.hpp"
#include "widgets/Notebook.hpp"
#include "widgets/dialogs/SettingsDialog.hpp"
#include "widgets/dialogs/UpdateDialog.hpp"
@ -377,13 +377,13 @@ void Window::onAccountSelected()
{
auto user = getApp()->accounts->twitch.getCurrent();
#ifdef CHATTERINO_NIGHTLY_VERSION_STRING
auto windowTitleEnd =
QString("Chatterino Nightly " CHATTERINO_VERSION
" (" UGLYMACROHACK(CHATTERINO_NIGHTLY_VERSION_STRING) ")");
#else
//#ifdef CHATTERINO_NIGHTLY_VERSION_STRING
// auto windowTitleEnd =
// QString("Chatterino Nightly " CHATTERINO_VERSION
// " (" UGLYMACROHACK(CHATTERINO_NIGHTLY_VERSION_STRING) ")");
//#else
auto windowTitleEnd = QString("Chatterino " CHATTERINO_VERSION);
#endif
//#endif
this->setWindowTitle(windowTitleEnd);

View file

@ -4,6 +4,7 @@
#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"
#include "providers/twitch/TwitchMessageBuilder.hpp"
@ -20,34 +21,30 @@ namespace chatterino {
LogsPopup::LogsPopup()
: channel_(Channel::getEmpty())
{
this->initLayout();
this->resize(400, 600);
}
void LogsPopup::initLayout()
{
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setMargin(0);
this->channelView_ = new ChannelView(this);
layout->addWidget(this->channelView_);
this->setLayout(layout);
}
void LogsPopup::setChannelName(QString channelName)
{
this->channelName_ = channelName;
}
void LogsPopup::setChannel(std::shared_ptr<Channel> channel)
void LogsPopup::setChannel(const ChannelPtr &channel)
{
this->channel_ = channel;
this->updateWindowTitle();
}
void LogsPopup::setTargetUserName(QString userName)
void LogsPopup::setChannelName(const QString &channelName)
{
this->channelName_ = channelName;
this->updateWindowTitle();
}
void LogsPopup::setTargetUserName(const QString &userName)
{
this->userName_ = userName;
this->updateWindowTitle();
}
void LogsPopup::updateWindowTitle()
{
this->setWindowTitle(this->userName_ + "'s logs in #" + this->channelName_);
}
void LogsPopup::getLogs()
@ -60,8 +57,6 @@ void LogsPopup::getLogs()
this->channelName_ = twitchChannel->getName();
this->getLogviewerLogs(twitchChannel->roomId());
this->setWindowTitle(this->userName_ + "'s logs in #" +
this->channelName_);
return;
}
}
@ -83,7 +78,7 @@ void LogsPopup::setMessages(std::vector<MessagePtr> &messages)
ChannelPtr logsChannel(new Channel("logs", Channel::Type::Misc));
logsChannel->addMessagesAtStart(messages);
this->channelView_->setChannel(logsChannel);
SearchPopup::setChannel(logsChannel);
}
void LogsPopup::getLogviewerLogs(const QString &roomID)
@ -121,6 +116,8 @@ void LogsPopup::getLogviewerLogs(const QString &roomID)
static_cast<Communi::IrcPrivateMessage *>(ircMessage);
TwitchMessageBuilder builder(this->channel_.get(), privMsg,
args);
builder.message().searchText = message;
messages.push_back(builder.build());
}
@ -165,6 +162,7 @@ void LogsPopup::getOverrustleLogs()
for (auto i : dataMessages)
{
QJsonObject singleMessage = i.toObject();
auto text = singleMessage.value("text").toString();
QTime timeStamp =
QDateTime::fromSecsSinceEpoch(
singleMessage.value("timestamp").toInt())
@ -175,9 +173,9 @@ void LogsPopup::getOverrustleLogs()
builder.emplace<TextElement>(this->userName_,
MessageElementFlag::Username,
MessageColor::System);
builder.emplace<TextElement>(
singleMessage.value("text").toString(),
MessageElementFlag::Text, MessageColor::Text);
builder.emplace<TextElement>(text, MessageElementFlag::Text,
MessageColor::Text);
builder.message().searchText = text;
messages.push_back(builder.release());
}
}
@ -191,4 +189,5 @@ void LogsPopup::getOverrustleLogs()
})
.execute();
}
} // namespace chatterino

View file

@ -1,37 +1,29 @@
#pragma once
#include "widgets/BaseWindow.hpp"
#include "widgets/helper/SearchPopup.hpp"
namespace chatterino {
class Channel;
class ChannelView;
class Channel;
using ChannelPtr = std::shared_ptr<Channel>;
struct Message;
using MessagePtr = std::shared_ptr<const Message>;
class LogsPopup : public BaseWindow
class LogsPopup : public SearchPopup
{
public:
LogsPopup();
void setChannelName(QString channelName);
void setChannel(std::shared_ptr<Channel> channel);
void setTargetUserName(QString userName);
void setChannel(const ChannelPtr &channel) override;
void setChannelName(const QString &channelName);
void setTargetUserName(const QString &userName);
void getLogs();
protected:
void updateWindowTitle() override;
private:
ChannelView *channelView_ = nullptr;
ChannelPtr channel_;
QString userName_;
QString channelName_;
void initLayout();
void setMessages(std::vector<MessagePtr> &messages);
void getOverrustleLogs();
void getLogviewerLogs(const QString &roomID);

View file

@ -1,27 +1,23 @@
#include "widgets/dialogs/SettingsDialog.hpp"
#include "Application.hpp"
#include "singletons/Resources.hpp"
#include "util/LayoutCreator.hpp"
#include "widgets/helper/Button.hpp"
#include "widgets/helper/SettingsDialogTab.hpp"
#include "widgets/settingspages/AboutPage.hpp"
#include "widgets/settingspages/AccountsPage.hpp"
#include "widgets/settingspages/AdvancedPage.hpp"
#include "widgets/settingspages/BrowserExtensionPage.hpp"
#include "widgets/settingspages/CommandPage.hpp"
#include "widgets/settingspages/EmotesPage.hpp"
#include "widgets/settingspages/ExternalToolsPage.hpp"
#include "widgets/settingspages/FeelPage.hpp"
#include "widgets/settingspages/GeneralPage.hpp"
#include "widgets/settingspages/HighlightingPage.hpp"
#include "widgets/settingspages/IgnoresPage.hpp"
#include "widgets/settingspages/KeyboardSettingsPage.hpp"
#include "widgets/settingspages/LogsPage.hpp"
#include "widgets/settingspages/LookPage.hpp"
#include "widgets/settingspages/ModerationPage.hpp"
#include "widgets/settingspages/NotificationPage.hpp"
#include "widgets/settingspages/SpecialChannelsPage.hpp"
#include <QDialogButtonBox>
#include <QLineEdit>
namespace chatterino {
@ -30,6 +26,8 @@ SettingsDialog *SettingsDialog::handle = nullptr;
SettingsDialog::SettingsDialog()
: BaseWindow(nullptr, BaseWindow::DisableCustomScaling)
{
this->setWindowTitle("Chatterino Settings");
this->initUi();
this->addTabs();
@ -43,39 +41,55 @@ SettingsDialog::SettingsDialog()
void SettingsDialog::initUi()
{
LayoutCreator<SettingsDialog> layoutCreator(this);
auto outerBox = LayoutCreator<SettingsDialog>(this)
.setLayoutType<QVBoxLayout>()
.withoutSpacing();
// tab pages
layoutCreator.setLayoutType<QHBoxLayout>()
.withoutSpacing()
.emplace<QWidget>()
// TOP
auto title = outerBox.emplace<PageHeader>();
auto edit = LayoutCreator<PageHeader>(title.getElement())
.setLayoutType<QHBoxLayout>()
.withoutMargin()
.emplace<QLineEdit>()
.assign(&this->ui_.search);
edit->setPlaceholderText("Find in settings...");
QObject::connect(edit.getElement(), &QLineEdit::textChanged, this,
&SettingsDialog::filterElements);
// CENTER
auto centerBox =
outerBox.emplace<QHBoxLayout>().withoutMargin().withoutSpacing();
// left side (tabs)
centerBox.emplace<QWidget>()
.assign(&this->ui_.tabContainerContainer)
.emplace<QVBoxLayout>()
.setLayoutType<QVBoxLayout>()
.withoutMargin()
.assign(&this->ui_.tabContainer);
this->ui_.tabContainerContainer->layout()->setContentsMargins(8, 8, 0, 39);
this->layout()->setSpacing(0);
// right side layout
auto right = layoutCreator.emplace<QVBoxLayout>().withoutMargin();
// right side (pages)
auto right =
centerBox.emplace<QVBoxLayout>().withoutMargin().withoutSpacing();
{
right.emplace<QStackedLayout>()
.assign(&this->ui_.pageStack)
.withoutMargin();
auto buttons = right.emplace<QDialogButtonBox>(Qt::Horizontal);
{
this->ui_.okButton =
buttons->addButton("Ok", QDialogButtonBox::YesRole);
this->ui_.cancelButton =
buttons->addButton("Cancel", QDialogButtonBox::NoRole);
}
}
this->ui_.pageStack->setMargin(0);
outerBox->addSpacing(12);
// BOTTOM
auto buttons = outerBox.emplace<QDialogButtonBox>(Qt::Horizontal);
{
this->ui_.okButton =
buttons->addButton("Ok", QDialogButtonBox::YesRole);
this->ui_.cancelButton =
buttons->addButton("Cancel", QDialogButtonBox::NoRole);
}
// ---- misc
this->ui_.tabContainerContainer->setObjectName("tabWidget");
this->ui_.pageStack->setObjectName("pages");
@ -86,6 +100,50 @@ void SettingsDialog::initUi()
&SettingsDialog::onCancelClicked);
}
void SettingsDialog::filterElements(const QString &text)
{
// filter elements and hide pages
for (auto &&page : this->pages_)
{
// filterElements returns true if anything on the page matches the search query
page->tab()->setVisible(page->filterElements(text));
}
// find next visible page
if (this->lastSelectedByUser_ && this->lastSelectedByUser_->isVisible())
{
this->selectTab(this->lastSelectedByUser_, false);
}
else if (!this->selectedTab_->isVisible())
{
for (auto &&tab : this->tabs_)
{
if (tab->isVisible())
{
this->selectTab(tab, false);
break;
}
}
}
// remove duplicate spaces
bool shouldShowSpace = false;
for (int i = 0; i < this->ui_.tabContainer->count(); i++)
{
auto item = this->ui_.tabContainer->itemAt(i);
if (auto x = dynamic_cast<QSpacerItem *>(item); x)
{
x->changeSize(10, shouldShowSpace ? int(16 * this->scale()) : 0);
shouldShowSpace = false;
}
else if (item->widget())
{
shouldShowSpace |= item->widget()->isVisible();
}
}
}
SettingsDialog *SettingsDialog::getHandle()
{
return SettingsDialog::handle;
@ -96,7 +154,7 @@ void SettingsDialog::addTabs()
this->ui_.tabContainer->setMargin(0);
this->ui_.tabContainer->setSpacing(0);
this->ui_.tabContainer->addSpacing(16);
this->ui_.tabContainer->setContentsMargins(0, 20, 0, 20);
this->addTab(new GeneralPage);
@ -104,32 +162,21 @@ void SettingsDialog::addTabs()
this->addTab(new AccountsPage);
// this->ui_.tabContainer->addSpacing(16);
// this->addTab(new LookPage);
// this->addTab(new FeelPage);
this->ui_.tabContainer->addSpacing(16);
this->addTab(new CommandPage);
// this->addTab(new EmotesPage);
this->addTab(new HighlightingPage);
this->addTab(new IgnoresPage);
this->ui_.tabContainer->addSpacing(16);
this->addTab(new KeyboardSettingsPage);
// this->addTab(new LogsPage);
this->addTab(this->ui_.moderationPage = new ModerationPage);
this->addTab(new NotificationPage);
// this->addTab(new SpecialChannelsPage);
// this->addTab(new BrowserExtensionPage);
this->addTab(new ExternalToolsPage);
this->addTab(new AdvancedPage);
this->ui_.tabContainer->addStretch(1);
this->addTab(new AboutPage, Qt::AlignBottom);
this->ui_.tabContainer->addSpacing(16);
}
void SettingsDialog::addTab(SettingsPage *page, Qt::Alignment alignment)
@ -140,6 +187,7 @@ void SettingsDialog::addTab(SettingsPage *page, Qt::Alignment alignment)
this->ui_.pageStack->addWidget(page);
this->ui_.tabContainer->addWidget(tab, 0, alignment);
this->tabs_.push_back(tab);
this->pages_.push_back(page);
if (this->tabs_.size() == 1)
{
@ -147,7 +195,7 @@ void SettingsDialog::addTab(SettingsPage *page, Qt::Alignment alignment)
}
}
void SettingsDialog::selectTab(SettingsDialogTab *tab)
void SettingsDialog::selectTab(SettingsDialogTab *tab, bool byUser)
{
this->ui_.pageStack->setCurrentWidget(tab->getSettingsPage());
@ -159,10 +207,12 @@ void SettingsDialog::selectTab(SettingsDialogTab *tab)
tab->setSelected(true);
tab->setStyleSheet("background: #222; color: #4FC3F7;"
"border-left: 1px solid #444;"
"border-top: 1px solid #444;"
"border-bottom: 1px solid #444;");
"/*border: 1px solid #555; border-right: none;*/");
this->selectedTab_ = tab;
if (byUser)
{
this->lastSelectedByUser_ = tab;
}
}
void SettingsDialog::selectPage(SettingsPage *page)

View file

@ -8,12 +8,19 @@
#include <QWidget>
#include <pajlada/settings/setting.hpp>
class QLineEdit;
namespace chatterino {
class SettingsPage;
class SettingsDialogTab;
class ModerationPage;
class PageHeader : public QFrame
{
Q_OBJECT
};
enum class SettingsDialogPreference {
NoPreference,
Accounts,
@ -41,8 +48,9 @@ private:
void initUi();
void addTabs();
void addTab(SettingsPage *page, Qt::Alignment alignment = Qt::AlignTop);
void selectTab(SettingsDialogTab *tab);
void selectTab(SettingsDialogTab *tab, bool byUser = true);
void selectPage(SettingsPage *page);
void filterElements(const QString &query);
void onOkClicked();
void onCancelClicked();
@ -54,9 +62,12 @@ private:
QPushButton *okButton{};
QPushButton *cancelButton{};
ModerationPage *moderationPage{};
QLineEdit *search{};
} ui_;
std::vector<SettingsDialogTab *> tabs_;
std::vector<SettingsPage *> pages_;
SettingsDialogTab *selectedTab_{};
SettingsDialogTab *lastSelectedByUser_{};
friend class SettingsDialogTab;
};

View file

@ -24,8 +24,23 @@
#define TEXT_FOLLOWERS "Followers: "
#define TEXT_VIEWS "Views: "
#define TEXT_CREATED "Created: "
#define TEXT_USER_ID "ID: "
namespace chatterino {
namespace {
void addCopyableLabel(LayoutCreator<QHBoxLayout> box, Label **assign)
{
auto label = box.emplace<Label>().assign(assign);
auto button = box.emplace<Button>();
button->setPixmap(getApp()->resources->buttons.copyDark);
button->setScaleIndependantSize(18, 18);
button->setDim(Button::Dim::Lots);
QObject::connect(button.getElement(), &Button::leftClicked,
[label = label.getElement()] {
qApp->clipboard()->setText(label->getText());
});
};
} // namespace
UserInfoPopup::UserInfoPopup()
: BaseWindow(nullptr, BaseWindow::Flags(BaseWindow::Frameless |
@ -50,6 +65,7 @@ UserInfoPopup::UserInfoPopup()
auto avatar =
head.emplace<Button>(nullptr).assign(&this->ui_.avatarButton);
avatar->setScaleIndependantSize(100, 100);
avatar->setDim(Button::Dim::None);
QObject::connect(avatar.getElement(), &Button::leftClicked, [this] {
QDesktopServices::openUrl(
QUrl("https://twitch.tv/" + this->userName_.toLower()));
@ -58,11 +74,19 @@ UserInfoPopup::UserInfoPopup()
// items on the right
auto vbox = head.emplace<QVBoxLayout>();
{
auto name = vbox.emplace<Label>().assign(&this->ui_.nameLabel);
{
auto box = vbox.emplace<QHBoxLayout>()
.withoutMargin()
.withoutSpacing();
addCopyableLabel(box, &this->ui_.nameLabel);
this->ui_.nameLabel->setFontStyle(FontStyle::UiMediumBold);
box->addStretch(1);
addCopyableLabel(box, &this->ui_.userIDLabel);
auto palette = QPalette();
palette.setColor(QPalette::WindowText, QColor("#aaa"));
this->ui_.userIDLabel->setPalette(palette);
}
auto font = name->font();
font.setBold(true);
name->setFont(font);
vbox.emplace<Label>(TEXT_VIEWS).assign(&this->ui_.viewCountLabel);
vbox.emplace<Label>(TEXT_FOLLOWERS)
.assign(&this->ui_.followerCountLabel);
@ -199,7 +223,7 @@ UserInfoPopup::UserInfoPopup()
});
}
this->setStyleSheet("font-size: 11pt;");
// this->setStyleSheet("font-size: 11pt;");
this->installEvents();
}
@ -208,7 +232,17 @@ void UserInfoPopup::themeChangedEvent()
{
BaseWindow::themeChangedEvent();
this->setStyleSheet("background: #333");
this->setStyleSheet(
"background: #333; font-size: " +
QString::number(getFonts()
->getFont(FontStyle::UiMediumBold, this->scale())
.pixelSize()) +
"px;");
}
void UserInfoPopup::scaleChangedEvent(float /*scale*/)
{
themeChangedEvent();
}
void UserInfoPopup::installEvents()
@ -348,6 +382,10 @@ void UserInfoPopup::updateUserData()
this->userId_ = id;
this->ui_.userIDLabel->setText(TEXT_USER_ID + this->userId_);
// don't wait for the request to complete, just put the user id in the card
// right away
QString url("https://api.twitch.tv/kraken/channels/" + id);
NetworkRequest::twitchRequest(url)

View file

@ -23,6 +23,7 @@ public:
protected:
virtual void themeChangedEvent() override;
virtual void scaleChangedEvent(float scale) override;
private:
void installEvents();
@ -48,6 +49,7 @@ private:
Label *viewCountLabel = nullptr;
Label *followerCountLabel = nullptr;
Label *createdDateLabel = nullptr;
Label *userIDLabel = nullptr;
QCheckBox *follow = nullptr;
QCheckBox *ignore = nullptr;

View file

@ -133,6 +133,8 @@ ChannelView::ChannelView(BaseWidget *parent)
this->clickTimer_ = new QTimer(this);
this->clickTimer_->setSingleShot(true);
this->clickTimer_->setInterval(500);
this->setFocusPolicy(Qt::FocusPolicy::StrongFocus);
}
void ChannelView::initializeLayout()
@ -1526,14 +1528,8 @@ void ChannelView::addContextMenuItems(
QString url = hoveredElement->getLink().value;
// open link
bool incognitoByDefault = supportsIncognitoLinks() &&
layout->getMessage()->loginName == "hemirt";
menu->addAction("Open link", [url, incognitoByDefault] {
if (incognitoByDefault)
openLinkIncognito(url);
else
QDesktopServices::openUrl(QUrl(url));
});
menu->addAction("Open link",
[url] { QDesktopServices::openUrl(QUrl(url)); });
// open link default
if (supportsIncognitoLinks())
{
@ -1699,7 +1695,10 @@ void ChannelView::handleLinkClick(QMouseEvent *event, const Link &link,
case Link::Url:
{
QDesktopServices::openUrl(QUrl(link.value));
if (getSettings()->openLinksIncognito && supportsIncognitoLinks())
openLinkIncognito(link.value);
else
QDesktopServices::openUrl(QUrl(link.value));
}
break;

View file

@ -10,6 +10,26 @@
#include "widgets/helper/ChannelView.hpp"
namespace chatterino {
namespace {
ChannelPtr filter(const QString &text, const QString &channelName,
const LimitedQueueSnapshot<MessagePtr> &snapshot)
{
ChannelPtr channel(new Channel(channelName, Channel::Type::None));
for (size_t i = 0; i < snapshot.size(); i++)
{
MessagePtr message = snapshot[i];
if (text.isEmpty() ||
message->searchText.indexOf(text, 0, Qt::CaseInsensitive) != -1)
{
channel->addMessage(message);
}
}
return channel;
}
} // namespace
SearchPopup::SearchPopup()
{
@ -17,13 +37,24 @@ SearchPopup::SearchPopup()
this->resize(400, 600);
}
void SearchPopup::setChannel(ChannelPtr channel)
void SearchPopup::setChannel(const ChannelPtr &channel)
{
this->channelName_ = channel->getName();
this->snapshot_ = channel->getMessageSnapshot();
this->performSearch();
this->search();
this->setWindowTitle("Searching in " + channel->getName() + "s history");
this->updateWindowTitle();
}
void SearchPopup::updateWindowTitle()
{
this->setWindowTitle("Searching in " + this->channelName_ + "s history");
}
void SearchPopup::search()
{
this->channelView_->setChannel(filter(this->searchInput_->text(),
this->channelName_, this->snapshot_));
}
void SearchPopup::keyPressEvent(QKeyEvent *e)
@ -43,18 +74,20 @@ void SearchPopup::initLayout()
{
QVBoxLayout *layout1 = new QVBoxLayout(this);
layout1->setMargin(0);
layout1->setSpacing(0);
// HBOX
{
QHBoxLayout *layout2 = new QHBoxLayout(this);
layout2->setMargin(6);
layout2->setMargin(8);
layout2->setSpacing(8);
// SEARCH INPUT
{
this->searchInput_ = new QLineEdit(this);
layout2->addWidget(this->searchInput_);
QObject::connect(this->searchInput_, &QLineEdit::returnPressed,
[this] { this->performSearch(); });
[this] { this->search(); });
}
// SEARCH BUTTON
@ -63,7 +96,7 @@ void SearchPopup::initLayout()
searchButton->setText("Search");
layout2->addWidget(searchButton);
QObject::connect(searchButton, &QPushButton::clicked,
[this] { this->performSearch(); });
[this] { this->search(); });
}
layout1->addLayout(layout2);
@ -80,25 +113,4 @@ void SearchPopup::initLayout()
}
}
void SearchPopup::performSearch()
{
QString text = searchInput_->text();
ChannelPtr channel(new Channel(this->channelName_, Channel::Type::None));
for (size_t i = 0; i < this->snapshot_.size(); i++)
{
MessagePtr message = this->snapshot_[i];
if (text.isEmpty() ||
message->searchText.indexOf(this->searchInput_->text(), 0,
Qt::CaseInsensitive) != -1)
{
channel->addMessage(message);
}
}
this->channelView_->setChannel(channel);
}
} // namespace chatterino

View file

@ -1,5 +1,6 @@
#pragma once
#include "ForwardDecl.hpp"
#include "messages/LimitedQueueSnapshot.hpp"
#include "widgets/BaseWindow.hpp"
@ -9,30 +10,26 @@ class QLineEdit;
namespace chatterino {
class Channel;
class ChannelView;
struct Message;
using MessagePtr = std::shared_ptr<const Message>;
class SearchPopup : public BaseWindow
{
public:
SearchPopup();
void setChannel(std::shared_ptr<Channel> channel);
virtual void setChannel(const ChannelPtr &channel);
protected:
void keyPressEvent(QKeyEvent *e) override;
virtual void updateWindowTitle();
private:
void initLayout();
void performSearch();
void search();
LimitedQueueSnapshot<MessagePtr> snapshot_;
QLineEdit *searchInput_;
ChannelView *channelView_;
QString channelName_;
QLineEdit *searchInput_{};
ChannelView *channelView_{};
QString channelName_{};
};
} // namespace chatterino

View file

@ -68,6 +68,8 @@ void SettingsDialogTab::mousePressEvent(QMouseEvent *event)
}
this->dialog_->selectTab(this);
this->setFocus();
}
} // namespace chatterino

View file

@ -1,78 +0,0 @@
#include "AdvancedPage.hpp"
#include "Application.hpp"
#include "controllers/taggedusers/TaggedUsersController.hpp"
#include "controllers/taggedusers/TaggedUsersModel.hpp"
#include "singletons/Logging.hpp"
#include "singletons/Paths.hpp"
#include "util/Helpers.hpp"
#include "util/LayoutCreator.hpp"
#include <QFileDialog>
#include <QFormLayout>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QListView>
#include <QPushButton>
#include <QTableView>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QtConcurrent/QtConcurrent>
namespace chatterino {
AdvancedPage::AdvancedPage()
: SettingsPage("Advanced", ":/settings/advanced.svg")
{
LayoutCreator<AdvancedPage> layoutCreator(this);
auto tabs = layoutCreator.emplace<QTabWidget>();
{
auto layout = tabs.appendTab(new QVBoxLayout, "Cache");
auto folderLabel = layout.emplace<QLabel>();
folderLabel->setTextFormat(Qt::RichText);
folderLabel->setTextInteractionFlags(Qt::TextBrowserInteraction |
Qt::LinksAccessibleByKeyboard |
Qt::LinksAccessibleByKeyboard);
folderLabel->setOpenExternalLinks(true);
getSettings()->cachePath.connect([folderLabel](const auto &,
auto) mutable {
QString newPath = getPaths()->cacheDirectory();
QString pathShortened = "Cache saved at <a href=\"file:///" +
newPath +
"\"><span style=\"color: white;\">" +
shortenString(newPath, 50) + "</span></a>";
folderLabel->setText(pathShortened);
folderLabel->setToolTip(newPath);
});
layout->addStretch(1);
auto selectDir = layout.emplace<QPushButton>("Set custom cache folder");
QObject::connect(
selectDir.getElement(), &QPushButton::clicked, this, [this] {
auto dirName = QFileDialog::getExistingDirectory(this);
getSettings()->cachePath = dirName;
});
auto resetDir =
layout.emplace<QPushButton>("Reset custom cache folder");
QObject::connect(resetDir.getElement(), &QPushButton::clicked, this,
[]() mutable {
getSettings()->cachePath = ""; //
});
// Logs end
}
}
} // namespace chatterino

View file

@ -1,13 +0,0 @@
#pragma once
#include "widgets/settingspages/SettingsPage.hpp"
namespace chatterino {
class AdvancedPage : public SettingsPage
{
public:
AdvancedPage();
};
} // namespace chatterino

View file

@ -1,36 +0,0 @@
#include "BrowserExtensionPage.hpp"
#include "util/LayoutCreator.hpp"
#include <QLabel>
#define CHROME_EXTENSION_LINK \
"https://chrome.google.com/webstore/detail/chatterino-native-host/" \
"glknmaideaikkmemifbfkhnomoknepka"
#define FIREFOX_EXTENSION_LINK \
"https://addons.mozilla.org/en-US/firefox/addon/chatterino-native-host/"
namespace chatterino {
BrowserExtensionPage::BrowserExtensionPage()
: SettingsPage("Browser integration", ":/settings/browser.svg")
{
auto layout =
LayoutCreator<BrowserExtensionPage>(this).setLayoutType<QVBoxLayout>();
auto label = layout.emplace<QLabel>(
"The browser extension will replace the default Twitch.tv chat with "
"chatterino while chatterino is running.");
label->setWordWrap(true);
auto chrome = layout.emplace<QLabel>("<a href=\"" CHROME_EXTENSION_LINK
"\">Download for Google Chrome</a>");
chrome->setOpenExternalLinks(true);
auto firefox =
layout.emplace<QLabel>("<a href=\"" FIREFOX_EXTENSION_LINK
"\">Download for Mozilla Firefox</a>");
firefox->setOpenExternalLinks(true);
layout->addStretch(1);
}
} // namespace chatterino

View file

@ -1,13 +0,0 @@
#pragma once
#include "widgets/settingspages/SettingsPage.hpp"
namespace chatterino {
class BrowserExtensionPage : public SettingsPage
{
public:
BrowserExtensionPage();
};
} // namespace chatterino

View file

@ -38,7 +38,7 @@ CommandPage::CommandPage()
auto app = getApp();
LayoutCreator<CommandPage> layoutCreator(this);
auto layout = layoutCreator.emplace<QVBoxLayout>().withoutMargin();
auto layout = layoutCreator.setLayoutType<QVBoxLayout>();
EditableModelView *view =
layout.emplace<EditableModelView>(app->commands->createModel(nullptr))

View file

@ -1,29 +0,0 @@
#include "EmotesPage.hpp"
#include "util/LayoutCreator.hpp"
namespace chatterino {
EmotesPage::EmotesPage()
: SettingsPage("Emotes", ":/settings/emote.svg")
{
// SettingManager &settings = SettingManager::getInstance();
// LayoutCreator<EmotesPage> layoutCreator(this);
// auto layout = layoutCreator.emplace<QVBoxLayout>().withoutMargin();
// // clang-format off
// layout.append(this->createCheckBox("Enable Twitch emotes",
// settings.enableTwitchEmotes));
// layout.append(this->createCheckBox("Enable BetterTTV emotes",
// settings.enableBttvEmotes));
// layout.append(this->createCheckBox("Enable FrankerFaceZ emotes",
// settings.enableFfzEmotes)); layout.append(this->createCheckBox("Enable
// emojis", settings.enableEmojis));
// layout.append(this->createCheckBox("Enable gif animations",
// settings.enableGifAnimations));
// // clang-format on
// layout->addStretch(1);
}
} // namespace chatterino

View file

@ -1,13 +0,0 @@
#pragma once
#include "widgets/settingspages/SettingsPage.hpp"
namespace chatterino {
class EmotesPage : public SettingsPage
{
public:
EmotesPage();
};
} // namespace chatterino

View file

@ -1,103 +0,0 @@
#include "FeelPage.hpp"
#include "Application.hpp"
#include "util/LayoutCreator.hpp"
#include <QFormLayout>
#include <QGroupBox>
#include <QLabel>
#include <QVBoxLayout>
#define PAUSE_HOVERING "When hovering"
#define SCROLL_SMOOTH "Smooth scrolling"
#define SCROLL_NEWMSG "Smooth scrolling for new messages"
#define LIMIT_CHATTERS_FOR_SMALLER_STREAMERS \
"Only fetch chatters list for viewers under X viewers"
namespace chatterino {
FeelPage::FeelPage()
: SettingsPage("Feel", ":/settings/behave.svg")
{
LayoutCreator<FeelPage> layoutCreator(this);
auto layout = layoutCreator.setLayoutType<QVBoxLayout>();
// layout.append(this->createCheckBox("Use a seperate write connection.",
// getSettings()->twitchSeperateWriteConnection));
layout.append(this->createCheckBox(SCROLL_SMOOTH,
getSettings()->enableSmoothScrolling));
layout.append(this->createCheckBox(
SCROLL_NEWMSG, getSettings()->enableSmoothScrollingNewMessages));
auto form = layout.emplace<QFormLayout>().withoutMargin();
{
form->addRow(
"", this->createCheckBox(
"Show which users joined the channel (up to 1000 chatters)",
getSettings()->showJoins));
form->addRow(
"", this->createCheckBox(
"Show which users parted the channel (up to 1000 chatters)",
getSettings()->showParts));
form->addRow("Pause chat:",
this->createCheckBox(PAUSE_HOVERING,
getSettings()->pauseChatOnHover));
form->addRow("Mouse scroll speed:", this->createMouseScrollSlider());
form->addRow("Links:",
this->createCheckBox("Open links only on double click",
getSettings()->linksDoubleClickOnly));
form->addRow("", this->createCheckBox("Show link info in tooltips",
getSettings()->linkInfoTooltip));
form->addRow(
"", this->createCheckBox("Auto unshort links (requires restart)",
getSettings()->unshortLinks));
}
layout->addSpacing(16);
{
auto group = layout.emplace<QGroupBox>("Auto-completion");
auto groupLayout = group.setLayoutType<QFormLayout>();
groupLayout->addRow(
LIMIT_CHATTERS_FOR_SMALLER_STREAMERS,
this->createCheckBox(
"", getSettings()->onlyFetchChattersForSmallerStreamers));
groupLayout->addRow(
"What viewer count counts as a \"smaller streamer\"",
this->createSpinBox(getSettings()->smallStreamerLimit, 10, 50000));
}
{
auto group = layout.emplace<QGroupBox>("Misc");
auto groupLayout = group.setLayoutType<QVBoxLayout>();
groupLayout.append(this->createCheckBox("Show whispers inline",
getSettings()->inlineWhispers));
}
layout->addStretch(1);
}
QSlider *FeelPage::createMouseScrollSlider()
{
auto slider = new QSlider(Qt::Horizontal);
float currentValue = getSettings()->mouseScrollMultiplier;
int sliderValue = int(((currentValue - 0.1f) / 2.f) * 99.f);
slider->setValue(sliderValue);
QObject::connect(slider, &QSlider::valueChanged, [=](int newValue) {
float mul = static_cast<float>(newValue) / 99.f;
float newSliderValue = (mul * 2.1f) + 0.1f;
getSettings()->mouseScrollMultiplier = newSliderValue;
});
return slider;
}
} // namespace chatterino

View file

@ -1,18 +0,0 @@
#pragma once
#include <QSlider>
#include "widgets/settingspages/SettingsPage.hpp"
namespace chatterino {
class FeelPage : public SettingsPage
{
public:
FeelPage();
private:
QSlider *createMouseScrollSlider();
};
} // namespace chatterino

View file

@ -11,6 +11,7 @@
#include "singletons/WindowManager.hpp"
#include "util/FuzzyConvert.hpp"
#include "util/Helpers.hpp"
#include "util/IncognitoBrowser.hpp"
#include "widgets/BaseWindow.hpp"
#include "widgets/helper/Line.hpp"
@ -20,36 +21,21 @@
#define FIREFOX_EXTENSION_LINK \
"https://addons.mozilla.org/en-US/firefox/addon/chatterino-native-host/"
#define addTitle addTitle
namespace chatterino {
namespace {
QPushButton *makeOpenSettingDirButton()
{
auto button = new QPushButton("Open settings directory");
QObject::connect(button, &QPushButton::clicked, [] {
QDesktopServices::openUrl(getPaths()->rootAppDataDirectory);
});
return button;
}
} // namespace
TitleLabel *SettingsLayout::addTitle(const QString &title)
{
auto label = new TitleLabel(title + ":");
if (this->count() != 0)
this->addSpacing(16);
if (this->count() == 0)
label->setStyleSheet("margin-top: 0");
this->addWidget(label);
return label;
}
TitleLabel2 *SettingsLayout::addTitle2(const QString &title)
{
auto label = new TitleLabel2(title + ":");
// groups
this->groups_.push_back(Group{title, label, {}});
this->addSpacing(16);
this->addWidget(label);
return label;
}
@ -71,6 +57,10 @@ QCheckBox *SettingsLayout::addCheckbox(const QString &text,
[&setting, inverse](bool state) { setting = inverse ^ state; });
this->addWidget(check);
// groups
this->groups_.back().widgets.push_back({check, {text}});
return check;
}
@ -82,11 +72,17 @@ ComboBox *SettingsLayout::addDropdown(const QString &text,
combo->setFocusPolicy(Qt::StrongFocus);
combo->addItems(list);
layout->addWidget(new QLabel(text + ":"));
auto label = new QLabel(text + ":");
layout->addWidget(label);
layout->addStretch(1);
layout->addWidget(combo);
this->addLayout(layout);
// groups
this->groups_.back().widgets.push_back({combo, {text}});
this->groups_.back().widgets.push_back({label, {text}});
return combo;
}
@ -124,6 +120,9 @@ DescriptionLabel *SettingsLayout::addDescription(const QString &text)
this->addWidget(label);
// groups
this->groups_.back().widgets.push_back({label, {text}});
return label;
}
@ -132,6 +131,64 @@ void SettingsLayout::addSeperator()
this->addWidget(new Line(false));
}
bool SettingsLayout::filterElements(const QString &query)
{
bool any{};
for (auto &&group : this->groups_)
{
// if a description in a group matches `query` then show the entire group
bool descriptionMatches{};
for (auto &&widget : group.widgets)
{
if (auto x = dynamic_cast<DescriptionLabel *>(widget.element); x)
{
if (x->text().contains(query, Qt::CaseInsensitive))
{
descriptionMatches = true;
break;
}
}
}
// if group name matches then all should be visible
if (group.name.contains(query, Qt::CaseInsensitive) ||
descriptionMatches)
{
for (auto &&widget : group.widgets)
widget.element->show();
group.title->show();
any = true;
}
// check if any match
else
{
auto groupAny = false;
for (auto &&widget : group.widgets)
{
for (auto &&keyword : widget.keywords)
{
if (keyword.contains(query, Qt::CaseInsensitive))
{
widget.element->show();
groupAny = true;
}
else
{
widget.element->hide();
}
}
}
group.title->setVisible(groupAny);
any |= groupAny;
}
}
return any;
}
GeneralPage::GeneralPage()
: SettingsPage("General", ":/settings/about.svg")
{
@ -141,6 +198,7 @@ GeneralPage::GeneralPage()
y->addWidget(scroll);
auto x = new QHBoxLayout;
auto layout = new SettingsLayout;
this->settingsLayout_ = layout;
x->addLayout(layout, 0);
x->addStretch(1);
auto z = new QFrame;
@ -155,6 +213,16 @@ GeneralPage::GeneralPage()
this->initExtra();
}
bool GeneralPage::filterElements(const QString &query)
{
if (this->settingsLayout_)
return this->settingsLayout_->filterElements(query) ||
this->name_.contains(query, Qt::CaseInsensitive) ||
query.isEmpty();
else
return false;
}
void GeneralPage::initLayout(SettingsLayout &layout)
{
auto &s = *getSettings();
@ -220,10 +288,6 @@ void GeneralPage::initLayout(SettingsLayout &layout)
// layout.addCheckbox("Mark last message you read");
// layout.addDropdown("Last read message style", {"Default"});
layout.addCheckbox("Show deleted messages", s.hideModerated, true);
layout.addCheckbox("Show moderation messages", s.hideModerationActions,
true);
layout.addCheckbox("Random username color for users who never set a color",
s.colorizeNicknames);
layout.addDropdown<QString>(
"Timestamps", {"Disable", "h:mm", "hh:mm", "h:mm a", "hh:mm a"},
s.timestampFormat,
@ -268,6 +332,11 @@ void GeneralPage::initLayout(SettingsLayout &layout)
return QString::number(val) + "x";
},
[](auto args) { return fuzzyToFloat(args.value, 1.f); });
layout.addDropdown<int>(
"Preview on hover", {"Don't show", "Always show", "Hold shift"},
s.emotesTooltipPreview, [](int index) { return index; },
[](auto args) { return args.index; }, false);
layout.addDropdown("Emoji set",
{"EmojiOne 2", "EmojiOne 3", "Twitter", "Facebook",
"Apple", "Google", "Messenger"},
@ -284,15 +353,40 @@ void GeneralPage::initLayout(SettingsLayout &layout)
layout.addCheckbox("Chatterino", getSettings()->showBadgesChatterino);
layout.addTitle("Chat title");
layout.addWidget(new QLabel("In live channels show:"));
layout.addDescription("In live channels show:");
layout.addCheckbox("Uptime", s.headerUptime);
layout.addCheckbox("Viewer count", s.headerViewerCount);
layout.addCheckbox("Category", s.headerGame);
layout.addCheckbox("Title", s.headerStreamTitle);
layout.addTitle("Beta");
layout.addDescription(
"You can receive updates earlier by ticking the box below. Report "
"issues <a href='https://chatterino.com/link/issues'>here</a>.");
layout.addCheckbox("Receive beta updates", s.betaUpdates);
#ifdef Q_OS_WIN
layout.addTitle("Browser Integration");
layout.addDescription("The browser extension replaces the default "
"Twitch.tv chat with chatterino.");
layout.addDescription(
createNamedLink(CHROME_EXTENSION_LINK, "Download for Google Chrome"));
layout.addDescription(
createNamedLink(FIREFOX_EXTENSION_LINK, "Download for Firefox"));
#endif
layout.addTitle("Miscellaneous");
//layout.addWidget(makeOpenSettingDirButton());
if (supportsIncognitoLinks())
{
layout.addCheckbox("Open links in incognito/private mode",
s.openLinksIncognito);
}
layout.addCheckbox("Show moderation messages", s.hideModerationActions,
true);
layout.addCheckbox("Random username color for users who never set a color",
s.colorizeNicknames);
layout.addCheckbox("Mention users with a comma (User,)",
s.mentionUsersWithComma);
layout.addCheckbox("Show joined users (< 1000 chatters)", s.showJoins);
@ -314,11 +408,6 @@ void GeneralPage::initLayout(SettingsLayout &layout)
s.linksDoubleClickOnly);
layout.addCheckbox("Unshorten links", s.unshortLinks);
layout.addCheckbox("Show live indicator in tabs", s.showTabLive);
layout.addDropdown<int>("Show emote preview in tooltip on hover",
{"Don't show", "Always show", "Hold shift"},
s.emotesTooltipPreview,
[](int index) { return index; },
[](auto args) { return args.index; }, false);
layout.addCheckbox(
"Only search for emote autocompletion at the start of emote names",
@ -330,25 +419,57 @@ void GeneralPage::initLayout(SettingsLayout &layout)
layout.addCheckbox("Load message history on connect",
s.loadTwitchMessageHistoryOnConnect);
#ifdef Q_OS_WIN
layout.addTitle("Browser Integration");
layout.addDescription("The browser extension replaces the default "
"Twitch.tv chat with chatterino.");
layout.addTitle("Cache");
layout.addDescription(
"Files that are used often (such as emotes) are saved to disk to "
"reduce bandwidth usage and tho speed up loading.");
layout.addDescription(
createNamedLink(CHROME_EXTENSION_LINK, "Download for Google Chrome"));
layout.addDescription(
createNamedLink(FIREFOX_EXTENSION_LINK, "Download for Firefox"));
#endif
} // namespace chatterino
auto cachePathLabel = layout.addDescription("placeholder :D");
getSettings()->cachePath.connect([cachePathLabel](const auto &,
auto) mutable {
QString newPath = getPaths()->cacheDirectory();
QString pathShortened = "Cache saved at <a href=\"file:///" + newPath +
"\"><span style=\"color: white;\">" +
shortenString(newPath, 50) + "</span></a>";
cachePathLabel->setText(pathShortened);
cachePathLabel->setToolTip(newPath);
});
// Choose and reset buttons
{
auto box = new QHBoxLayout;
box->addWidget(layout.makeButton("Choose cache path", [this]() {
getSettings()->cachePath = QFileDialog::getExistingDirectory(this);
}));
box->addWidget(layout.makeButton(
"Reset", []() { getSettings()->cachePath = ""; }));
box->addStretch(1);
layout.addLayout(box);
}
layout.addTitle("AppData");
layout.addDescription("All local files like settings and cache files are "
"store in this directory.");
layout.addButton("Open AppData directory", [] {
QDesktopServices::openUrl(getPaths()->rootAppDataDirectory);
});
// invisible element for width
auto inv = new BaseWidget(this);
inv->setScaleIndependantWidth(500);
layout.addWidget(inv);
}
void GeneralPage::initExtra()
{
/// update cache path
if (this->cachePath)
if (this->cachePath_)
{
getSettings()->cachePath.connect(
[cachePath = this->cachePath](const auto &, auto) mutable {
[cachePath = this->cachePath_](const auto &, auto) mutable {
QString newPath = getPaths()->cacheDirectory();
QString pathShortened = "Current location: <a href=\"file:///" +

View file

@ -28,17 +28,6 @@ public:
}
};
class TitleLabel2 : public QLabel
{
Q_OBJECT
public:
TitleLabel2(const QString &text)
: QLabel(text)
{
}
};
class DescriptionLabel : public QLabel
{
Q_OBJECT
@ -71,7 +60,6 @@ class SettingsLayout : public QVBoxLayout
public:
TitleLabel *addTitle(const QString &text);
TitleLabel2 *addTitle2(const QString &text);
/// @param inverse Inverses true to false and vice versa
QCheckBox *addCheckbox(const QString &text, BoolSetting &setting,
bool inverse = false);
@ -80,6 +68,26 @@ public:
pajlada::Settings::Setting<QString> &setting,
bool editable = false);
template <typename OnClick>
QPushButton *makeButton(const QString &text, OnClick onClick)
{
auto button = new QPushButton(text);
this->groups_.back().widgets.push_back({button, {text}});
QObject::connect(button, &QPushButton::clicked, onClick);
return button;
}
template <typename OnClick>
QPushButton *addButton(const QString &text, OnClick onClick)
{
auto button = makeButton(text, onClick);
auto layout = new QHBoxLayout();
layout->addWidget(button);
layout->addStretch(1);
this->addLayout(layout);
return button;
}
template <typename T>
ComboBox *addDropdown(
const QString &text, const QStringList &items,
@ -141,9 +149,23 @@ public:
return combo;
}
DescriptionLabel *addDescription(const QString &text);
void addSeperator();
bool filterElements(const QString &query);
private:
struct Widget {
QWidget *element;
QStringList keywords;
};
struct Group {
QString name;
QWidget *title{};
std::vector<Widget> widgets;
};
std::vector<Group> groups_;
std::vector<pajlada::Signals::ScopedConnection> managedConnections_;
};
@ -154,13 +176,16 @@ class GeneralPage : public SettingsPage
public:
GeneralPage();
bool filterElements(const QString &query);
private:
void initLayout(SettingsLayout &layout);
void initExtra();
QString getFont(const DropdownArgs &args) const;
DescriptionLabel *cachePath{};
DescriptionLabel *cachePath_{};
SettingsLayout *settingsLayout_{};
};
} // namespace chatterino

View file

@ -57,7 +57,7 @@ HighlightingPage::HighlightingPage()
view->addRegexHelpLink();
view->setTitles({"Pattern", "Flash\ntaskbar", "Play\nsound",
"Enable\nregex"});
"Enable\nregex", "Case-\nsensitive"});
view->getTableView()->horizontalHeader()->setSectionResizeMode(
QHeaderView::Fixed);
view->getTableView()->horizontalHeader()->setSectionResizeMode(
@ -71,8 +71,8 @@ HighlightingPage::HighlightingPage()
});
view->addButtonPressed.connect([] {
getApp()->highlights->phrases.appendItem(
HighlightPhrase{"my phrase", true, false, false});
getApp()->highlights->phrases.appendItem(HighlightPhrase{
"my phrase", true, false, false, false});
});
}
@ -87,6 +87,10 @@ HighlightingPage::HighlightingPage()
.getElement();
view->addRegexHelpLink();
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
view->setTitles({"Username", "Flash\ntaskbar", "Play\nsound",
"Enable\nregex"});
view->getTableView()->horizontalHeader()->setSectionResizeMode(
@ -103,7 +107,7 @@ HighlightingPage::HighlightingPage()
view->addButtonPressed.connect([] {
getApp()->highlights->highlightedUsers.appendItem(
HighlightPhrase{"highlighted user", true, false,
HighlightPhrase{"highlighted user", true, false, false,
false});
});
}

View file

@ -13,7 +13,7 @@ KeyboardSettingsPage::KeyboardSettingsPage()
auto layout =
LayoutCreator<KeyboardSettingsPage>(this).setLayoutType<QVBoxLayout>();
auto form = layout.emplace<QFormLayout>();
auto form = layout.emplace<QFormLayout>().withoutMargin();
form->addRow(new QLabel("Hold Ctrl"), new QLabel("Show resize handles"));
form->addRow(new QLabel("Hold Ctrl + Alt"),

View file

@ -1,53 +0,0 @@
//#include "LogsPage.hpp"
//#include "Application.hpp"
//#include "singletons/PathManager.hpp"
//#include <QFormLayout>
//#include <QVBoxLayout>
//#include "util/LayoutCreator.hpp"
// namespace chatterino {
// namespace widgets {
// namespace settingspages {
// inline QString CreateLink(const QString &url, bool file = false)
//{
// if (file) {
// return QString("<a href=\"file:///" + url + "\"><span style=\"color:
// white;\">" + url +
// "</span></a>");
// }
// return QString("<a href=\"" + url + "\"><span style=\"color: white;\">" +
// url +
// "</span></a>");
//}
// LogsPage::LogsPage()
// : SettingsPage("Logs", "")
//{
// auto app = getApp();
// LayoutCreator<LogsPage> layoutCreator(this);
// auto layout = layoutCreator.emplace<QVBoxLayout>().withoutMargin();
// auto logPath = getPaths()->logsFolderPath;
// auto created = layout.emplace<QLabel>();
// created->setText("Logs are saved to " + CreateLink(logPath, true));
// created->setTextFormat(Qt::RichText);
// created->setTextInteractionFlags(Qt::TextBrowserInteraction |
// Qt::LinksAccessibleByKeyboard |
// Qt::LinksAccessibleByKeyboard);
// created->setOpenExternalLinks(true);
// layout.append(this->createCheckBox("Enable logging",
// getSettings()->enableLogging));
// layout->addStretch(1);
//}
//} // namespace settingspages
//} // namespace widgets
//} // namespace chatterino

View file

@ -1,17 +0,0 @@
//#pragma once
//#include "widgets/settingspages/SettingsPage.hpp"
// namespace chatterino {
// namespace widgets {
// namespace settingspages {
// class LogsPage : public SettingsPage
//{
// public:
// LogsPage();
//};
//} // namespace settingspages
//} // namespace widgets
//} // namespace chatterino

View file

@ -1,596 +0,0 @@
#include "LookPage.hpp"
#include "Application.hpp"
#include "messages/Image.hpp"
#include "messages/MessageBuilder.hpp"
#include "singletons/Resources.hpp"
#include "singletons/Theme.hpp"
#include "singletons/WindowManager.hpp"
#include "util/LayoutCreator.hpp"
#include "util/RemoveScrollAreaBackground.hpp"
#include "widgets/helper/ChannelView.hpp"
#include "widgets/helper/Line.hpp"
#include <QColorDialog>
#include <QFontDialog>
#include <QFormLayout>
#include <QGroupBox>
#include <QLabel>
#include <QPushButton>
#include <QScrollArea>
#include <QSlider>
#include <QVBoxLayout>
#define THEME_ITEMS "White", "Light", "Dark", "Black", "Custom"
#define TAB_X "Show tab close button"
#define TAB_PREF "Hide preferences button (ctrl+p to show)"
#define TAB_USER "Hide user button"
// clang-format off
#define TIMESTAMP_FORMATS "hh:mm a", "h:mm a", "hh:mm:ss a", "h:mm:ss a", "HH:mm", "H:mm", "HH:mm:ss", "H:mm:ss"
// clang-format on
#ifdef USEWINSDK
# define WINDOW_TOPMOST "Window always on top"
#else
# define WINDOW_TOPMOST "Window always on top (requires restart)"
#endif
#define INPUT_EMPTY "Show input box when empty"
#define LAST_MSG "Mark the last message you read"
namespace chatterino {
LookPage::LookPage()
: SettingsPage("Look", ":/settings/theme.svg")
{
this->initializeUi();
}
void LookPage::initializeUi()
{
LayoutCreator<LookPage> layoutCreator(this);
auto layout = layoutCreator.emplace<QVBoxLayout>().withoutMargin();
// settings
auto tabs = layout.emplace<QTabWidget>();
this->addInterfaceTab(tabs.appendTab(new QVBoxLayout, "Interface"));
this->addMessageTab(tabs.appendTab(new QVBoxLayout, "Messages"));
this->addEmoteTab(tabs.appendTab(new QVBoxLayout, "Emotes"));
this->addSplitHeaderTab(tabs.appendTab(new QVBoxLayout, "Split header"));
this->addBadgesTab(tabs.appendTab(new QVBoxLayout, "Badges"));
layout->addStretch(1);
// preview
layout.emplace<Line>(false);
auto channelView = layout.emplace<ChannelView>();
auto channel = this->createPreviewChannel();
channelView->setChannel(channel);
channelView->setScaleIndependantHeight(74);
}
void LookPage::addInterfaceTab(LayoutCreator<QVBoxLayout> layout)
{
// theme
{
auto *theme =
this->createComboBox({THEME_ITEMS}, getApp()->themes->themeName);
QDoubleSpinBox *w = new QDoubleSpinBox;
QObject::connect(theme, &QComboBox::currentTextChanged,
[w](const QString &themeName) {
if (themeName == "Custom")
{
w->show();
}
else
{
w->hide();
}
getApp()->windows->forceLayoutChannelViews();
});
auto box = layout.emplace<QHBoxLayout>().withoutMargin();
box.emplace<QLabel>("Theme: ");
box.append(theme);
{
w->setButtonSymbols(QDoubleSpinBox::NoButtons);
if (getApp()->themes->themeName.getValue() != "Custom")
{
w->hide();
}
else
{
w->show();
}
w->setRange(-1.0, 1.0);
w->setSingleStep(0.05);
w->setValue(getSettings()->customThemeMultiplier.getValue());
QObject::connect(
w, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
[](double value) {
getSettings()->customThemeMultiplier.setValue(float(value));
getApp()->themes->update();
getApp()->windows->forceLayoutChannelViews();
});
box.append(w);
}
box->addStretch(1);
}
layout.append(
this->createCheckBox(WINDOW_TOPMOST, getSettings()->windowTopMost));
// --
layout.emplace<Line>(false);
// tab x
layout.append(
this->createCheckBox(TAB_X, getSettings()->showTabCloseButton));
// show buttons
#ifndef USEWINSDK
layout.append(
this->createCheckBox(TAB_PREF, getSettings()->hidePreferencesButton));
layout.append(
this->createCheckBox(TAB_USER, getSettings()->hideUserButton));
#endif
// empty input
layout.append(
this->createCheckBox(INPUT_EMPTY, getSettings()->showEmptyInput));
layout.append(this->createCheckBox("Show message length while typing",
getSettings()->showMessageLength));
layout->addStretch(1);
}
void LookPage::addMessageTab(LayoutCreator<QVBoxLayout> layout)
{
// font
layout.append(this->createFontChanger());
// --
layout.emplace<Line>(false);
// timestamps
{
auto box = layout.emplace<QHBoxLayout>().withoutMargin();
box.append(this->createCheckBox("Show timestamps",
getSettings()->showTimestamps));
box.append(this->createComboBox({TIMESTAMP_FORMATS},
getSettings()->timestampFormat));
box->addStretch(1);
}
// --
layout.emplace<Line>(false);
// separate
layout.append(this->createCheckBox("Lines between messages",
getSettings()->separateMessages));
// alternate
layout.append(this->createCheckBox("Alternate background",
getSettings()->alternateMessages));
layout.append(
this->createCheckBox("Compact emotes", getSettings()->compactEmotes));
layout.emplace<Line>(false);
// bold-slider
{
auto box = layout.emplace<QHBoxLayout>().withoutMargin();
box.emplace<QLabel>("Username boldness: ");
box.append(this->createBoldScaleSlider());
}
// bold usernames
layout.append(this->createCheckBox("Bold mentions (@username)",
getSettings()->boldUsernames));
// --
layout.emplace<Line>(false);
// lowercase links
layout.append(this->createCheckBox("Lowercase domains",
getSettings()->lowercaseDomains));
// collapsing
{
auto *combo = new QComboBox(this);
combo->addItems({"Never", "2", "3", "4", "5", "6", "7", "8", "9", "10",
"11", "12", "13", "14", "15"});
const auto currentIndex = []() -> int {
auto val = getSettings()->collpseMessagesMinLines.getValue();
if (val > 0)
{
--val;
}
return val;
}();
combo->setCurrentIndex(currentIndex);
QObject::connect(
combo, &QComboBox::currentTextChanged, [](const QString &str) {
getSettings()->collpseMessagesMinLines = str.toInt();
});
auto hbox = layout.emplace<QHBoxLayout>().withoutMargin();
hbox.emplace<QLabel>("Collapse messages longer than");
hbox.append(combo);
hbox.emplace<QLabel>("lines");
}
// last read message
this->addLastReadMessageIndicatorPatternSelector(layout);
// --
layout->addStretch(1);
}
void LookPage::addEmoteTab(LayoutCreator<QVBoxLayout> layout)
{
layout.append(
this->createCheckBox("Animations", getSettings()->animateEmotes));
layout.append(
this->createCheckBox("Animations only when chatterino has focus",
getSettings()->animationsWhenFocused));
auto scaleBox = layout.emplace<QHBoxLayout>().withoutMargin();
{
scaleBox.emplace<QLabel>("Size:");
auto emoteScale = scaleBox.emplace<QSlider>(Qt::Horizontal);
emoteScale->setMinimum(5);
emoteScale->setMaximum(50);
auto scaleLabel = scaleBox.emplace<QLabel>("1.0");
scaleLabel->setFixedWidth(100);
QObject::connect(emoteScale.getElement(), &QSlider::valueChanged,
[scaleLabel](int value) mutable {
float f = float(value) / 10.f;
scaleLabel->setText(QString::number(f));
getSettings()->emoteScale.setValue(f);
});
emoteScale->setValue(std::max<int>(
5, std::min<int>(
50, int(getSettings()->emoteScale.getValue() * 10.f))));
scaleLabel->setText(
QString::number(getSettings()->emoteScale.getValue()));
}
{
auto *combo = new QComboBox(this);
combo->addItems({"EmojiOne 2", "EmojiOne 3", "Twitter", "Facebook",
"Apple", "Google", "Messenger"});
combo->setCurrentText(getSettings()->emojiSet);
QObject::connect(combo, &QComboBox::currentTextChanged,
[](const QString &str) {
getSettings()->emojiSet = str; //
});
auto hbox = layout.emplace<QHBoxLayout>().withoutMargin();
hbox.emplace<QLabel>("Emoji set:");
hbox.append(combo);
}
layout->addStretch(1);
}
void LookPage::addSplitHeaderTab(LayoutCreator<QVBoxLayout> layout)
{
layout.append(
this->createCheckBox("Show uptime", getSettings()->headerUptime));
layout.append(this->createCheckBox("Show viewer count",
getSettings()->headerViewerCount));
layout.append(this->createCheckBox("Show game", getSettings()->headerGame));
layout.append(
this->createCheckBox("Show title", getSettings()->headerStreamTitle));
layout->addStretch(1);
}
void LookPage::addBadgesTab(LayoutCreator<QVBoxLayout> layout)
{
// layout.append(
// this->createCheckBox(("Show all badges"), getSettings()->showBadges));
auto fastSelection = layout.emplace<QHBoxLayout>();
{
auto addAll = fastSelection.emplace<QPushButton>("Enable all");
QObject::connect(addAll.getElement(), &QPushButton::clicked, this, [] {
getSettings()->showBadgesGlobalAuthority = true;
getSettings()->showBadgesChannelAuthority = true;
getSettings()->showBadgesSubscription = true;
getSettings()->showBadgesVanity = true;
getSettings()->showBadgesChatterino = true;
});
auto removeAll = fastSelection.emplace<QPushButton>("Disable all");
QObject::connect(removeAll.getElement(), &QPushButton::clicked, this,
[] {
getSettings()->showBadgesGlobalAuthority = false;
getSettings()->showBadgesChannelAuthority = false;
getSettings()->showBadgesSubscription = false;
getSettings()->showBadgesVanity = false;
getSettings()->showBadgesChatterino = false;
});
}
layout.emplace<Line>(false);
layout.append(this->createCheckBox(
("Show authority badges (staff, admin, turbo, etc)"),
getSettings()->showBadgesGlobalAuthority));
layout.append(this->createCheckBox(
("Show channel badges (broadcaster, moderator, VIP)"),
getSettings()->showBadgesChannelAuthority));
layout.append(this->createCheckBox(("Show subscriber badges "),
getSettings()->showBadgesSubscription));
layout.append(
this->createCheckBox(("Show vanity badges (prime, bits, subgifter)"),
getSettings()->showBadgesVanity));
layout.append(this->createCheckBox(("Show chatterino badges"),
getSettings()->showBadgesChatterino));
layout->addStretch(1);
}
void LookPage::addLastReadMessageIndicatorPatternSelector(
LayoutCreator<QVBoxLayout> layout)
{
// combo
auto *combo = new QComboBox(this);
combo->addItems({"Dotted line", "Solid line"});
const auto currentIndex = []() -> int {
switch (getSettings()->lastMessagePattern.getValue())
{
case Qt::SolidLine:
return 1;
case Qt::VerPattern:
default:
return 0;
}
}();
combo->setCurrentIndex(currentIndex);
QObject::connect(
combo,
static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
[](int index) {
getSettings()->lastMessagePattern = [&] {
switch (index)
{
case 1:
return Qt::SolidPattern;
case 0:
default:
return Qt::VerPattern;
}
}();
});
// color picker
QLabel *colorPreview = new QLabel();
auto updatePreviewColor = [colorPreview](QColor newColor) {
QPixmap pixmap(16, 16);
pixmap.fill(QColor(0, 0, 0, 255));
QPainter painter(&pixmap);
QBrush brush(newColor);
painter.fillRect(1, 1, pixmap.width() - 2, pixmap.height() - 2, brush);
colorPreview->setPixmap(pixmap);
};
auto getCurrentColor = []() {
return getSettings()->lastMessageColor != ""
? QColor(getSettings()->lastMessageColor.getValue())
: getApp()
->themes->tabs.selected.backgrounds.regular.color();
};
updatePreviewColor(getCurrentColor());
QPushButton *button = new QPushButton("Select Color");
button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Policy::Fixed);
QObject::connect(
button, &QPushButton::clicked, [updatePreviewColor, getCurrentColor]() {
QColor newColor = QColorDialog::getColor(getCurrentColor());
if (newColor.isValid())
{
updatePreviewColor(newColor);
getSettings()->lastMessageColor = newColor.name();
}
});
QPushButton *resetButton = new QPushButton("Reset Color");
resetButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Policy::Fixed);
QObject::connect(
resetButton, &QPushButton::clicked, [updatePreviewColor]() {
QColor defaultColor =
getApp()->themes->tabs.selected.backgrounds.regular.color();
updatePreviewColor(defaultColor);
getSettings()->lastMessageColor = "";
});
// layout
auto hbox = layout.emplace<QHBoxLayout>().withoutMargin();
hbox.append(this->createCheckBox(LAST_MSG,
getSettings()->showLastMessageIndicator));
hbox.append(combo);
hbox.append(colorPreview);
hbox.append(button);
hbox.append(resetButton);
hbox->addStretch(1);
}
ChannelPtr LookPage::createPreviewChannel()
{
auto channel = ChannelPtr(new Channel("preview", Channel::Type::Misc));
// clang-format off
{
MessageBuilder builder;
builder.emplace<TimestampElement>(QTime(8, 13, 42));
builder.emplace<ImageElement>(Image::fromPixmap(getApp()->resources->twitch.moderator), MessageElementFlag::BadgeChannelAuthority);
builder.emplace<ImageElement>(Image::fromPixmap(getApp()->resources->twitch.subscriber, 0.25), MessageElementFlag::BadgeSubscription);
builder.emplace<TextElement>("username1:", MessageElementFlag::Username, QColor("#0094FF"), FontStyle::ChatMediumBold);
builder.emplace<TextElement>("This is a preview message", MessageElementFlag::Text);
builder.emplace<ImageElement>(Image::fromPixmap(getApp()->resources->pajaDank, 0.25), MessageElementFlag::AlwaysShow);
builder.emplace<TextElement>("@fourtf", MessageElementFlag::BoldUsername, MessageColor::Text, FontStyle::ChatMediumBold);
builder.emplace<TextElement>("@fourtf", MessageElementFlag::NonBoldUsername);
channel->addMessage(builder.release());
}
{
MessageBuilder message;
message.emplace<TimestampElement>(QTime(8, 15, 21));
message.emplace<ImageElement>(Image::fromPixmap(getApp()->resources->twitch.broadcaster), MessageElementFlag::BadgeChannelAuthority);
message.emplace<TextElement>("username2:", MessageElementFlag::Username, QColor("#FF6A00"), FontStyle::ChatMediumBold);
message.emplace<TextElement>("This is another one", MessageElementFlag::Text);
// message.emplace<ImageElement>(Image::fromNonOwningPixmap(&getApp()->resources->ppHop), MessageElementFlag::BttvEmote);
message.emplace<TextElement>("www.fourtf.com", MessageElementFlag::LowercaseLink, MessageColor::Link)->setLink(Link(Link::Url, "https://www.fourtf.com"));
message.emplace<TextElement>("wWw.FoUrTf.CoM", MessageElementFlag::OriginalLink, MessageColor::Link)->setLink(Link(Link::Url, "https://www.fourtf.com"));
channel->addMessage(message.release());
}
// clang-format on
return channel;
}
QLayout *LookPage::createThemeColorChanger()
{
auto app = getApp();
QHBoxLayout *layout = new QHBoxLayout;
auto &themeHue = app->themes->themeHue;
// SLIDER
QSlider *slider = new QSlider(Qt::Horizontal);
layout->addWidget(slider);
slider->setValue(
int(std::min(std::max(themeHue.getValue(), 0.0), 1.0) * 100));
// BUTTON
QPushButton *button = new QPushButton;
layout->addWidget(button);
button->setFlat(true);
button->setFixedWidth(64);
auto setButtonColor = [button, app](int value) mutable {
double newValue = value / 100.0;
app->themes->themeHue.setValue(newValue);
QPalette pal = button->palette();
QColor color;
color.setHsvF(newValue, 1.0, 1.0, 1.0);
pal.setColor(QPalette::Button, color);
button->setAutoFillBackground(true);
button->setPalette(pal);
button->update();
};
// SIGNALS
QObject::connect(slider, &QSlider::valueChanged, this, setButtonColor);
setButtonColor(themeHue * 100);
return layout;
}
QLayout *LookPage::createFontChanger()
{
auto app = getApp();
QHBoxLayout *layout = new QHBoxLayout;
layout->setMargin(0);
// LABEL
QLabel *label = new QLabel();
layout->addWidget(label);
auto updateFontFamilyLabel = [=]() {
label->setText("Font (" + app->fonts->chatFontFamily.getValue() + ", " +
QString::number(app->fonts->chatFontSize) + "pt)");
};
app->fonts->chatFontFamily.connect(updateFontFamilyLabel,
this->managedConnections_);
app->fonts->chatFontSize.connect(updateFontFamilyLabel,
this->managedConnections_);
// BUTTON
QPushButton *button = new QPushButton("Select");
layout->addWidget(button);
button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Policy::Fixed);
QObject::connect(button, &QPushButton::clicked, [=]() {
QFontDialog dialog(app->fonts->getFont(FontStyle::ChatMedium, 1.));
dialog.setWindowFlag(Qt::WindowStaysOnTopHint);
dialog.connect(&dialog, &QFontDialog::fontSelected,
[=](const QFont &font) {
app->fonts->chatFontFamily = font.family();
app->fonts->chatFontSize = font.pointSize();
});
dialog.show();
dialog.exec();
});
layout->addStretch(1);
return layout;
}
QLayout *LookPage::createBoldScaleSlider()
{
auto layout = new QHBoxLayout();
auto slider = new QSlider(Qt::Horizontal);
auto label = new QLabel();
layout->addWidget(slider);
layout->addWidget(label);
slider->setMinimum(50);
slider->setMaximum(100);
slider->setValue(getSettings()->boldScale.getValue());
label->setMinimumWidth(100);
QObject::connect(slider, &QSlider::valueChanged, [](auto value) {
getSettings()->boldScale.setValue(value);
});
// show value
// getSettings()->boldScale.connect(
// [label](auto, auto) {
// label->setText(QString::number(getSettings()->boldScale.getValue()));
// },
// this->connections_);
// QPushButton *button = new QPushButton("Reset");
// layout->addWidget(button);
// button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Policy::Fixed);
// QObject::connect(button, &QPushButton::clicked, [=]() {
// getSettings()->boldScale.setValue(57);
// slider->setValue(57);
//});
return layout;
}
} // namespace chatterino

View file

@ -1,40 +0,0 @@
#pragma once
#include "common/Channel.hpp"
#include "util/LayoutCreator.hpp"
#include "widgets/settingspages/SettingsPage.hpp"
#include <QScrollArea>
#include <pajlada/signals/signalholder.hpp>
class QVBoxLayout;
namespace chatterino {
class LookPage : public SettingsPage
{
public:
LookPage();
private:
void initializeUi();
void addInterfaceTab(LayoutCreator<QVBoxLayout> layout);
void addMessageTab(LayoutCreator<QVBoxLayout> layout);
void addEmoteTab(LayoutCreator<QVBoxLayout> layout);
void addSplitHeaderTab(LayoutCreator<QVBoxLayout> layout);
void addBadgesTab(LayoutCreator<QVBoxLayout> layout);
void addLastReadMessageIndicatorPatternSelector(
LayoutCreator<QVBoxLayout> layout);
QLayout *createThemeColorChanger();
QLayout *createFontChanger();
QLayout *createBoldScaleSlider();
ChannelPtr createPreviewChannel();
std::vector<pajlada::Signals::ScopedConnection> connections_;
};
} // namespace chatterino

View file

@ -83,13 +83,10 @@ ModerationPage::ModerationPage()
auto logs = tabs.appendTab(new QVBoxLayout, "Logs");
{
logs.append(this->createCheckBox("Enable logging",
getSettings()->enableLogging));
auto logsPathLabel = logs.emplace<QLabel>();
// Show how big (size-wise) the logs are
auto logsPathSizeLabel = logs.emplace<QLabel>();
logsPathSizeLabel->setText(
QtConcurrent::run([] { return fetchLogDirectorySize(); }));
// Logs (copied from LoggingMananger)
getSettings()->logPath.connect([logsPathLabel](const QString &logPath,
auto) mutable {
@ -109,13 +106,27 @@ ModerationPage::ModerationPage()
logsPathLabel->setTextInteractionFlags(Qt::TextBrowserInteraction |
Qt::LinksAccessibleByKeyboard);
logsPathLabel->setOpenExternalLinks(true);
logs.append(this->createCheckBox("Enable logging",
getSettings()->enableLogging));
auto buttons = logs.emplace<QHBoxLayout>().withoutMargin();
// Select and Reset
auto selectDir = buttons.emplace<QPushButton>("Select log directory ");
auto resetDir = buttons.emplace<QPushButton>("Reset");
getSettings()->logPath.connect(
[element = resetDir.getElement()](const QString &path) {
element->setEnabled(!path.isEmpty());
});
buttons->addStretch();
logs->addStretch(1);
auto selectDir = logs.emplace<QPushButton>("Set custom logpath");
// Setting custom logpath
// Show how big (size-wise) the logs are
auto logsPathSizeLabel = logs.emplace<QLabel>();
logsPathSizeLabel->setText(
QtConcurrent::run([] { return fetchLogDirectorySize(); }));
// Select event
QObject::connect(
selectDir.getElement(), &QPushButton::clicked, this,
[this, logsPathSizeLabel]() mutable {
@ -128,8 +139,9 @@ ModerationPage::ModerationPage()
QtConcurrent::run([] { return fetchLogDirectorySize(); }));
});
buttons->addSpacing(16);
// Reset custom logpath
auto resetDir = logs.emplace<QPushButton>("Reset logpath");
QObject::connect(resetDir.getElement(), &QPushButton::clicked, this,
[logsPathSizeLabel]() mutable {
getSettings()->logPath = "";
@ -139,8 +151,7 @@ ModerationPage::ModerationPage()
[] { return fetchLogDirectorySize(); }));
});
// Logs end
}
} // logs end
auto modMode = tabs.appendTab(new QVBoxLayout, "Moderation buttons");
{

View file

@ -5,15 +5,77 @@
#include <QDebug>
#include <QPainter>
#include <util/FunctionEventFilter.hpp>
namespace chatterino {
bool filterItemsRec(QObject *object, const QString &query)
{
bool any{};
for (auto &&child : object->children())
{
auto setOpacity = [&](auto *widget, bool condition) {
any |= condition;
widget->greyedOut = !condition;
widget->update();
};
if (auto x = dynamic_cast<SCheckBox *>(child); x)
{
setOpacity(x, x->text().contains(query, Qt::CaseInsensitive));
}
else if (auto x = dynamic_cast<SLabel *>(child); x)
{
setOpacity(x, x->text().contains(query, Qt::CaseInsensitive));
}
else if (auto x = dynamic_cast<SComboBox *>(child); x)
{
setOpacity(x, [=]() {
for (int i = 0; i < x->count(); i++)
{
if (x->itemText(i).contains(query, Qt::CaseInsensitive))
return true;
}
return false;
}());
}
else if (auto x = dynamic_cast<QTabWidget *>(child); x)
{
for (int i = 0; i < x->count(); i++)
{
bool tabAny{};
if (x->tabText(i).contains(query, Qt::CaseInsensitive))
{
tabAny = true;
}
auto widget = x->widget(i);
tabAny |= filterItemsRec(widget, query);
any |= tabAny;
}
}
else
{
any |= filterItemsRec(child, query);
}
}
return any;
}
SettingsPage::SettingsPage(const QString &name, const QString &iconResource)
: name_(name)
, iconResource_(iconResource)
{
}
bool SettingsPage::filterElements(const QString &query)
{
return filterItemsRec(this, query) || query.isEmpty() ||
this->name_.contains(query, Qt::CaseInsensitive);
}
const QString &SettingsPage::getName()
{
return this->name_;
@ -42,7 +104,7 @@ void SettingsPage::cancel()
QCheckBox *SettingsPage::createCheckBox(
const QString &text, pajlada::Settings::Setting<bool> &setting)
{
QCheckBox *checkbox = new QCheckBox(text);
QCheckBox *checkbox = new SCheckBox(text);
// update when setting changes
setting.connect(
@ -64,7 +126,7 @@ QCheckBox *SettingsPage::createCheckBox(
QComboBox *SettingsPage::createComboBox(
const QStringList &items, pajlada::Settings::Setting<QString> &setting)
{
QComboBox *combo = new QComboBox();
QComboBox *combo = new SComboBox();
// update setting on toogle
combo->addItems(items);

View file

@ -8,8 +8,38 @@
#include "singletons/Settings.hpp"
#define SETTINGS_PAGE_WIDGET_BOILERPLATE(type, parent) \
class type : public parent \
{ \
using parent::parent; \
\
public: \
bool greyedOut{}; \
\
protected: \
void paintEvent(QPaintEvent *e) override \
{ \
parent::paintEvent(e); \
\
if (this->greyedOut) \
{ \
QPainter painter(this); \
QColor color = QColor("#222222"); \
color.setAlphaF(0.7); \
painter.fillRect(this->rect(), color); \
} \
} \
};
namespace chatterino {
// S* widgets are the same as their Q* counterparts,
// but they can be greyed out and will be if you search.
SETTINGS_PAGE_WIDGET_BOILERPLATE(SCheckBox, QCheckBox)
SETTINGS_PAGE_WIDGET_BOILERPLATE(SLabel, QLabel)
SETTINGS_PAGE_WIDGET_BOILERPLATE(SComboBox, QComboBox)
SETTINGS_PAGE_WIDGET_BOILERPLATE(SPushButton, QPushButton)
class SettingsDialogTab;
class SettingsPage : public QFrame
@ -22,6 +52,8 @@ public:
const QString &getName();
const QString &getIconResource();
virtual bool filterElements(const QString &query);
SettingsDialogTab *tab() const;
void setTab(SettingsDialogTab *tab);

View file

@ -1,34 +0,0 @@
#include "SpecialChannelsPage.hpp"
#include "Application.hpp"
#include "singletons/Settings.hpp"
#include "util/LayoutCreator.hpp"
#include <QGroupBox>
#include <QLabel>
#include <QVBoxLayout>
namespace chatterino {
SpecialChannelsPage::SpecialChannelsPage()
: SettingsPage("Special channels", "")
{
LayoutCreator<SpecialChannelsPage> layoutCreator(this);
auto layout = layoutCreator.setLayoutType<QVBoxLayout>();
auto mentions = layout.emplace<QGroupBox>("Mentions channel")
.setLayoutType<QVBoxLayout>();
{
mentions.emplace<QLabel>("Join /mentions to view your mentions.");
}
auto whispers =
layout.emplace<QGroupBox>("Whispers").setLayoutType<QVBoxLayout>();
{
whispers.emplace<QLabel>("Join /whispers to view your mentions.");
}
layout->addStretch(1);
}
} // namespace chatterino

View file

@ -1,13 +0,0 @@
#pragma once
#include "widgets/settingspages/SettingsPage.hpp"
namespace chatterino {
class SpecialChannelsPage : public SettingsPage
{
public:
SpecialChannelsPage();
};
} // namespace chatterino

View file

@ -89,6 +89,7 @@ Split::Split(QWidget *parent)
{
this->setMouseTracking(true);
this->view_->setPausable(true);
this->view_->setFocusPolicy(Qt::FocusPolicy::NoFocus);
this->vbox_->setSpacing(0);
this->vbox_->setMargin(1);
@ -663,7 +664,10 @@ void Split::reloadChannelAndSubscriberEmotes()
auto channel = this->getChannel();
if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
twitchChannel->refreshChannelEmotes();
{
twitchChannel->refreshBTTVChannelEmotes();
twitchChannel->refreshFFZChannelEmotes();
}
}
template <typename Iter, typename RandomGenerator>

View file

@ -189,7 +189,6 @@ void SplitHeader::initializeLayout()
SettingsDialogPreference::
ModerationActions);
this->split_->setModerationMode(true);
w->setDim(true);
}
else
{
@ -198,7 +197,7 @@ void SplitHeader::initializeLayout()
this->split_->setModerationMode(
!moderationMode);
w->setDim(moderationMode);
w->setDim(Button::Dim(moderationMode));
}
break;
@ -713,7 +712,10 @@ void SplitHeader::reloadChannelEmotes()
auto channel = this->split_->getChannel();
if (auto twitchChannel = dynamic_cast<TwitchChannel *>(channel.get()))
twitchChannel->refreshChannelEmotes();
{
twitchChannel->refreshFFZChannelEmotes();
twitchChannel->refreshBTTVChannelEmotes();
}
}
void SplitHeader::reloadSubscriberEmotes()