Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom image functionality for inline mod buttons. #5369

Merged
merged 42 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
1a9588b
Clean up ModerationAction
Mm2PL Apr 28, 2024
27d3e0a
Add image path to ModerationAction
Mm2PL Apr 28, 2024
f10481a
Allow editing the icon path
Mm2PL Apr 28, 2024
43b5257
Render the emote image in settings dialog
Mm2PL Apr 28, 2024
f2940b1
Move the image loading code to its own file
Mm2PL Apr 28, 2024
8b9cfb3
Update the image when selecting a new file
Mm2PL Apr 28, 2024
5450161
Allow for more file types
Mm2PL Apr 28, 2024
ec695b4
Changelog
Mm2PL Apr 28, 2024
2b6bac6
Include QUrl in ModerationAction.cpp
Mm2PL Apr 29, 2024
1de6306
Add a whole ton more includes
Mm2PL Apr 29, 2024
be3b595
Move changelog
Mm2PL May 1, 2024
43b4057
Add table header for mod action icon
Mm2PL May 1, 2024
569b24e
Specify ModerationActionModel::Column values
Mm2PL May 1, 2024
0c9b6c3
Document loadPixmapFromUrlLazy
Mm2PL May 1, 2024
2235418
Use loadPixmapFromUrlLazy in TwitchBadges
Mm2PL May 1, 2024
baa4296
Return image if we already have it in ModerationAction
Mm2PL May 1, 2024
a2981e8
Don't translate
Mm2PL May 1, 2024
0aad901
Change error texts and reformat in LoadPixmapLazy.cpp
Mm2PL May 1, 2024
f511abb
Send pixmap to main thread
Mm2PL May 1, 2024
673d427
Rename loadPixmapFromUrl{Lazy,}
Mm2PL May 1, 2024
d9ff6ca
Document callback param
Mm2PL May 1, 2024
ca5b62d
Correct callback in ModerationPage
Mm2PL May 1, 2024
8e62136
Merge branch 'master' of github.com:Chatterino/chatterino2 into featu…
Mm2PL May 1, 2024
6e4722f
Remove useless const_cast
Mm2PL May 3, 2024
918076d
Get rid of None enum value
Mm2PL May 4, 2024
0d27299
rename BuiltInImage to ActionIconType and TrashCan to Delete
Mm2PL May 4, 2024
af30aac
Add tests ensuring moderationaction parses ban/delete/timeouts properly
pajlada May 4, 2024
f28c97c
Make the icon type a full action type
pajlada May 4, 2024
62730db
Remove unused stuff from tests
pajlada May 4, 2024
23a141e
Test custom icon path parsing
pajlada May 4, 2024
12041df
Merge branch 'master' into feature/inline-mod-button-custom-images
pajlada May 5, 2024
c22ccd4
nit: add self-include to LoadPixmap
pajlada May 5, 2024
01c9b03
nit: change LoadPixmap logging from chatterinoSettings to
pajlada May 5, 2024
ae669a5
nit: remove unused Image include from LoadPixmap
pajlada May 5, 2024
85d6798
use Url{} instead of Url() in tests
pajlada May 5, 2024
ff7e382
Remove unused `openImageDialog` definition in ModerationPage
pajlada May 5, 2024
81a477f
Scale icons down (or up) to fit within the row
pajlada May 5, 2024
6da9cf1
Sort CMakeLists.txt sources
pajlada May 5, 2024
e2ad5e2
Move iconPath to source file
pajlada May 5, 2024
786d1a6
Validate getImage return value before accessing it
pajlada May 5, 2024
44dc2e3
clean up twitchbadges loademoteimage
pajlada May 5, 2024
90388cd
Merge branch 'master' into feature/inline-mod-button-custom-images
pajlada May 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 21 additions & 39 deletions src/controllers/moderationactions/ModerationAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,11 @@
#include "singletons/Resources.hpp"

#include <QRegularExpression>
#include <QUrl>

namespace chatterino {

// ModerationAction::ModerationAction(Image *_image, const QString &_action)
// : _isImage(true)
// , image(_image)
// , action(_action)
//{
//}

// ModerationAction::ModerationAction(const QString &_line1, const QString
// &_line2,
// const QString &_action)
// : _isImage(false)
// , image(nullptr)
// , line1(_line1)
// , line2(_line2)
// , action(_action)
//{
//}

ModerationAction::ModerationAction(const QString &action)
ModerationAction::ModerationAction(const QString &action, const QUrl &iconPath)
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
: action_(action)
{
static QRegularExpression replaceRegex("[!/.]");
Expand Down Expand Up @@ -99,21 +82,14 @@ ModerationAction::ModerationAction(const QString &action)
}
this->line2_ = "w";
}

// line1 = this->line1_;
// line2 = this->line2_;
// } else {
// this->_moderationActions.emplace_back(getResources().buttonTimeout,
// str);
// }
}
else if (action.startsWith("/ban "))
{
this->imageToLoad_ = 1;
this->builtInImageToLoad_ = BuiltInImage::Ban;
}
else if (action.startsWith("/delete "))
{
this->imageToLoad_ = 2;
this->builtInImageToLoad_ = BuiltInImage::TrashCan;
}
else
{
Expand All @@ -124,6 +100,12 @@ ModerationAction::ModerationAction(const QString &action)
this->line1_ = xD.mid(0, 2);
this->line2_ = xD.mid(2, 2);
}

if (!iconPath.isEmpty())
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
{
this->builtInImageToLoad_ = BuiltInImage::None;
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
this->iconPath_ = iconPath;
}
}

bool ModerationAction::operator==(const ModerationAction &other) const
Expand All @@ -140,18 +122,18 @@ const std::optional<ImagePtr> &ModerationAction::getImage() const
{
assertInGuiThread();

if (this->imageToLoad_ != 0)
if (this->builtInImageToLoad_ == BuiltInImage::Ban)
{
if (this->imageToLoad_ == 1)
{
this->image_ =
Image::fromResourcePixmap(getResources().buttons.ban);
}
else if (this->imageToLoad_ == 2)
{
this->image_ =
Image::fromResourcePixmap(getResources().buttons.trashCan);
}
this->image_ = Image::fromResourcePixmap(getResources().buttons.ban);
}
else if (this->builtInImageToLoad_ == BuiltInImage::TrashCan)
{
this->image_ =
Image::fromResourcePixmap(getResources().buttons.trashCan);
}
else if (!this->iconPath_.isEmpty())
{
this->image_ = Image::fromUrl({this->iconPath_.toString()});
}
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved

return this->image_;
Expand Down
24 changes: 20 additions & 4 deletions src/controllers/moderationactions/ModerationAction.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ using ImagePtr = std::shared_ptr<Image>;
class ModerationAction
{
public:
ModerationAction(const QString &action);
ModerationAction(const QString &action, const QUrl &iconPath = {});
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved

bool operator==(const ModerationAction &other) const;

Expand All @@ -26,12 +26,25 @@ class ModerationAction
const QString &getLine2() const;
const QString &getAction() const;

const QUrl &iconPath() const
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
{
return this->iconPath_;
};

private:
mutable std::optional<ImagePtr> image_;
QString line1_;
QString line2_;
QString action_;
int imageToLoad_{};

enum class BuiltInImage {
None,
Ban,
TrashCan,
};
BuiltInImage builtInImageToLoad_{};

QUrl iconPath_;
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
};

} // namespace chatterino
Expand All @@ -46,6 +59,7 @@ struct Serialize<chatterino::ModerationAction> {
rapidjson::Value ret(rapidjson::kObjectType);

chatterino::rj::set(ret, "pattern", value.getAction(), a);
chatterino::rj::set(ret, "icon", value.iconPath().toString(), a);
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved

return ret;
}
Expand All @@ -63,10 +77,12 @@ struct Deserialize<chatterino::ModerationAction> {
}

QString pattern;

chatterino::rj::getSafe(value, "pattern", pattern);

return chatterino::ModerationAction(pattern);
QString icon;
chatterino::rj::getSafe(value, "icon", icon);

return chatterino::ModerationAction(pattern, QUrl(icon));
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
}
};

Expand Down
67 changes: 64 additions & 3 deletions src/controllers/moderationactions/ModerationActionModel.cpp
Original file line number Diff line number Diff line change
@@ -1,28 +1,89 @@
#include "controllers/moderationactions/ModerationActionModel.hpp"

#include "Application.hpp"
#include "common/network/NetworkRequest.hpp"
#include "common/network/NetworkResult.hpp"
#include "common/QLogging.hpp"
#include "controllers/moderationactions/ModerationAction.hpp"
#include "messages/Image.hpp"
#include "util/StandardItemHelper.hpp"

#include <QIcon>
#include <QLoggingCategory>
#include <QPixmap>
namespace {

using namespace chatterino;

// This was copied from TwitchBadges::loadEmoteImage and modified.
void loadEmoteImage(const ImagePtr &image,
std::function<void(QPixmap)> &&callback)
{
auto url = image->url().string;
NetworkRequest(url)
.concurrent()
.cache()
.onSuccess(
[callback = std::move(callback), url](const NetworkResult &result) {
auto data = result.getData();

Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
// const cast since we are only reading from it
QBuffer buffer(const_cast<QByteArray *>(&data));
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
buffer.open(QIODevice::ReadOnly);
QImageReader reader(&buffer);
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved

if (!reader.canRead() || reader.size().isEmpty())
{
qCWarning(chatterinoSettings)
<< "Can't read mod action image at" << url << ":"
<< reader.errorString();
return;
}

QImage image = reader.read();
if (image.isNull())
{
qCWarning(chatterinoSettings)
<< "Failed reading mod action image at" << url << ":"
<< reader.errorString();
return;
}

callback(QPixmap::fromImage(image));
})
.execute();
}
} // namespace

namespace chatterino {

// commandmodel
ModerationActionModel ::ModerationActionModel(QObject *parent)
: SignalVectorModel<ModerationAction>(1, parent)
: SignalVectorModel<ModerationAction>(2, parent)
{
}

// turn a vector item into a model row
ModerationAction ModerationActionModel::getItemFromRow(
std::vector<QStandardItem *> &row, const ModerationAction &original)
{
return ModerationAction(row[0]->data(Qt::DisplayRole).toString());
return ModerationAction(
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
row[Column::Command]->data(Qt::DisplayRole).toString(),
row[Column::Icon]->data(Qt::UserRole).toString());
}

// turns a row in the model into a vector item
void ModerationActionModel::getRowFromItem(const ModerationAction &item,
std::vector<QStandardItem *> &row)
{
setStringItem(row[0], item.getAction());
setStringItem(row[Column::Command], item.getAction());
setFilePathItem(row[Column::Icon], item.iconPath());
if (!item.iconPath().isEmpty())
{
loadEmoteImage(*item.getImage(), [row](const QPixmap &pixmap) {
row[Column::Icon]->setData(pixmap, Qt::DecorationRole);
});
}
}

} // namespace chatterino
5 changes: 5 additions & 0 deletions src/controllers/moderationactions/ModerationActionModel.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ class ModerationActionModel : public SignalVectorModel<ModerationAction>
public:
explicit ModerationActionModel(QObject *parent);

enum Column {
Mm2PL marked this conversation as resolved.
Show resolved Hide resolved
Command,
Icon,
};

protected:
// turn a vector item into a model row
ModerationAction getItemFromRow(std::vector<QStandardItem *> &row,
Expand Down
13 changes: 13 additions & 0 deletions src/widgets/settingspages/ModerationPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,19 @@ ModerationPage::ModerationPage()
QHeaderView::Fixed);
view->getTableView()->horizontalHeader()->setSectionResizeMode(
0, QHeaderView::Stretch);
QObject::connect(
view->getTableView(), &QTableView::clicked,
[this, view](const QModelIndex &clicked) {
if (clicked.column() == ModerationActionModel::Column::Icon)
{
auto fileUrl = QFileDialog::getOpenFileUrl(
this, tr("Open Image"), QUrl(),
tr("Image Files (*.png)"));
view->getModel()->setData(clicked, fileUrl, Qt::UserRole);
view->getModel()->setData(clicked, fileUrl.fileName(),
Qt::DisplayRole);
}
});

// We can safely ignore this signal connection since we own the view
std::ignore = view->addButtonPressed.connect([] {
Expand Down
3 changes: 3 additions & 0 deletions src/widgets/settingspages/ModerationPage.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace chatterino {

template <typename X>
class LayoutCreator;
class EditableModelView;

class ModerationPage : public SettingsPage
{
Expand All @@ -27,6 +28,8 @@ class ModerationPage : public SettingsPage

std::vector<QLineEdit *> durationInputs_;
std::vector<QComboBox *> unitInputs_;

void openImageDialog(const QModelIndex &clicked, EditableModelView *view);
};

} // namespace chatterino
Loading