diff --git a/Fluent/CMakeLists.txt b/Fluent/CMakeLists.txt index 92c0182..964319a 100644 --- a/Fluent/CMakeLists.txt +++ b/Fluent/CMakeLists.txt @@ -21,12 +21,20 @@ qt6_add_qml_module(Fluent Icons.h Rectangle.h Rectangle.cpp Theme.h Theme.cpp + Utilities.h Utilities.cpp QML_FILES + qml/Acrylic.qml qml/AppBar.qml + qml/Icon.qml qml/IconButton.qml + qml/InfoBar.qml + qml/Object.qml qml/Router.qml + qml/Shadow.qml qml/Text.qml qml/Window.qml + RESOURCES + resources/noise.png ) target_include_directories(Fluent diff --git a/Fluent/Frameless.cpp b/Fluent/Frameless.cpp index e32c9d8..3994172 100644 --- a/Fluent/Frameless.cpp +++ b/Fluent/Frameless.cpp @@ -1,6 +1,39 @@ #include "Frameless.h" +#include "Utilities.h" +#include +#include +#include +#include + +static inline void setShadow(HWND hwnd) { + const MARGINS shadow = {1, 0, 0, 0}; + typedef HRESULT(WINAPI * DwmExtendFrameIntoClientAreaPtr)(HWND hWnd, const MARGINS *pMarInset); + HMODULE module = LoadLibrary(L"dwmapi.dll"); + if (module) { + DwmExtendFrameIntoClientAreaPtr dwm_extendframe_into_client_area_; + dwm_extendframe_into_client_area_ = + reinterpret_cast(GetProcAddress(module, "DwmExtendFrameIntoClientArea")); + if (dwm_extendframe_into_client_area_) { + dwm_extendframe_into_client_area_(hwnd, &shadow); + } + } +} + +static bool containsCursorToItem(QQuickItem *item) { + auto window = item->window(); + if ((window == nullptr) || !item || !item->isVisible()) { + return false; + } + auto point = window->mapFromGlobal(QCursor::pos()); + auto rect = QRectF(item->mapToItem(window->contentItem(), QPointF(0, 0)), item->size()); + if (rect.contains(point)) { + return true; + } + return false; +} Frameless::Frameless(QQuickItem *parent) : QQuickItem{parent} { + m_isWindows11OrGreater = Utilities::instance()->isWindows11OrGreater(); } QQuickItem *Frameless::appBar() const { @@ -25,6 +58,28 @@ void Frameless::setMaximizeButton(QQuickItem *button) { } } +QQuickItem *Frameless::minimizedButton() const { + return m_minimizedButton; +} + +void Frameless::setMinimizedButton(QQuickItem *button) { + if (m_minimizedButton != button) { + m_minimizedButton = button; + emit minimizedButtonChanged(); + } +} + +QQuickItem *Frameless::closeButton() const { + return m_closeButton; +} + +void Frameless::setCloseButton(QQuickItem *button) { + if (m_closeButton != button) { + m_closeButton = button; + emit closeButtonChanged(); + } +} + bool Frameless::fixSize() const { return m_fixSize; } @@ -69,8 +124,370 @@ void Frameless::onDestruction() { } void Frameless::componentComplete() { + if (m_disabled) return; + int w = window()->width(); + int h = window()->height(); + m_current = window()->winId(); + window()->setFlags((window()->flags()) | Qt::CustomizeWindowHint | Qt::WindowMinimizeButtonHint | + Qt::WindowCloseButtonHint); + if (!m_fixSize) { + window()->setFlag(Qt::WindowMaximizeButtonHint); + } + window()->installEventFilter(this); + QGuiApplication::instance()->installNativeEventFilter(this); + if (m_maximizeButton) { + setHitTestVisible(m_maximizeButton); + } + if (m_minimizedButton) { + setHitTestVisible(m_minimizedButton); + } + if (m_closeButton) { + setHitTestVisible(m_closeButton); + } +#ifdef Q_OS_WIN +#if (QT_VERSION == QT_VERSION_CHECK(6, 5, 3)) + qWarning() << "Qt's own frameless bug, currently only exist in 6.5.3, please use other versions"; +#endif + HWND hwnd = reinterpret_cast(window()->winId()); + DWORD style = ::GetWindowLongPtr(hwnd, GWL_STYLE); + if (m_fixSize) { + ::SetWindowLongPtr(hwnd, GWL_STYLE, style | WS_THICKFRAME | WS_CAPTION); + for (int i = 0; i <= QGuiApplication::screens().count() - 1; ++i) { + connect(QGuiApplication::screens().at(i), &QScreen::logicalDotsPerInchChanged, this, [=] { + SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, + SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOMOVE | SWP_FRAMECHANGED); + }); + } + } else { + ::SetWindowLongPtr(hwnd, GWL_STYLE, style | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_CAPTION); + } + SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, + SWP_NOZORDER | SWP_NOOWNERZORDER | SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED); + connect(window(), &QQuickWindow::screenChanged, this, [hwnd] { + ::SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED | SWP_NOOWNERZORDER); + ::RedrawWindow(hwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW); + }); + if (!window()->property("_hideShadow").toBool()) { + setShadow(hwnd); + } +#endif + auto appBarHeight = m_appBar->height(); + h = qRound(h + appBarHeight); + if (m_fixSize) { + window()->setMaximumSize(QSize(w, h)); + window()->setMinimumSize(QSize(w, h)); + } else { + window()->setMinimumHeight(window()->minimumHeight() + appBarHeight); + window()->setMaximumHeight(window()->maximumHeight() + appBarHeight); + } + window()->resize(QSize(w, h)); + connect(this, &Frameless::topmostChanged, this, [this] { setWindowTopmost(topmost()); }); + setWindowTopmost(topmost()); +} + +bool Frameless::eventFilter(QObject *obj, QEvent *event) { +#ifndef Q_OS_WIN + switch (ev->type()) { + case QEvent::MouseButtonPress: + if (_edges != 0) { + QMouseEvent *event = static_cast(ev); + if (event->button() == Qt::LeftButton) { + _updateCursor(_edges); + window()->startSystemResize(Qt::Edges(_edges)); + } + } else { + if (_hitAppBar()) { + qint64 clickTimer = QDateTime::currentMSecsSinceEpoch(); + qint64 offset = clickTimer - this->_clickTimer; + this->_clickTimer = clickTimer; + if (offset < 300) { + if (_isMaximized()) { + showNormal(); + } else { + showMaximized(); + } + } else { + window()->startSystemMove(); + } + } + } + break; + case QEvent::MouseButtonRelease: + _edges = 0; + break; + case QEvent::MouseMove: { + if (_isMaximized() || _isFullScreen()) { + break; + } + if (_fixSize) { + break; + } + QMouseEvent *event = static_cast(ev); + QPoint p = +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + event->pos(); +#else + event->position().toPoint(); +#endif + if (p.x() >= _margins && p.x() <= (window()->width() - _margins) && p.y() >= _margins && + p.y() <= (window()->height() - _margins)) { + if (_edges != 0) { + _edges = 0; + _updateCursor(_edges); + } + break; + } + _edges = 0; + if (p.x() < _margins) { + _edges |= Qt::LeftEdge; + } + if (p.x() > (window()->width() - _margins)) { + _edges |= Qt::RightEdge; + } + if (p.y() < _margins) { + _edges |= Qt::TopEdge; + } + if (p.y() > (window()->height() - _margins)) { + _edges |= Qt::BottomEdge; + } + _updateCursor(_edges); + break; + } + default: + break; + } +#endif + return QObject::eventFilter(obj, event); } bool Frameless::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result) { +#ifdef Q_OS_WIN + if ((eventType != "windows_generic_MSG") || !message) { + return false; + } + const auto msg = static_cast(message); + auto hwnd = msg->hwnd; + if (!hwnd) { + return false; + } + const quint64 wid = reinterpret_cast(hwnd); + if (wid != m_current) { + return false; + } + const auto uMsg = msg->message; + const auto wParam = msg->wParam; + const auto lParam = msg->lParam; + if (uMsg == WM_WINDOWPOSCHANGING) { + auto *wp = reinterpret_cast(lParam); + if (wp != nullptr && (wp->flags & SWP_NOSIZE) == 0) { + wp->flags |= SWP_NOCOPYBITS; + *result = static_cast(::DefWindowProcW(hwnd, uMsg, wParam, lParam)); + return true; + } + return false; + } else if (uMsg == WM_NCCALCSIZE && wParam == TRUE) { + bool isMaximum = ::IsZoomed(hwnd); + if (isMaximum) { + window()->setProperty("__margins", 7); + } else { + window()->setProperty("__margins", 0); + } + setMaximizeHovered(false); + *result = WVR_REDRAW; + return true; + } else if (uMsg == WM_NCHITTEST) { + if (m_isWindows11OrGreater) { + if (hitMaximizeButton()) { + if (*result == HTNOWHERE) { + *result = HTZOOM; + } + setMaximizeHovered(true); + return true; + } + setMaximizeHovered(false); + setMaximizePressed(false); + } + *result = 0; + POINT nativeGlobalPos{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; + POINT nativeLocalPos = nativeGlobalPos; + ::ScreenToClient(hwnd, &nativeLocalPos); + RECT clientRect{0, 0, 0, 0}; + ::GetClientRect(hwnd, &clientRect); + auto clientWidth = clientRect.right - clientRect.left; + auto clientHeight = clientRect.bottom - clientRect.top; + bool left = nativeLocalPos.x < m_margins; + bool right = nativeLocalPos.x > clientWidth - m_margins; + bool top = nativeLocalPos.y < m_margins; + bool bottom = nativeLocalPos.y > clientHeight - m_margins; + *result = 0; + if (!m_fixSize && !isFullScreen() && !isMaximized()) { + if (left && top) { + *result = HTTOPLEFT; + } else if (left && bottom) { + *result = HTBOTTOMLEFT; + } else if (right && top) { + *result = HTTOPRIGHT; + } else if (right && bottom) { + *result = HTBOTTOMRIGHT; + } else if (left) { + *result = HTLEFT; + } else if (right) { + *result = HTRIGHT; + } else if (top) { + *result = HTTOP; + } else if (bottom) { + *result = HTBOTTOM; + } + } + if (0 != *result) { + return true; + } + if (hitAppBar()) { + *result = HTCAPTION; + return true; + } + *result = HTCLIENT; + return true; + } else if (uMsg == WM_NCPAINT) { + *result = FALSE; + return false; + } else if (uMsg == WM_NCACTIVATE) { + *result = TRUE; + return true; + } else if (m_isWindows11OrGreater && (uMsg == WM_NCLBUTTONDBLCLK || uMsg == WM_NCLBUTTONDOWN)) { + if (hitMaximizeButton()) { + QMouseEvent event = QMouseEvent(QEvent::MouseButtonPress, QPoint(), QPoint(), Qt::LeftButton, + Qt::LeftButton, Qt::NoModifier); + QGuiApplication::sendEvent(m_maximizeButton, &event); + setMaximizePressed(true); + return true; + } + } else if (m_isWindows11OrGreater && (uMsg == WM_NCLBUTTONUP || uMsg == WM_NCRBUTTONUP)) { + if (hitMaximizeButton()) { + QMouseEvent event = QMouseEvent(QEvent::MouseButtonRelease, QPoint(), QPoint(), Qt::LeftButton, + Qt::LeftButton, Qt::NoModifier); + QGuiApplication::sendEvent(m_maximizeButton, &event); + setMaximizePressed(false); + return true; + } + } else if (uMsg == WM_NCRBUTTONDOWN) { + if (wParam == HTCAPTION) { + auto pos = window()->position(); + auto offset = window()->mapFromGlobal(QCursor::pos()); + showSystemMenu(QPoint(pos.x() + offset.x(), pos.y() + offset.y())); + } + } else if (uMsg == WM_KEYDOWN || uMsg == WM_SYSKEYDOWN) { + const bool altPressed = ((wParam == VK_MENU) || (::GetKeyState(VK_MENU) < 0)); + const bool spacePressed = ((wParam == VK_SPACE) || (::GetKeyState(VK_SPACE) < 0)); + if (altPressed && spacePressed) { + auto pos = window()->position(); + showSystemMenu(QPoint(pos.x(), qRound(pos.y() + m_appBar->height()))); + } + } else if (uMsg == WM_SYSCOMMAND) { + if (wParam == SC_MINIMIZE) { + if (window()->transientParent()) { + auto _hwnd = reinterpret_cast(window()->transientParent()->winId()); + ::ShowWindow(_hwnd, 2); + } else { + auto _hwnd = reinterpret_cast(window()->winId()); + ::ShowWindow(_hwnd, 2); + } + return true; + } + return false; + } + return false; +#else + return false; +#endif +} + +bool Frameless::isFullScreen() { + return window()->visibility() == QWindow::FullScreen; +} + +bool Frameless::isMaximized() { + return window()->visibility() == QWindow::Maximized; +} + +void Frameless::setMaximizeHovered(bool val) { + if (m_maximizeButton) { + m_maximizeButton->setProperty("hover", val); + } +} + +void Frameless::setMaximizePressed(bool val) { + if (m_maximizeButton) { + m_maximizeButton->setProperty("down", val); + } +} + +void Frameless::setWindowTopmost(bool topmost) { +#ifdef Q_OS_WIN + HWND hwnd = reinterpret_cast(window()->winId()); + if (topmost) { + ::SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); + } else { + ::SetWindowPos(hwnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); + } +#else + window()->setFlag(Qt::WindowStaysOnTopHint, topmost); +#endif +} + +bool Frameless::hitMaximizeButton() { + if (containsCursorToItem(m_maximizeButton)) { + return true; + } return false; } + +bool Frameless::hitAppBar() { + for (int i = 0; i <= m_hitTestList.size() - 1; ++i) { + auto item = m_hitTestList.at(i); + if (containsCursorToItem(item)) { + return false; + } + } + if ((m_appBar != nullptr) && containsCursorToItem(m_appBar)) { + return true; + } + return false; +} + +void Frameless::showSystemMenu(QPoint point) { +#ifdef Q_OS_WIN + QScreen *screen = window()->screen(); + if (!screen) { + screen = QGuiApplication::primaryScreen(); + } + if (!screen) { + return; + } + const QPoint origin = screen->geometry().topLeft(); + auto nativePos = QPointF(QPointF(point - origin) * window()->devicePixelRatio()).toPoint() + origin; + HWND hwnd = reinterpret_cast(window()->winId()); + auto hMenu = ::GetSystemMenu(hwnd, FALSE); + if (isMaximized() || isFullScreen()) { + ::EnableMenuItem(hMenu, SC_MOVE, MFS_DISABLED); + ::EnableMenuItem(hMenu, SC_RESTORE, MFS_ENABLED); + } else { + ::EnableMenuItem(hMenu, SC_MOVE, MFS_ENABLED); + ::EnableMenuItem(hMenu, SC_RESTORE, MFS_DISABLED); + } + if (!m_fixSize && !isMaximized() && !isFullScreen()) { + ::EnableMenuItem(hMenu, SC_SIZE, MFS_ENABLED); + ::EnableMenuItem(hMenu, SC_MAXIMIZE, MFS_ENABLED); + } else { + ::EnableMenuItem(hMenu, SC_SIZE, MFS_DISABLED); + ::EnableMenuItem(hMenu, SC_MAXIMIZE, MFS_DISABLED); + } + const int result = + ::TrackPopupMenu(hMenu, (TPM_RETURNCMD | (QGuiApplication::isRightToLeft() ? TPM_RIGHTALIGN : TPM_LEFTALIGN)), + nativePos.x(), nativePos.y(), 0, hwnd, nullptr); + if (result) { + ::PostMessageW(hwnd, WM_SYSCOMMAND, result, 0); + } +#endif +} diff --git a/Fluent/Frameless.h b/Fluent/Frameless.h index 470f20f..9b25041 100644 --- a/Fluent/Frameless.h +++ b/Fluent/Frameless.h @@ -9,6 +9,9 @@ class Frameless : public QQuickItem, QAbstractNativeEventFilter { QML_ELEMENT Q_PROPERTY(QQuickItem *appBar READ appBar WRITE setAppBar NOTIFY appBarChanged) Q_PROPERTY(QQuickItem *maximizeButton READ maximizeButton WRITE setMaximizeButton NOTIFY maximizeButtonChanged) + Q_PROPERTY(QQuickItem *minimizedButton READ minimizedButton WRITE setMinimizedButton NOTIFY minimizedButtonChanged) + Q_PROPERTY(QQuickItem *closeButton READ closeButton WRITE setCloseButton NOTIFY closeButtonChanged) + Q_PROPERTY(bool fixSize READ fixSize WRITE setFixSize NOTIFY fixSizeChanged) Q_PROPERTY(bool topmost READ topmost WRITE setTopmost NOTIFY topmostChanged) Q_PROPERTY(bool disabled READ disabled WRITE setDisabled NOTIFY disabledChanged) @@ -21,6 +24,12 @@ public: QQuickItem *maximizeButton() const; void setMaximizeButton(QQuickItem *button); + QQuickItem *minimizedButton() const; + void setMinimizedButton(QQuickItem *button); + + QQuickItem *closeButton() const; + void setCloseButton(QQuickItem *button); + bool fixSize() const; void setFixSize(bool fix); @@ -39,17 +48,35 @@ public: signals: void appBarChanged(); void maximizeButtonChanged(); + void minimizedButtonChanged(); + void closeButtonChanged(); void fixSizeChanged(); void topmostChanged(); void disabledChanged(); +protected: + bool isFullScreen(); + bool isMaximized(); + void setMaximizeHovered(bool val); + void setMaximizePressed(bool val); + void setWindowTopmost(bool topmost); + bool hitMaximizeButton(); + bool hitAppBar(); + void showSystemMenu(QPoint point); + bool eventFilter(QObject *obj, QEvent *event) final; + private: + quint64 m_current = 0; QQuickItem *m_appBar = nullptr; QQuickItem *m_maximizeButton = nullptr; + QQuickItem *m_minimizedButton = nullptr; + QQuickItem *m_closeButton = nullptr; bool m_fixSize = false; bool m_topmost = false; bool m_disabled = false; + int m_margins = 8; QList> m_hitTestList; + bool m_isWindows11OrGreater = false; }; #endif // FRAMELESS_H diff --git a/Fluent/Theme.cpp b/Fluent/Theme.cpp index 7a1ebbd..8787632 100644 --- a/Fluent/Theme.cpp +++ b/Fluent/Theme.cpp @@ -24,3 +24,58 @@ void Theme::setItemNormalColor(const QColor &color) { emit itemNormalColorChanged(); } } + +QColor Theme::itemHoverColor() const { + return m_itemHoverColor; +} + +void Theme::setItemHoverColor(const QColor &color) { + if (m_itemHoverColor != color) { + m_itemHoverColor = color; + emit itemHoverColorChanged(); + } +} + +QColor Theme::windowBackgroundColor() const { + return m_windowBackgroundColor; +} + +void Theme::setWindowBackgroundColor(const QColor &color) { + if (m_windowBackgroundColor != color) { + m_windowBackgroundColor = color; + emit windowBackgroundColorChanged(); + } +} + +QColor Theme::windowActiveBackgroundColor() const { + return m_windowActiveBackgroundColor; +} + +void Theme::setWindowActiveBackgroundColor(const QColor &color) { + if (m_windowActiveBackgroundColor != color) { + m_windowActiveBackgroundColor = color; + emit windowActiveBackgroundColorChanged(); + } +} + +QString Theme::desktopImagePath() const { + return m_desktopImagePath; +} + +void Theme::setDesktopImagePath(const QString &path) { + if (m_desktopImagePath != path) { + m_desktopImagePath = path; + emit desktopImagePathChanged(); + } +} + +bool Theme::blurBehindWindowEnabled() const { + return m_blurBehindWindowEnabled; +} + +void Theme::setBlurBehindWindowEnabled(bool enabled) { + if (m_blurBehindWindowEnabled != enabled) { + m_blurBehindWindowEnabled = enabled; + emit blurBehindWindowEnabledChanged(); + } +} diff --git a/Fluent/Theme.h b/Fluent/Theme.h index 6de0c60..36131f3 100644 --- a/Fluent/Theme.h +++ b/Fluent/Theme.h @@ -11,6 +11,16 @@ class Theme : public QObject { QML_SINGLETON Q_PROPERTY(QColor fontPrimaryColor READ fontPrimaryColor WRITE setFontPrimaryColor NOTIFY fontPrimaryColorChanged) Q_PROPERTY(QColor itemNormalColor READ itemNormalColor WRITE setItemNormalColor NOTIFY itemNormalColorChanged) + Q_PROPERTY(QColor itemHoverColor READ itemHoverColor WRITE setItemHoverColor NOTIFY itemHoverColorChanged) + + Q_PROPERTY(QColor windowBackgroundColor READ windowBackgroundColor WRITE setWindowBackgroundColor NOTIFY + windowBackgroundColorChanged) + Q_PROPERTY(QColor windowActiveBackgroundColor READ windowActiveBackgroundColor WRITE setWindowActiveBackgroundColor + NOTIFY windowActiveBackgroundColorChanged) + + Q_PROPERTY(QString desktopImagePath READ desktopImagePath WRITE setDesktopImagePath NOTIFY desktopImagePathChanged) + Q_PROPERTY(bool blurBehindWindowEnabled READ blurBehindWindowEnabled WRITE setBlurBehindWindowEnabled NOTIFY + blurBehindWindowEnabledChanged) public: Theme(QObject *parent = nullptr); @@ -21,13 +31,39 @@ public: QColor itemNormalColor() const; void setItemNormalColor(const QColor &color); + QColor itemHoverColor() const; + void setItemHoverColor(const QColor &color); + + QColor windowBackgroundColor() const; + void setWindowBackgroundColor(const QColor &color); + + QColor windowActiveBackgroundColor() const; + void setWindowActiveBackgroundColor(const QColor &color); + + QString desktopImagePath() const; + void setDesktopImagePath(const QString &path); + + bool blurBehindWindowEnabled() const; + void setBlurBehindWindowEnabled(bool enabled); + signals: void fontPrimaryColorChanged(); void itemNormalColorChanged(); + void windowBackgroundColorChanged(); + void windowActiveBackgroundColorChanged(); + void desktopImagePathChanged(); + void blurBehindWindowEnabledChanged(); + void itemHoverColorChanged(); private: QColor m_fontPrimaryColor; QColor m_itemNormalColor; + QColor m_itemHoverColor; + QColor m_windowBackgroundColor; + QColor m_windowActiveBackgroundColor; + + QString m_desktopImagePath; + bool m_blurBehindWindowEnabled; }; #endif // THEME_H diff --git a/Fluent/Utilities.cpp b/Fluent/Utilities.cpp new file mode 100644 index 0000000..2091f5c --- /dev/null +++ b/Fluent/Utilities.cpp @@ -0,0 +1,72 @@ +#include "Utilities.h" +#include + +Utilities *Utilities::instance() { + static Utilities *self = nullptr; + if (self == nullptr) { + self = new Utilities(); + } + return self; +} + +Utilities *Utilities::create(QQmlEngine *, QJSEngine *) { + auto ret = instance(); + QJSEngine::setObjectOwnership(ret, QJSEngine::CppOwnership); + return ret; +} + +Utilities::Utilities(QObject *parent) : QObject{parent} { +} + +QRect Utilities::desktopAvailableGeometry(QQuickWindow *window) { + return window->screen()->availableGeometry(); +} + +QUrl Utilities::getUrlByFilePath(const QString &path) { + return QUrl::fromLocalFile(path); +} + +bool Utilities::isMacos() { +#if defined(Q_OS_MACOS) + return true; +#else + return false; +#endif +} + +bool Utilities::isWin() { +#if defined(Q_OS_WIN) + return true; +#else + return false; +#endif +} + +int Utilities::windowBuildNumber() { +#if defined(Q_OS_WIN) + QSettings regKey{QString::fromUtf8(R"(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion)"), + QSettings::NativeFormat}; + if (regKey.contains(QString::fromUtf8("CurrentBuildNumber"))) { + auto buildNumber = regKey.value(QString::fromUtf8("CurrentBuildNumber")).toInt(); + return buildNumber; + } +#endif + return -1; +} + +bool Utilities::isWindows11OrGreater() { + static QVariant var; + if (var.isNull()) { +#if defined(Q_OS_WIN) + auto buildNumber = windowBuildNumber(); + if (buildNumber >= 22000) { + var = QVariant::fromValue(true); + return true; + } +#endif + var = QVariant::fromValue(false); + return false; + } else { + return var.toBool(); + } +} diff --git a/Fluent/Utilities.h b/Fluent/Utilities.h new file mode 100644 index 0000000..8279333 --- /dev/null +++ b/Fluent/Utilities.h @@ -0,0 +1,26 @@ +#ifndef UTILITIES_H +#define UTILITIES_H + +#include +#include +#include + +class Utilities : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON +public: + static Utilities *instance(); + static Utilities *create(QQmlEngine *, QJSEngine *); + Q_INVOKABLE int windowBuildNumber(); + Q_INVOKABLE bool isWindows11OrGreater(); + Q_INVOKABLE bool isWin(); + Q_INVOKABLE bool isMacos(); + Q_INVOKABLE QRect desktopAvailableGeometry(QQuickWindow *window); + Q_INVOKABLE QUrl getUrlByFilePath(const QString &path); + +protected: + Utilities(QObject *parent = nullptr); +}; + +#endif // UTILITIES_H diff --git a/Fluent/qml/Acrylic.qml b/Fluent/qml/Acrylic.qml new file mode 100644 index 0000000..61c97b0 --- /dev/null +++ b/Fluent/qml/Acrylic.qml @@ -0,0 +1,33 @@ +import QtQuick + +Item { + id: control + property color tintColor: Qt.rgba(1, 1, 1, 1) + property real tintOpacity: 0.65 + property real luminosity: 0.01 + property real noiseOpacity: 0.02 + property var target + property int blurRadius: 32 + property rect targetRect: Qt.rect(control.x, control.y, control.width,control.height) + ShaderEffectSource { + id: effect_source + anchors.fill: parent + visible: false + sourceRect: control.targetRect + sourceItem: control.target + } + Rectangle { + anchors.fill: parent + color: Qt.rgba(1, 1, 1, luminosity) + } + Rectangle { + anchors.fill: parent + color: Qt.rgba(tintColor.r, tintColor.g, tintColor.b, tintOpacity) + } + Image { + anchors.fill: parent + source: "qrc:/qt/qml/Fluent/resources/noise.png" + fillMode: Image.Tile + opacity: control.noiseOpacity + } +} diff --git a/Fluent/qml/AppBar.qml b/Fluent/qml/AppBar.qml index 69b2a17..100cafb 100644 --- a/Fluent/qml/AppBar.qml +++ b/Fluent/qml/AppBar.qml @@ -14,7 +14,11 @@ Quick.Rectangle { property string maximizeText : qsTr("Maximize") property Quick.color textColor: Theme.fontPrimaryColor property Quick.color maximizeNormalColor: Theme.itemNormalColor + property Quick.color maximizeHoverColor: Theme.itemHoverColor + property bool isMac: Utilities.isMacos() property alias buttonMaximize: btn_maximize + property alias layoutMacosButtons: layout_macos_buttons + property alias layoutStandardbuttons: layout_standard_buttons Quick.Item{ id:d @@ -40,6 +44,11 @@ Quick.Rectangle { } RowLayout { + id:layout_standard_buttons + height: parent.height + anchors.right: parent.right + spacing: 0 + IconButton{ id:btn_maximize property bool hover: btn_maximize.hovered @@ -65,5 +74,14 @@ Quick.Rectangle { } } - + Quick.Loader{ + id:layout_macos_buttons + anchors{ + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: 10 + } + sourceComponent: isMac ? com_macos_buttons : undefined + Quick.Component.onDestruction: sourceComponent = undefined + } } diff --git a/Fluent/qml/Icon.qml b/Fluent/qml/Icon.qml new file mode 100644 index 0000000..5bbba9b --- /dev/null +++ b/Fluent/qml/Icon.qml @@ -0,0 +1,19 @@ +import QtQuick + +Text { + property int iconSource + property int iconSize: 20 + property color iconColor: FluTheme.dark ? "#FFFFFF" : "#000000" + id:control + font.family: font_loader.name + font.pixelSize: iconSize + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: iconColor + text: (String.fromCharCode(iconSource).toString(16)) + opacity: iconSource>0 + FontLoader{ + id: font_loader + source: "qrc:/qt/qml/FluentUI/Font/FluentIcons.ttf" + } +} diff --git a/Fluent/qml/InfoBar.qml b/Fluent/qml/InfoBar.qml new file mode 100644 index 0000000..1fbbcca --- /dev/null +++ b/Fluent/qml/InfoBar.qml @@ -0,0 +1,245 @@ +import QtQuick as Quick +import QtQuick.Controls +import Fluent + +Object { + property var root + property int layoutY: 75 + id:control + Object{ + id:mcontrol + property string const_success: "success" + property string const_info: "info" + property string const_warning: "warning" + property string const_error: "error" + property int maxWidth: 300 + property var screenLayout: null + function create(type,text,duration,moremsg){ + if(screenLayout){ + var last = screenLayout.getLastloader() + if(last.type === type && last.text === text && moremsg === last.moremsg){ + last.duration = duration + if (duration > 0) last.restart() + return last + } + } + initScreenLayout() + return contentComponent.createObject(screenLayout,{type:type,text:text,duration:duration,moremsg:moremsg,}) + } + function createCustom(itemcomponent,duration){ + initScreenLayout() + if(itemcomponent){ + return contentComponent.createObject(screenLayout,{itemcomponent:itemcomponent,duration:duration}) + } + } + function initScreenLayout(){ + if(screenLayout == null){ + screenLayout = screenlayoutComponent.createObject(root) + screenLayout.y = control.layoutY + screenLayout.z = 100000 + } + } + Quick.Component { + id:screenlayoutComponent + Quick.Column{ + parent: Overlay.overlay + z:999 + spacing: 20 + width: root.width + move: Quick.Transition { + Quick.NumberAnimation { + properties: "y" + easing.type: Easing.OutCubic + duration: FluTheme.animationEnabled ? 333 : 0 + } + } + onChildrenChanged: if(children.length === 0) destroy() + function getLastloader(){ + if(children.length > 0){ + return children[children.length - 1] + } + return null + } + } + } + Quick.Component{ + id:contentComponent + Quick.Item{ + id:content + property int duration: 1500 + property var itemcomponent + property string type + property string text + property string moremsg + width: parent.width + height: loader.height + function close(){ + content.destroy() + } + function restart(){ + delayTimer.restart() + } + Quick.Timer { + id:delayTimer + interval: duration + running: duration > 0 + repeat: duration > 0 + onTriggered: content.close() + } + Quick.Loader{ + id:loader + x:(parent.width - width) / 2 + property var _super: content + scale: item ? 1 : 0 + asynchronous: true + Quick.Behavior on scale { + enabled: FluTheme.animationEnabled + Quick.NumberAnimation { + easing.type: Easing.OutCubic + duration: 167 + } + } + sourceComponent:itemcomponent ? itemcomponent : mcontrol.fluent_sytle + Quick.Component.onDestruction: sourceComponent = undefined + } + } + } + property Quick.Component fluent_sytle: Quick.Rectangle{ + width: rowlayout.width + (btn_close.visible ? 30 : 48) + height: rowlayout.height + 20 + color: { + if(FluTheme.dark){ + switch(_super.type){ + case mcontrol.const_success: return Qt.rgba(57/255,61/255,27/255,1) + case mcontrol.const_warning: return Qt.rgba(67/255,53/255,25/255,1) + case mcontrol.const_info: return Qt.rgba(39/255,39/255,39/255,1) + case mcontrol.const_error: return Qt.rgba(68/255,39/255,38/255,1) + } + return Qt.rgba(1,1,1,1) + }else{ + switch(_super.type){ + case mcontrol.const_success: return Qt.rgba(223/255,246/255,221/255,1) + case mcontrol.const_warning: return Qt.rgba(255/255,244/255,206/255,1) + case mcontrol.const_info: return Qt.rgba(244/255,244/255,244/255,1) + case mcontrol.const_error: return Qt.rgba(253/255,231/255,233/255,1) + } + return Qt.rgba(1,1,1,1) + } + } + Shadow { + radius: 4 + } + radius: 4 + border.width: 1 + border.color: { + if(FluTheme.dark){ + switch(_super.type){ + case mcontrol.const_success: return Qt.rgba(56/255,61/255,27/255,1) + case mcontrol.const_warning: return Qt.rgba(66/255,53/255,25/255,1) + case mcontrol.const_info: return Qt.rgba(38/255,39/255,39/255,1) + case mcontrol.const_error: return Qt.rgba(67/255,39/255,38/255,1) + } + return Qt.rgba(1,1,1,1) + }else{ + switch(_super.type){ + case mcontrol.const_success: return Qt.rgba(210/255,232/255,208/255,1) + case mcontrol.const_warning: return Qt.rgba(240/255,230/255,194/255,1) + case mcontrol.const_info: return Qt.rgba(230/255,230/255,230/255,1) + case mcontrol.const_error: return Qt.rgba(238/255,217/255,219/255,1) + } + return Qt.rgba(1,1,1,1) + } + } + Quick.Row{ + id:rowlayout + x:20 + y:(parent.height - height) / 2 + spacing: 10 + Icon{ + iconSource:{ + switch(_super.type){ + case mcontrol.const_success: return FluentIcons.CompletedSolid + case mcontrol.const_warning: return FluentIcons.InfoSolid + case mcontrol.const_info: return FluentIcons.InfoSolid + case mcontrol.const_error: return FluentIcons.StatusErrorFull + }FluentIcons.StatusErrorFull + return FluentIcons.FA_info_circle + } + iconSize:20 + iconColor: { + if(FluTheme.dark){ + switch(_super.type){ + case mcontrol.const_success: return Qt.rgba(108/255,203/255,95/255,1) + case mcontrol.const_warning: return Qt.rgba(252/255,225/255,0/255,1) + case mcontrol.const_info: return FluTheme.primaryColor + case mcontrol.const_error: return Qt.rgba(255/255,153/255,164/255,1) + } + return Qt.rgba(1,1,1,1) + }else{ + switch(_super.type){ + case mcontrol.const_success: return Qt.rgba(15/255,123/255,15/255,1) + case mcontrol.const_warning: return Qt.rgba(157/255,93/255,0/255,1) + case mcontrol.const_info: return Qt.rgba(0/255,102/255,180/255,1) + case mcontrol.const_error: return Qt.rgba(196/255,43/255,28/255,1) + } + return Qt.rgba(1,1,1,1) + } + } + } + + Quick.Column{ + spacing: 5 + Text{ + text:_super.text + wrapMode: Text.WrapAnywhere + width: Math.min(implicitWidth,mcontrol.maxWidth) + } + Text{ + text: _super.moremsg + visible: _super.moremsg + wrapMode : Text.WrapAnywhere + textColor: FluColors.Grey120 + width: Math.min(implicitWidth,mcontrol.maxWidth) + } + } + + IconButton{ + id:btn_close + iconSource: FluentIcons.ChromeClose + iconSize: 10 + verticalPadding: 0 + horizontalPadding: 0 + width: 30 + height: 20 + visible: _super.duration<=0 + anchors.verticalCenter: parent.verticalCenter + iconColor: FluTheme.dark ? Qt.rgba(222/255,222/255,222/255,1) : Qt.rgba(97/255,97/255,97/255,1) + onClicked: _super.close() + } + } + } + } + function showSuccess(text,duration=1000,moremsg){ + return mcontrol.create(mcontrol.const_success,text,duration,moremsg ? moremsg : "") + } + function showInfo(text,duration=1000,moremsg){ + return mcontrol.create(mcontrol.const_info,text,duration,moremsg ? moremsg : "") + } + function showWarning(text,duration=1000,moremsg){ + return mcontrol.create(mcontrol.const_warning,text,duration,moremsg ? moremsg : "") + } + function showError(text,duration=1000,moremsg){ + return mcontrol.create(mcontrol.const_error,text,duration,moremsg ? moremsg : "") + } + function showCustom(itemcomponent,duration=1000){ + return mcontrol.createCustom(itemcomponent,duration) + } + function clearAllInfo(){ + if(mcontrol.screenLayout != null) { + mcontrol.screenLayout.destroy() + mcontrol.screenLayout = null + } + + return true + } +} diff --git a/Fluent/qml/Object.qml b/Fluent/qml/Object.qml new file mode 100644 index 0000000..f435c08 --- /dev/null +++ b/Fluent/qml/Object.qml @@ -0,0 +1,7 @@ +import QtQuick + +QtObject { + id:root + default property list children + +} diff --git a/Fluent/qml/Shadow.qml b/Fluent/qml/Shadow.qml new file mode 100644 index 0000000..7b68cf1 --- /dev/null +++ b/Fluent/qml/Shadow.qml @@ -0,0 +1,21 @@ +import QtQuick + +Item { + property color color: FluTheme.dark ? "#000000" : "#999999" + property int elevation: 5 + property int radius: 4 + id:control + anchors.fill: parent + Repeater{ + model: elevation + Rectangle{ + anchors.fill: parent + color: "#00000000" + opacity: 0.01 * (elevation-index+1) + anchors.margins: -index + radius: control.radius+index + border.width: index + border.color: control.color + } + } +} diff --git a/Fluent/qml/Text.qml b/Fluent/qml/Text.qml index 8f788db..899646a 100644 --- a/Fluent/qml/Text.qml +++ b/Fluent/qml/Text.qml @@ -1,4 +1,11 @@ -import QtQuick +import QtQuick as Quick -Item { +import Fluent + +Quick.Text { + property Quick.color textColor: FluTheme.fontPrimaryColor + id:text + color: textColor + renderType: FluTheme.nativeText ? Text.NativeRendering : Text.QtRendering + font: FluTextStyle.Body } diff --git a/Fluent/qml/Window.qml b/Fluent/qml/Window.qml index 1aa21f1..1631d15 100644 --- a/Fluent/qml/Window.qml +++ b/Fluent/qml/Window.qml @@ -11,6 +11,14 @@ Quick.Window { property bool showDark: false property bool fixSize: false property bool stayTop: false + property int __margins: 0 + property var background : com_background + property Quick.color backgroundColor: { + if(active){ + return Theme.windowActiveBackgroundColor + } + return Theme.windowBackgroundColor + } property Quick.Item appBar: AppBar { title: root.title height: 30 @@ -39,4 +47,140 @@ Quick.Window { Quick.Component.onCompleted: { Router.addWindow(root) } + + Quick.Component { + id:com_app_bar + Quick.Item{ + data: root.appBar + Quick.Component.onCompleted: { + root.appBar.width = Qt.binding(function(){ + return this.parent.width + }) + } + } + } + + + Quick.Item{ + id: layout_container + anchors.fill: parent + anchors.margins: root.__margins + Quick.Loader{ + anchors.fill: parent + sourceComponent: background + Quick.Component.onDestruction: sourceComponent = undefined + } + Quick.Loader{ + id:loader_app_bar + anchors { + top: parent.top + left: parent.left + right: parent.right + } + height: { + if(root.useSystemAppBar){ + return 0 + } + return root.fitsAppBarWindows ? 0 : root.appBar.height + } + sourceComponent: root.useSystemAppBar ? undefined : com_app_bar + Quick.Component.onDestruction: sourceComponent = undefined + } + Quick.Item{ + id:layout_content + anchors{ + top: loader_app_bar.bottom + left: parent.left + right: parent.right + bottom: parent.bottom + } + clip: true + } + Quick.Loader{ + property string loadingText + property bool cancel: false + id:loader_loading + anchors.fill: parent + Quick.Component.onDestruction: sourceComponent = undefined + } + InfoBar{ + id:info_bar + root: layout_container + } + + Quick.Loader{ + id:loader_border + anchors.fill: parent + sourceComponent: { + if(root.useSystemAppBar || Utilities.isWin() || root.visibility === Window.Maximized || root.visibility === Window.FullScreen){ + return undefined + } + return com_border + } + Quick.Component.onDestruction: sourceComponent = undefined + } + } + + Quick.Component { + id:com_background + Quick.Item{ + Rectangle{ + anchors.fill: parent + color: root.backgroundColor + } + Quick.Image{ + id:img_back + visible: false + cache: false + fillMode: Quick.Image.PreserveAspectCrop + asynchronous: true + Quick.Component.onCompleted: { + img_back.updateLayout() + source = Utilities.getUrlByFilePath(Theme.desktopImagePath) + } + Quick.Connections{ + target: root + function onScreenChanged(){ + img_back.updateLayout() + } + } + function updateLayout(){ + var geometry = Utilities.desktopAvailableGeometry(root) + img_back.width = geometry.width + img_back.height = geometry.height + img_back.sourceSize = Qt.size(img_back.width,img_back.height) + } + Quick.Connections{ + target: Theme + function onDesktopImagePathChanged(){ + timer_update_image.restart() + } + function onBlurBehindWindowEnabledChanged(){ + if(Theme.blurBehindWindowEnabled){ + img_back.source = Utilities.getUrlByFilePath(Theme.desktopImagePath) + }else{ + img_back.source = "" + } + } + } + Quick.Timer{ + id:timer_update_image + interval: 150 + onTriggered: { + img_back.source = "" + img_back.source = Utilities.getUrlByFilePath(Theme.desktopImagePath) + } + } + } + Acrylic{ + anchors.fill: parent + target: img_back + tintOpacity: Theme.dark ? 0.80 : 0.75 + blurRadius: 64 + visible: root.active && Theme.blurBehindWindowEnabled + tintColor: Theme.dark ? Qt.rgba(0, 0, 0, 1) : Qt.rgba(1, 1, 1, 1) + targetRect: Qt.rect(root.x-root.screen.virtualX,root.y-root.screen.virtualY,root.width,root.height) + } + } + } } diff --git a/Fluent/resources/noise.png b/Fluent/resources/noise.png new file mode 100644 index 0000000..a78e829 Binary files /dev/null and b/Fluent/resources/noise.png differ