diff --git a/GlosSI.sln b/GlosSI.sln index 3defd40..5b2d049 100644 --- a/GlosSI.sln +++ b/GlosSI.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.31729.503 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GlosSITarget", "GlosSITarget\GlosSITarget.vcxproj", "{076E263E-0687-4435-836E-8F4EF6668843}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GlosSIConfig", "GlosSIConfig\GlosSIConfig.vcxproj", "{4B42920B-3CC6-475F-A5B3-441337968483}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -21,6 +23,12 @@ Global {076E263E-0687-4435-836E-8F4EF6668843}.Release|x64.Build.0 = Release|x64 {076E263E-0687-4435-836E-8F4EF6668843}.Release|x86.ActiveCfg = Release|Win32 {076E263E-0687-4435-836E-8F4EF6668843}.Release|x86.Build.0 = Release|Win32 + {4B42920B-3CC6-475F-A5B3-441337968483}.Debug|x64.ActiveCfg = Debug|x64 + {4B42920B-3CC6-475F-A5B3-441337968483}.Debug|x64.Build.0 = Debug|x64 + {4B42920B-3CC6-475F-A5B3-441337968483}.Debug|x86.ActiveCfg = Debug|x64 + {4B42920B-3CC6-475F-A5B3-441337968483}.Release|x64.ActiveCfg = Release|x64 + {4B42920B-3CC6-475F-A5B3-441337968483}.Release|x64.Build.0 = Release|x64 + {4B42920B-3CC6-475F-A5B3-441337968483}.Release|x86.ActiveCfg = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/GlosSIConfig/GlosSIConfig.vcxproj b/GlosSIConfig/GlosSIConfig.vcxproj new file mode 100644 index 0000000..4ba32a1 --- /dev/null +++ b/GlosSIConfig/GlosSIConfig.vcxproj @@ -0,0 +1,118 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {4B42920B-3CC6-475F-A5B3-441337968483} + QtVS_v304 + 10.0.19041.0 + 10.0.19041.0 + $(MSBuildProjectDirectory)\QtMsBuild + + + + Application + v142 + + + Application + v142 + + + + + + + 6.2.0 + quick + debug + + + 6.2.0 + quick + release + + + + + + + + + + + + + + + + + + + + + + stdcpp20 + /Zc:__cplusplus %(AdditionalOptions) + + + + + stdcpp20 + /Zc:__cplusplus %(AdditionalOptions) + + + + + true + true + ProgramDatabase + Disabled + MultiThreadedDebugDLL + + + Windows + true + + + + + true + true + None + MaxSpeed + MultiThreadedDLL + + + Windows + false + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GlosSIConfig/GlosSIConfig.vcxproj.filters b/GlosSIConfig/GlosSIConfig.vcxproj.filters new file mode 100644 index 0000000..eb79931 --- /dev/null +++ b/GlosSIConfig/GlosSIConfig.vcxproj.filters @@ -0,0 +1,53 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {639EADAA-A684-42e4-A9AD-28FC9BCB8F7C} + ts + + + {4abd6243-b275-40c0-8927-27cebe0e7b5a} + + + + + Source Files + + + Resource Files + + + Source Files + + + + + qml + + + qml + + + + + Header Files + + + + + Header Files + + + \ No newline at end of file diff --git a/GlosSIConfig/UIModel.cpp b/GlosSIConfig/UIModel.cpp new file mode 100644 index 0000000..9032f32 --- /dev/null +++ b/GlosSIConfig/UIModel.cpp @@ -0,0 +1,40 @@ +#include "UIModel.h" + +#include + +UIModel::UIModel() +{ + auto path = std::filesystem::temp_directory_path() + .parent_path() + .parent_path() + .parent_path(); + + path /= "Roaming"; + path /= "GlosSI"; + if (!std::filesystem::exists(path)) + std::filesystem::create_directories(path); + + config_path_ = path; + config_dir_name_ = (path /= "Targets").string().data(); +} + +QStringList UIModel::getTargetList() const +{ + QDir dir(config_dir_name_); + auto entries = dir.entryList(QDir::Files, QDir::SortFlag::Name); + entries.removeIf([](const auto& entry) { + return entry.endsWith(".json"); + }); + QStringList res; + std::ranges::transform(entries, std::back_inserter(res), [](const auto& entry) + { + return entry.mid(0, entry.lastIndexOf(".json")); + }); + res.push_back("Debug"); + return res; +} + +bool UIModel::getIsWindows() const +{ + return is_windows_; +} diff --git a/GlosSIConfig/UIModel.h b/GlosSIConfig/UIModel.h new file mode 100644 index 0000000..af4b5b2 --- /dev/null +++ b/GlosSIConfig/UIModel.h @@ -0,0 +1,42 @@ +/* +Copyright 2021 Peter Repukat - FlatspotSoftware + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#pragma once +#include +#include + +class UIModel : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool isWindows READ getIsWindows CONSTANT) + +public: + UIModel(); + + Q_INVOKABLE QStringList getTargetList() const; + + bool getIsWindows() const; + +private: + std::filesystem::path config_path_; + QString config_dir_name_; +#ifdef _WIN32 + bool is_windows_ = true; +#else + bool is_windows_ = false; +#endif +}; + diff --git a/GlosSIConfig/main.cpp b/GlosSIConfig/main.cpp new file mode 100644 index 0000000..5aff396 --- /dev/null +++ b/GlosSIConfig/main.cpp @@ -0,0 +1,144 @@ +/* +Copyright 2021 Peter Repukat - FlatspotSoftware + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +#include +#include +#include +#include + +#ifdef _WIN32 +#define NOMINMAX +#include +#include +#pragma comment(lib, "Dwmapi.lib") +#endif + +#include "UIModel.h" +#include "qml/WinEventFilter.h" + +#ifdef _WIN32 +// Some undocument stuff to enable aero on win10 or higher... +enum AccentState +{ + ACCENT_DISABLED = 0, + ACCENT_ENABLE_GRADIENT = 1, + ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, + ACCENT_ENABLE_BLURBEHIND = 3, + ACCENT_INVALID_STATE = 4 +}; +struct AccentPolicy +{ + + AccentState AccentState; + int AccentFlags; + int GradientColor; + int AnimationId; +}; + +enum WindowCompositionAttribute +{ + // ... + WCA_ACCENT_POLICY = 19 + // ... +}; +struct WindowCompositionAttributeData +{ + WindowCompositionAttribute Attribute; + void* Data; + int SizeOfData; +}; + +typedef HRESULT(__stdcall* PSetWindowCompositionAttribute)(HWND hwnd, WindowCompositionAttributeData* pattrs); + +#endif + + +int main(int argc, char* argv[]) +{ +#if defined(Q_OS_WIN) + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); +#endif + + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + UIModel uimodel; + engine.rootContext()->setContextProperty("uiModel", QVariant::fromValue(&uimodel)); + engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); + if (engine.rootObjects().isEmpty()) + return -1; +#ifdef _WIN32 + + // As a Qt.Frameless window sucks ass, and doesn't support Aerosnap and resizing and all that stuff is cumbersome on windows... + // Use good old dwm... + + // First get window from QML. + auto window = qobject_cast(engine.rootObjects()[0]); + const HWND hwnd = reinterpret_cast(window->winId()); + // Clear it's title bar + // NO!(!!) We wan't to keep WS_THICKFRAME, as that's what gives us a shadow, aerosnap, proper resizing, win11 round corners + // ...and all that good stuff! + auto style = GetWindowLong(hwnd, GWL_STYLE); + style &= ~(WS_CAPTION | WS_MINIMIZE | WS_MAXIMIZE | WS_SYSMENU); + SetWindowLong(hwnd, GWL_STYLE, style); + + // Enable blurbehind (not needed?) anyway gives nice background on win7 and 8 + DWM_BLURBEHIND bb{ .dwFlags = DWM_BB_ENABLE, .fEnable = true, .hRgnBlur = nullptr }; + DwmEnableBlurBehindWindow(hwnd, &bb); + + //// undoc stuff for aero >= Win10 + //// leave for now... performance is crap! + //// for now we have to live with unblurred transparency. lol + //AccentPolicy accPol = { .AccentState = ACCENT_ENABLE_BLURBEHIND }; + //WindowCompositionAttributeData data = { + // .Attribute = WindowCompositionAttribute::WCA_ACCENT_POLICY, + // .Data = &accPol, + // .SizeOfData = sizeof(accPol) + //}; + //auto user32dll = GetModuleHandle(L"user32.dll"); + //if (user32dll) { + // PSetWindowCompositionAttribute SetWindowCompositionAttribute = ( + // reinterpret_cast(GetProcAddress(user32dll, "SetWindowCompositionAttribute")) + // ); + // if (SetWindowCompositionAttribute) + // { + // SetWindowCompositionAttribute(hwnd, &data); + // } + //} + + // extend the frame fully into the client area => draw all outside the window frame. + MARGINS margins = { -1 }; + DwmExtendFrameIntoClientArea(hwnd, &margins); + + // To Fix top window frame, install native event filter + // basically the Qt. equivalent of having a own WndProc + // for more info see WinEventFilter.h + auto filter = std::make_shared(); + app.installNativeEventFilter(filter.get()); + + RECT rcClient; + GetWindowRect(hwnd, &rcClient); + + // Inform the application of the frame change. + SetWindowPos(hwnd, + NULL, + rcClient.left, rcClient.top, + rcClient.right - rcClient.left, rcClient.bottom - rcClient.top, + SWP_FRAMECHANGED); + +#endif + + return app.exec(); +} diff --git a/GlosSIConfig/qml.qrc b/GlosSIConfig/qml.qrc new file mode 100644 index 0000000..0106af5 --- /dev/null +++ b/GlosSIConfig/qml.qrc @@ -0,0 +1,6 @@ + + + qml/main.qml + qml/RPane.qml + + diff --git a/GlosSIConfig/qml/RPane.qml b/GlosSIConfig/qml/RPane.qml new file mode 100644 index 0000000..eae2d70 --- /dev/null +++ b/GlosSIConfig/qml/RPane.qml @@ -0,0 +1,21 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.9 +import QtQuick.Controls.Material 2.9 +import QtQuick.Controls.Material.impl 2.9 + +Pane { + id: control + property int radius: 0 + property color color: control.Material.backgroundColor + property real bgOpacity: 1 + background: Rectangle { + color: parent.color + opacity: parent.bgOpacity + radius: control.Material.elevation > 0 ? control.radius : 0 + + layer.enabled: control.enabled && control.Material.elevation > 0 + layer.effect: ElevationEffect { + elevation: control.Material.elevation + } + } +} \ No newline at end of file diff --git a/GlosSIConfig/qml/WinEventFilter.h b/GlosSIConfig/qml/WinEventFilter.h new file mode 100644 index 0000000..b8a8fff --- /dev/null +++ b/GlosSIConfig/qml/WinEventFilter.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#define NOMINMAX +#include + +class WinEventFilter : public QAbstractNativeEventFilter +{ +public: + WinEventFilter() = default; + + /* + * When having the DWM frame fully extended into client area + * ever since WIN10 a 6px border is displayed on top. + * to remove that one has to catch the WM_NCCALCSIZE event and re-calculate the window-rect + * https://stackoverflow.com/a/2135120/5106063 + * https://docs.microsoft.com/en-us/windows/win32/dwm/customframe + */ + bool nativeEventFilter(const QByteArray& eventType, void* message, qintptr* result) override + { + if (QString(eventType) == "windows_generic_MSG") { + auto msg = static_cast(message)->message; + auto lParam = static_cast(message)->lParam; + if (msg == WM_NCCALCSIZE) + { + auto sz = reinterpret_cast(lParam); + sz->rgrc[0].top -= 6; + } + } + return false; + } +}; diff --git a/GlosSIConfig/qml/main.qml b/GlosSIConfig/qml/main.qml new file mode 100644 index 0000000..7a592b5 --- /dev/null +++ b/GlosSIConfig/qml/main.qml @@ -0,0 +1,197 @@ +/* +Copyright 2021 Peter Repukat - FlatspotSoftware + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +import QtQuick 6.2 +import QtQuick.Layouts 6.2 +import QtQuick.Controls.Material 6.2 +import QtQuick.Dialogs + +Window { + id: window + visible: true + width: 1280 + height: 719 + Material.theme: Material.Dark + Material.accent: Material.color(Material.Blue, Material.Shade900) + + property bool itemSelected: false; + + title: qsTr("GlosSI - Config") + + color: colorAlpha(Material.background, 0.98) + + function toggleMaximized() { + if (window.visibility === Window.Maximized || window.visibility === Window.FullScreen) { + window.visibility = Window.Windowed + } else { + window.visibility = Window.Maximized + } + } + + function colorAlpha(color, alpha) { + return Qt.rgba(color.r, color.g, color.b, alpha); + } + + Rectangle { + id: titleBar + visible: uiModel.isWindows + color: colorAlpha(Qt.darker(Material.background, 1.1), 0.90) + height: visible ? 24 : 0 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + TapHandler { + onTapped: if (tapCount === 2) toggleMaximized() + gesturePolicy: TapHandler.DragThreshold + } + DragHandler { + grabPermissions: TapHandler.CanTakeOverFromAnything + onActiveChanged: if (active) { window.startSystemMove(); } + } + + Label { + text: window.title + font.bold: true + anchors.leftMargin: 16 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + } + + RowLayout { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + ToolButton { + text: "🗕" + onClicked: window.showMinimized(); + } + ToolButton { + text: window.visibility === Window.Maximized || window.visibility === Window.FullScreen ? "🗗" : "🗖" + onClicked: window.toggleMaximized() + } + ToolButton { + id: closbtn + text: "🗙" + onClicked: window.close() + background: Rectangle { + implicitWidth: 32 + implicitHeight: 32 + radius: 16 + color: Qt.darker("red", closbtn.enabled && (closbtn.checked || closbtn.highlighted) ? 1.6 : 1.2) + opacity: closbtn.hovered ? 0.5 : 0 + Behavior on opacity { + NumberAnimation { + duration: 350 + easing.type: Easing.InOutQuad + } + } + } + } + } + + } + + Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: titleBar.bottom + anchors.bottom: parent.bottom + + RPane { + id: existingTargetsPane + anchors.left: parent.left + width:window.width / 3.301 + 16 + Component.onCompleted: console.log(width) + anchors.top: parent.top + anchors.bottom: parent.bottom + Material.elevation: 6 + anchors.leftMargin: -16 + radius: 16 + color: Qt.lighter(Material.background, 1.6) + bgOpacity: 0.8 + + Item { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 16 + clip: true + ListView { + anchors.fill: parent + spacing: 0 + model: uiModel.getTargetList(); + delegate: Item { + width: parent.width + height: lbl.height + lbl.anchors.topMargin + lbl.anchors.bottomMargin + // TODO: Left size App icon + Label { + id: lbl + anchors.left: parent.left + anchors.right: parent.right + anchors.rightMargin: 48 + anchors.topMargin: 8 + anchors.bottomMargin: 8 + anchors.leftMargin: 4 + anchors.verticalCenter: parent.verticalCenter + text: modelData + font.pixelSize: 16 + } + // TODO: Right side icon if in steam + Rectangle { + width: parent.width + height: 1 + anchors.bottom: parent.bottom + color: Qt.rgba(1,1,1,0.3) + } + } + } + } + } + + RoundButton { + id: addBtn + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 24 + width: 64 + height: 64 + text: "+" + contentItem: Label { + anchors.centerIn: parent + text: addBtn.text + font.pixelSize: 32 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + highlighted: true + onClicked: fileDialog.open(); + } + + FileDialog { + id: fileDialog + title: qsTr("Please choose a Program to Launch") + nameFilters: uiModel.isWindows ? ["Executable files (*.exe *.bat *.ps1)"] : [] + onAccepted: { + console.log("You chose: " + fileDialog.selectedFile) + } + onRejected: { + console.log("Canceled") + } + } + + } +} diff --git a/glosc.code-workspace b/glosc.code-workspace index 18308c3..ba57d0b 100644 --- a/glosc.code-workspace +++ b/glosc.code-workspace @@ -1,5 +1,8 @@ { "folders": [ + { + "path": "GlosSIConfig" + }, { "path": "GlosSITarget" },