project: Implement a brand new UI with fancy windows

Implements support for various new UI features that weren't possible up until now, such as an 'About StreamFX' window with a thank you to everyone that supported the project up until now.
This commit is contained in:
Michael Fabian 'Xaymar' Dirks 2020-04-05 18:52:06 +02:00 committed by Michael Fabian Dirks
parent d0941895ad
commit f58ce9f421
19 changed files with 1250 additions and 0 deletions

View file

@ -501,8 +501,21 @@ endif()
## OBS Studio - Frontend/Qt
if(HAVE_OBS_FRONTEND)
list(APPEND PROJECT_UI
"ui/streamfx.qrc"
"ui/about.ui"
"ui/about-entry.ui"
)
list(APPEND PROJECT_PRIVATE_SOURCE
"source/ui/ui-common.hpp"
"source/ui/ui.hpp"
"source/ui/ui.cpp"
"source/ui/ui-about.hpp"
"source/ui/ui-about.cpp"
"source/ui/ui-about-entry.hpp"
"source/ui/ui-about-entry.cpp"
)
list(APPEND PROJECT_DATA
)
list(APPEND PROJECT_INCLUDE_DIRS
"source/ui"

View file

@ -18,6 +18,7 @@
*/
#include "plugin.hpp"
#include <fstream>
#include <stdexcept>
#include "configuration.hpp"
#include "obs/obs-source-tracker.hpp"
@ -62,6 +63,10 @@
#include "transitions/transition-shader.hpp"
#endif
#ifdef ENABLE_FRONTEND
#include "ui/ui.hpp"
#endif
static std::shared_ptr<util::threadpool> _threadpool;
MODULE_EXPORT bool obs_module_load(void)
@ -133,6 +138,11 @@ try {
#endif
}
// Frontend
#ifdef ENABLE_FRONTEND
streamfx::ui::handler::initialize();
#endif
LOG_INFO("Loaded Version %s", STREAMFX_VERSION_STRING);
return true;
} catch (...) {
@ -144,6 +154,11 @@ MODULE_EXPORT void obs_module_unload(void)
try {
LOG_INFO("Unloading Version %s", STREAMFX_VERSION_STRING);
// Frontend
#ifdef ENABLE_FRONTEND
streamfx::ui::handler::finalize();
#endif
// Transitions
{
using namespace streamfx::transition;

View file

@ -0,0 +1,130 @@
/*
* Modern effects for a modern Streamer
* Copyright (C) 2020 Michael Fabian Dirks
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "ui-about-entry.hpp"
constexpr std::string_view i18n_role_contributor = "UI.About.Role.Contributor";
constexpr std::string_view i18n_role_translator = "UI.About.Role.Translator";
constexpr std::string_view i18n_role_family = "UI.About.Role.Family";
constexpr std::string_view i18n_role_friend = "UI.About.Role.Friend";
constexpr std::string_view i18n_role_supporter_patreon = "UI.About.Role.Supporter.Patreon";
constexpr std::string_view i18n_role_supporter_github = "UI.About.Role.Supporter.Github";
constexpr std::string_view i18n_role_supporter_twitch = "UI.About.Role.Supporter.Twitch";
constexpr std::string_view i18n_role_creator = "UI.About.Role.Creator";
streamfx::ui::about_entry::about_entry(QWidget* parent, streamfx::ui::about::entry& entry)
: QWidget(parent), _last_click(std::chrono::high_resolution_clock::now()), _link1_url(), _link2_url()
{
setupUi(this);
name->setText(QString::fromStdString(entry.name));
switch (entry.role) {
case streamfx::ui::about::role_type::NONE:
title->setText(QString::fromStdString(entry.role_custom));
break;
case streamfx::ui::about::role_type::CONTRIBUTOR:
title->setText(D_TRANSLATE(i18n_role_contributor.data()));
break;
case streamfx::ui::about::role_type::TRANSLATOR:
title->setText(D_TRANSLATE(i18n_role_translator.data()));
break;
case streamfx::ui::about::role_type::FAMILY: {
const char* txt = D_TRANSLATE(i18n_role_family.data());
std::vector<char> buf(2048);
snprintf(buf.data(), buf.size(), txt, entry.role_custom.c_str());
title->setText(QString::fromUtf8(buf.data()));
break;
}
case streamfx::ui::about::role_type::FRIEND: {
const char* txt = D_TRANSLATE(i18n_role_friend.data());
std::vector<char> buf(2048);
snprintf(buf.data(), buf.size(), txt, entry.role_custom.c_str());
title->setText(QString::fromUtf8(buf.data()));
break;
}
case streamfx::ui::about::role_type::PATREON_SUPPORTER:
title->setText(D_TRANSLATE(i18n_role_supporter_patreon.data()));
break;
case streamfx::ui::about::role_type::GITHUB_SUPPORTER:
title->setText(D_TRANSLATE(i18n_role_supporter_github.data()));
break;
case streamfx::ui::about::role_type::TWITCH_SUPPORTER:
title->setText(D_TRANSLATE(i18n_role_supporter_twitch.data()));
break;
case streamfx::ui::about::role_type::CREATOR:
title->setText(D_TRANSLATE(i18n_role_creator.data()));
break;
}
std::tuple<streamfx::ui::about::link_type, std::string, std::string, QPushButton*, QUrl&> els[]{
{entry.link1_type, entry.link1_address, entry.link1_text, link1, _link1_url},
{entry.link2_type, entry.link2_address, entry.link2_text, link2, _link2_url},
};
for (auto el : els) {
switch (std::get<0>(el)) {
case streamfx::ui::about::link_type::NONE:
std::get<3>(el)->setHidden(true);
break;
case streamfx::ui::about::link_type::GENERIC:
std::get<3>(el)->setIcon(QIcon(":/linktype/generic"));
break;
case streamfx::ui::about::link_type::SOCIAL_TWITCH:
std::get<3>(el)->setIcon(QIcon(":/linktype/twitch"));
break;
case streamfx::ui::about::link_type::SOCIAL_YOUTUBE:
std::get<3>(el)->setIcon(QIcon(":/linktype/youtube"));
break;
case streamfx::ui::about::link_type::SOCIAL_DISCORD:
std::get<3>(el)->setIcon(QIcon(":/linktype/discord"));
break;
case streamfx::ui::about::link_type::SOCIAL_TWITTER:
std::get<3>(el)->setIcon(QIcon(":/linktype/twitter"));
break;
case streamfx::ui::about::link_type::SOCIAL_FACEBOOK:
std::get<3>(el)->setIcon(QIcon(":/linktype/facebook"));
break;
}
std::get<3>(el)->setText(QString::fromUtf8(std::get<2>(el).c_str()));
std::get<4>(el).setUrl(QString::fromStdString(std::get<1>(el)));
}
connect(link1, &QPushButton::pressed, this, &streamfx::ui::about_entry::on_link1_clicked);
connect(link2, &QPushButton::pressed, this, &streamfx::ui::about_entry::on_link2_clicked);
// Don't free up space when hidden.
/*if (!(link1->isVisible() || link2->isVisible())) {
QSizePolicy qsp = link1->sizePolicy();
qsp.setRetainSizeWhenHidden(true);
link1->setSizePolicy(qsp);
}*/
}
streamfx::ui::about_entry::~about_entry() {}
void streamfx::ui::about_entry::on_link1_clicked()
{
// FIXME! Button clicks twice?
for (size_t attempt = 0; (attempt < 10) && (!QDesktopServices::openUrl(_link1_url)); attempt++) {
}
}
void streamfx::ui::about_entry::on_link2_clicked()
{
for (size_t attempt = 0; (attempt < 10) && (!QDesktopServices::openUrl(_link2_url)); attempt++) {
}
}

View file

@ -0,0 +1,51 @@
/*
* Modern effects for a modern Streamer
* Copyright (C) 2020 Michael Fabian Dirks
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#pragma once
#include <chrono>
#include "ui-about.hpp"
#include "ui-common.hpp"
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4251 4365 4371 4619 4946)
#endif
#include "ui_about-entry.h"
#ifdef _MSC_VER
#pragma warning(pop)
#endif
namespace streamfx::ui {
class about_entry : public QWidget, public Ui::AboutEntry {
Q_OBJECT
private:
std::chrono::high_resolution_clock::time_point _last_click;
QUrl _link1_url;
QUrl _link2_url;
public:
about_entry(QWidget* parent, ui::about::entry& entry);
~about_entry();
public slots:
void on_link1_clicked();
void on_link2_clicked();
};
} // namespace streamfx::ui

204
source/ui/ui-about.cpp Normal file
View file

@ -0,0 +1,204 @@
/*
* Modern effects for a modern Streamer
* Copyright (C) 2020 Michael Fabian Dirks
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "ui-about.hpp"
#include <obs-frontend-api.h>
#include <map>
#include "ui-about-entry.hpp"
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4251 4365 4371 4619 4946)
#endif
#include <QLayout>
#include <QLayoutItem>
#ifdef _MSC_VER
#pragma warning(pop)
#endif
constexpr std::string_view text_social_facebook = "Facebook";
constexpr std::string_view text_social_twitch = "Twitch";
constexpr std::string_view text_social_twitter = "Twitter";
constexpr std::string_view text_social_youtube = "YouTube";
static const std::list<streamfx::ui::about::entry> _entries = {
// Contributers
streamfx::ui::about::entry{"Michael \"Xaymar\" Dirks", streamfx::ui::about::role_type::CONTRIBUTOR, "",
streamfx::ui::about::link_type::GENERIC, "https://xaymar.com", "Blog & Stuff",
streamfx::ui::about::link_type::SOCIAL_TWITCH, "https://twitch.tv/xaymar",
text_social_twitch.data()},
// Translators
streamfx::ui::about::entry{"FrozenNortherner", streamfx::ui::about::role_type::TRANSLATOR, "",
streamfx::ui::about::link_type::SOCIAL_TWITCH, "https://twitch.tv/frozennortherner",
text_social_twitch.data(), streamfx::ui::about::link_type::NONE, "", ""},
streamfx::ui::about::entry{"HANAWINS", streamfx::ui::about::role_type::TRANSLATOR, "",
streamfx::ui::about::link_type::SOCIAL_TWITCH, "https://www.twitch.tv/hanawins",
text_social_twitch.data(), streamfx::ui::about::link_type::NONE, "", ""},
streamfx::ui::about::entry{"hydargos", streamfx::ui::about::role_type::TRANSLATOR, "",
streamfx::ui::about::link_type::SOCIAL_TWITCH, "https://www.youtube.com/hydargos",
text_social_youtube.data(), streamfx::ui::about::link_type::NONE, "", ""},
streamfx::ui::about::entry{"Monsteer", streamfx::ui::about::role_type::TRANSLATOR, "",
streamfx::ui::about::link_type::SOCIAL_TWITTER, "https://twitter.com/cooliguay",
text_social_twitter.data(), streamfx::ui::about::link_type::NONE, "", ""},
streamfx::ui::about::entry{"Nanito", streamfx::ui::about::role_type::TRANSLATOR, "",
streamfx::ui::about::link_type::SOCIAL_TWITCH, "https://twitch.tv/nanito",
text_social_twitch.data(), streamfx::ui::about::link_type::SOCIAL_FACEBOOK,
"https://facebook.com/nanitotv", text_social_facebook.data()},
// Separator
streamfx::ui::about::entry{"", streamfx::ui::about::role_type::NONE, "", streamfx::ui::about::link_type::NONE, "",
"", streamfx::ui::about::link_type::NONE, "", ""},
// Supporters
streamfx::ui::about::entry{"GranDroidTonight", streamfx::ui::about::role_type::PATREON_SUPPORTER, "",
streamfx::ui::about::link_type::SOCIAL_TWITCH, "https://www.twitch.tv/GranDroidTonight",
text_social_twitch.data(), streamfx::ui::about::link_type::SOCIAL_YOUTUBE,
"https://youtube.com/channel/UCGoT2XFPpeKaL1QuY_NPDuA", text_social_youtube.data()},
streamfx::ui::about::entry{"chi11estpanda", streamfx::ui::about::role_type::PATREON_SUPPORTER, "",
streamfx::ui::about::link_type::SOCIAL_TWITCH, "https://www.twitch.tv/chi11estpanda",
"Gaming, Life and Laughs", streamfx::ui::about::link_type::NONE, "", ""},
streamfx::ui::about::entry{"DandiDoesIt", streamfx::ui::about::role_type::PATREON_SUPPORTER, "",
streamfx::ui::about::link_type::SOCIAL_TWITCH, "https://www.twitch.tv/DandiDoesIt",
"Twitch Channel", streamfx::ui::about::link_type::NONE, "", ""},
streamfx::ui::about::entry{"iamresist", streamfx::ui::about::role_type::PATREON_SUPPORTER, "",
streamfx::ui::about::link_type::SOCIAL_TWITCH, "https://twitch.tv/iamresist",
text_social_twitch.data(), streamfx::ui::about::link_type::NONE, "", ""},
streamfx::ui::about::entry{"Nordern", streamfx::ui::about::role_type::PATREON_SUPPORTER, "",
streamfx::ui::about::link_type::SOCIAL_YOUTUBE, "https://youtube.com/nordern",
text_social_youtube.data(), streamfx::ui::about::link_type::SOCIAL_TWITCH,
"https://www.twitch.tv/thenordern", text_social_twitch.data()},
// Separator
streamfx::ui::about::entry{"", streamfx::ui::about::role_type::NONE, "", streamfx::ui::about::link_type::NONE, "",
"", streamfx::ui::about::link_type::NONE, "", ""},
// Family
streamfx::ui::about::entry{"Andrea Stenschke", streamfx::ui::about::role_type::FAMILY, "Xaymar",
streamfx::ui::about::link_type::NONE, "", "", streamfx::ui::about::link_type::NONE, "",
""},
streamfx::ui::about::entry{"Carsten Dirks", streamfx::ui::about::role_type::FAMILY, "Xaymar",
streamfx::ui::about::link_type::NONE, "", "", streamfx::ui::about::link_type::NONE, "",
""},
streamfx::ui::about::entry{"Christian \"Azekil\" Dirks", streamfx::ui::about::role_type::FAMILY, "Xaymar",
streamfx::ui::about::link_type::NONE, "", "", streamfx::ui::about::link_type::NONE, "",
""},
streamfx::ui::about::entry{"Gabriele Rantfl", streamfx::ui::about::role_type::FAMILY, "Xaymar",
streamfx::ui::about::link_type::NONE, "", "", streamfx::ui::about::link_type::NONE, "",
""},
streamfx::ui::about::entry{"Reiner Rantfl", streamfx::ui::about::role_type::FAMILY, "Xaymar",
streamfx::ui::about::link_type::NONE, "", "", streamfx::ui::about::link_type::NONE, "",
""},
streamfx::ui::about::entry{"René \"Dex\" Dirks", streamfx::ui::about::role_type::FAMILY, "Xaymar",
streamfx::ui::about::link_type::SOCIAL_TWITCH, "https://twitch.tv/vektordex",
text_social_twitch.data(), streamfx::ui::about::link_type::GENERIC,
"https://worldofdex.de", "Website"},
// Friends
streamfx::ui::about::entry{"Axelle", streamfx::ui::about::role_type::FRIEND, "Xaymar",
streamfx::ui::about::link_type::SOCIAL_TWITCH, "https://www.twitch.tv/axelle123",
text_social_twitch.data(), streamfx::ui::about::link_type::SOCIAL_TWITTER,
"https://twitter.com/AxellesNobody", text_social_twitter.data()},
// Separator
streamfx::ui::about::entry{"", streamfx::ui::about::role_type::NONE, "", streamfx::ui::about::link_type::NONE, "",
"", streamfx::ui::about::link_type::NONE, "", ""},
// Creators
streamfx::ui::about::entry{"EposVox", streamfx::ui::about::role_type::CREATOR, "",
streamfx::ui::about::link_type::SOCIAL_TWITCH, "https://www.twitch.tv/EposVox",
text_social_twitch.data(), streamfx::ui::about::link_type::SOCIAL_YOUTUBE,
"https://youtube.com/c/EposVox", text_social_youtube.data()},
};
streamfx::ui::about::about() : QDialog(reinterpret_cast<QWidget*>(obs_frontend_get_main_window()))
{
setupUi(this);
// Remove some extra styling.
setWindowFlag(Qt::WindowContextHelpButtonHint, false); // Remove the unimplemented help button.
// Thank every single helper.
bool column_selector = false;
size_t row_selector = 0;
QGridLayout* content_layout = dynamic_cast<QGridLayout*>(content->layout());
for (auto entry : _entries) {
if (entry.name.size() == 0) {
row_selector += 2;
column_selector = 0;
// Add a separator line.
auto separator = new QFrame(content);
{
auto sp = separator->sizePolicy();
sp.setVerticalPolicy(QSizePolicy::Fixed);
separator->setSizePolicy(sp);
}
separator->setFrameShape(QFrame::HLine);
separator->setFrameShadow(QFrame::Sunken);
separator->setMaximumHeight(1);
separator->setMinimumHeight(1);
separator->setFixedHeight(1);
separator->setLineWidth(1);
content_layout->addWidget(separator, static_cast<int>(row_selector - 1), 0, 1, 2);
content_layout->setRowStretch(static_cast<int>(row_selector - 1), 0);
} else {
streamfx::ui::about_entry* v = new streamfx::ui::about_entry(content, entry);
content_layout->addWidget(v, static_cast<int>(row_selector), column_selector ? 1 : 0);
content_layout->setRowStretch(static_cast<int>(row_selector), 0);
if (column_selector) {
row_selector++;
}
column_selector = !column_selector;
}
}
{
row_selector++;
auto padder = new QFrame(content);
{
auto sp = padder->sizePolicy();
sp.setVerticalPolicy(QSizePolicy::Minimum);
sp.setVerticalStretch(1);
padder->setSizePolicy(sp);
}
padder->setObjectName("PaddleMeDaddy");
padder->setMaximumHeight(QWIDGETSIZE_MAX);
padder->setMinimumHeight(1);
padder->setFrameShape(QFrame::NoFrame);
content_layout->addWidget(padder, static_cast<int>(row_selector), 0, 1, 2);
content_layout->setRowStretch(static_cast<int>(row_selector), 9999);
}
content_layout->setColumnStretch(0, 1);
content_layout->setColumnStretch(1, 1);
content->setSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Maximum);
// Make the OK button do things.
connect(buttonBox, &QDialogButtonBox::accepted, this, &streamfx::ui::about::on_ok);
}
streamfx::ui::about::~about() {}
void streamfx::ui::about::on_ok()
{
hide();
}

83
source/ui/ui-about.hpp Normal file
View file

@ -0,0 +1,83 @@
/*
* Modern effects for a modern Streamer
* Copyright (C) 2020 Michael Fabian Dirks
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#pragma once
#include "common.hpp"
#include "ui-common.hpp"
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4251 4365 4371 4619 4946)
#endif
#include "ui_about.h"
#ifdef _MSC_VER
#pragma warning(pop)
#endif
namespace streamfx::ui {
class about : public QDialog, public Ui::About {
Q_OBJECT
public:
enum class role_type : std::int32_t {
NONE,
CONTRIBUTOR,
TRANSLATOR,
FAMILY,
FRIEND,
PATREON_SUPPORTER,
GITHUB_SUPPORTER,
TWITCH_SUPPORTER,
CREATOR,
};
enum class link_type : std::int32_t {
NONE,
GENERIC,
// Social Links
SOCIAL_TWITCH = 2000,
SOCIAL_YOUTUBE,
SOCIAL_DISCORD,
SOCIAL_TWITTER,
SOCIAL_FACEBOOK,
};
struct entry {
std::string name;
ui::about::role_type role;
std::string role_custom;
ui::about::link_type link1_type;
std::string link1_address;
std::string link1_text;
ui::about::link_type link2_type;
std::string link2_address;
std::string link2_text;
};
public:
about();
~about();
public slots:
; // Not having this breaks some linters.
void on_ok();
};
} // namespace streamfx::ui

43
source/ui/ui-common.hpp Normal file
View file

@ -0,0 +1,43 @@
/*
* Modern effects for a modern Streamer
* Copyright (C) 2017 Michael Fabian Dirks
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#pragma once
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4251 4365 4371 4619 4946)
#endif
#include <QAction>
#include <QDesktopServices>
#include <QDialog>
#include <QMenu>
#include <QObject>
#include <QString>
#include <QTranslator>
#include <QUrl>
#include <QWidget>
extern "C" {
#include <obs-frontend-api.h>
}
#ifdef _MSC_VER
#pragma warning(pop)
#endif

200
source/ui/ui.cpp Normal file
View file

@ -0,0 +1,200 @@
/*
* Modern effects for a modern Streamer
* Copyright (C) 2020 Michael Fabian Dirks
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "ui.hpp"
#include "common.hpp"
#include "strings.hpp"
#include <string_view>
#include "configuration.hpp"
#include "obs/obs-tools.hpp"
#include "plugin.hpp"
#include <obs-frontend-api.h>
// Translation Keys
constexpr std::string_view _i18n_prefix = "StreamFX::";
constexpr std::string_view _i18n_menu = "UI.Menu";
constexpr std::string_view _i18n_menu_report_issue = "UI.Menu.ReportIssue";
constexpr std::string_view _i18n_menu_request_help = "UI.Menu.RequestHelp";
constexpr std::string_view _i18n_menu_website = "UI.Menu.Website";
constexpr std::string_view _i18n_menu_discord = "UI.Menu.Discord";
constexpr std::string_view _i18n_menu_github = "UI.Menu.Github";
constexpr std::string_view _i18n_menu_about = "UI.Menu.About";
// Configuration
constexpr std::string_view _cfg_have_shown_about = "UI.HaveShownAboutStreamFX";
// URLs
constexpr std::string_view _url_report_issue = "https://github.com/Xaymar/obs-StreamFX/issues/new?template=issue.md";
constexpr std::string_view _url_request_help = "https://github.com/Xaymar/obs-StreamFX/issues/new?template=help.md";
constexpr std::string_view _url_website = "https://streamfx.xaymar.com";
constexpr std::string_view _url_discord = "https://discordapp.com/invite/DaeJg7M";
constexpr std::string_view _url_github = "https://github.com/Xaymar/obs-StreamFX";
bool streamfx::ui::handler::have_shown_about_streamfx(bool shown)
{
if (shown) {
obs_data_set_bool(streamfx::configuration::instance()->get().get(), _cfg_have_shown_about.data(), true);
}
if (streamfx::configuration::instance()->is_different_version()) {
return false;
} else {
return obs_data_get_bool(streamfx::configuration::instance()->get().get(), _cfg_have_shown_about.data());
}
}
streamfx::ui::handler::handler()
: QObject(), _menu_action(), _menu(),
_report_issue(), _request_help(),
_link_website(), _link_discord(), _link_github(),
_about_action(), _about_dialog()
{
// Qt Resources and Translators
Q_INIT_RESOURCE(streamfx);
QCoreApplication::installTranslator(new streamfx::ui::translator(this));
// Handle all frontend events.
obs_frontend_add_event_callback(frontend_event_handler, this);
{ // Build StreamFX menu.
_menu = new QMenu(reinterpret_cast<QWidget*>(obs_frontend_get_main_window()));
{ // Github Issues
//_menu->addSeparator();
_report_issue = _menu->addAction(QString::fromUtf8(D_TRANSLATE(_i18n_menu_report_issue.data())));
_request_help = _menu->addAction(QString::fromUtf8(D_TRANSLATE(_i18n_menu_request_help.data())));
connect(_report_issue, &QAction::triggered, this, &streamfx::ui::handler::on_action_report_issue);
connect(_request_help, &QAction::triggered, this, &streamfx::ui::handler::on_action_request_help);
}
{ // Official Links
_menu->addSeparator();
_link_website = _menu->addAction(QString::fromUtf8(D_TRANSLATE(_i18n_menu_website.data())));
_link_discord = _menu->addAction(QString::fromUtf8(D_TRANSLATE(_i18n_menu_discord.data())));
_link_github = _menu->addAction(QString::fromUtf8(D_TRANSLATE(_i18n_menu_github.data())));
connect(_link_website, &QAction::triggered, this, &streamfx::ui::handler::on_action_website);
connect(_link_discord, &QAction::triggered, this, &streamfx::ui::handler::on_action_discord);
connect(_link_github, &QAction::triggered, this, &streamfx::ui::handler::on_action_github);
}
{ // About StreamFX
_about_dialog = new streamfx::ui::about();
_menu->addSeparator();
_about_action = _menu->addAction(QString::fromUtf8(D_TRANSLATE(_i18n_menu_about.data())));
connect(_about_action, &QAction::triggered, this, &streamfx::ui::handler::on_action_about);
if (!have_shown_about_streamfx()) {
// Automatically show it if it has not yet been shown.
_about_dialog->show();
have_shown_about_streamfx(true);
}
}
}
}
streamfx::ui::handler::~handler()
{
// Handle all frontend events.
obs_frontend_remove_event_callback(frontend_event_handler, this);
// Qt Resources and Translators
Q_CLEANUP_RESOURCE(streamfx);
}
void streamfx::ui::handler::frontend_event_handler(obs_frontend_event event, void* private_data)
{
streamfx::ui::handler* ptr = reinterpret_cast<streamfx::ui::handler*>(private_data);
switch (event) {
case OBS_FRONTEND_EVENT_FINISHED_LOADING:
ptr->on_obs_loaded();
break;
default:
break;
}
}
void streamfx::ui::handler::on_obs_loaded()
{
// Add StreamFX menu.
_menu_action = reinterpret_cast<QAction*>(obs_frontend_add_tools_menu_qaction(D_TRANSLATE(_i18n_menu.data())));
_menu_action->setMenu(_menu);
}
void streamfx::ui::handler::on_action_report_issue(bool)
{
QDesktopServices::openUrl(QUrl(QString::fromUtf8(_url_report_issue.data())));
}
void streamfx::ui::handler::on_action_request_help(bool)
{
QDesktopServices::openUrl(QUrl(QString::fromUtf8(_url_request_help.data())));
}
void streamfx::ui::handler::on_action_website(bool)
{
QDesktopServices::openUrl(QUrl(QString::fromUtf8(_url_website.data())));
}
void streamfx::ui::handler::on_action_discord(bool)
{
QDesktopServices::openUrl(QUrl(QString::fromUtf8(_url_discord.data())));
}
void streamfx::ui::handler::on_action_github(bool)
{
QDesktopServices::openUrl(QUrl(QString::fromUtf8(_url_github.data())));
}
void streamfx::ui::handler::on_action_about(bool checked)
{
_about_dialog->show();
}
static std::shared_ptr<streamfx::ui::handler> _handler_singleton;
void streamfx::ui::handler::initialize()
{
_handler_singleton = std::make_shared<streamfx::ui::handler>();
}
void streamfx::ui::handler::finalize()
{
_handler_singleton.reset();
}
std::shared_ptr<streamfx::ui::handler> streamfx::ui::handler::get()
{
return _handler_singleton;
}
streamfx::ui::translator::translator(QObject* parent) {}
streamfx::ui::translator::~translator() {}
QString streamfx::ui::translator::translate(const char* context, const char* sourceText, const char* disambiguation,
int n) const
{
std::string_view sourceView{sourceText};
if (sourceView.substr(0, _i18n_prefix.length()) == _i18n_prefix) {
return QString::fromUtf8(D_TRANSLATE(sourceView.substr(_i18n_prefix.length()).data()));
}
return QString();
}

82
source/ui/ui.hpp Normal file
View file

@ -0,0 +1,82 @@
/*
* Modern effects for a modern Streamer
* Copyright (C) 2020 Michael Fabian Dirks
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#pragma once
#include "ui-about.hpp"
#include "ui-common.hpp"
namespace streamfx::ui {
class handler : public QObject {
Q_OBJECT
private:
QAction* _menu_action;
QMenu* _menu;
// Bug Report, Help Request
QAction* _report_issue;
QAction* _request_help;
// Website, Discord, Source
QAction* _link_website;
QAction* _link_discord;
QAction* _link_github;
// About Dialog
QAction* _about_action;
ui::about* _about_dialog;
public:
handler();
~handler();
bool have_shown_about_streamfx(bool shown = false);
private:
static void frontend_event_handler(obs_frontend_event event, void* private_data);
void on_obs_loaded();
public slots:
; // Not having this breaks some linters.
void on_action_report_issue(bool);
void on_action_request_help(bool);
void on_action_website(bool);
void on_action_discord(bool);
void on_action_github(bool);
void on_action_about(bool);
public /* Singleton */:
static void initialize();
static void finalize();
static std::shared_ptr<ui::handler> get();
};
class translator : public QTranslator {
public:
translator(QObject* parent = nullptr);
~translator();
virtual QString translate(const char* context, const char* sourceText, const char* disambiguation = nullptr,
int n = -1) const override;
};
} // namespace streamfx::ui

243
ui/about-entry.ui Normal file
View file

@ -0,0 +1,243 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AboutEntry</class>
<widget class="QWidget" name="AboutEntry">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>195</width>
<height>99</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true">QWidget {
background: hsl(0, 0%, 20%);
color: hsl(0, 0%, 100%);
}
QPushButton,
QPushButton:default,
QPushButton:flat,
QPushButton:checked {
background: hsl(0, 0%, 25%);
border: 0;
padding: 5px;
box-shadow: 0;
cursor: pointer;
padding: 4px;
}
QWidget[objectName=&quot;title&quot;] {
color: hsl(200, 100%, 80%);
}</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>5</number>
</property>
<property name="topMargin">
<number>5</number>
</property>
<property name="rightMargin">
<number>5</number>
</property>
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<widget class="QLabel" name="name">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>14</pointsize>
<weight>75</weight>
<bold>true</bold>
<stylestrategy>PreferAntialias</stylestrategy>
</font>
</property>
<property name="text">
<string notr="true">Supporter Name</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="title">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string notr="true">Supporter Title</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="linkLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QPushButton" name="link1">
<property name="minimumSize">
<size>
<width>0</width>
<height>36</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>36</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>36</height>
</size>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string notr="true">YouTube</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/social/youtube</normaloff>:/social/youtube</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="link2">
<property name="minimumSize">
<size>
<width>0</width>
<height>36</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>36</height>
</size>
</property>
<property name="baseSize">
<size>
<width>0</width>
<height>36</height>
</size>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string notr="true">Twitch</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/social/twitch</normaloff>:/social/twitch</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="streamfx.qrc"/>
</resources>
<connections/>
</ui>

173
ui/about.ui Normal file
View file

@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>About</class>
<widget class="QWidget" name="About">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>512</width>
<height>512</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>512</width>
<height>512</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>1024</width>
<height>16777215</height>
</size>
</property>
<property name="windowTitle">
<string comment="StreamFX::UI.About.Title">StreamFX::UI.About.Title</string>
</property>
<property name="styleSheet">
<string notr="true">QWidget {
background: transparent;
color: hsl(0, 0%, 100%);
}
QWidget[objectName=&quot;About&quot;],
QWidget[objectName=&quot;contentContainer&quot;],
QWidget[objectName=&quot;content&quot;] {
background: hsl(0, 0%, 10%);
border: 0;
}
QWidget[objectName=&quot;PaddleMeDaddy&quot;] {
background: hsl(0, 0%, 10%);
}</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>10</number>
</property>
<property name="topMargin">
<number>10</number>
</property>
<property name="rightMargin">
<number>10</number>
</property>
<property name="bottomMargin">
<number>10</number>
</property>
<item>
<widget class="QLabel" name="logo">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="streamfx.qrc">:/logos/streamfx_logo</pixmap>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="text">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>12</pointsize>
<kerning>true</kerning>
</font>
</property>
<property name="text">
<string comment="StreamFX::UI.About.Text">&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;StreamFX is made possible by all the supporters on &lt;a href=&quot;https://patreon.com/Xaymar&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#00aaff;&quot;&gt;Patreon&lt;/span&gt;&lt;/a&gt;, on &lt;a href=&quot;https://github.com/sponsors/xaymar&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#00aaff;&quot;&gt;Github Sponsors&lt;/span&gt;&lt;/a&gt;, and anyone donating through &lt;a href=&quot;https://paypal.me/Xaymar&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#00aaff;&quot;&gt;PayPal&lt;/span&gt;&lt;/a&gt;. Additional thanks go out to all the translators helping out with the localization on &lt;a href=&quot;https://crowdin.com/project/obs-stream-effects&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#00aaff;&quot;&gt;Crowdin&lt;/span&gt;&lt;/a&gt;. You all are amazing!&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignHCenter|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QScrollArea" name="contentContainer">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<widget class="QWidget" name="content">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>492</width>
<height>282</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>5</number>
</property>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="streamfx.qrc"/>
</resources>
<connections/>
</ui>

BIN
ui/linktype_discord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

BIN
ui/linktype_facebook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
ui/linktype_generic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 937 B

BIN
ui/linktype_twitch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

BIN
ui/linktype_twitter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

BIN
ui/linktype_youtube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
ui/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

13
ui/streamfx.qrc Normal file
View file

@ -0,0 +1,13 @@
<RCC>
<qresource prefix="logos">
<file alias="streamfx_logo">logo.png</file>
</qresource>
<qresource prefix="linktype">
<file alias="discord">linktype_discord.png</file>
<file alias="facebook">linktype_facebook.png</file>
<file alias="generic">linktype_generic.png</file>
<file alias="twitch">linktype_twitch.png</file>
<file alias="twitter">linktype_twitter.png</file>
<file alias="youtube">linktype_youtube.png</file>
</qresource>
</RCC>