mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-07 11:46:30 +02:00
[FancyZones] Split zones-settings: custom layouts (#15642)
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
#include <common/SettingsAPI/FileWatcher.h>
|
||||
|
||||
#include <FancyZonesLib/FancyZonesData.h>
|
||||
#include <FancyZonesLib/FancyZonesData/CustomLayouts.h>
|
||||
#include <FancyZonesLib/FancyZonesData/LayoutHotkeys.h>
|
||||
#include <FancyZonesLib/FancyZonesWindowProperties.h>
|
||||
#include <FancyZonesLib/FancyZonesWinHookEventIDs.h>
|
||||
@@ -72,6 +73,7 @@ public:
|
||||
|
||||
FancyZonesDataInstance().ReplaceZoneSettingsFileFromOlderVersions();
|
||||
LayoutHotkeys::instance().LoadData();
|
||||
CustomLayouts::instance().LoadData();
|
||||
}
|
||||
|
||||
// IFancyZones
|
||||
@@ -775,6 +777,10 @@ LRESULT FancyZones::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lpa
|
||||
{
|
||||
LayoutHotkeys::instance().LoadData();
|
||||
}
|
||||
else if (message == WM_PRIV_CUSTOM_LAYOUTS_FILE_UPDATE)
|
||||
{
|
||||
CustomLayouts::instance().LoadData();
|
||||
}
|
||||
else if (message == WM_PRIV_QUICK_LAYOUT_KEY)
|
||||
{
|
||||
ApplyQuickLayout(static_cast<int>(lparam));
|
||||
@@ -1307,23 +1313,22 @@ void FancyZones::ApplyQuickLayout(int key) noexcept
|
||||
return;
|
||||
}
|
||||
|
||||
// Find a custom zone set with this uuid and apply it
|
||||
auto layout = CustomLayouts::instance().GetLayout(layoutId.value());
|
||||
if (!layout)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto uuidStr = FancyZonesUtils::GuidToString(layoutId.value());
|
||||
if (!uuidStr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto workArea = m_workAreaHandler.GetWorkAreaFromCursor(m_currentDesktopId);
|
||||
|
||||
// Find a custom zone set with this uuid and apply it
|
||||
auto customZoneSets = FancyZonesDataInstance().GetCustomZoneSetsMap();
|
||||
|
||||
if (!customZoneSets.contains(uuidStr.value()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FancyZonesDataTypes::ZoneSetData data{ .uuid = uuidStr.value(), .type = FancyZonesDataTypes::ZoneSetLayoutType::Custom };
|
||||
|
||||
auto workArea = m_workAreaHandler.GetWorkAreaFromCursor(m_currentDesktopId);
|
||||
FancyZonesDataInstance().SetActiveZoneSet(workArea->UniqueId(), data);
|
||||
FancyZonesDataInstance().SaveZoneSettings();
|
||||
UpdateZoneSets();
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <common/utils/process_path.h>
|
||||
#include <common/logger/logger.h>
|
||||
|
||||
#include <FancyZonesLib/FancyZonesData/CustomLayouts.h>
|
||||
#include <FancyZonesLib/FancyZonesData/LayoutHotkeys.h>
|
||||
#include <FancyZonesLib/ModuleConstants.h>
|
||||
|
||||
@@ -170,6 +171,12 @@ void FancyZonesData::ReplaceZoneSettingsFileFromOlderVersions()
|
||||
//deviceInfoMap = JSONHelpers::ParseDeviceInfos(fancyZonesDataJSON);
|
||||
//customZoneSetsMap = JSONHelpers::ParseCustomZoneSets(fancyZonesDataJSON);
|
||||
|
||||
auto customLayouts = JSONHelpers::ParseCustomZoneSets(fancyZonesDataJSON);
|
||||
if (customLayouts)
|
||||
{
|
||||
JSONHelpers::SaveCustomLayouts(customLayouts.value());
|
||||
}
|
||||
|
||||
auto templates = JSONHelpers::ParseLayoutTemplates(fancyZonesDataJSON);
|
||||
if (templates)
|
||||
{
|
||||
@@ -197,12 +204,6 @@ const JSONHelpers::TDeviceInfoMap& FancyZonesData::GetDeviceInfoMap() const
|
||||
return deviceInfoMap;
|
||||
}
|
||||
|
||||
const JSONHelpers::TCustomZoneSetsMap& FancyZonesData::GetCustomZoneSetsMap() const
|
||||
{
|
||||
std::scoped_lock lock{ dataLock };
|
||||
return customZoneSetsMap;
|
||||
}
|
||||
|
||||
const std::unordered_map<std::wstring, std::vector<FancyZonesDataTypes::AppZoneHistoryData>>& FancyZonesData::GetAppZoneHistoryMap() const
|
||||
{
|
||||
std::scoped_lock lock{ dataLock };
|
||||
@@ -223,13 +224,6 @@ std::optional<FancyZonesDataTypes::DeviceInfoData> FancyZonesData::FindDeviceInf
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<FancyZonesDataTypes::CustomZoneSetData> FancyZonesData::FindCustomZoneSet(const std::wstring& guid) const
|
||||
{
|
||||
std::scoped_lock lock{ dataLock };
|
||||
auto it = customZoneSetsMap.find(guid);
|
||||
return it != end(customZoneSetsMap) ? std::optional{ it->second } : std::nullopt;
|
||||
}
|
||||
|
||||
bool FancyZonesData::AddDevice(const FancyZonesDataTypes::DeviceIdData& deviceId)
|
||||
{
|
||||
_TRACER_;
|
||||
@@ -604,25 +598,29 @@ void FancyZonesData::SetActiveZoneSet(const FancyZonesDataTypes::DeviceIdData& d
|
||||
deviceInfo.activeZoneSet = data;
|
||||
|
||||
// If the zone set is custom, we need to copy its properties to the device
|
||||
auto zonesetIt = customZoneSetsMap.find(data.uuid);
|
||||
if (zonesetIt != customZoneSetsMap.end())
|
||||
auto id = FancyZonesUtils::GuidFromString(data.uuid);
|
||||
if (id.has_value())
|
||||
{
|
||||
if (zonesetIt->second.type == FancyZonesDataTypes::CustomLayoutType::Grid)
|
||||
auto layout = CustomLayouts::instance().GetLayout(id.value());
|
||||
if (layout)
|
||||
{
|
||||
auto layoutInfo = std::get<FancyZonesDataTypes::GridLayoutInfo>(zonesetIt->second.info);
|
||||
deviceInfo.sensitivityRadius = layoutInfo.sensitivityRadius();
|
||||
deviceInfo.showSpacing = layoutInfo.showSpacing();
|
||||
deviceInfo.spacing = layoutInfo.spacing();
|
||||
deviceInfo.zoneCount = layoutInfo.zoneCount();
|
||||
}
|
||||
else if (zonesetIt->second.type == FancyZonesDataTypes::CustomLayoutType::Canvas)
|
||||
{
|
||||
auto layoutInfo = std::get<FancyZonesDataTypes::CanvasLayoutInfo>(zonesetIt->second.info);
|
||||
deviceInfo.sensitivityRadius = layoutInfo.sensitivityRadius;
|
||||
deviceInfo.zoneCount = (int)layoutInfo.zones.size();
|
||||
if (layout.value().type == FancyZonesDataTypes::CustomLayoutType::Grid)
|
||||
{
|
||||
auto layoutInfo = std::get<FancyZonesDataTypes::GridLayoutInfo>(layout.value().info);
|
||||
deviceInfo.sensitivityRadius = layoutInfo.sensitivityRadius();
|
||||
deviceInfo.showSpacing = layoutInfo.showSpacing();
|
||||
deviceInfo.spacing = layoutInfo.spacing();
|
||||
deviceInfo.zoneCount = layoutInfo.zoneCount();
|
||||
}
|
||||
else if (layout.value().type == FancyZonesDataTypes::CustomLayoutType::Canvas)
|
||||
{
|
||||
auto layoutInfo = std::get<FancyZonesDataTypes::CanvasLayoutInfo>(layout.value().info);
|
||||
deviceInfo.sensitivityRadius = layoutInfo.sensitivityRadius;
|
||||
deviceInfo.zoneCount = (int)layoutInfo.zones.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -644,8 +642,7 @@ void FancyZonesData::LoadFancyZonesData()
|
||||
json::JsonObject fancyZonesDataJSON = GetPersistFancyZonesJSON();
|
||||
|
||||
appZoneHistoryMap = JSONHelpers::ParseAppZoneHistory(fancyZonesDataJSON);
|
||||
deviceInfoMap = JSONHelpers::ParseDeviceInfos(fancyZonesDataJSON);
|
||||
customZoneSetsMap = JSONHelpers::ParseCustomZoneSets(fancyZonesDataJSON);
|
||||
deviceInfoMap = JSONHelpers::ParseDeviceInfos(fancyZonesDataJSON);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,11 +676,11 @@ void FancyZonesData::SaveZoneSettings() const
|
||||
|
||||
if (dirtyFlag)
|
||||
{
|
||||
JSONHelpers::SaveZoneSettings(zonesSettingsFileName, updatedDeviceInfoMap, customZoneSetsMap);
|
||||
JSONHelpers::SaveZoneSettings(zonesSettingsFileName, updatedDeviceInfoMap);
|
||||
}
|
||||
else
|
||||
{
|
||||
JSONHelpers::SaveZoneSettings(zonesSettingsFileName, deviceInfoMap, customZoneSetsMap);
|
||||
JSONHelpers::SaveZoneSettings(zonesSettingsFileName, deviceInfoMap);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace FancyZonesDataTypes
|
||||
struct ZoneSetData;
|
||||
struct DeviceIdData;
|
||||
struct DeviceInfoData;
|
||||
struct CustomZoneSetData;
|
||||
struct CustomLayoutData;
|
||||
struct AppZoneHistoryData;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace FancyZonesUnitTests
|
||||
class WorkAreaCreationUnitTests;
|
||||
class LayoutHotkeysUnitTests;
|
||||
class LayoutTemplatesUnitTests;
|
||||
class CustomLayoutsUnitTests;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -45,10 +46,8 @@ public:
|
||||
void SetVirtualDesktopCheckCallback(std::function<bool(GUID)> callback);
|
||||
|
||||
std::optional<FancyZonesDataTypes::DeviceInfoData> FindDeviceInfo(const FancyZonesDataTypes::DeviceIdData& id) const;
|
||||
std::optional<FancyZonesDataTypes::CustomZoneSetData> FindCustomZoneSet(const std::wstring& guid) const;
|
||||
|
||||
const JSONHelpers::TDeviceInfoMap& GetDeviceInfoMap() const;
|
||||
const JSONHelpers::TCustomZoneSetsMap& GetCustomZoneSetsMap() const;
|
||||
const std::unordered_map<std::wstring, std::vector<FancyZonesDataTypes::AppZoneHistoryData>>& GetAppZoneHistoryMap() const;
|
||||
|
||||
inline const std::wstring& GetZonesSettingsFileName() const
|
||||
@@ -92,17 +91,13 @@ private:
|
||||
friend class FancyZonesUnitTests::ZoneSetCalculateZonesUnitTests;
|
||||
friend class FancyZonesUnitTests::LayoutHotkeysUnitTests;
|
||||
friend class FancyZonesUnitTests::LayoutTemplatesUnitTests;
|
||||
friend class FancyZonesUnitTests::CustomLayoutsUnitTests;
|
||||
|
||||
inline void SetDeviceInfo(const FancyZonesDataTypes::DeviceIdData& deviceId, FancyZonesDataTypes::DeviceInfoData data)
|
||||
{
|
||||
deviceInfoMap[deviceId] = data;
|
||||
}
|
||||
|
||||
inline void SetCustomZonesets(const std::wstring& uuid, FancyZonesDataTypes::CustomZoneSetData data)
|
||||
{
|
||||
customZoneSetsMap[uuid] = data;
|
||||
}
|
||||
|
||||
inline bool ParseDeviceInfos(const json::JsonObject& fancyZonesDataJSON)
|
||||
{
|
||||
deviceInfoMap = JSONHelpers::ParseDeviceInfos(fancyZonesDataJSON);
|
||||
@@ -113,7 +108,6 @@ private:
|
||||
{
|
||||
appZoneHistoryMap.clear();
|
||||
deviceInfoMap.clear();
|
||||
customZoneSetsMap.clear();
|
||||
}
|
||||
|
||||
inline void SetSettingsModulePath(std::wstring_view moduleName)
|
||||
@@ -135,8 +129,6 @@ private:
|
||||
std::unordered_map<std::wstring, std::vector<FancyZonesDataTypes::AppZoneHistoryData>> appZoneHistoryMap{};
|
||||
// Maps device unique ID to device data
|
||||
JSONHelpers::TDeviceInfoMap deviceInfoMap{};
|
||||
// Maps custom zoneset UUID to it's data
|
||||
JSONHelpers::TCustomZoneSetsMap customZoneSetsMap{};
|
||||
|
||||
std::wstring settingsFileName;
|
||||
std::wstring zonesSettingsFileName;
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
#include "../pch.h"
|
||||
#include "CustomLayouts.h"
|
||||
|
||||
#include <common/logger/logger.h>
|
||||
|
||||
#include <FancyZonesLib/FancyZonesData.h> // layout defaults
|
||||
#include <FancyZonesLib/FancyZonesWinHookEventIDs.h>
|
||||
#include <FancyZonesLib/JsonHelpers.h>
|
||||
#include <FancyZonesLib/util.h>
|
||||
|
||||
namespace JsonUtils
|
||||
{
|
||||
std::vector<int> JsonArrayToNumVec(const json::JsonArray& arr)
|
||||
{
|
||||
std::vector<int> vec;
|
||||
for (const auto& val : arr)
|
||||
{
|
||||
vec.emplace_back(static_cast<int>(val.GetNumber()));
|
||||
}
|
||||
|
||||
return vec;
|
||||
}
|
||||
|
||||
namespace CanvasLayoutInfoJSON
|
||||
{
|
||||
std::optional<FancyZonesDataTypes::CanvasLayoutInfo> FromJson(const json::JsonObject& infoJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
FancyZonesDataTypes::CanvasLayoutInfo info;
|
||||
info.lastWorkAreaWidth = static_cast<int>(infoJson.GetNamedNumber(NonLocalizable::CustomLayoutsIds::RefWidthID));
|
||||
info.lastWorkAreaHeight = static_cast<int>(infoJson.GetNamedNumber(NonLocalizable::CustomLayoutsIds::RefHeightID));
|
||||
|
||||
json::JsonArray zonesJson = infoJson.GetNamedArray(NonLocalizable::CustomLayoutsIds::ZonesID);
|
||||
uint32_t size = zonesJson.Size();
|
||||
info.zones.reserve(size);
|
||||
for (uint32_t i = 0; i < size; ++i)
|
||||
{
|
||||
json::JsonObject zoneJson = zonesJson.GetObjectAt(i);
|
||||
const int x = static_cast<int>(zoneJson.GetNamedNumber(NonLocalizable::CustomLayoutsIds::XID));
|
||||
const int y = static_cast<int>(zoneJson.GetNamedNumber(NonLocalizable::CustomLayoutsIds::YID));
|
||||
const int width = static_cast<int>(zoneJson.GetNamedNumber(NonLocalizable::CustomLayoutsIds::WidthID));
|
||||
const int height = static_cast<int>(zoneJson.GetNamedNumber(NonLocalizable::CustomLayoutsIds::HeightID));
|
||||
FancyZonesDataTypes::CanvasLayoutInfo::Rect zone{ x, y, width, height };
|
||||
info.zones.push_back(zone);
|
||||
}
|
||||
|
||||
info.sensitivityRadius = static_cast<int>(infoJson.GetNamedNumber(NonLocalizable::CustomLayoutsIds::SensitivityRadiusID, DefaultValues::SensitivityRadius));
|
||||
return info;
|
||||
}
|
||||
catch (const winrt::hresult_error&)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace GridLayoutInfoJSON
|
||||
{
|
||||
std::optional<FancyZonesDataTypes::GridLayoutInfo> FromJson(const json::JsonObject& infoJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
FancyZonesDataTypes::GridLayoutInfo info(FancyZonesDataTypes::GridLayoutInfo::Minimal{});
|
||||
|
||||
info.m_rows = static_cast<int>(infoJson.GetNamedNumber(NonLocalizable::CustomLayoutsIds::RowsID));
|
||||
info.m_columns = static_cast<int>(infoJson.GetNamedNumber(NonLocalizable::CustomLayoutsIds::ColumnsID));
|
||||
|
||||
json::JsonArray rowsPercentage = infoJson.GetNamedArray(NonLocalizable::CustomLayoutsIds::RowsPercentageID);
|
||||
json::JsonArray columnsPercentage = infoJson.GetNamedArray(NonLocalizable::CustomLayoutsIds::ColumnsPercentageID);
|
||||
json::JsonArray cellChildMap = infoJson.GetNamedArray(NonLocalizable::CustomLayoutsIds::CellChildMapID);
|
||||
|
||||
if (rowsPercentage.Size() != info.m_rows || columnsPercentage.Size() != info.m_columns || cellChildMap.Size() != info.m_rows)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
info.m_rowsPercents = JsonArrayToNumVec(rowsPercentage);
|
||||
info.m_columnsPercents = JsonArrayToNumVec(columnsPercentage);
|
||||
for (const auto& cellsRow : cellChildMap)
|
||||
{
|
||||
const auto cellsArray = cellsRow.GetArray();
|
||||
if (cellsArray.Size() != info.m_columns)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
info.cellChildMap().push_back(JsonArrayToNumVec(cellsArray));
|
||||
}
|
||||
|
||||
info.m_showSpacing = infoJson.GetNamedBoolean(NonLocalizable::CustomLayoutsIds::ShowSpacingID, DefaultValues::ShowSpacing);
|
||||
info.m_spacing = static_cast<int>(infoJson.GetNamedNumber(NonLocalizable::CustomLayoutsIds::SpacingID, DefaultValues::Spacing));
|
||||
info.m_sensitivityRadius = static_cast<int>(infoJson.GetNamedNumber(NonLocalizable::CustomLayoutsIds::SensitivityRadiusID, DefaultValues::SensitivityRadius));
|
||||
|
||||
return info;
|
||||
}
|
||||
catch (const winrt::hresult_error&)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomLayoutJSON
|
||||
{
|
||||
GUID layoutId;
|
||||
FancyZonesDataTypes::CustomLayoutData data;
|
||||
|
||||
static std::optional<CustomLayoutJSON> FromJson(const json::JsonObject& json)
|
||||
{
|
||||
try
|
||||
{
|
||||
CustomLayoutJSON result;
|
||||
|
||||
auto idStr = json.GetNamedString(NonLocalizable::CustomLayoutsIds::UuidID);
|
||||
auto id = FancyZonesUtils::GuidFromString(idStr.c_str());
|
||||
if (!id)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
result.layoutId = id.value();
|
||||
result.data.name = json.GetNamedString(NonLocalizable::CustomLayoutsIds::NameID);
|
||||
|
||||
json::JsonObject infoJson = json.GetNamedObject(NonLocalizable::CustomLayoutsIds::InfoID);
|
||||
std::wstring zoneSetType = std::wstring{ json.GetNamedString(NonLocalizable::CustomLayoutsIds::TypeID) };
|
||||
if (zoneSetType.compare(NonLocalizable::CustomLayoutsIds::CanvasID) == 0)
|
||||
{
|
||||
if (auto info = CanvasLayoutInfoJSON::FromJson(infoJson); info.has_value())
|
||||
{
|
||||
result.data.type = FancyZonesDataTypes::CustomLayoutType::Canvas;
|
||||
result.data.info = std::move(info.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
else if (zoneSetType.compare(NonLocalizable::CustomLayoutsIds::GridID) == 0)
|
||||
{
|
||||
if (auto info = GridLayoutInfoJSON::FromJson(infoJson); info.has_value())
|
||||
{
|
||||
result.data.type = FancyZonesDataTypes::CustomLayoutType::Grid;
|
||||
result.data.info = std::move(info.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (const winrt::hresult_error&)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CustomLayouts::TCustomLayoutMap ParseJson(const json::JsonObject& json)
|
||||
{
|
||||
CustomLayouts::TCustomLayoutMap map{};
|
||||
auto layouts = json.GetNamedArray(NonLocalizable::CustomLayoutsIds::CustomLayoutsArrayID);
|
||||
|
||||
for (uint32_t i = 0; i < layouts.Size(); ++i)
|
||||
{
|
||||
if (auto obj = CustomLayoutJSON::FromJson(layouts.GetObjectAt(i)); obj.has_value())
|
||||
{
|
||||
map[obj->layoutId] = std::move(obj->data);
|
||||
}
|
||||
}
|
||||
|
||||
return std::move(map);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
CustomLayouts::CustomLayouts()
|
||||
{
|
||||
const std::wstring& dataFileName = CustomLayoutsFileName();
|
||||
m_fileWatcher = std::make_unique<FileWatcher>(dataFileName, [&]() {
|
||||
PostMessageW(HWND_BROADCAST, WM_PRIV_CUSTOM_LAYOUTS_FILE_UPDATE, NULL, NULL);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
CustomLayouts& CustomLayouts::instance()
|
||||
{
|
||||
static CustomLayouts self;
|
||||
return self;
|
||||
}
|
||||
|
||||
void CustomLayouts::LoadData()
|
||||
{
|
||||
auto data = json::from_file(CustomLayoutsFileName());
|
||||
|
||||
try
|
||||
{
|
||||
if (data)
|
||||
{
|
||||
m_layouts = JsonUtils::ParseJson(data.value());
|
||||
}
|
||||
else
|
||||
{
|
||||
m_layouts.clear();
|
||||
Logger::info(L"custom-layouts.json file is missing or malformed");
|
||||
}
|
||||
}
|
||||
catch (const winrt::hresult_error& e)
|
||||
{
|
||||
Logger::error(L"Parsing custom-layouts error: {}", e.message());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<FancyZonesDataTypes::CustomLayoutData> CustomLayouts::GetLayout(const GUID& id) const noexcept
|
||||
{
|
||||
auto iter = m_layouts.find(id);
|
||||
if (iter != m_layouts.end())
|
||||
{
|
||||
return iter->second;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const CustomLayouts::TCustomLayoutMap& CustomLayouts::GetAllLayouts() const noexcept
|
||||
{
|
||||
return m_layouts;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include <guiddef.h>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include <FancyZonesLib/FancyZonesDataTypes.h>
|
||||
#include <FancyZonesLib/GuidUtils.h>
|
||||
#include <FancyZonesLib/ModuleConstants.h>
|
||||
|
||||
#include <common/SettingsAPI/FileWatcher.h>
|
||||
#include <common/SettingsAPI/settings_helpers.h>
|
||||
|
||||
namespace NonLocalizable
|
||||
{
|
||||
namespace CustomLayoutsIds
|
||||
{
|
||||
const static wchar_t* CustomLayoutsArrayID = L"custom-layouts";
|
||||
const static wchar_t* UuidID = L"uuid";
|
||||
const static wchar_t* NameID = L"name";
|
||||
const static wchar_t* InfoID = L"info";
|
||||
const static wchar_t* TypeID = L"type";
|
||||
const static wchar_t* CanvasID = L"canvas";
|
||||
const static wchar_t* GridID = L"grid";
|
||||
const static wchar_t* SensitivityRadiusID = L"sensitivity-radius";
|
||||
|
||||
// canvas
|
||||
const static wchar_t* RefHeightID = L"ref-height";
|
||||
const static wchar_t* RefWidthID = L"ref-width";
|
||||
const static wchar_t* ZonesID = L"zones";
|
||||
const static wchar_t* XID = L"X";
|
||||
const static wchar_t* YID = L"Y";
|
||||
const static wchar_t* WidthID = L"width";
|
||||
const static wchar_t* HeightID = L"height";
|
||||
|
||||
// grid
|
||||
const static wchar_t* RowsID = L"rows";
|
||||
const static wchar_t* ColumnsID = L"columns";
|
||||
const static wchar_t* RowsPercentageID = L"rows-percentage";
|
||||
const static wchar_t* ColumnsPercentageID = L"columns-percentage";
|
||||
const static wchar_t* CellChildMapID = L"cell-child-map";
|
||||
const static wchar_t* ShowSpacingID = L"show-spacing";
|
||||
const static wchar_t* SpacingID = L"spacing";
|
||||
}
|
||||
}
|
||||
|
||||
class CustomLayouts
|
||||
{
|
||||
public:
|
||||
using TCustomLayoutMap = std::unordered_map<GUID, FancyZonesDataTypes::CustomLayoutData>;
|
||||
|
||||
static CustomLayouts& instance();
|
||||
|
||||
inline static std::wstring CustomLayoutsFileName()
|
||||
{
|
||||
std::wstring saveFolderPath = PTSettingsHelper::get_module_save_folder_location(NonLocalizable::ModuleKey);
|
||||
#if defined(UNIT_TESTS)
|
||||
return saveFolderPath + L"\\test-custom-layouts.json";
|
||||
#endif
|
||||
return saveFolderPath + L"\\custom-layouts.json";
|
||||
}
|
||||
|
||||
void LoadData();
|
||||
|
||||
std::optional<FancyZonesDataTypes::CustomLayoutData> GetLayout(const GUID& id) const noexcept;
|
||||
const TCustomLayoutMap& GetAllLayouts() const noexcept;
|
||||
|
||||
private:
|
||||
CustomLayouts();
|
||||
~CustomLayouts() = default;
|
||||
|
||||
TCustomLayoutMap m_layouts;
|
||||
std::unique_ptr<FileWatcher> m_fileWatcher;
|
||||
};
|
||||
@@ -100,7 +100,7 @@ namespace FancyZonesDataTypes
|
||||
int m_sensitivityRadius;
|
||||
};
|
||||
|
||||
struct CustomZoneSetData
|
||||
struct CustomLayoutData
|
||||
{
|
||||
std::wstring name;
|
||||
CustomLayoutType type;
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
</ClCompile>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="FancyZonesData\CustomLayouts.h" />
|
||||
<ClInclude Include="FancyZones.h" />
|
||||
<ClInclude Include="FancyZonesDataTypes.h" />
|
||||
<ClInclude Include="FancyZonesData\LayoutTemplates.h" />
|
||||
@@ -67,6 +68,10 @@
|
||||
<ClInclude Include="ZonesOverlay.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="FancyZonesData\CustomLayouts.cpp">
|
||||
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">../pch.h</PrecompiledHeaderFile>
|
||||
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Release|x64'">../pch.h</PrecompiledHeaderFile>
|
||||
</ClCompile>
|
||||
<ClCompile Include="FancyZones.cpp" />
|
||||
<ClCompile Include="FancyZonesDataTypes.cpp" />
|
||||
<ClCompile Include="FancyZonesWinHookEventIDs.cpp" />
|
||||
|
||||
@@ -90,6 +90,9 @@
|
||||
<ClInclude Include="GuidUtils.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="FancyZonesData\CustomLayouts.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="FancyZonesData\LayoutHotkeys.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
@@ -158,6 +161,9 @@
|
||||
<ClCompile Include="MonitorUtils.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="FancyZonesData\CustomLayouts.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="FancyZonesData\LayoutHotkeys.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
|
||||
@@ -13,6 +13,7 @@ UINT WM_PRIV_VD_UPDATE;
|
||||
UINT WM_PRIV_EDITOR;
|
||||
UINT WM_PRIV_FILE_UPDATE;
|
||||
UINT WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE;
|
||||
UINT WM_PRIV_CUSTOM_LAYOUTS_FILE_UPDATE;
|
||||
UINT WM_PRIV_SNAP_HOTKEY;
|
||||
UINT WM_PRIV_QUICK_LAYOUT_KEY;
|
||||
UINT WM_PRIV_SETTINGS_CHANGED;
|
||||
@@ -33,6 +34,7 @@ void InitializeWinhookEventIds()
|
||||
WM_PRIV_EDITOR = RegisterWindowMessage(L"{87543824-7080-4e91-9d9c-0404642fc7b6}");
|
||||
WM_PRIV_FILE_UPDATE = RegisterWindowMessage(L"{632f17a9-55a7-45f1-a4db-162e39271d92}");
|
||||
WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE = RegisterWindowMessage(L"{07229b7e-4f22-4357-b136-33c289be2295}");
|
||||
WM_PRIV_CUSTOM_LAYOUTS_FILE_UPDATE = RegisterWindowMessage(L"{0972787e-cdab-4e16-b228-91acdc38f40f}");
|
||||
WM_PRIV_SNAP_HOTKEY = RegisterWindowMessage(L"{72f4fd8e-23f1-43ab-bbbc-029363df9a84}");
|
||||
WM_PRIV_QUICK_LAYOUT_KEY = RegisterWindowMessage(L"{15baab3d-c67b-4a15-aFF0-13610e05e947}");
|
||||
WM_PRIV_SETTINGS_CHANGED = RegisterWindowMessage(L"{89ca3Daa-bf2d-4e73-9f3f-c60716364e27}");
|
||||
|
||||
@@ -11,6 +11,7 @@ extern UINT WM_PRIV_VD_UPDATE; // Scheduled on virtual desktops update (creation
|
||||
extern UINT WM_PRIV_EDITOR; // Scheduled when the editor exits
|
||||
extern UINT WM_PRIV_FILE_UPDATE; // Scheduled when the watched zone-settings file is updated
|
||||
extern UINT WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE; // Scheduled when the watched layout-hotkeys.json file is updated
|
||||
extern UINT WM_PRIV_CUSTOM_LAYOUTS_FILE_UPDATE; // Scheduled when the watched custom-layouts.json file is updated
|
||||
extern UINT WM_PRIV_SNAP_HOTKEY; // Scheduled when we receive a snap hotkey key down press
|
||||
extern UINT WM_PRIV_QUICK_LAYOUT_KEY; // Scheduled when we receive a key down press to quickly apply a layout
|
||||
extern UINT WM_PRIV_SETTINGS_CHANGED; // Scheduled when the a watched settings file is updated
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "trace.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <FancyZonesLib/FancyZonesData/CustomLayouts.h>
|
||||
#include <FancyZonesLib/FancyZonesData/LayoutHotkeys.h>
|
||||
#include <FancyZonesLib/FancyZonesData/LayoutTemplates.h>
|
||||
|
||||
@@ -579,14 +580,13 @@ namespace JSONHelpers
|
||||
}
|
||||
}
|
||||
|
||||
void SaveZoneSettings(const std::wstring& zonesSettingsFileName, const TDeviceInfoMap& deviceInfoMap, const TCustomZoneSetsMap& customZoneSetsMap)
|
||||
void SaveZoneSettings(const std::wstring& zonesSettingsFileName, const TDeviceInfoMap& deviceInfoMap)
|
||||
{
|
||||
auto before = json::from_file(zonesSettingsFileName);
|
||||
|
||||
json::JsonObject root{};
|
||||
|
||||
root.SetNamedValue(NonLocalizable::DevicesStr, JSONHelpers::SerializeDeviceInfos(deviceInfoMap));
|
||||
root.SetNamedValue(NonLocalizable::CustomZoneSetsStr, JSONHelpers::SerializeCustomZoneSets(customZoneSetsMap));
|
||||
|
||||
if (!before.has_value() || before.value().Stringify() != root.Stringify())
|
||||
{
|
||||
@@ -679,29 +679,6 @@ namespace JSONHelpers
|
||||
return DeviceInfosJSON;
|
||||
}
|
||||
|
||||
TCustomZoneSetsMap ParseCustomZoneSets(const json::JsonObject& fancyZonesDataJSON)
|
||||
{
|
||||
try
|
||||
{
|
||||
TCustomZoneSetsMap customZoneSetsMap{};
|
||||
auto customZoneSets = fancyZonesDataJSON.GetNamedArray(NonLocalizable::CustomZoneSetsStr);
|
||||
|
||||
for (uint32_t i = 0; i < customZoneSets.Size(); ++i)
|
||||
{
|
||||
if (auto zoneSet = CustomZoneSetJSON::FromJson(customZoneSets.GetObjectAt(i)); zoneSet.has_value())
|
||||
{
|
||||
customZoneSetsMap[zoneSet->uuid] = std::move(zoneSet->data);
|
||||
}
|
||||
}
|
||||
|
||||
return std::move(customZoneSetsMap);
|
||||
}
|
||||
catch (const winrt::hresult_error&)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
json::JsonArray SerializeCustomZoneSets(const TCustomZoneSetsMap& customZoneSetsMap)
|
||||
{
|
||||
json::JsonArray customZoneSetsJSON{};
|
||||
@@ -777,4 +754,43 @@ namespace JSONHelpers
|
||||
root.SetNamedValue(NonLocalizable::LayoutTemplatesIds::LayoutTemplatesArrayID, templates);
|
||||
json::to_file(LayoutTemplates::LayoutTemplatesFileName(), root);
|
||||
}
|
||||
|
||||
std::optional<TCustomZoneSetsMap> ParseCustomZoneSets(const json::JsonObject& fancyZonesDataJSON)
|
||||
{
|
||||
try
|
||||
{
|
||||
TCustomZoneSetsMap customZoneSetsMap{};
|
||||
auto customZoneSets = fancyZonesDataJSON.GetNamedArray(NonLocalizable::CustomZoneSetsStr);
|
||||
|
||||
for (uint32_t i = 0; i < customZoneSets.Size(); ++i)
|
||||
{
|
||||
if (auto zoneSet = CustomZoneSetJSON::FromJson(customZoneSets.GetObjectAt(i)); zoneSet.has_value())
|
||||
{
|
||||
customZoneSetsMap[zoneSet->uuid] = std::move(zoneSet->data);
|
||||
}
|
||||
}
|
||||
|
||||
return std::move(customZoneSetsMap);
|
||||
}
|
||||
catch (const winrt::hresult_error& e)
|
||||
{
|
||||
Logger::error(L"Parsing custom layouts error: {}", e.message());
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
void SaveCustomLayouts(const TCustomZoneSetsMap& map)
|
||||
{
|
||||
json::JsonObject root{};
|
||||
json::JsonArray layoutsArray{};
|
||||
|
||||
for (const auto& [uuid, data] : map)
|
||||
{
|
||||
json::JsonObject layoutJson{};
|
||||
layoutsArray.Append(CustomZoneSetJSON::ToJson(CustomZoneSetJSON{ uuid, data }));
|
||||
}
|
||||
|
||||
root.SetNamedValue(NonLocalizable::CustomLayoutsIds::CustomLayoutsArrayID, layoutsArray);
|
||||
json::to_file(CustomLayouts::CustomLayoutsFileName(), root);
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ namespace JSONHelpers
|
||||
struct CustomZoneSetJSON
|
||||
{
|
||||
std::wstring uuid;
|
||||
FancyZonesDataTypes::CustomZoneSetData data;
|
||||
FancyZonesDataTypes::CustomLayoutData data;
|
||||
|
||||
static json::JsonObject ToJson(const CustomZoneSetJSON& device);
|
||||
static std::optional<CustomZoneSetJSON> FromJson(const json::JsonObject& customZoneSet);
|
||||
@@ -66,7 +66,7 @@ namespace JSONHelpers
|
||||
|
||||
using TAppZoneHistoryMap = std::unordered_map<std::wstring, std::vector<FancyZonesDataTypes::AppZoneHistoryData>>;
|
||||
using TDeviceInfoMap = std::unordered_map<FancyZonesDataTypes::DeviceIdData, FancyZonesDataTypes::DeviceInfoData>;
|
||||
using TCustomZoneSetsMap = std::unordered_map<std::wstring, FancyZonesDataTypes::CustomZoneSetData>;
|
||||
using TCustomZoneSetsMap = std::unordered_map<std::wstring, FancyZonesDataTypes::CustomLayoutData>;
|
||||
using TLayoutQuickKeysMap = std::unordered_map<std::wstring, int>;
|
||||
|
||||
struct MonitorInfo
|
||||
@@ -93,7 +93,7 @@ namespace JSONHelpers
|
||||
|
||||
json::JsonObject GetPersistFancyZonesJSON(const std::wstring& zonesSettingsFileName, const std::wstring& appZoneHistoryFileName);
|
||||
|
||||
void SaveZoneSettings(const std::wstring& zonesSettingsFileName, const TDeviceInfoMap& deviceInfoMap, const TCustomZoneSetsMap& customZoneSetsMap);
|
||||
void SaveZoneSettings(const std::wstring& zonesSettingsFileName, const TDeviceInfoMap& deviceInfoMap);
|
||||
void SaveAppZoneHistory(const std::wstring& appZoneHistoryFileName, const TAppZoneHistoryMap& appZoneHistoryMap);
|
||||
|
||||
TAppZoneHistoryMap ParseAppZoneHistory(const json::JsonObject& fancyZonesDataJSON);
|
||||
@@ -102,9 +102,6 @@ namespace JSONHelpers
|
||||
TDeviceInfoMap ParseDeviceInfos(const json::JsonObject& fancyZonesDataJSON);
|
||||
json::JsonArray SerializeDeviceInfos(const TDeviceInfoMap& deviceInfoMap);
|
||||
|
||||
TCustomZoneSetsMap ParseCustomZoneSets(const json::JsonObject& fancyZonesDataJSON);
|
||||
json::JsonArray SerializeCustomZoneSets(const TCustomZoneSetsMap& customZoneSetsMap);
|
||||
|
||||
// replace zones-settings: layout hotkeys
|
||||
std::optional<TLayoutQuickKeysMap> ParseQuickKeys(const json::JsonObject& fancyZonesDataJSON);
|
||||
void SaveLayoutHotkeys(const TLayoutQuickKeysMap& quickKeysMap);
|
||||
@@ -112,4 +109,8 @@ namespace JSONHelpers
|
||||
// replace zones-settings: layout templates
|
||||
std::optional<json::JsonArray> ParseLayoutTemplates(const json::JsonObject& fancyZonesDataJSON);
|
||||
void SaveLayoutTemplates(const json::JsonArray& templates);
|
||||
|
||||
// replace zones-settings: custom layouts
|
||||
std::optional<TCustomZoneSetsMap> ParseCustomZoneSets(const json::JsonObject& fancyZonesDataJSON);
|
||||
void SaveCustomLayouts(const TCustomZoneSetsMap& map);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "ZoneSet.h"
|
||||
|
||||
#include "FancyZonesData.h"
|
||||
#include <FancyZonesLib/FancyZonesData/CustomLayouts.h>
|
||||
#include "FancyZonesDataTypes.h"
|
||||
#include "FancyZonesWindowProperties.h"
|
||||
#include "Settings.h"
|
||||
@@ -880,52 +881,45 @@ bool ZoneSet::CalculateUniquePriorityGridLayout(Rect workArea, int zoneCount, in
|
||||
|
||||
bool ZoneSet::CalculateCustomLayout(Rect workArea, int spacing) noexcept
|
||||
{
|
||||
wil::unique_cotaskmem_string guidStr;
|
||||
if (SUCCEEDED(StringFromCLSID(m_config.Id, &guidStr)))
|
||||
const auto zoneSetSearchResult = CustomLayouts::instance().GetLayout(m_config.Id);
|
||||
if (!zoneSetSearchResult.has_value())
|
||||
{
|
||||
const std::wstring guid = guidStr.get();
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto zoneSetSearchResult = FancyZonesDataInstance().FindCustomZoneSet(guid);
|
||||
|
||||
if (!zoneSetSearchResult.has_value())
|
||||
const auto& zoneSet = *zoneSetSearchResult;
|
||||
if (zoneSet.type == FancyZonesDataTypes::CustomLayoutType::Canvas && std::holds_alternative<FancyZonesDataTypes::CanvasLayoutInfo>(zoneSet.info))
|
||||
{
|
||||
const auto& zoneSetInfo = std::get<FancyZonesDataTypes::CanvasLayoutInfo>(zoneSet.info);
|
||||
for (const auto& zone : zoneSetInfo.zones)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
int x = zone.x;
|
||||
int y = zone.y;
|
||||
int width = zone.width;
|
||||
int height = zone.height;
|
||||
|
||||
const auto& zoneSet = *zoneSetSearchResult;
|
||||
if (zoneSet.type == FancyZonesDataTypes::CustomLayoutType::Canvas && std::holds_alternative<FancyZonesDataTypes::CanvasLayoutInfo>(zoneSet.info))
|
||||
{
|
||||
const auto& zoneSetInfo = std::get<FancyZonesDataTypes::CanvasLayoutInfo>(zoneSet.info);
|
||||
for (const auto& zone : zoneSetInfo.zones)
|
||||
DPIAware::Convert(m_config.Monitor, x, y);
|
||||
DPIAware::Convert(m_config.Monitor, width, height);
|
||||
|
||||
auto zone = MakeZone(RECT{ x, y, x + width, y + height }, m_zones.size());
|
||||
if (zone)
|
||||
{
|
||||
int x = zone.x;
|
||||
int y = zone.y;
|
||||
int width = zone.width;
|
||||
int height = zone.height;
|
||||
|
||||
DPIAware::Convert(m_config.Monitor, x, y);
|
||||
DPIAware::Convert(m_config.Monitor, width, height);
|
||||
|
||||
auto zone = MakeZone(RECT{ x, y, x + width, y + height }, m_zones.size());
|
||||
if (zone)
|
||||
{
|
||||
AddZone(zone);
|
||||
}
|
||||
else
|
||||
{
|
||||
// All zones within zone set should be valid in order to use its functionality.
|
||||
m_zones.clear();
|
||||
return false;
|
||||
}
|
||||
AddZone(zone);
|
||||
}
|
||||
else
|
||||
{
|
||||
// All zones within zone set should be valid in order to use its functionality.
|
||||
m_zones.clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (zoneSet.type == FancyZonesDataTypes::CustomLayoutType::Grid && std::holds_alternative<FancyZonesDataTypes::GridLayoutInfo>(zoneSet.info))
|
||||
{
|
||||
const auto& info = std::get<FancyZonesDataTypes::GridLayoutInfo>(zoneSet.info);
|
||||
return CalculateGridZones(workArea, info, spacing);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (zoneSet.type == FancyZonesDataTypes::CustomLayoutType::Grid && std::holds_alternative<FancyZonesDataTypes::GridLayoutInfo>(zoneSet.info))
|
||||
{
|
||||
const auto& info = std::get<FancyZonesDataTypes::GridLayoutInfo>(zoneSet.info);
|
||||
return CalculateGridZones(workArea, info, spacing);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
#include "FancyZonesLib/ZoneSet.h"
|
||||
#include "FancyZonesLib/Settings.h"
|
||||
#include "FancyZonesLib/FancyZonesData.h"
|
||||
#include "FancyZonesLib/FancyZonesData/CustomLayouts.h"
|
||||
#include "FancyZonesLib/FancyZonesData/LayoutHotkeys.h"
|
||||
#include "FancyZonesLib/FancyZonesDataTypes.h"
|
||||
#include "FancyZonesLib/util.h"
|
||||
|
||||
// Telemetry strings should not be localized.
|
||||
#define LoggingProviderKey "Microsoft.PowerToys"
|
||||
@@ -144,7 +146,7 @@ void Trace::FancyZones::DataChanged() noexcept
|
||||
{
|
||||
const FancyZonesData& data = FancyZonesDataInstance();
|
||||
int appsHistorySize = static_cast<int>(data.GetAppZoneHistoryMap().size());
|
||||
const auto& customZones = data.GetCustomZoneSetsMap();
|
||||
const auto& customZones = CustomLayouts::instance().GetAllLayouts();
|
||||
const auto& devices = data.GetDeviceInfoMap();
|
||||
auto quickKeysCount = LayoutHotkeys::instance().GetHotkeysCount();
|
||||
|
||||
@@ -190,11 +192,15 @@ void Trace::FancyZones::DataChanged() noexcept
|
||||
int zoneCount = -1;
|
||||
if (type == FancyZonesDataTypes::ZoneSetLayoutType::Custom)
|
||||
{
|
||||
const auto& activeCustomZone = customZones.find(device.activeZoneSet.uuid);
|
||||
if (activeCustomZone != customZones.end())
|
||||
auto guid = FancyZonesUtils::GuidFromString(device.activeZoneSet.uuid);
|
||||
if (guid)
|
||||
{
|
||||
zoneCount = getCustomZoneCount(activeCustomZone->second.info);
|
||||
}
|
||||
const auto& activeCustomZone = customZones.find(guid.value());
|
||||
if (activeCustomZone != customZones.end())
|
||||
{
|
||||
zoneCount = getCustomZoneCount(activeCustomZone->second.info);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user