diff --git a/.gitmodules b/.gitmodules index da905b54d..28a1b9d6a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,7 +14,15 @@ [submodule "lib/WinToast"] path = lib/WinToast url = https://github.com/mohabouje/WinToast.git - -[submodule "lib/appbase"] - path = lib/appbase - url = https://github.com/Chatterino/appbase +[submodule "lib/settings"] + path = lib/settings + url = https://github.com/pajlada/settings +[submodule "lib/signals"] + path = lib/signals + url = https://github.com/pajlada/signals +[submodule "lib/serialize"] + path = lib/serialize + url = https://github.com/pajlada/serialize +[submodule "lib/rapidjson"] + path = lib/rapidjson + url = https://github.com/Tencent/rapidjson diff --git a/chatterino.pro b/chatterino.pro index 56517ef3c..9966ab3f6 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -19,6 +19,13 @@ useBreakpad { DEFINES += C_USE_BREAKPAD } +# use C++17 +win32-msvc* { + QMAKE_CXXFLAGS += /std:c++17 +} else { + QMAKE_CXXFLAGS += -std=c++17 +} + # https://bugreports.qt.io/browse/QTBUG-27018 equals(QMAKE_CXX, "clang++")|equals(QMAKE_CXX, "g++") { TARGET = bin/chatterino @@ -33,13 +40,21 @@ macx { } # Submodules -include(lib/appbase.pri) -include(lib/humanize.pri) DEFINES += IRC_NAMESPACE=Communi + +include(lib/appbase.pri) +include(lib/boost.pri) +include(lib/fmt.pri) +include(lib/humanize.pri) include(lib/libcommuni.pri) include(lib/websocketpp.pri) include(lib/openssl.pri) include(lib/wintoast.pri) +include(lib/signals.pri) +include(lib/settings.pri) +include(lib/serialize.pri) +include(lib/winsdk.pri) +include(lib/rapidjson.pri) # Optional feature: QtWebEngine #exists ($(QTDIR)/include/QtWebEngine/QtWebEngine) { diff --git a/lib/appbase b/lib/appbase deleted file mode 160000 index d05492573..000000000 --- a/lib/appbase +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d054925734cf26576346a1da856f7ab0d4b6c0a5 diff --git a/lib/appbase.pri b/lib/appbase.pri index 8ac699223..cbae08c54 100644 --- a/lib/appbase.pri +++ b/lib/appbase.pri @@ -1,3 +1,3 @@ # include appbase -include($$PWD/appbase/appbase/main/main.pro) -INCLUDEPATH += $$PWD/appbase/appbase/main +include($$PWD/appbase/main.pro) +INCLUDEPATH += $$PWD/appbase diff --git a/lib/appbase/BaseSettings.cpp b/lib/appbase/BaseSettings.cpp new file mode 100644 index 000000000..efcea6f7b --- /dev/null +++ b/lib/appbase/BaseSettings.cpp @@ -0,0 +1,128 @@ +#include "BaseSettings.hpp" + +#include + +#include "util/Clamp.hpp" + +namespace AB_NAMESPACE { + +std::vector> _settings; + +AB_SETTINGS_CLASS *AB_SETTINGS_CLASS::instance = nullptr; + +void _actuallyRegisterSetting( + std::weak_ptr setting) +{ + _settings.push_back(setting); +} + +AB_SETTINGS_CLASS::AB_SETTINGS_CLASS(const QString &settingsDirectory) +{ + AB_SETTINGS_CLASS::instance = this; + + QString settingsPath = settingsDirectory + "/settings.json"; + + // get global instance of the settings library + auto settingsInstance = pajlada::Settings::SettingManager::getInstance(); + + settingsInstance->load(qPrintable(settingsPath)); + + settingsInstance->setBackupEnabled(true); + settingsInstance->setBackupSlots(9); + settingsInstance->saveMethod = + pajlada::Settings::SettingManager::SaveMethod::SaveOnExit; +} + +void AB_SETTINGS_CLASS::saveSnapshot() +{ + rapidjson::Document *d = new rapidjson::Document(rapidjson::kObjectType); + rapidjson::Document::AllocatorType &a = d->GetAllocator(); + + for (const auto &weakSetting : _settings) + { + auto setting = weakSetting.lock(); + if (!setting) + { + continue; + } + + rapidjson::Value key(setting->getPath().c_str(), a); + auto curVal = setting->unmarshalJSON(); + if (curVal == nullptr) + { + continue; + } + + rapidjson::Value val; + val.CopyFrom(*curVal, a); + d->AddMember(key.Move(), val.Move(), a); + } + + // log("Snapshot state: {}", rj::stringify(*d)); + + this->snapshot_.reset(d); +} + +void AB_SETTINGS_CLASS::restoreSnapshot() +{ + if (!this->snapshot_) + { + return; + } + + const auto &snapshot = *(this->snapshot_.get()); + + if (!snapshot.IsObject()) + { + return; + } + + for (const auto &weakSetting : _settings) + { + auto setting = weakSetting.lock(); + if (!setting) + { + continue; + } + + const char *path = setting->getPath().c_str(); + + if (!snapshot.HasMember(path)) + { + continue; + } + + setting->marshalJSON(snapshot[path]); + } +} + +float AB_SETTINGS_CLASS::getClampedUiScale() const +{ + return clamp(this->uiScale.getValue(), 0.1, 10); +} + +void AB_SETTINGS_CLASS::setClampedUiScale(float value) +{ + this->uiScale.setValue(clamp(value, 0.1, 10)); +} + +#ifndef AB_CUSTOM_SETTINGS +Settings *getSettings() +{ + static_assert(std::is_same_v, + "`AB_SETTINGS_CLASS` must be the same as `Settings`"); + + assert(AB_SETTINGS_CLASS::instance); + + return AB_SETTINGS_CLASS::instance; +} +#endif + +AB_SETTINGS_CLASS *getABSettings() +{ + assert(AB_SETTINGS_CLASS::instance); + + return AB_SETTINGS_CLASS::instance; +} + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/BaseSettings.hpp b/lib/appbase/BaseSettings.hpp new file mode 100644 index 000000000..c8a2cba36 --- /dev/null +++ b/lib/appbase/BaseSettings.hpp @@ -0,0 +1,52 @@ +#ifndef AB_SETTINGS_H +#define AB_SETTINGS_H + +#include +#include +#include +#include + +#include "common/ChatterinoSetting.hpp" + +#ifdef AB_CUSTOM_SETTINGS +# define AB_SETTINGS_CLASS ABSettings +#else +# define AB_SETTINGS_CLASS Settings +#endif + +namespace AB_NAMESPACE { + +class Settings; + +void _actuallyRegisterSetting( + std::weak_ptr setting); + +class AB_SETTINGS_CLASS +{ +public: + AB_SETTINGS_CLASS(const QString &settingsDirectory); + + void saveSnapshot(); + void restoreSnapshot(); + + static AB_SETTINGS_CLASS *instance; + + FloatSetting uiScale = {"/appearance/uiScale2", 1}; + BoolSetting windowTopMost = {"/appearance/windowAlwaysOnTop", false}; + + float getClampedUiScale() const; + void setClampedUiScale(float value); + +private: + std::unique_ptr snapshot_; +}; + +Settings *getSettings(); +AB_SETTINGS_CLASS *getABSettings(); + +} // namespace AB_NAMESPACE + +#ifdef CHATTERINO +# include "singletons/Settings.hpp" +#endif +#endif diff --git a/lib/appbase/BaseTheme.cpp b/lib/appbase/BaseTheme.cpp new file mode 100644 index 000000000..a3f849ac6 --- /dev/null +++ b/lib/appbase/BaseTheme.cpp @@ -0,0 +1,226 @@ +#include "BaseTheme.hpp" + +namespace AB_NAMESPACE { +namespace { + double getMultiplierByTheme(const QString &themeName) + { + if (themeName == "Light") + { + return 0.8; + } + else if (themeName == "White") + { + return 1.0; + } + else if (themeName == "Black") + { + return -1.0; + } + else if (themeName == "Dark") + { + return -0.8; + } + /* + else if (themeName == "Custom") + { + return getSettings()->customThemeMultiplier.getValue(); + } + */ + + return -0.8; + } +} // namespace + +bool AB_THEME_CLASS::isLightTheme() const +{ + return this->isLight_; +} + +void AB_THEME_CLASS::update() +{ + this->actuallyUpdate(this->themeHue, + getMultiplierByTheme(this->themeName.getValue())); + + this->updated.invoke(); +} + +void AB_THEME_CLASS::actuallyUpdate(double hue, double multiplier) +{ + this->isLight_ = multiplier > 0; + bool lightWin = isLight_; + + // QColor themeColor = QColor::fromHslF(hue, 0.43, 0.5); + QColor themeColor = QColor::fromHslF(hue, 0.8, 0.5); + QColor themeColorNoSat = QColor::fromHslF(hue, 0, 0.5); + + qreal sat = 0; + // 0.05; + + auto getColor = [multiplier](double h, double s, double l, double a = 1.0) { + return QColor::fromHslF(h, s, ((l - 0.5) * multiplier) + 0.5, a); + }; + + /// WINDOW + { + QColor bg = +#ifdef Q_OS_LINUX + this->window.background = lightWin ? "#fff" : QColor(61, 60, 56); +#else + this->window.background = lightWin ? "#fff" : "#111"; +#endif + + QColor fg = this->window.text = lightWin ? "#000" : "#eee"; + this->window.borderFocused = lightWin ? "#ccc" : themeColor; + this->window.borderUnfocused = lightWin ? "#ccc" : themeColorNoSat; + + // Ubuntu style + // TODO: add setting for this + // TabText = QColor(210, 210, 210); + // TabBackground = QColor(61, 60, 56); + // TabHoverText = QColor(210, 210, 210); + // TabHoverBackground = QColor(73, 72, 68); + + // message (referenced later) + this->messages.textColors.caret = // + this->messages.textColors.regular = isLight_ ? "#000" : "#fff"; + + QColor highlighted = lightWin ? QColor("#ff0000") : QColor("#ee6166"); + + /// TABS + if (lightWin) + { + this->tabs.regular = { + QColor("#444"), + {QColor("#fff"), QColor("#eee"), QColor("#fff")}, + {QColor("#fff"), QColor("#fff"), QColor("#fff")}}; + this->tabs.newMessage = { + QColor("#222"), + {QColor("#fff"), QColor("#eee"), QColor("#fff")}, + {QColor("#bbb"), QColor("#bbb"), QColor("#bbb")}}; + this->tabs.highlighted = { + fg, + {QColor("#fff"), QColor("#eee"), QColor("#fff")}, + {highlighted, highlighted, highlighted}}; + this->tabs.selected = { + QColor("#000"), + {QColor("#b4d7ff"), QColor("#b4d7ff"), QColor("#b4d7ff")}, + {QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}}; + } + else + { + this->tabs.regular = { + QColor("#aaa"), + {QColor("#252525"), QColor("#252525"), QColor("#252525")}, + {QColor("#444"), QColor("#444"), QColor("#444")}}; + this->tabs.newMessage = { + fg, + {QColor("#252525"), QColor("#252525"), QColor("#252525")}, + {QColor("#888"), QColor("#888"), QColor("#888")}}; + this->tabs.highlighted = { + fg, + {QColor("#252525"), QColor("#252525"), QColor("#252525")}, + {highlighted, highlighted, highlighted}}; + + this->tabs.selected = { + QColor("#fff"), + {QColor("#555555"), QColor("#555555"), QColor("#555555")}, + {QColor("#00aeef"), QColor("#00aeef"), QColor("#00aeef")}}; + } + + // scrollbar + this->scrollbars.highlights.highlight = QColor("#ee6166"); + this->scrollbars.highlights.subscription = QColor("#C466FF"); + + // this->tabs.newMessage = { + // fg, + // {QBrush(blendColors(themeColor, "#ccc", 0.9), Qt::FDiagPattern), + // QBrush(blendColors(themeColor, "#ccc", 0.9), Qt::FDiagPattern), + // QBrush(blendColors(themeColorNoSat, "#ccc", 0.9), + // Qt::FDiagPattern)}}; + + // this->tabs.newMessage = { + // fg, + // {QBrush(blendColors(themeColor, "#666", 0.7), + // Qt::FDiagPattern), + // QBrush(blendColors(themeColor, "#666", 0.5), + // Qt::FDiagPattern), + // QBrush(blendColors(themeColorNoSat, "#666", 0.7), + // Qt::FDiagPattern)}}; + // this->tabs.highlighted = {fg, {QColor("#777"), + // QColor("#777"), QColor("#666")}}; + + this->tabs.bottomLine = this->tabs.selected.backgrounds.regular.color(); + } // namespace AB_NAMESPACE + + // Split + bool flat = isLight_; + + // Message + this->messages.textColors.link = + isLight_ ? QColor(66, 134, 244) : QColor(66, 134, 244); + this->messages.textColors.system = QColor(140, 127, 127); + + this->messages.backgrounds.regular = getColor(0, sat, 1); + this->messages.backgrounds.alternate = getColor(0, sat, 0.96); + + if (isLight_) + { + this->messages.backgrounds.highlighted = + blendColors(themeColor, this->messages.backgrounds.regular, 0.8); + } + else + { + // REMOVED + // this->messages.backgrounds.highlighted = + // QColor(getSettings()->highlightColor); + } + + this->messages.backgrounds.subscription = + blendColors(QColor("#C466FF"), this->messages.backgrounds.regular, 0.7); + + // this->messages.backgrounds.resub + // this->messages.backgrounds.whisper + this->messages.disabled = getColor(0, sat, 1, 0.6); + // this->messages.seperator = + // this->messages.seperatorInner = + + // Scrollbar + this->scrollbars.background = QColor(0, 0, 0, 0); + // this->scrollbars.background = splits.background; + // this->scrollbars.background.setAlphaF(qreal(0.2)); + this->scrollbars.thumb = getColor(0, sat, 0.70); + this->scrollbars.thumbSelected = getColor(0, sat, 0.65); + + // tooltip + this->tooltip.background = QColor(0, 0, 0); + this->tooltip.text = QColor(255, 255, 255); + + // Selection + this->messages.selection = + isLightTheme() ? QColor(0, 0, 0, 64) : QColor(255, 255, 255, 64); +} + +QColor AB_THEME_CLASS::blendColors(const QColor &color1, const QColor &color2, + qreal ratio) +{ + int r = int(color1.red() * (1 - ratio) + color2.red() * ratio); + int g = int(color1.green() * (1 - ratio) + color2.green() * ratio); + int b = int(color1.blue() * (1 - ratio) + color2.blue() * ratio); + + return QColor(r, g, b, 255); +} + +#ifndef AB_CUSTOM_THEME +Theme *getTheme() +{ + static auto theme = [] { + auto theme = new Theme(); + theme->update(); + return theme; + }(); + + return theme; +} +#endif + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/BaseTheme.hpp b/lib/appbase/BaseTheme.hpp new file mode 100644 index 000000000..d5bc9c406 --- /dev/null +++ b/lib/appbase/BaseTheme.hpp @@ -0,0 +1,117 @@ +#ifndef AB_THEME_H +#define AB_THEME_H + +#include +#include +#include + +#ifdef AB_CUSTOM_THEME +# define AB_THEME_CLASS BaseTheme +#else +# define AB_THEME_CLASS Theme +#endif + +namespace AB_NAMESPACE { + +class Theme; + +class AB_THEME_CLASS +{ +public: + bool isLightTheme() const; + + struct TabColors { + QColor text; + struct { + QBrush regular; + QBrush hover; + QBrush unfocused; + } backgrounds; + struct { + QColor regular; + QColor hover; + QColor unfocused; + } line; + }; + + /// WINDOW + struct { + QColor background; + QColor text; + QColor borderUnfocused; + QColor borderFocused; + } window; + + /// TABS + struct { + TabColors regular; + TabColors newMessage; + TabColors highlighted; + TabColors selected; + QColor border; + QColor bottomLine; + } tabs; + + /// MESSAGES + struct { + struct { + QColor regular; + QColor caret; + QColor link; + QColor system; + } textColors; + + struct { + QColor regular; + QColor alternate; + QColor highlighted; + QColor subscription; + // QColor whisper; + } backgrounds; + + QColor disabled; + // QColor seperator; + // QColor seperatorInner; + QColor selection; + } messages; + + /// SCROLLBAR + struct { + QColor background; + QColor thumb; + QColor thumbSelected; + struct { + QColor highlight; + QColor subscription; + } highlights; + } scrollbars; + + /// TOOLTIP + struct { + QColor text; + QColor background; + } tooltip; + + void update(); + virtual void actuallyUpdate(double hue, double multiplier); + QColor blendColors(const QColor &color1, const QColor &color2, qreal ratio); + + pajlada::Signals::NoArgSignal updated; + + QStringSetting themeName{"/appearance/theme/name", "Dark"}; + DoubleSetting themeHue{"/appearance/theme/hue", 0.0}; + +private: + bool isLight_ = false; +}; + +// Implemented in parent project if AB_CUSTOM_THEME is set. +// Otherwise implemented in BaseThemecpp +Theme *getTheme(); + +} // namespace AB_NAMESPACE + +#ifdef CHATTERINO +# include "singletons/Theme.hpp" +#endif +#endif diff --git a/lib/appbase/common/Aliases.hpp b/lib/appbase/common/Aliases.hpp new file mode 100644 index 000000000..2dea876b7 --- /dev/null +++ b/lib/appbase/common/Aliases.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +#define QStringAlias(name) \ + namespace chatterino { \ + struct name { \ + QString string; \ + bool operator==(const name &other) const \ + { \ + return this->string == other.string; \ + } \ + bool operator!=(const name &other) const \ + { \ + return this->string != other.string; \ + } \ + }; \ + } /* namespace chatterino */ \ + namespace std { \ + template <> \ + struct hash { \ + size_t operator()(const chatterino::name &s) const \ + { \ + return qHash(s.string); \ + } \ + }; \ + } /* namespace std */ + +QStringAlias(UserName); +QStringAlias(UserId); +QStringAlias(Url); +QStringAlias(Tooltip); +QStringAlias(EmoteId); +QStringAlias(EmoteName); diff --git a/lib/appbase/common/ChatterinoSetting.cpp b/lib/appbase/common/ChatterinoSetting.cpp new file mode 100644 index 000000000..0f249c285 --- /dev/null +++ b/lib/appbase/common/ChatterinoSetting.cpp @@ -0,0 +1,12 @@ +#include "common/ChatterinoSetting.hpp" + +#include "BaseSettings.hpp" + +namespace AB_NAMESPACE { + +void _registerSetting(std::weak_ptr setting) +{ + _actuallyRegisterSetting(setting); +} + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/common/ChatterinoSetting.hpp b/lib/appbase/common/ChatterinoSetting.hpp new file mode 100644 index 000000000..6b965f230 --- /dev/null +++ b/lib/appbase/common/ChatterinoSetting.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +namespace AB_NAMESPACE { + +void _registerSetting(std::weak_ptr setting); + +template +class ChatterinoSetting : public pajlada::Settings::Setting +{ +public: + ChatterinoSetting(const std::string &path) + : pajlada::Settings::Setting(path) + { + _registerSetting(this->getData()); + } + + ChatterinoSetting(const std::string &path, const Type &defaultValue) + : pajlada::Settings::Setting(path, defaultValue) + { + _registerSetting(this->getData()); + } + + template + ChatterinoSetting &operator=(const T2 &newValue) + { + this->setValue(newValue); + + return *this; + } + + ChatterinoSetting &operator=(Type &&newValue) noexcept + { + pajlada::Settings::Setting::operator=(newValue); + + return *this; + } + + using pajlada::Settings::Setting::operator==; + using pajlada::Settings::Setting::operator!=; + + using pajlada::Settings::Setting::operator Type; +}; + +using BoolSetting = ChatterinoSetting; +using FloatSetting = ChatterinoSetting; +using DoubleSetting = ChatterinoSetting; +using IntSetting = ChatterinoSetting; +using StringSetting = ChatterinoSetting; +using QStringSetting = ChatterinoSetting; + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/common/FlagsEnum.hpp b/lib/appbase/common/FlagsEnum.hpp new file mode 100644 index 000000000..1a393e9c6 --- /dev/null +++ b/lib/appbase/common/FlagsEnum.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include + +namespace chatterino { + +template ::type> +class FlagsEnum +{ +public: + FlagsEnum() + : value_(static_cast(0)) + { + } + + FlagsEnum(T value) + : value_(value) + { + } + + FlagsEnum(std::initializer_list flags) + { + for (auto flag : flags) + { + this->set(flag); + } + } + + bool operator==(const FlagsEnum &other) + { + return this->value_ == other.value_; + } + + bool operator!=(const FlagsEnum &other) + { + return this->value_ != other.value_; + } + + void set(T flag) + { + reinterpret_cast(this->value_) |= static_cast(flag); + } + + void unset(T flag) + { + reinterpret_cast(this->value_) &= ~static_cast(flag); + } + + void set(T flag, bool value) + { + if (value) + this->set(flag); + else + this->unset(flag); + } + + bool has(T flag) const + { + return static_cast(this->value_) & static_cast(flag); + } + + bool hasAny(FlagsEnum flags) const + { + return static_cast(this->value_) & static_cast(flags.value_); + } + + bool hasAll(FlagsEnum flags) const + { + return (static_cast(this->value_) & static_cast(flags.value_)) && + static_cast(flags->value); + } + + bool hasNone(std::initializer_list flags) const + { + return !this->hasAny(flags); + } + +private: + T value_{}; +}; + +} // namespace chatterino diff --git a/lib/appbase/common/Outcome.hpp b/lib/appbase/common/Outcome.hpp new file mode 100644 index 000000000..01be69fd8 --- /dev/null +++ b/lib/appbase/common/Outcome.hpp @@ -0,0 +1,51 @@ +#pragma once + +namespace chatterino { + +struct SuccessTag { +}; + +struct FailureTag { +}; + +const SuccessTag Success{}; +const FailureTag Failure{}; + +class Outcome +{ +public: + Outcome(SuccessTag) + : success_(true) + { + } + + Outcome(FailureTag) + : success_(false) + { + } + + explicit operator bool() const + { + return this->success_; + } + + bool operator!() const + { + return !this->success_; + } + + bool operator==(const Outcome &other) const + { + return this->success_ == other.success_; + } + + bool operator!=(const Outcome &other) const + { + return !this->operator==(other); + } + +private: + bool success_; +}; + +} // namespace chatterino diff --git a/lib/appbase/common/Singleton.hpp b/lib/appbase/common/Singleton.hpp new file mode 100644 index 000000000..3aa6c5f83 --- /dev/null +++ b/lib/appbase/common/Singleton.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace AB_NAMESPACE { + +class Settings; +class Paths; + +class Singleton : boost::noncopyable +{ +public: + virtual ~Singleton() = default; + + virtual void initialize(Settings &settings, Paths &paths) + { + (void)(settings); + (void)(paths); + } + + virtual void save() + { + } +}; + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/debug/AssertInGuiThread.hpp b/lib/appbase/debug/AssertInGuiThread.hpp new file mode 100644 index 000000000..707aa8046 --- /dev/null +++ b/lib/appbase/debug/AssertInGuiThread.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +namespace AB_NAMESPACE { + +static void assertInGuiThread() +{ +#ifdef _DEBUG + assert(QCoreApplication::instance()->thread() == QThread::currentThread()); +#endif +} + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/debug/Benchmark.cpp b/lib/appbase/debug/Benchmark.cpp new file mode 100644 index 000000000..765ebcb09 --- /dev/null +++ b/lib/appbase/debug/Benchmark.cpp @@ -0,0 +1,21 @@ +#include "Benchmark.hpp" + +namespace AB_NAMESPACE { + +BenchmarkGuard::BenchmarkGuard(const QString &_name) + : name_(_name) +{ + timer_.start(); +} + +BenchmarkGuard::~BenchmarkGuard() +{ + log("{} {} ms", this->name_, float(timer_.nsecsElapsed()) / 1000000.0f); +} + +qreal BenchmarkGuard::getElapsedMs() +{ + return qreal(timer_.nsecsElapsed()) / 1000000.0; +} + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/debug/Benchmark.hpp b/lib/appbase/debug/Benchmark.hpp new file mode 100644 index 000000000..c477aa346 --- /dev/null +++ b/lib/appbase/debug/Benchmark.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "debug/Log.hpp" + +#include +#include + +namespace AB_NAMESPACE { + +class BenchmarkGuard : boost::noncopyable +{ +public: + BenchmarkGuard(const QString &_name); + ~BenchmarkGuard(); + qreal getElapsedMs(); + +private: + QElapsedTimer timer_; + QString name_; +}; + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/debug/Log.hpp b/lib/appbase/debug/Log.hpp new file mode 100644 index 000000000..af8b136e2 --- /dev/null +++ b/lib/appbase/debug/Log.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "util/Helpers.hpp" + +#include +#include + +namespace AB_NAMESPACE { + +template +inline void log(const std::string &formatString, Args &&... args) +{ + qDebug().noquote() << QTime::currentTime().toString("hh:mm:ss.zzz") + << fS(formatString, std::forward(args)...).c_str(); +} + +template +inline void log(const char *formatString, Args &&... args) +{ + log(std::string(formatString), std::forward(args)...); +} + +template +inline void log(const QString &formatString, Args &&... args) +{ + log(formatString.toStdString(), std::forward(args)...); +} + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/main.cpp b/lib/appbase/main.cpp new file mode 100644 index 000000000..7a1254e89 --- /dev/null +++ b/lib/appbase/main.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include + +#include "ABSettings.hpp" +#include "ABTheme.hpp" +#include "singletons/Fonts.hpp" +#include "widgets/BaseWindow.hpp" + +int main(int argc, char *argv[]) +{ + using namespace AB_NAMESPACE; + + QApplication a(argc, argv); + + auto path = + QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + qDebug() << path; + + QDir(path).mkdir("."); + + new Settings(path); + new Fonts(); + + BaseWindow widget(nullptr, BaseWindow::EnableCustomFrame); + widget.setWindowTitle("asdf"); + widget.show(); + + return a.exec(); +} diff --git a/lib/appbase/main.pro b/lib/appbase/main.pro new file mode 100644 index 000000000..d590517e8 --- /dev/null +++ b/lib/appbase/main.pro @@ -0,0 +1,108 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2018-11-19T19:03:22 +# +#------------------------------------------------- + +!AB_NOT_STANDALONE { + message(appbase standalone) + QT += core gui widgets + TARGET = main + TEMPLATE = app + SOURCES += main.cpp + + # https://bugreports.qt.io/browse/QTBUG-27018 + equals(QMAKE_CXX, "clang++")|equals(QMAKE_CXX, "g++") { + TARGET = bin/appbase + } +} + +#DEFINES += QT_DEPRECATED_WARNINGS +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 + +macx { + # osx (Tested on macOS Mojave and High Sierra) + CONFIG += c++17 +} else { + CONFIG += c++17 + win32-msvc* { + # win32 msvc + QMAKE_CXXFLAGS += /std:c++17 + } else { + # clang/gcc on linux or win32 + QMAKE_CXXFLAGS += -std=c++17 + } +} + +include(../warnings.pri) +include(../lib/winsdk.pri) +include(../lib/fmt.pri) +include(../lib/boost.pri) +include(../lib/rapidjson.pri) +include(../lib/serialize.pri) +include(../lib/signals.pri) +include(../lib/settings.pri) + +debug { + DEFINES += QT_DEBUG +} + +linux { + LIBS += -lrt + QMAKE_LFLAGS += -lrt +} + +macx { + INCLUDEPATH += /usr/local/include + INCLUDEPATH += /usr/local/opt/openssl/include + LIBS += -L/usr/local/opt/openssl/lib +} + +SOURCES += \ + $$PWD/BaseSettings.cpp \ + $$PWD/BaseTheme.cpp \ + $$PWD/common/ChatterinoSetting.cpp \ + $$PWD/debug/Benchmark.cpp \ + $$PWD/singletons/Fonts.cpp \ + $$PWD/util/FunctionEventFilter.cpp \ + $$PWD/util/FuzzyConvert.cpp \ + $$PWD/util/WindowsHelper.cpp \ + $$PWD/widgets/BaseWidget.cpp \ + $$PWD/widgets/BaseWindow.cpp \ + $$PWD/widgets/Label.cpp \ + $$PWD/widgets/TooltipWidget.cpp \ + $$PWD/widgets/helper/Button.cpp \ + $$PWD/widgets/helper/EffectLabel.cpp \ + $$PWD/widgets/helper/SignalLabel.cpp \ + $$PWD/widgets/helper/TitlebarButton.cpp \ + +HEADERS += \ + $$PWD/BaseSettings.hpp \ + $$PWD/BaseTheme.hpp \ + $$PWD/common/ChatterinoSetting.hpp \ + $$PWD/common/FlagsEnum.hpp \ + $$PWD/common/Outcome.hpp \ + $$PWD/common/Singleton.hpp \ + $$PWD/debug/AssertInGuiThread.hpp \ + $$PWD/debug/Benchmark.hpp \ + $$PWD/debug/Log.hpp \ + $$PWD/singletons/Fonts.hpp \ + $$PWD/util/Clamp.hpp \ + $$PWD/util/CombinePath.hpp \ + $$PWD/util/DistanceBetweenPoints.hpp \ + $$PWD/util/FunctionEventFilter.hpp \ + $$PWD/util/FuzzyConvert.hpp \ + $$PWD/util/Helpers.hpp \ + $$PWD/util/LayoutHelper.hpp \ + $$PWD/util/PostToThread.hpp \ + $$PWD/util/RapidJsonSerializeQString.hpp \ + $$PWD/util/Shortcut.hpp \ + $$PWD/util/WindowsHelper.hpp \ + $$PWD/widgets/BaseWidget.hpp \ + $$PWD/widgets/BaseWindow.hpp \ + $$PWD/widgets/Label.hpp \ + $$PWD/widgets/TooltipWidget.hpp \ + $$PWD/widgets/helper/Button.hpp \ + $$PWD/widgets/helper/EffectLabel.hpp \ + $$PWD/widgets/helper/SignalLabel.hpp \ + $$PWD/widgets/helper/TitlebarButton.hpp \ diff --git a/lib/appbase/singletons/Fonts.cpp b/lib/appbase/singletons/Fonts.cpp new file mode 100644 index 000000000..e3ccf4007 --- /dev/null +++ b/lib/appbase/singletons/Fonts.cpp @@ -0,0 +1,179 @@ +#include "singletons/Fonts.hpp" + +#include "BaseSettings.hpp" +#include "debug/AssertInGuiThread.hpp" + +#include +#include + +#ifdef CHATTERINO +# include "Application.hpp" +# include "singletons/WindowManager.hpp" +#endif + +#ifdef Q_OS_WIN32 +# define DEFAULT_FONT_FAMILY "Segoe UI" +# define DEFAULT_FONT_SIZE 10 +#else +# ifdef Q_OS_MACOS +# define DEFAULT_FONT_FAMILY "Helvetica Neue" +# define DEFAULT_FONT_SIZE 12 +# else +# define DEFAULT_FONT_FAMILY "Arial" +# define DEFAULT_FONT_SIZE 11 +# endif +#endif + +namespace AB_NAMESPACE { +namespace { + int getBoldness() + { +#ifdef CHATTERINO + return getSettings()->boldScale.getValue(); +#else + return QFont::Bold; +#endif + } +} // namespace + +Fonts *Fonts::instance = nullptr; + +Fonts::Fonts() + : chatFontFamily("/appearance/currentFontFamily", DEFAULT_FONT_FAMILY) + , chatFontSize("/appearance/currentFontSize", DEFAULT_FONT_SIZE) +{ + Fonts::instance = this; + + this->fontsByType_.resize(size_t(FontStyle::EndType)); +} + +void Fonts::initialize(Settings &, Paths &) +{ + this->chatFontFamily.connect( + [this]() { + assertInGuiThread(); + + for (auto &map : this->fontsByType_) + { + map.clear(); + } + this->fontChanged.invoke(); + }, + false); + + this->chatFontSize.connect( + [this]() { + assertInGuiThread(); + + for (auto &map : this->fontsByType_) + { + map.clear(); + } + this->fontChanged.invoke(); + }, + false); + +#ifdef CHATTERINO + getSettings()->boldScale.connect( + [this]() { + assertInGuiThread(); + + // REMOVED + getApp()->windows->incGeneration(); + + for (auto &map : this->fontsByType_) + { + map.clear(); + } + this->fontChanged.invoke(); + }, + false); +#endif +} // namespace AB_NAMESPACE + +QFont Fonts::getFont(FontStyle type, float scale) +{ + return this->getOrCreateFontData(type, scale).font; +} + +QFontMetrics Fonts::getFontMetrics(FontStyle type, float scale) +{ + return this->getOrCreateFontData(type, scale).metrics; +} + +Fonts::FontData &Fonts::getOrCreateFontData(FontStyle type, float scale) +{ + assertInGuiThread(); + + assert(type < FontStyle::EndType); + + auto &map = this->fontsByType_[size_t(type)]; + + // find element + auto it = map.find(scale); + if (it != map.end()) + { + // return if found + + return it->second; + } + + // emplace new element + auto result = map.emplace(scale, this->createFontData(type, scale)); + assert(result.second); + + return result.first->second; +} + +Fonts::FontData Fonts::createFontData(FontStyle type, float scale) +{ + // check if it's a chat (scale the setting) + if (type >= FontStyle::ChatStart && type <= FontStyle::ChatEnd) + { + static std::unordered_map sizeScale{ + {FontStyle::ChatSmall, {0.6f, false, QFont::Normal}}, + {FontStyle::ChatMediumSmall, {0.8f, false, QFont::Normal}}, + {FontStyle::ChatMedium, {1, false, QFont::Normal}}, + {FontStyle::ChatMediumBold, + {1, false, QFont::Weight(getBoldness())}}, + {FontStyle::ChatMediumItalic, {1, true, QFont::Normal}}, + {FontStyle::ChatLarge, {1.2f, false, QFont::Normal}}, + {FontStyle::ChatVeryLarge, {1.4f, false, QFont::Normal}}, + }; + sizeScale[FontStyle::ChatMediumBold] = {1, false, + QFont::Weight(getBoldness())}; + auto data = sizeScale[type]; + return FontData( + QFont(this->chatFontFamily.getValue(), + int(this->chatFontSize.getValue() * data.scale * scale), + data.weight, data.italic)); + } + + // normal Ui font (use pt size) + { +#ifdef Q_OS_MAC + constexpr float multiplier = 0.8f; +#else + constexpr float multiplier = 1.f; +#endif + + static std::unordered_map defaultSize{ + {FontStyle::Tiny, {8, "Monospace", false, QFont::Normal}}, + {FontStyle::UiMedium, + {int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}}, + {FontStyle::UiTabs, + {int(9 * multiplier), DEFAULT_FONT_FAMILY, false, QFont::Normal}}, + }; + + UiFontData &data = defaultSize[type]; + QFont font(data.name, int(data.size * scale), data.weight, data.italic); + return FontData(font); + } +} + +Fonts *getFonts() +{ + return Fonts::instance; +} + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/singletons/Fonts.hpp b/lib/appbase/singletons/Fonts.hpp new file mode 100644 index 000000000..ff67bc3e7 --- /dev/null +++ b/lib/appbase/singletons/Fonts.hpp @@ -0,0 +1,92 @@ +#pragma once + +#include "common/ChatterinoSetting.hpp" +#include "common/Singleton.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace AB_NAMESPACE { + +class Settings; +class Paths; + +enum class FontStyle : uint8_t { + Tiny, + ChatSmall, + ChatMediumSmall, + ChatMedium, + ChatMediumBold, + ChatMediumItalic, + ChatLarge, + ChatVeryLarge, + + UiMedium, + UiTabs, + + // don't remove this value + EndType, + + // make sure to update these values accordingly! + ChatStart = ChatSmall, + ChatEnd = ChatVeryLarge, +}; + +class Fonts final : public Singleton +{ +public: + Fonts(); + + virtual void initialize(Settings &settings, Paths &paths) override; + + // font data gets set in createFontData(...) + + QFont getFont(FontStyle type, float scale); + QFontMetrics getFontMetrics(FontStyle type, float scale); + + QStringSetting chatFontFamily; + IntSetting chatFontSize; + + pajlada::Signals::NoArgSignal fontChanged; + static Fonts *instance; + +private: + struct FontData { + FontData(const QFont &_font) + : font(_font) + , metrics(_font) + { + } + + const QFont font; + const QFontMetrics metrics; + }; + + struct ChatFontData { + float scale; + bool italic; + QFont::Weight weight; + }; + + struct UiFontData { + float size; + const char *name; + bool italic; + QFont::Weight weight; + }; + + FontData &getOrCreateFontData(FontStyle type, float scale); + FontData createFontData(FontStyle type, float scale); + + std::vector> fontsByType_; +}; + +Fonts *getFonts(); + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/util/Clamp.hpp b/lib/appbase/util/Clamp.hpp new file mode 100644 index 000000000..867e891ad --- /dev/null +++ b/lib/appbase/util/Clamp.hpp @@ -0,0 +1,13 @@ +#pragma once + +namespace AB_NAMESPACE { + +// http://en.cppreference.com/w/cpp/algorithm/clamp + +template +constexpr const T &clamp(const T &v, const T &lo, const T &hi) +{ + return assert(!(hi < lo)), (v < lo) ? lo : (hi < v) ? hi : v; +} + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/util/CombinePath.hpp b/lib/appbase/util/CombinePath.hpp new file mode 100644 index 000000000..d0ed8e7ad --- /dev/null +++ b/lib/appbase/util/CombinePath.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace chatterino { + +// https://stackoverflow.com/a/13014491 +inline QString combinePath(const QString &a, const QString &b) +{ + return QDir::cleanPath(a + QDir::separator() + b); +} + +} // namespace chatterino diff --git a/lib/appbase/util/DistanceBetweenPoints.hpp b/lib/appbase/util/DistanceBetweenPoints.hpp new file mode 100644 index 000000000..7a2a834cc --- /dev/null +++ b/lib/appbase/util/DistanceBetweenPoints.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include + +namespace chatterino { + +inline float distanceBetweenPoints(const QPointF &p1, const QPointF &p2) +{ + QPointF tmp = p1 - p2; + + float distance = 0.f; + distance += tmp.x() * tmp.x(); + distance += tmp.y() * tmp.y(); + + return sqrt(distance); +} + +} // namespace chatterino diff --git a/lib/appbase/util/FunctionEventFilter.cpp b/lib/appbase/util/FunctionEventFilter.cpp new file mode 100644 index 000000000..923c2e7fd --- /dev/null +++ b/lib/appbase/util/FunctionEventFilter.cpp @@ -0,0 +1,17 @@ +#include "FunctionEventFilter.hpp" + +namespace AB_NAMESPACE { + +FunctionEventFilter::FunctionEventFilter( + QObject *parent, std::function function) + : QObject(parent) + , function_(std::move(function)) +{ +} + +bool FunctionEventFilter::eventFilter(QObject *watched, QEvent *event) +{ + return this->function_(watched, event); +} + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/util/FunctionEventFilter.hpp b/lib/appbase/util/FunctionEventFilter.hpp new file mode 100644 index 000000000..cecac1bf1 --- /dev/null +++ b/lib/appbase/util/FunctionEventFilter.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +namespace AB_NAMESPACE { + +class FunctionEventFilter : public QObject +{ + Q_OBJECT + +public: + FunctionEventFilter(QObject *parent, + std::function function); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +private: + std::function function_; +}; + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/util/FuzzyConvert.cpp b/lib/appbase/util/FuzzyConvert.cpp new file mode 100644 index 000000000..cfdc3f01c --- /dev/null +++ b/lib/appbase/util/FuzzyConvert.cpp @@ -0,0 +1,33 @@ +#include "FuzzyConvert.hpp" + +#include + +namespace chatterino { + +int fuzzyToInt(const QString &str, int default_) +{ + static auto intFinder = QRegularExpression("[0-9]+"); + + auto match = intFinder.match(str); + if (match.hasMatch()) + { + return match.captured().toInt(); + } + + return default_; +} + +float fuzzyToFloat(const QString &str, float default_) +{ + static auto floatFinder = QRegularExpression("[0-9]+(\\.[0-9]+)?"); + + auto match = floatFinder.match(str); + if (match.hasMatch()) + { + return match.captured().toFloat(); + } + + return default_; +} + +} // namespace chatterino diff --git a/lib/appbase/util/FuzzyConvert.hpp b/lib/appbase/util/FuzzyConvert.hpp new file mode 100644 index 000000000..a3875da97 --- /dev/null +++ b/lib/appbase/util/FuzzyConvert.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace chatterino { + +int fuzzyToInt(const QString &str, int default_); +float fuzzyToFloat(const QString &str, float default_); + +} // namespace chatterino diff --git a/lib/appbase/util/Helpers.hpp b/lib/appbase/util/Helpers.hpp new file mode 100644 index 000000000..4106880c4 --- /dev/null +++ b/lib/appbase/util/Helpers.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +namespace AB_NAMESPACE { + +template +auto fS(Args &&... args) +{ + return fmt::format(std::forward(args)...); +} + +static QString CreateUUID() +{ + auto uuid = QUuid::createUuid(); + return uuid.toString(); +} + +static QString createLink(const QString &url, bool file = false) +{ + return QString("" + + url + ""; +} + +static QString createNamedLink(const QString &url, const QString &name, + bool file = false) +{ + return QString("" + + name + ""; +} + +static QString shortenString(const QString &str, unsigned maxWidth = 50) +{ + auto shortened = QString(str); + + if (str.size() > int(maxWidth)) + { + shortened.resize(int(maxWidth)); + shortened += "..."; + } + + return shortened; +} + +} // namespace AB_NAMESPACE + +namespace fmt { + +// format_arg for QString +inline void format_arg(BasicFormatter &f, const char *&, const QString &v) +{ + f.writer().write("{}", v.toStdString()); +} + +} // namespace fmt diff --git a/lib/appbase/util/LayoutHelper.hpp b/lib/appbase/util/LayoutHelper.hpp new file mode 100644 index 000000000..f66fbef87 --- /dev/null +++ b/lib/appbase/util/LayoutHelper.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +namespace chatterino { + +using LayoutItem = boost::variant; + +template +T *makeLayout(std::initializer_list items) +{ + auto t = new T; + + for (auto &item : items) + { + switch (item.which()) + { + case 0: + t->addItem(new QWidgetItem(boost::get(item))); + break; + case 1: + t->addItem(boost::get(item)); + break; + } + } + + return t; +} + +template +T *makeWidget(With with) +{ + auto t = new T; + + with(t); + + return t; +} + +} // namespace chatterino diff --git a/lib/appbase/util/PostToThread.hpp b/lib/appbase/util/PostToThread.hpp new file mode 100644 index 000000000..5ea30c1ab --- /dev/null +++ b/lib/appbase/util/PostToThread.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include +#include + +#include + +#define async_exec(a) \ + QThreadPool::globalInstance()->start(new LambdaRunnable(a)); + +namespace AB_NAMESPACE { + +class LambdaRunnable : public QRunnable +{ +public: + LambdaRunnable(std::function action) + { + this->action_ = action; + } + + void run() + { + this->action_(); + } + +private: + std::function action_; +}; + +// Taken from +// https://stackoverflow.com/questions/21646467/how-to-execute-a-functor-or-a-lambda-in-a-given-thread-in-qt-gcd-style +// Qt 5/4 - preferred, has least allocations +template +static void postToThread(F &&fun, QObject *obj = qApp) +{ + struct Event : public QEvent { + using Fun = typename std::decay::type; + Fun fun; + Event(Fun &&fun) + : QEvent(QEvent::None) + , fun(std::move(fun)) + { + } + Event(const Fun &fun) + : QEvent(QEvent::None) + , fun(fun) + { + } + ~Event() override + { + fun(); + } + }; + QCoreApplication::postEvent(obj, new Event(std::forward(fun))); +} + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/util/RapidJsonSerializeQString.hpp b/lib/appbase/util/RapidJsonSerializeQString.hpp new file mode 100644 index 000000000..1c685d16f --- /dev/null +++ b/lib/appbase/util/RapidJsonSerializeQString.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +namespace pajlada { + +template <> +struct Serialize { + static rapidjson::Value get(const QString &value, + rapidjson::Document::AllocatorType &a) + { + return rapidjson::Value(value.toUtf8(), a); + } +}; + +template <> +struct Deserialize { + static QString get(const rapidjson::Value &value, bool *error = nullptr) + { + if (!value.IsString()) + { + PAJLADA_REPORT_ERROR(error) + return QString{}; + } + + try + { + return QString::fromUtf8(value.GetString(), + value.GetStringLength()); + } + catch (const std::exception &) + { + // int x = 5; + } + catch (...) + { + // int y = 5; + } + + return QString{}; + } +}; + +} // namespace pajlada diff --git a/lib/appbase/util/Shortcut.hpp b/lib/appbase/util/Shortcut.hpp new file mode 100644 index 000000000..faddab9b0 --- /dev/null +++ b/lib/appbase/util/Shortcut.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +namespace AB_NAMESPACE { + +template +inline void createShortcut(WidgetType *w, const char *key, Func func) +{ + auto s = new QShortcut(QKeySequence(key), w); + s->setContext(Qt::WidgetWithChildrenShortcut); + QObject::connect(s, &QShortcut::activated, w, func); +} + +template +inline void createWindowShortcut(WidgetType *w, const char *key, Func func) +{ + auto s = new QShortcut(QKeySequence(key), w); + s->setContext(Qt::WindowShortcut); + QObject::connect(s, &QShortcut::activated, w, func); +} + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/util/WindowsHelper.cpp b/lib/appbase/util/WindowsHelper.cpp new file mode 100644 index 000000000..9d6ba612a --- /dev/null +++ b/lib/appbase/util/WindowsHelper.cpp @@ -0,0 +1,86 @@ +#include "WindowsHelper.hpp" + +#include + +#ifdef USEWINSDK + +namespace AB_NAMESPACE { + +typedef enum MONITOR_DPI_TYPE { + MDT_EFFECTIVE_DPI = 0, + MDT_ANGULAR_DPI = 1, + MDT_RAW_DPI = 2, + MDT_DEFAULT = MDT_EFFECTIVE_DPI +} MONITOR_DPI_TYPE; + +typedef HRESULT(CALLBACK *GetDpiForMonitor_)(HMONITOR, MONITOR_DPI_TYPE, UINT *, + UINT *); + +boost::optional getWindowDpi(HWND hwnd) +{ + static HINSTANCE shcore = LoadLibrary(L"Shcore.dll"); + if (shcore != nullptr) + { + if (auto getDpiForMonitor = + GetDpiForMonitor_(GetProcAddress(shcore, "GetDpiForMonitor"))) + { + HMONITOR monitor = + MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + + UINT xScale, yScale; + + getDpiForMonitor(monitor, MDT_DEFAULT, &xScale, &yScale); + + return xScale; + } + } + + return boost::none; +} + +typedef HRESULT(CALLBACK *OleFlushClipboard_)(); + +void flushClipboard() +{ + static HINSTANCE ole32 = LoadLibrary(L"Ole32.dll"); + if (ole32 != nullptr) + { + if (auto oleFlushClipboard = + OleFlushClipboard_(GetProcAddress(ole32, "OleFlushClipboard"))) + { + oleFlushClipboard(); + } + } +} + +constexpr const char *runKey = + "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"; + +bool isRegisteredForStartup() +{ + QSettings settings(runKey, QSettings::NativeFormat); + + return !settings.value("Chatterino").toString().isEmpty(); +} + +void setRegisteredForStartup(bool isRegistered) +{ + QSettings settings(runKey, QSettings::NativeFormat); + + if (isRegistered) + { + auto exePath = QFileInfo(QCoreApplication::applicationFilePath()) + .absoluteFilePath() + .replace('/', '\\'); + + settings.setValue("Chatterino", "\"" + exePath + "\" --autorun"); + } + else + { + settings.remove("Chatterino"); + } +} + +} // namespace AB_NAMESPACE + +#endif diff --git a/lib/appbase/util/WindowsHelper.hpp b/lib/appbase/util/WindowsHelper.hpp new file mode 100644 index 000000000..2665ad0ed --- /dev/null +++ b/lib/appbase/util/WindowsHelper.hpp @@ -0,0 +1,18 @@ +#pragma once + +#ifdef USEWINSDK + +# include +# include + +namespace AB_NAMESPACE { + +boost::optional getWindowDpi(HWND hwnd); +void flushClipboard(); + +bool isRegisteredForStartup(); +void setRegisteredForStartup(bool isRegistered); + +} // namespace AB_NAMESPACE + +#endif diff --git a/lib/appbase/widgets/BaseWidget.cpp b/lib/appbase/widgets/BaseWidget.cpp new file mode 100644 index 000000000..0a80d91ec --- /dev/null +++ b/lib/appbase/widgets/BaseWidget.cpp @@ -0,0 +1,164 @@ +#include "widgets/BaseWidget.hpp" + +#include "BaseSettings.hpp" +#include "BaseTheme.hpp" +#include "debug/Log.hpp" +#include "widgets/BaseWindow.hpp" + +#include +#include +#include +#include +#include + +namespace AB_NAMESPACE { + +BaseWidget::BaseWidget(QWidget *parent, Qt::WindowFlags f) + : QWidget(parent, f) +{ + // REMOVED + this->theme = getTheme(); + + this->signalHolder_.managedConnect(this->theme->updated, [this]() { + this->themeChangedEvent(); + + this->update(); + }); +} + +float BaseWidget::scale() const +{ + if (this->overrideScale_) + { + return this->overrideScale_.get(); + } + else if (auto baseWidget = dynamic_cast(this->window())) + { + return baseWidget->scale_; + } + else + { + return 1.f; + } +} + +void BaseWidget::setScale(float value) +{ + // update scale value + this->scale_ = value; + + this->scaleChangedEvent(this->scale()); + this->scaleChanged.invoke(this->scale()); + + this->setScaleIndependantSize(this->scaleIndependantSize()); +} + +void BaseWidget::setOverrideScale(boost::optional value) +{ + this->overrideScale_ = value; + this->setScale(this->scale()); +} + +boost::optional BaseWidget::overrideScale() const +{ + return this->overrideScale_; +} + +QSize BaseWidget::scaleIndependantSize() const +{ + return this->scaleIndependantSize_; +} + +int BaseWidget::scaleIndependantWidth() const +{ + return this->scaleIndependantSize_.width(); +} + +int BaseWidget::scaleIndependantHeight() const +{ + return this->scaleIndependantSize_.height(); +} + +void BaseWidget::setScaleIndependantSize(int width, int height) +{ + this->setScaleIndependantSize(QSize(width, height)); +} + +void BaseWidget::setScaleIndependantSize(QSize size) +{ + this->scaleIndependantSize_ = size; + + if (size.width() > 0) + { + this->setFixedWidth(int(size.width() * this->scale())); + } + if (size.height() > 0) + { + this->setFixedHeight(int(size.height() * this->scale())); + } +} + +void BaseWidget::setScaleIndependantWidth(int value) +{ + this->setScaleIndependantSize( + QSize(value, this->scaleIndependantSize_.height())); +} + +void BaseWidget::setScaleIndependantHeight(int value) +{ + this->setScaleIndependantSize( + QSize(this->scaleIndependantSize_.width(), value)); +} + +float BaseWidget::qtFontScale() const +{ + if (auto window = dynamic_cast(this->window())) + { + return this->scale() / window->nativeScale_; + } + else + { + return this->scale(); + } +} + +void BaseWidget::childEvent(QChildEvent *event) +{ + if (event->added()) + { + // add element if it's a basewidget + if (auto widget = dynamic_cast(event->child())) + { + this->widgets_.push_back(widget); + } + } + else if (event->removed()) + { + // find element to be removed + auto it = std::find_if(this->widgets_.begin(), this->widgets_.end(), + [&](auto &&x) { return x == event->child(); }); + + // remove if found + if (it != this->widgets_.end()) + { + this->widgets_.erase(it); + } + } +} + +void BaseWidget::showEvent(QShowEvent *) +{ + this->setScale(this->scale()); + this->themeChangedEvent(); +} + +void BaseWidget::scaleChangedEvent(float newDpi) +{ +} + +void BaseWidget::themeChangedEvent() +{ + // Do any color scheme updates here +} + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/widgets/BaseWidget.hpp b/lib/appbase/widgets/BaseWidget.hpp new file mode 100644 index 000000000..6b3908570 --- /dev/null +++ b/lib/appbase/widgets/BaseWidget.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include + +namespace AB_NAMESPACE { + +class Theme; +class BaseWindow; + +class BaseWidget : public QWidget +{ + Q_OBJECT + +public: + explicit BaseWidget(QWidget *parent, Qt::WindowFlags f = Qt::WindowFlags()); + + virtual float scale() const; + pajlada::Signals::Signal scaleChanged; + + boost::optional overrideScale() const; + void setOverrideScale(boost::optional); + + QSize scaleIndependantSize() const; + int scaleIndependantWidth() const; + int scaleIndependantHeight() const; + void setScaleIndependantSize(int width, int height); + void setScaleIndependantSize(QSize); + void setScaleIndependantWidth(int value); + void setScaleIndependantHeight(int value); + + float qtFontScale() const; + +protected: + virtual void childEvent(QChildEvent *) override; + virtual void showEvent(QShowEvent *) override; + + virtual void scaleChangedEvent(float newScale); + virtual void themeChangedEvent(); + + void setScale(float value); + + Theme *theme; + +private: + float scale_{1.f}; + boost::optional overrideScale_; + QSize scaleIndependantSize_; + + std::vector widgets_; + + pajlada::Signals::SignalHolder signalHolder_; + + friend class BaseWindow; +}; + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/widgets/BaseWindow.cpp b/lib/appbase/widgets/BaseWindow.cpp new file mode 100644 index 000000000..c3fa8eca4 --- /dev/null +++ b/lib/appbase/widgets/BaseWindow.cpp @@ -0,0 +1,917 @@ +#include "BaseWindow.hpp" + +#include "BaseSettings.hpp" +#include "BaseTheme.hpp" +#include "boost/algorithm/algorithm.hpp" +#include "debug/Log.hpp" +#include "util/PostToThread.hpp" +#include "util/Shortcut.hpp" +#include "util/WindowsHelper.hpp" +#include "widgets/Label.hpp" +#include "widgets/TooltipWidget.hpp" +#include "widgets/helper/EffectLabel.hpp" + +#include +#include +#include +#include +#include +#include + +#ifdef CHATTERINO +# include "Application.hpp" +# include "singletons/WindowManager.hpp" +#endif + +#ifdef USEWINSDK +# include +# include +# include +# include +# include +# include + +//#include +# pragma comment(lib, "Dwmapi.lib") + +# include +# include + +# define WM_DPICHANGED 0x02E0 +#endif + +#include "widgets/helper/TitlebarButton.hpp" + +namespace AB_NAMESPACE { + +BaseWindow::BaseWindow(QWidget *parent, Flags _flags) + : BaseWidget(parent, + Qt::Window | ((_flags & TopMost) ? Qt::WindowStaysOnTopHint + : Qt::WindowFlags())) + , enableCustomFrame_(_flags & EnableCustomFrame) + , frameless_(_flags & Frameless) + , flags_(_flags) +{ + if (this->frameless_) + { + this->enableCustomFrame_ = false; + this->setWindowFlag(Qt::FramelessWindowHint); + } + + this->init(); + + getSettings()->uiScale.connect( + [this]() { postToThread([this] { this->updateScale(); }); }, + this->connections_); + + this->updateScale(); + + createWindowShortcut(this, "CTRL+0", + [] { getSettings()->uiScale.setValue(1); }); + + // QTimer::this->scaleChangedEvent(this->getScale()); + + this->resize(300, 300); +} + +float BaseWindow::scale() const +{ + return this->overrideScale().value_or(this->scale_); +} + +float BaseWindow::qtFontScale() const +{ + return this->scale() / this->nativeScale_; +} + +BaseWindow::Flags BaseWindow::getFlags() +{ + return this->flags_; +} + +void BaseWindow::init() +{ + this->setWindowIcon(QIcon(":/images/icon.png")); + +#ifdef USEWINSDK + if (this->hasCustomWindowFrame()) + { + // CUSTOM WINDOW FRAME + QVBoxLayout *layout = new QVBoxLayout(); + this->ui_.windowLayout = layout; + layout->setContentsMargins(0, 1, 0, 0); + layout->setSpacing(0); + this->setLayout(layout); + { + if (!this->frameless_) + { + QHBoxLayout *buttonLayout = this->ui_.titlebarBox = + new QHBoxLayout(); + buttonLayout->setMargin(0); + layout->addLayout(buttonLayout); + + // title + Label *title = new Label; + QObject::connect( + this, &QWidget::windowTitleChanged, + [title](const QString &text) { title->setText(text); }); + + QSizePolicy policy(QSizePolicy::Ignored, + QSizePolicy::Preferred); + policy.setHorizontalStretch(1); + title->setSizePolicy(policy); + buttonLayout->addWidget(title); + this->ui_.titleLabel = title; + + // buttons + TitleBarButton *_minButton = new TitleBarButton; + _minButton->setButtonStyle(TitleBarButtonStyle::Minimize); + TitleBarButton *_maxButton = new TitleBarButton; + _maxButton->setButtonStyle(TitleBarButtonStyle::Maximize); + TitleBarButton *_exitButton = new TitleBarButton; + _exitButton->setButtonStyle(TitleBarButtonStyle::Close); + + QObject::connect(_minButton, &TitleBarButton::leftClicked, this, + [this] { + this->setWindowState(Qt::WindowMinimized | + this->windowState()); + }); + QObject::connect(_maxButton, &TitleBarButton::leftClicked, this, + [this, _maxButton] { + this->setWindowState( + _maxButton->getButtonStyle() != + TitleBarButtonStyle::Maximize + ? Qt::WindowActive + : Qt::WindowMaximized); + }); + QObject::connect(_exitButton, &TitleBarButton::leftClicked, + this, [this] { this->close(); }); + + this->ui_.minButton = _minButton; + this->ui_.maxButton = _maxButton; + this->ui_.exitButton = _exitButton; + + this->ui_.buttons.push_back(_minButton); + this->ui_.buttons.push_back(_maxButton); + this->ui_.buttons.push_back(_exitButton); + + // buttonLayout->addStretch(1); + buttonLayout->addWidget(_minButton); + buttonLayout->addWidget(_maxButton); + buttonLayout->addWidget(_exitButton); + buttonLayout->setSpacing(0); + } + } + this->ui_.layoutBase = new BaseWidget(this); + layout->addWidget(this->ui_.layoutBase); + } + +// DPI +// auto dpi = getWindowDpi(this->winId()); + +// if (dpi) { +// this->scale = dpi.value() / 96.f; +// } +#endif + +#ifdef USEWINSDK + // fourtf: don't ask me why we need to delay this + if (!(this->flags_ & Flags::TopMost)) + { + QTimer::singleShot(1, this, [this] { + getSettings()->windowTopMost.connect( + [this](bool topMost, auto) { + ::SetWindowPos(HWND(this->winId()), + topMost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, + 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + }, + this->managedConnections_); + }); + } +#else +// if (getSettings()->windowTopMost.getValue()) { +// this->setWindowFlag(Qt::WindowStaysOnTopHint); +// } +#endif +} + +void BaseWindow::setStayInScreenRect(bool value) +{ + this->stayInScreenRect_ = value; + + this->moveIntoDesktopRect(this); +} + +bool BaseWindow::getStayInScreenRect() const +{ + return this->stayInScreenRect_; +} + +void BaseWindow::setActionOnFocusLoss(ActionOnFocusLoss value) +{ + this->actionOnFocusLoss_ = value; +} + +BaseWindow::ActionOnFocusLoss BaseWindow::getActionOnFocusLoss() const +{ + return this->actionOnFocusLoss_; +} + +QWidget *BaseWindow::getLayoutContainer() +{ + if (this->hasCustomWindowFrame()) + { + return this->ui_.layoutBase; + } + else + { + return this; + } +} + +bool BaseWindow::hasCustomWindowFrame() +{ +#ifdef USEWINSDK + static bool isWin8 = IsWindows8OrGreater(); + + return isWin8 && this->enableCustomFrame_; +#else + return false; +#endif +} + +void BaseWindow::themeChangedEvent() +{ + if (this->hasCustomWindowFrame()) + { + QPalette palette; + palette.setColor(QPalette::Background, QColor(0, 0, 0, 0)); + palette.setColor(QPalette::Foreground, this->theme->window.text); + this->setPalette(palette); + + if (this->ui_.titleLabel) + { + QPalette palette_title; + palette_title.setColor( + QPalette::Foreground, + this->theme->isLightTheme() ? "#333" : "#ccc"); + this->ui_.titleLabel->setPalette(palette_title); + } + + for (Button *button : this->ui_.buttons) + { + button->setMouseEffectColor(this->theme->window.text); + } + } + else + { + QPalette palette; + palette.setColor(QPalette::Background, this->theme->window.background); + palette.setColor(QPalette::Foreground, this->theme->window.text); + this->setPalette(palette); + } +} + +bool BaseWindow::event(QEvent *event) +{ + if (event->type() == + QEvent::WindowDeactivate /*|| event->type() == QEvent::FocusOut*/) + { + this->onFocusLost(); + } + + return QWidget::event(event); +} + +void BaseWindow::wheelEvent(QWheelEvent *event) +{ + if (event->orientation() != Qt::Vertical) + { + return; + } + + if (event->modifiers() & Qt::ControlModifier) + { + if (event->delta() > 0) + { + getSettings()->setClampedUiScale( + getSettings()->getClampedUiScale() + 0.1); + } + else + { + getSettings()->setClampedUiScale( + getSettings()->getClampedUiScale() - 0.1); + } + } +} + +void BaseWindow::onFocusLost() +{ + switch (this->getActionOnFocusLoss()) + { + case Delete: + { + this->deleteLater(); + } + break; + + case Close: + { + this->close(); + } + break; + + case Hide: + { + this->hide(); + } + break; + + default:; + } +} + +void BaseWindow::mousePressEvent(QMouseEvent *event) +{ +#ifndef Q_OS_WIN + if (this->flags_ & FramelessDraggable) + { + this->movingRelativePos = event->localPos(); + if (auto widget = + this->childAt(event->localPos().x(), event->localPos().y())) + { + std::function recursiveCheckMouseTracking; + recursiveCheckMouseTracking = [&](QWidget *widget) { + if (widget == nullptr) + { + return false; + } + + if (widget->hasMouseTracking()) + { + return true; + } + + return recursiveCheckMouseTracking(widget->parentWidget()); + }; + + if (!recursiveCheckMouseTracking(widget)) + { + log("Start moving"); + this->moving = true; + } + } + } +#endif + + BaseWidget::mousePressEvent(event); +} + +void BaseWindow::mouseReleaseEvent(QMouseEvent *event) +{ +#ifndef Q_OS_WIN + if (this->flags_ & FramelessDraggable) + { + if (this->moving) + { + log("Stop moving"); + this->moving = false; + } + } +#endif + + BaseWidget::mouseReleaseEvent(event); +} + +void BaseWindow::mouseMoveEvent(QMouseEvent *event) +{ +#ifndef Q_OS_WIN + if (this->flags_ & FramelessDraggable) + { + if (this->moving) + { + const auto &newPos = event->screenPos() - this->movingRelativePos; + this->move(newPos.x(), newPos.y()); + } + } +#endif + + BaseWidget::mouseMoveEvent(event); +} + +TitleBarButton *BaseWindow::addTitleBarButton(const TitleBarButtonStyle &style, + std::function onClicked) +{ + TitleBarButton *button = new TitleBarButton; + button->setScaleIndependantSize(30, 30); + + this->ui_.buttons.push_back(button); + this->ui_.titlebarBox->insertWidget(1, button); + button->setButtonStyle(style); + + QObject::connect(button, &TitleBarButton::leftClicked, this, + [onClicked] { onClicked(); }); + + return button; +} + +EffectLabel *BaseWindow::addTitleBarLabel(std::function onClicked) +{ + EffectLabel *button = new EffectLabel; + button->setScaleIndependantHeight(30); + + this->ui_.buttons.push_back(button); + this->ui_.titlebarBox->insertWidget(1, button); + + QObject::connect(button, &EffectLabel::leftClicked, this, + [onClicked] { onClicked(); }); + + return button; +} + +void BaseWindow::changeEvent(QEvent *) +{ + if (this->isVisible()) + { + TooltipWidget::getInstance()->hide(); + } + +#ifdef USEWINSDK + if (this->ui_.maxButton) + { + this->ui_.maxButton->setButtonStyle( + this->windowState() & Qt::WindowMaximized + ? TitleBarButtonStyle::Unmaximize + : TitleBarButtonStyle::Maximize); + } +#endif + +#ifndef Q_OS_WIN + this->update(); +#endif +} + +void BaseWindow::leaveEvent(QEvent *) +{ + TooltipWidget::getInstance()->hide(); +} + +void BaseWindow::moveTo(QWidget *parent, QPoint point, bool offset) +{ + if (offset) + { + point.rx() += 16; + point.ry() += 16; + } + + this->move(point); + this->moveIntoDesktopRect(parent); +} + +void BaseWindow::resizeEvent(QResizeEvent *) +{ + // Queue up save because: Window resized +#ifdef CHATTERINO + getApp()->windows->queueSave(); +#endif + + this->moveIntoDesktopRect(this); + + this->calcButtonsSizes(); +} + +void BaseWindow::moveEvent(QMoveEvent *event) +{ + // Queue up save because: Window position changed +#ifdef CHATTERINO + getApp()->windows->queueSave(); +#endif + + BaseWidget::moveEvent(event); +} + +void BaseWindow::closeEvent(QCloseEvent *) +{ + this->closing.invoke(); +} + +void BaseWindow::moveIntoDesktopRect(QWidget *parent) +{ + if (!this->stayInScreenRect_) + return; + + // move the widget into the screen geometry if it's not already in there + QDesktopWidget *desktop = QApplication::desktop(); + + QRect s = desktop->availableGeometry(parent); + QPoint p = this->pos(); + + if (p.x() < s.left()) + { + p.setX(s.left()); + } + if (p.y() < s.top()) + { + p.setY(s.top()); + } + if (p.x() + this->width() > s.right()) + { + p.setX(s.right() - this->width()); + } + if (p.y() + this->height() > s.bottom()) + { + p.setY(s.bottom() - this->height()); + } + + if (p != this->pos()) + this->move(p); +} + +bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, + long *result) +{ +#ifdef USEWINSDK +# if (QT_VERSION == QT_VERSION_CHECK(5, 11, 1)) + MSG *msg = *reinterpret_cast(message); +# else + MSG *msg = reinterpret_cast(message); +# endif + + bool returnValue = false; + + switch (msg->message) + { + case WM_DPICHANGED: + returnValue = handleDPICHANGED(msg); + break; + + case WM_SHOWWINDOW: + returnValue = this->handleSHOWWINDOW(msg); + break; + + case WM_NCCALCSIZE: + returnValue = this->handleNCCALCSIZE(msg, result); + break; + + case WM_SIZE: + returnValue = this->handleSIZE(msg); + break; + + case WM_NCHITTEST: + returnValue = this->handleNCHITTEST(msg, result); + break; + + default: + return QWidget::nativeEvent(eventType, message, result); + } + + QWidget::nativeEvent(eventType, message, result); + + return returnValue; +#else + return QWidget::nativeEvent(eventType, message, result); +#endif +} + +void BaseWindow::scaleChangedEvent(float scale) +{ +#ifdef USEWINSDK + this->calcButtonsSizes(); +#endif + + this->setFont(getFonts()->getFont(FontStyle::UiTabs, this->qtFontScale())); +} + +void BaseWindow::paintEvent(QPaintEvent *) +{ + QPainter painter(this); + + if (this->frameless_) + { + painter.setPen(QColor("#999")); + painter.drawRect(0, 0, this->width() - 1, this->height() - 1); + } + + this->drawCustomWindowFrame(painter); +} + +void BaseWindow::updateScale() +{ + auto scale = + this->nativeScale_ * (this->flags_ & DisableCustomScaling + ? 1 + : getABSettings()->getClampedUiScale()); + + this->setScale(scale); + + for (auto child : this->findChildren()) + { + child->setScale(scale); + } +} + +void BaseWindow::calcButtonsSizes() +{ + if (!this->shown_) + { + return; + } + + if ((this->width() / this->scale()) < 300) + { + if (this->ui_.minButton) + this->ui_.minButton->setScaleIndependantSize(30, 30); + if (this->ui_.maxButton) + this->ui_.maxButton->setScaleIndependantSize(30, 30); + if (this->ui_.exitButton) + this->ui_.exitButton->setScaleIndependantSize(30, 30); + } + else + { + if (this->ui_.minButton) + this->ui_.minButton->setScaleIndependantSize(46, 30); + if (this->ui_.maxButton) + this->ui_.maxButton->setScaleIndependantSize(46, 30); + if (this->ui_.exitButton) + this->ui_.exitButton->setScaleIndependantSize(46, 30); + } +} + +void BaseWindow::drawCustomWindowFrame(QPainter &painter) +{ +#ifdef USEWINSDK + if (this->hasCustomWindowFrame()) + { + QPainter painter(this); + + QColor bg = this->overrideBackgroundColor_.value_or( + this->theme->window.background); + + painter.fillRect(QRect(0, 1, this->width() - 0, this->height() - 0), + bg); + } +#endif +} + +bool BaseWindow::handleDPICHANGED(MSG *msg) +{ +#ifdef USEWINSDK + int dpi = HIWORD(msg->wParam); + + float _scale = dpi / 96.f; + + static bool firstResize = true; + + if (!firstResize) + { + auto *prcNewWindow = reinterpret_cast(msg->lParam); + SetWindowPos(msg->hwnd, nullptr, prcNewWindow->left, prcNewWindow->top, + prcNewWindow->right - prcNewWindow->left, + prcNewWindow->bottom - prcNewWindow->top, + SWP_NOZORDER | SWP_NOACTIVATE); + } + firstResize = false; + + this->nativeScale_ = _scale; + this->updateScale(); + + return true; +#else + return false; +#endif +} + +bool BaseWindow::handleSHOWWINDOW(MSG *msg) +{ +#ifdef USEWINSDK + if (auto dpi = getWindowDpi(msg->hwnd)) + { + this->nativeScale_ = dpi.get() / 96.f; + this->updateScale(); + } + + if (!this->shown_ && this->isVisible() && this->hasCustomWindowFrame()) + { + this->shown_ = true; + + const MARGINS shadow = {8, 8, 8, 8}; + DwmExtendFrameIntoClientArea(HWND(this->winId()), &shadow); + } + + this->calcButtonsSizes(); + + return true; +#else + return false; +#endif +} + +bool BaseWindow::handleNCCALCSIZE(MSG *msg, long *result) +{ +#ifdef USEWINSDK + if (this->hasCustomWindowFrame()) + { + // int cx = GetSystemMetrics(SM_CXSIZEFRAME); + // int cy = GetSystemMetrics(SM_CYSIZEFRAME); + + if (msg->wParam == TRUE) + { + NCCALCSIZE_PARAMS *ncp = + (reinterpret_cast(msg->lParam)); + ncp->lppos->flags |= SWP_NOREDRAW; + RECT *clientRect = &ncp->rgrc[0]; + + clientRect->left += 1; + clientRect->top += 0; + clientRect->right -= 1; + clientRect->bottom -= 1; + } + + *result = 0; + return true; + } + return false; +#else + return false; +#endif +} + +bool BaseWindow::handleSIZE(MSG *msg) +{ +#ifdef USEWINSDK + if (this->ui_.windowLayout) + { + if (this->frameless_) + { + // + } + else if (this->hasCustomWindowFrame()) + { + if (msg->wParam == SIZE_MAXIMIZED) + { + auto offset = int(this->scale() * 8); + + this->ui_.windowLayout->setContentsMargins(offset, offset, + offset, offset); + } + else + { + this->ui_.windowLayout->setContentsMargins(0, 1, 0, 0); + } + } + } + return false; +#else + return false; +#endif +} + +bool BaseWindow::handleNCHITTEST(MSG *msg, long *result) +{ +#ifdef USEWINSDK + const LONG border_width = 8; // in pixels + RECT winrect; + GetWindowRect(HWND(winId()), &winrect); + + long x = GET_X_LPARAM(msg->lParam); + long y = GET_Y_LPARAM(msg->lParam); + + QPoint point(x - winrect.left, y - winrect.top); + + if (this->hasCustomWindowFrame()) + { + *result = 0; + + bool resizeWidth = minimumWidth() != maximumWidth(); + bool resizeHeight = minimumHeight() != maximumHeight(); + + if (resizeWidth) + { + // left border + if (x < winrect.left + border_width) + { + *result = HTLEFT; + } + // right border + if (x >= winrect.right - border_width) + { + *result = HTRIGHT; + } + } + if (resizeHeight) + { + // bottom border + if (y >= winrect.bottom - border_width) + { + *result = HTBOTTOM; + } + // top border + if (y < winrect.top + border_width) + { + *result = HTTOP; + } + } + if (resizeWidth && resizeHeight) + { + // bottom left corner + if (x >= winrect.left && x < winrect.left + border_width && + y < winrect.bottom && y >= winrect.bottom - border_width) + { + *result = HTBOTTOMLEFT; + } + // bottom right corner + if (x < winrect.right && x >= winrect.right - border_width && + y < winrect.bottom && y >= winrect.bottom - border_width) + { + *result = HTBOTTOMRIGHT; + } + // top left corner + if (x >= winrect.left && x < winrect.left + border_width && + y >= winrect.top && y < winrect.top + border_width) + { + *result = HTTOPLEFT; + } + // top right corner + if (x < winrect.right && x >= winrect.right - border_width && + y >= winrect.top && y < winrect.top + border_width) + { + *result = HTTOPRIGHT; + } + } + + if (*result == 0) + { + bool client = false; + + for (QWidget *widget : this->ui_.buttons) + { + if (widget->geometry().contains(point)) + { + client = true; + } + } + + if (this->ui_.layoutBase->geometry().contains(point)) + { + client = true; + } + + if (client) + { + *result = HTCLIENT; + } + else + { + *result = HTCAPTION; + } + } + + return true; + } + else if (this->flags_ & FramelessDraggable) + { + *result = 0; + bool client = false; + + if (auto widget = this->childAt(point)) + { + std::function recursiveCheckMouseTracking; + recursiveCheckMouseTracking = [&](QWidget *widget) { + if (widget == nullptr) + { + return false; + } + + if (widget->hasMouseTracking()) + { + return true; + } + + return recursiveCheckMouseTracking(widget->parentWidget()); + }; + + if (recursiveCheckMouseTracking(widget)) + { + client = true; + } + } + + if (client) + { + *result = HTCLIENT; + } + else + { + *result = HTCAPTION; + } + + return true; + } + return false; +#else + return false; +#endif +} + +} // namespace AB_NAMESPACE diff --git a/lib/appbase/widgets/BaseWindow.hpp b/lib/appbase/widgets/BaseWindow.hpp new file mode 100644 index 000000000..be08d4ed0 --- /dev/null +++ b/lib/appbase/widgets/BaseWindow.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include "widgets/BaseWidget.hpp" + +#include +#include + +class QHBoxLayout; +struct tagMSG; +typedef struct tagMSG MSG; + +namespace AB_NAMESPACE { + +class Button; +class EffectLabel; +class TitleBarButton; +enum class TitleBarButtonStyle; + +class BaseWindow : public BaseWidget +{ + Q_OBJECT + +public: + enum Flags { + None = 0, + EnableCustomFrame = 1, + Frameless = 2, + TopMost = 4, + DisableCustomScaling = 8, + FramelessDraggable = 16, + }; + + enum ActionOnFocusLoss { Nothing, Delete, Close, Hide }; + + explicit BaseWindow(QWidget *parent = nullptr, Flags flags_ = None); + + QWidget *getLayoutContainer(); + bool hasCustomWindowFrame(); + TitleBarButton *addTitleBarButton(const TitleBarButtonStyle &style, + std::function onClicked); + EffectLabel *addTitleBarLabel(std::function onClicked); + + void setStayInScreenRect(bool value); + bool getStayInScreenRect() const; + + void setActionOnFocusLoss(ActionOnFocusLoss value); + ActionOnFocusLoss getActionOnFocusLoss() const; + + void moveTo(QWidget *widget, QPoint point, bool offset = true); + + virtual float scale() const override; + float qtFontScale() const; + + Flags getFlags(); + + pajlada::Signals::NoArgSignal closing; + +protected: + virtual bool nativeEvent(const QByteArray &eventType, void *message, + long *result) override; + virtual void scaleChangedEvent(float) override; + + virtual void paintEvent(QPaintEvent *) override; + + virtual void changeEvent(QEvent *) override; + virtual void leaveEvent(QEvent *) override; + virtual void resizeEvent(QResizeEvent *) override; + virtual void moveEvent(QMoveEvent *) override; + virtual void closeEvent(QCloseEvent *) override; + + virtual void themeChangedEvent() override; + virtual bool event(QEvent *event) override; + virtual void wheelEvent(QWheelEvent *event) override; + + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + QPointF movingRelativePos; + bool moving{}; + + void updateScale(); + + boost::optional overrideBackgroundColor_; + +private: + void init(); + void moveIntoDesktopRect(QWidget *parent); + void calcButtonsSizes(); + void drawCustomWindowFrame(QPainter &painter); + void onFocusLost(); + + bool handleDPICHANGED(MSG *msg); + bool handleSHOWWINDOW(MSG *msg); + bool handleNCCALCSIZE(MSG *msg, long *result); + bool handleSIZE(MSG *msg); + bool handleNCHITTEST(MSG *msg, long *result); + + bool enableCustomFrame_; + ActionOnFocusLoss actionOnFocusLoss_ = Nothing; + bool frameless_; + bool stayInScreenRect_ = false; + bool shown_ = false; + Flags flags_; + float nativeScale_ = 1; + + struct { + QLayout *windowLayout = nullptr; + QHBoxLayout *titlebarBox = nullptr; + QWidget *titleLabel = nullptr; + TitleBarButton *minButton = nullptr; + TitleBarButton *maxButton = nullptr; + TitleBarButton *exitButton = nullptr; + QWidget *layoutBase = nullptr; + std::vector