FancyZones and Shortcut Guide initial commit

Co-authored-by: Alexis Campailla <alexis@janeasystems.com>
Co-authored-by: Bret Anderson <bretan@microsoft.com>
Co-authored-by: Enrico Giordani <enrico.giordani@gmail.com>
Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
Co-authored-by: Jeff Bogdan <jeffbog@microsoft.com>
Co-authored-by: March Rogers <marchr@microsoft.com>
Co-authored-by: Mike Harsh <mharsh@microsoft.com>
Co-authored-by: Nachum Bundak <Nachum.Bundak@microsoft.com>
Co-authored-by: Oliver Jones <ojones@microsoft.com>
Co-authored-by: Patrick Little <plittle@microsoft.com>
This commit is contained in:
Bartosz Sosnowski
2019-09-04 18:26:26 +02:00
committed by Bartosz Sosnowski
parent 10c5396099
commit 8431b80e48
341 changed files with 54766 additions and 62 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -0,0 +1,41 @@
# Fancy Zones
Fancy Zones is a window manager that is designed to make it easy to arrange and snap windows into efficient layouts for your workflow and also to restore these layouts quickly. Fancy Zones allows the user to define a set of window locations for a desktop that are drag targets for windows. When the user drags a window into a zone, the windows is resized and repositioned to fill that zone.
![Fancy Zones](FancyZones.png)
To get started with Fancy Zones, you need to enable the utility in Power Toys settings and then invoke the Fancy Zones setup UI. There is a button in settings to invoke this UI, or you can press Win+~ to launch it. When first launched, the UI presents a list of zone layouts that can be quickly adjusted by how many windows are on the monitor. Choosing a layout shows a preview of that layout on the monitor. Pressing the save and close button sets that layout to the monitor.
![Fancy Zones Picker](Picker.png)
The editor also supports creating and saving custom layouts. This functionality is in the “Custom” tab in the editor UI. There are two ways to create custom zone layouts, window layout and table layout. These can also be thought of as additive and subtractive models. The additive window layout model starts with a blank layout and supports adding zones that can be dragged and resized similar to windows.
![Fancy Zones Window Editor Mode](WindowEditor.png)
The subtractive table layout model starts with a table layout and allows zones to be created by splitting and merging zones and then resizing the gutter between zones.
![Fancy Zones Table Editor Mode](TableEditor.png)
The backlog for the utility can be found [here](https://github.com/Microsoft/PowerToys/tree/master/doc/planning/FancyZonesBacklog.md) and the source code is [here](https://github.com/Microsoft/PowerToys/tree/master/src/modules/fancyzones).
## Shortcut Keys
| Shortcut | Action |
| ----------- | ----------- |
| Win + ~ | Launches editor |
| Win+Ctrl+<Number> | Cycles through saved layouts with the coresponding number of zones |
| Win+Left/Right Arrow | Move focused window between zones (only if Override snap hotkeys setting is enabled) |
## Settings
| Setting | Description |
| --------- | ------------- |
| Enable zones while dragging with the shift key | Toggles between auto-snap mode with the shift key disabling snapping during a drag and manual snap mode where pressing the shift key during a drag enables snapping |
| Override Windows Snap hotkeys (win+arrow) to move between zones | Enables or disables Windows Aero Snapping when Fancy Zones is running |
| Keep windows in their zones when the screen resolution changes | After a screen resolution change, if this setting is enabled, Fancy Zones will resize and reposition windows into the zones they were previously in |
| Keep windows in their zones when the active FancyZones layout changes | When this option is on, Fancy Zones will resize and position windows into the new zone layout by maintining the previous zone number location of each window |
| Flash zones when the active FancyZones layout changes | Briefly flash the zone layout when the layout changes or a new virtual desktop is activated |
| Keep windows in their zones when the active virtual desktop changes | If an application is pinned to all virtual desktops, this setting will keep that window in the same zone on all desktops |
| Zone Highlight Color (Default #0078D7) | the color that a zone becomes when it is the active drop target during a window drag
| Use new zone editing experience (Preview) | When enabled, the new editor is enabled. The original editor has better support for multi-mon layouts |
| Move newly created windows to the last known zone | Automatically move a newly opened window into the last zone location that application was in |
![Fancy Zones Settings UI](FancyZonesSettings.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

View File

@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>15.0</VCProjectVersion>
<ProjectGuid>{48804216-2A0E-4168-A6D8-9CD068D14227}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>fancyzones</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>fancyzones</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.190716.2\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.190716.2\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\modules\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\modules\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;FANCYZONES_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<AdditionalIncludeDirectories>..\;..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;..\..\..\..\deps\cpprestsdk\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<AdditionalDependencies>gdiplus.lib;dwmapi.lib;shlwapi.lib;uxtheme.lib;shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<ModuleDefinitionFile>fancyzones.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;FANCYZONES_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<AdditionalIncludeDirectories>..\;..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;..\..\..\..\deps\cpprestsdk\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
<LanguageStandard>stdcpplatest</LanguageStandard>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
<AdditionalDependencies>gdiplus.lib;dwmapi.lib;shlwapi.lib;uxtheme.lib;shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
<ModuleDefinitionFile>fancyzones.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="targetver.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Release|x64'">pch.h</PrecompiledHeaderFile>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\common.vcxproj">
<Project>{74485049-c722-400f-abe5-86ac52d929b3}</Project>
</ProjectReference>
<ProjectReference Include="..\lib\FancyZonesLib.vcxproj">
<Project>{f9c68edf-ac74-4b77-9af1-005d9c9f6a99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="fancyzones.def" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\lib\fancyzones.rc" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.190716.2\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.190716.2\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="targetver.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{21926bf1-03b3-482d-8f60-8bc4fbfc6564}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{2f10207d-d8d1-4a42-8027-8ca597b3cb23}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="fancyzones.def" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\lib\fancyzones.rc" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,53 @@
# Introduction
Super FancyZones! Just like FancyZones except super.
# Getting Started
Grab FancyZones.exe from the bin directory or \\\\wexfs\users\bretan\proto\superfancyzones and run it
## Dragging windows
* While dragging a window around, zones will appear that the window can be dropped in. Dropping the window in a zone will position it in the zone.
* While dragging a window, you can hit number keys to cycle the active zone set. Eg, drag a window around and hit the '3' key will change to a zone set with 3 zones.
## Hotkeys
* Win+~ - opens the zone viewer/editor
* Win+ctrl+number - cycle through layouts with the corresponding number of zones (only if Override snap hotkeys setting is enabled)
* Win+left arrow, Win+right arrow - move foreground window between zones
## Zone Viewer/Editor (Win + ~)
* Hitting a number key cycles through layouts matching the number of zones (eg 3 cycles through layouts with 3 zones)
* R resets the focused monitor back to defaults
* C clears the current layout so you can start fresh
* W opens a dialog to choose wallpaper
* Left click moves the zone to the top
* Right click moves the zone to the bottom
### E enters editor mode (hit E or Escape to exit editor mode)
* Left/Right/Up/Down arrows adjust the grid spacing
* PgUp/PgDn adjust grid margins
* Ctrl+left click splits the clicked zone in half horizontally
* Ctrl+right click splits the clicked zone in half vertically
# Options
### Default Drag Mode
* None - don't do anything when dragging windows around (shift enters normal mode, ctrl enters adjusted mode)
* Normal - show zones when dragging windows around (shift disables, ctrl enters adjusted mode)
* Adjusted - show zones when dragging windows around with an accelerated cursor
### Display change
* Move windows - automatically move windows around when display changes
### Virtual Desktop change
* Move windows - automatically move windows around when virtual desktop changes
* Change wallpaper - use custom wallpaper per-monitor per-desktop
* Flash zones - flash zones on each monitor
### Miscellanious
* Override snap hotkeys - steal hotkeys normally used by shell (win+left/right, win+ctrl+num)
* Colorful zones - use colored zones in zone viewer
# Known issues
* See open bugs for full list of issues
* Win+left and Win+right don't move between monitors
* If you use Virtual Desktops, make sure to perform at least one virtual desktop switch before launching the fancyzones (it relies on a volatile regkey that explorer writes)
* Sometimes you have to click on a zone viewer window before it gets keyboard focus when opening views with Win+~
* Quickly switching virtual desktops with win+ctrl+arrow hotkeys can crash fancyzones

View File

@@ -0,0 +1,319 @@
#include "pch.h"
#include <common/settings_objects.h>
#include <interface/powertoy_module_interface.h>
#include <interface/lowlevel_keyboard_event_data.h>
#include <interface/win_hook_event_data.h>
#include <lib/ZoneSet.h>
#include <lib/RegistryHelpers.h>
extern "C" IMAGE_DOS_HEADER __ImageBase;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
// TODO: multimon support, need to pass the HMONITOR from the editor to here instead
// of using MonitorFromPoint
// This function is exported and called from FancyZonesEditor.exe to save a layout from the editor.
STDAPI PersistZoneSet(
PCWSTR activeKey, // Registry key holding ActiveZoneSet
PCWSTR resolutionKey, // Registry key for screen resolution
WORD layoutId, // LayoutModel Id
int zoneCount, // Number of zones in zones
int zones[]) // Array of zones serialized in left/top/right/bottom chunks
{
// See if we have already persisted this layout we can update.
UUID id{GUID_NULL};
if (wil::unique_hkey key{ RegistryHelpers::OpenKey(resolutionKey) })
{
ZoneSetPersistedData data{};
DWORD dataSize = sizeof(data);
wchar_t value[256]{};
DWORD valueLength = ARRAYSIZE(value);
DWORD i = 0;
while (RegEnumValueW(key.get(), i++, value, &valueLength, nullptr, nullptr, reinterpret_cast<BYTE*>(&data), &dataSize) == ERROR_SUCCESS)
{
if (data.LayoutId == layoutId)
{
if (data.ZoneCount == zoneCount)
{
CLSIDFromString(value, &id);
break;
}
}
valueLength = ARRAYSIZE(value);
dataSize = sizeof(data);
}
}
if (id == GUID_NULL)
{
// No existing layout found so let's create a new one.
UuidCreate(&id);
}
if (id != GUID_NULL)
{
winrt::com_ptr<IZoneSet> zoneSet = MakeZoneSet(
ZoneSetConfig(
id,
layoutId,
MonitorFromPoint({}, MONITOR_DEFAULTTOPRIMARY),
resolutionKey,
ZoneSetLayout::Custom,
0, 0, 0));
for (int i = 0; i < zoneCount; i++)
{
const int baseIndex = i * 4;
const int left = zones[baseIndex];
const int top = zones[baseIndex+1];
const int right = zones[baseIndex+2];
const int bottom = zones[baseIndex+3];
zoneSet->AddZone(MakeZone({ left, top, right, bottom }), false);
}
zoneSet->Save();
wil::unique_cotaskmem_string zoneSetId;
if (SUCCEEDED_LOG(StringFromCLSID(id, &zoneSetId)))
{
RegistryHelpers::SetString(activeKey, L"ActiveZoneSetId", zoneSetId.get());
}
return S_OK;
}
return E_FAIL;
}
class FancyZonesModule : public PowertoyModuleIface
{
public:
// Return the display name of the powertoy, this will be cached
virtual PCWSTR get_name() override
{
return L"FancyZones";
}
// Return array of the names of all events that this powertoy listens for, with
// nullptr as the last element of the array. Nullptr can also be retured for empty list.
virtual PCWSTR* get_events() override
{
static PCWSTR events[] = { ll_keyboard, win_hook_event, nullptr };
return events;
}
// Return JSON with the configuration options.
// These are the settings shown on the settings page along with their current values.
virtual bool get_config(_Out_ PWSTR buffer, _Out_ int *buffer_size) override
{
return m_settings->GetConfig(buffer, buffer_size);
}
// Passes JSON with the configuration settings for the powertoy.
// This is called when the user hits Save on the settings page.
virtual void set_config(PCWSTR config) override
{
m_settings->SetConfig(config);
}
// Signal from the Settings editor to call a custom action.
// This can be used to spawn more complex editors.
virtual void call_custom_action(const wchar_t* action) override
{
m_settings->CallCustomAction(action);
}
// Enable the powertoy
virtual void enable()
{
if (!m_app)
{
Trace::FancyZones::EnableFancyZones(true);
m_app = MakeFancyZones(reinterpret_cast<HINSTANCE>(&__ImageBase), m_settings.get());
if (m_app)
{
m_app->Run();
}
}
}
// Disable the powertoy
virtual void disable()
{
if (m_app)
{
Trace::FancyZones::EnableFancyZones(false);
m_app->Destroy();
m_app = nullptr;
}
}
// Returns if the powertoy is enabled
virtual bool is_enabled() override
{
return (m_app != nullptr);
}
// Handle incoming event, data is event-specific
virtual intptr_t signal_event(const wchar_t* name, intptr_t data) override
{
if (m_app)
{
if (wcscmp(name, ll_keyboard) == 0)
{
// Return 1 if the keypress is to be suppressed (not forwarded to Windows), otherwise return 0.
return HandleKeyboardHookEvent(reinterpret_cast<LowlevelKeyboardEvent*>(data));
}
else if (wcscmp(name, win_hook_event) == 0)
{
// Return value is ignored
HandleWinHookEvent(reinterpret_cast<WinHookEvent*>(data));
}
}
return 0;
}
// Destroy the powertoy and free memory
virtual void destroy() override
{
disable();
delete this;
}
FancyZonesModule()
{
m_settings = MakeFancyZonesSettings(reinterpret_cast<HINSTANCE>(&__ImageBase), get_name());
}
private:
static bool IsInterestingWindow(HWND window)
{
auto style = GetWindowLongPtr(window, GWL_STYLE);
auto exStyle = GetWindowLongPtr(window, GWL_EXSTYLE);
return WI_IsFlagSet(style, WS_MAXIMIZEBOX) && WI_IsFlagClear(style, WS_CHILD) && WI_IsFlagClear(exStyle, WS_EX_TOOLWINDOW);
}
intptr_t HandleKeyboardHookEvent(LowlevelKeyboardEvent* data) noexcept;
void HandleWinHookEvent(WinHookEvent* data) noexcept;
void MoveSizeStart(HWND window, POINT const& ptScreen) noexcept;
void MoveSizeEnd(HWND window, POINT const& ptScreen) noexcept;
void MoveSizeUpdate(POINT const& ptScreen) noexcept;
winrt::com_ptr<IFancyZones> m_app;
winrt::com_ptr<IFancyZonesSettings> m_settings;
};
intptr_t FancyZonesModule::HandleKeyboardHookEvent(LowlevelKeyboardEvent* data) noexcept
{
if (data->wParam == WM_KEYDOWN)
{
return m_app.as<IFancyZonesCallback>()->OnKeyDown(data->lParam) ? 1 : 0;
}
return 0;
}
void FancyZonesModule::HandleWinHookEvent(WinHookEvent* data) noexcept
{
POINT ptScreen;
GetPhysicalCursorPos(&ptScreen);
switch (data->event)
{
case EVENT_SYSTEM_MOVESIZESTART:
{
MoveSizeStart(data->hwnd, ptScreen);
}
break;
case EVENT_SYSTEM_MOVESIZEEND:
{
MoveSizeEnd(data->hwnd, ptScreen);
}
break;
case EVENT_OBJECT_LOCATIONCHANGE:
{
if (m_app.as<IFancyZonesCallback>()->InMoveSize())
{
MoveSizeUpdate(ptScreen);
}
}
break;
case EVENT_OBJECT_NAMECHANGE:
{
// The accessibility name of the desktop window changes whenever the user
// switches virtual desktops.
if (data->hwnd == GetDesktopWindow())
{
Trace::VirtualDesktopChanged();
m_app.as<IFancyZonesCallback>()->VirtualDesktopChanged();
}
}
break;
case EVENT_OBJECT_CREATE:
{
if (data->idObject == OBJID_WINDOW)
{
if (IsInterestingWindow(data->hwnd))
{
m_app.as<IFancyZonesCallback>()->WindowCreated(data->hwnd);
}
}
}
break;
default:
break;
}
}
void FancyZonesModule::MoveSizeStart(HWND window, POINT const& ptScreen) noexcept
{
if (IsInterestingWindow(window))
{
if (auto monitor = MonitorFromPoint(ptScreen, MONITOR_DEFAULTTONULL))
{
m_app.as<IFancyZonesCallback>()->MoveSizeStart(window, monitor, ptScreen);
}
}
}
void FancyZonesModule::MoveSizeEnd(HWND window, POINT const& ptScreen) noexcept
{
if (IsInterestingWindow(window))
{
m_app.as<IFancyZonesCallback>()->MoveSizeEnd(window, ptScreen);
}
}
void FancyZonesModule::MoveSizeUpdate(POINT const& ptScreen) noexcept
{
if (auto monitor = MonitorFromPoint(ptScreen, MONITOR_DEFAULTTONULL))
{
m_app.as<IFancyZonesCallback>()->MoveSizeUpdate(monitor, ptScreen);
}
}
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new FancyZonesModule();
}

View File

@@ -0,0 +1,4 @@
LIBRARY fancyzones.dll
EXPORTS
PersistZoneSet PRIVATE

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.190716.2" targetFramework="native" />
</packages>

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,18 @@
#pragma once
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#include <Unknwn.h>
#include <winrt/base.h>
#include <ProjectTelemetry.h>
#include <TraceLoggingActivity.h>
#include <wil\common.h>
#include <wil\result.h>
#include "lib\trace.h"
#include "lib\FancyZones.h"
#include "lib\Settings.h"
namespace winrt
{
using namespace ::winrt;
}

Binary file not shown.

View File

@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27703.2042
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FancyZonesEditor", "FancyZonesEditor\FancyZonesEditor.csproj", "{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9A90742C-8AA0-4A56-A01F-70C8754B4684}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
</startup>
</configuration>

View File

@@ -0,0 +1,17 @@
<Application x:Class="FancyZonesEditor.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FancyZonesEditor"
Startup="OnStartup">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- MahApps.Metro resource dictionaries. Make sure that all file names are Case Sensitive! -->
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
<!-- Accent and AppTheme setting -->
<ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,74 @@
using FancyZonesEditor;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using FancyZonesEditor.Models;
namespace FancyZonesEditor
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public Settings ZoneSettings { get { return _settings; } }
private Settings _settings;
private ushort _idInitial = 0;
public App()
{
//init settings
_settings = new Settings();
}
private void OnStartup(object sender, StartupEventArgs e)
{
if (e.Args.Length > 1)
{
UInt16.TryParse(e.Args[1], out _idInitial);
}
LayoutModel foundModel = null;
if (_idInitial != 0)
{
foreach (LayoutModel model in _settings.DefaultModels)
{
if (model.Id == _idInitial)
{
// found match
foundModel = model;
break;
}
}
if (foundModel == null)
{
foreach (LayoutModel model in _settings.CustomModels)
{
if (model.Id == _idInitial)
{
// found match
foundModel = model;
break;
}
}
}
}
if (foundModel == null)
{
foundModel = _settings.DefaultModels[0];
}
foundModel.IsSelected = true;
// TODO: multimon
// Pass in the correct args to show on the desired monitor
EditorOverlay overlay = new EditorOverlay();
overlay.Show();
overlay.DataContext = foundModel;
}
}
}

View File

@@ -0,0 +1,12 @@
<UserControl x:Class="FancyZonesEditor.CanvasEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyZonesEditor"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Canvas x:Name="Preview"/>
</Grid>
</UserControl>

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using FancyZonesEditor.Models;
namespace FancyZonesEditor
{
/// <summary>
/// Interaction logic for CanvasEditor.xaml
/// </summary>
public partial class CanvasEditor : UserControl
{
public CanvasEditor()
{
InitializeComponent();
Loaded += CanvasEditor_Loaded;
}
private void CanvasEditor_Loaded(object sender, RoutedEventArgs e)
{
CanvasLayoutModel model = (CanvasLayoutModel)DataContext;
if (model != null)
{
Model = model;
UpdateZoneRects();
model.PropertyChanged += OnModelChanged;
}
}
private void OnModelChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "Zones")
{
UpdateZoneRects();
}
}
private void UpdateZoneRects()
{
UIElementCollection previewChildren = Preview.Children;
int previewChildrenCount = previewChildren.Count;
while (previewChildrenCount < Model.Zones.Count)
{
CanvasZone zone = new CanvasZone();
zone.Model = Model;
Preview.Children.Add(zone);
previewChildrenCount++;
}
for (int i = 0; i < previewChildrenCount; i++)
{
Int32Rect rect = Model.Zones[i];
CanvasZone zone = previewChildren[i] as CanvasZone;
zone.ZoneIndex = i;
Canvas.SetLeft(zone, rect.X);
Canvas.SetTop(zone, rect.Y);
zone.MinHeight = rect.Height;
zone.MinWidth = rect.Width;
}
}
public CanvasLayoutModel Model;
}
}

View File

@@ -0,0 +1,189 @@
<local:EditorWindow x:Class="FancyZonesEditor.CanvasEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:local="clr-namespace:FancyZonesEditor"
mc:Ignorable="d"
Title=""
Width="528"
SizeToContent="Height"
Background="White"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
Closed="OnClosed">
<Window.Resources>
<Style x:Key="titleText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="LineHeight" Value="24" />
<Setter Property="FontSize" Value="18"/>
<Setter Property="Margin" Value="16,16,0,12" />
</Style>
<Style x:Key="zoneCount" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="Regular" />
<Setter Property="FontSize" Value="24"/>
<Setter Property="LineHeight" Value="24" />
<Setter Property="Margin" Value="20,0,20,0" />
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="tabText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="#C4C4C4"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="LineHeight" Value="20" />
<Setter Property="Margin" Value="24,20,0,0" />
<Setter Property="TextAlignment" Value="Center" />
</Style>
<Style x:Key="settingText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="LineHeight" Value="20" />
<Setter Property="Margin" Value="5,10,0,0" />
<Setter Property="TextAlignment" Value="Left" />
</Style>
<Style x:Key="templateTitleText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="Regular" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="0,20,0,0" />
<Setter Property="TextAlignment" Value="Center" />
<Setter Property="Height" Value="30"/>
<Setter Property="Width" Value="250"/>
<Setter Property="VerticalAlignment" Value="Top"/>
</Style>
<Style x:Key="secondaryButton" TargetType="Button">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="0,5,0,5"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="#F2F2F2"/>
<Setter Property="Margin" Value="16,10,0,0" />
<Setter Property="Width" Value="239"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#BCBCBD"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="primaryButton" TargetType="Button">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="0,5,0,5"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="#0078D7"/>
<Setter Property="Margin" Value="16,10,0,0" />
<Setter Property="Width" Value="239"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#024D89"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="spinnerButton" TargetType="Button">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="24"/>
<Setter Property="Padding" Value="0,0,0,5"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="#F2F2F2"/>
<Setter Property="Margin" Value="0,0,0,0" />
</Style>
<Style x:Key="templateBackground" TargetType="Rectangle">
<Setter Property="Fill" Value="Black"/>
<Setter Property="Opacity" Value="0.05"/>
<Setter Property="RadiusX" Value="4"/>
<Setter Property="RadiusY" Value="4"/>
</Style>
<Style x:Key="templateBackgroundSelected" TargetType="Rectangle">
<Setter Property="Fill" Value="#F2F2F2"/>
<Setter Property="Opacity" Value="1"/>
<Setter Property="RadiusX" Value="4"/>
<Setter Property="RadiusY" Value="4"/>
<Setter Property="Stroke" Value="#0078D7"/>
<Setter Property="StrokeThickness" Value="2"/>
</Style>
<Style x:Key="newZoneButton" TargetType="Button">
<Setter Property="Background" Value="#f2f2f2"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontFamily" Value="SegoeUI"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="FontSize" Value="120"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="textLabel" TargetType="TextBlock">
<Setter Property="FontFamily" Value="SegoeUI"/>
<Setter Property="FontWeight" Value="Regular"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Margin" Value="16,12,0,0"/>
</Style>
<Style x:Key="textBox" TargetType="TextBox">
<Setter Property="BorderBrush" Value="#cccccc"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontFamily" Value="SegoeUI"/>
<Setter Property="FontWeight" Value="Regular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="0,5,0,0"/>
<Setter Property="Padding" Value="5,5,5,5"/>
</Style>
</Window.Resources>
<StackPanel>
<TextBlock Name="windowEditorDialogTitle" Text="Custom layout creator" Style="{StaticResource titleText}" />
<Button x:Name="newZoneButton" Width="496" Height="136" Style="{StaticResource newZoneButton}" Click="OnAddZone">
<StackPanel>
<TextBlock x:Name="newSoneName" Text="Add new zone" Style="{StaticResource templateTitleText}" />
<TextBlock HorizontalAlignment="Center" Text="+" Margin="0,-40,0,0"/>
</StackPanel>
</Button>
<TextBlock Text="Name" Style="{StaticResource textLabel}" />
<TextBox Text="{Binding Name}" Width="496" Style="{StaticResource textBox}" />
<!--
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<CheckBox x:Name="showGridSetting" VerticalAlignment="Center" HorizontalAlignment="Center" IsChecked="True" Margin="16,4,0,0"/>
<TextBlock Text="Show snap grid" Style="{StaticResource settingText}" />
<TextBlock Text="Grid spacing" Style="{StaticResource settingText}" Margin="40,10,10,0" />
<TextBox x:Name="gridValue" Text="{Binding Path=Spacing,Mode=TwoWay}" Style="{StaticResource textBox}"/>
</StackPanel>
-->
<StackPanel Orientation="Horizontal" Margin="0,12,0,16">
<Button Content="Cancel" Style="{StaticResource secondaryButton}" Click="OnCancel"/>
<Button Content="Save and apply" Style="{StaticResource primaryButton}" Click="OnSaveApplyTemplate" />
</StackPanel>
</StackPanel>
</local:EditorWindow>

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using MahApps.Metro.Controls;
using FancyZonesEditor.Models;
namespace FancyZonesEditor
{
/// <summary>
/// Interaction logic for windowEditor.xaml
/// </summary>
public partial class CanvasEditorWindow : EditorWindow
{
public CanvasEditorWindow()
{
InitializeComponent();
Model = EditorOverlay.Current.DataContext as CanvasLayoutModel;
}
private void OnAddZone(object sender, RoutedEventArgs e)
{
Model.AddZone(new Int32Rect(_offset, _offset, (int) (Model.ReferenceWidth * 0.6), (int) (Model.ReferenceHeight * 0.6)));
_offset += 100;
}
private int _offset = 100;
private CanvasLayoutModel Model;
}
}

View File

@@ -0,0 +1,44 @@
<UserControl x:Class="FancyZonesEditor.CanvasZone"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyZonesEditor"
mc:Ignorable="d"
Background="LightGray"
Opacity="0.75"
d:DesignHeight="450" d:DesignWidth="800">
<Grid x:Name="Frame">
<Grid.RowDefinitions>
<RowDefinition Height="8"/>
<RowDefinition Height="16"/>
<RowDefinition Height="24"/>
<RowDefinition Height="*"/>
<RowDefinition Height="16"/>
<RowDefinition Height="8"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="8"/>
</Grid.ColumnDefinitions>
<Thumb x:Name="NWResize" Cursor="SizeNWSE" Background="Black" Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2" DragDelta="NWResize_DragDelta"/>
<Thumb x:Name="NEResize" Cursor="SizeNESW" Background="Black" Grid.Row="0" Grid.Column="3" Grid.RowSpan="2" Grid.ColumnSpan="2" DragDelta="NEResize_DragDelta"/>
<Thumb x:Name="SWResize" Cursor="SizeNESW" Background="Black" Grid.Row="4" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2" DragDelta="SWResize_DragDelta"/>
<Thumb x:Name="SEResize" Cursor="SizeNWSE" Background="Black" Grid.Row="4" Grid.Column="3" Grid.RowSpan="2" Grid.ColumnSpan="2" DragDelta="SEResize_DragDelta"/>
<Thumb x:Name="NResize" Cursor="SizeNS" Background="Black" Margin="1,0,1,0" Grid.Row="0" Grid.Column="2" DragDelta="NResize_DragDelta"/>
<Thumb x:Name="SResize" Cursor="SizeNS" Background="Black" Margin="1,0,1,0" Grid.Row="5" Grid.Column="2" DragDelta="SResize_DragDelta"/>
<Thumb x:Name="WResize" Cursor="SizeWE" Background="Black" Margin="0,1,0,1" Grid.Row="2" Grid.Column="0" Grid.RowSpan="2" DragDelta="WResize_DragDelta"/>
<Thumb x:Name="EResize" Cursor="SizeWE" Background="Black" Margin="0,1,0,1" Grid.Row="2" Grid.Column="4" Grid.RowSpan="2" DragDelta="EResize_DragDelta"/>
<DockPanel Grid.Row="1" Grid.Column="1" Grid.RowSpan="2" Grid.ColumnSpan="3">
<Button DockPanel.Dock="Right" Padding="8,0" Click="OnClose">
<Image Source="images/ChromeClose.png" Height="24" Width="24" />
</Button>
<Thumb x:Name="Caption" Cursor="SizeAll" Background="DarkGray" DragDelta="Caption_DragDelta"/>
</DockPanel>
<Rectangle Fill="LightGray" Grid.Row="3" Grid.Column="1" Grid.RowSpan="2" Grid.ColumnSpan="3"/>
<Canvas x:Name="Body" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using FancyZonesEditor.Models;
namespace FancyZonesEditor
{
/// <summary>
/// Once you've "Committ"ed the starter grid, then the Zones within the grid come to life for you to be able to further subdivide them
/// using splitters
/// </summary>
public partial class CanvasZone : UserControl
{
public CanvasZone()
{
InitializeComponent();
Canvas.SetZIndex(this, c_zIndex++);
}
public CanvasLayoutModel Model;
public int ZoneIndex;
private Point Move(double xDelta, double yDelta)
{
Int32Rect rect = Model.Zones[ZoneIndex];
if (xDelta < 0)
{
xDelta = Math.Max(xDelta, -rect.X);
}
else if (xDelta > 0)
{
xDelta = Math.Min(xDelta, c_workArea.Width - rect.Width - rect.X);
}
if (yDelta < 0)
{
yDelta = Math.Max(yDelta, -rect.Y);
}
else if (yDelta > 0)
{
yDelta = Math.Min(yDelta, c_workArea.Height - rect.Height - rect.Y);
}
rect.X += (int) xDelta;
rect.Y += (int) yDelta;
Canvas.SetLeft(this, rect.X);
Canvas.SetTop(this, rect.Y);
Model.Zones[ZoneIndex] = rect;
return new Point(xDelta, yDelta);
}
private void Size(double xDelta, double yDelta)
{
Int32Rect rect = Model.Zones[ZoneIndex];
if (xDelta != 0)
{
int newWidth = rect.Width + (int) xDelta;
if (newWidth < 48)
{
newWidth = 48;
}
else if (newWidth > (c_workArea.Width - rect.X))
{
newWidth = (int) c_workArea.Width - rect.X;
}
MinWidth = rect.Width = newWidth;
}
if (yDelta != 0)
{
int newHeight = rect.Height + (int)yDelta;
if (newHeight < 48)
{
newHeight = 48;
}
else if (newHeight > (c_workArea.Height - rect.Y))
{
newHeight = (int)c_workArea.Height - rect.Y;
}
MinHeight = rect.Height = newHeight;
}
Model.Zones[ZoneIndex] = rect;
}
private static int c_zIndex = 0;
// TODO: multimon
// This needs to be the work area of the monitor we get launched on
private static Rect c_workArea = System.Windows.SystemParameters.WorkArea;
protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
{
Canvas.SetZIndex(this, c_zIndex++);
base.OnPreviewMouseDown(e);
}
private void NWResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
Point actualChange = Move(e.HorizontalChange, e.VerticalChange);
Size(-actualChange.X, -actualChange.Y);
}
private void NEResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
Point actualChange = Move(0, e.VerticalChange);
Size(e.HorizontalChange, -actualChange.Y);
}
private void SWResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
Point actualChange = Move(e.HorizontalChange, 0);
Size(-actualChange.X, e.VerticalChange);
}
private void SEResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
Size(e.HorizontalChange, e.VerticalChange);
}
private void NResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
Point actualChange = Move(0, e.VerticalChange);
Size(0, -actualChange.Y);
}
private void SResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
Size(0, e.VerticalChange);
}
private void WResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
Point actualChange = Move(e.HorizontalChange, 0);
Size(-actualChange.X, 0);
}
private void EResize_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
Size(e.HorizontalChange, 0);
}
private void Caption_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
Move(e.HorizontalChange, e.VerticalChange);
}
private void OnClose(object sender, RoutedEventArgs e)
{
((Panel)Parent).Children.Remove(this);
Model.RemoveZoneAt(ZoneIndex);
}
}
}

View File

@@ -0,0 +1,15 @@
<Window x:Class="FancyZonesEditor.EditorOverlay"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FancyZonesEditor"
mc:Ignorable="d"
Title="Window1" Height="450" Width="800"
ShowInTaskbar="False"
ResizeMode="NoResize"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
Loaded="onLoad"/>

View File

@@ -0,0 +1,138 @@
using FancyZonesEditor.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.NetworkInformation;
using System.Security.RightsManagement;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace FancyZonesEditor
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class EditorOverlay : Window
{
public Int32Rect[] GetZoneRects()
{
// TODO: the ideal here is that the ArrangeRects logic is entirely inside the model, so we don't have to walk the UIElement children to get the rect info
Panel previewPanel = null;
if (_editor != null)
{
GridEditor gridEditor = _editor as GridEditor;
if (gridEditor != null)
{
previewPanel = gridEditor.PreviewPanel;
}
else
{
//CanvasEditor
previewPanel = ((CanvasEditor)_editor).Preview;
}
}
else
{
previewPanel = _layoutPreview.PreviewPanel;
}
var count = previewPanel.Children.Count;
Int32Rect[] zones = new Int32Rect[count];
int i = 0;
foreach (FrameworkElement child in previewPanel.Children)
{
Point topLeft = child.TransformToAncestor(previewPanel).Transform(new Point());
var right = topLeft.X + child.ActualWidth;
var bottom = topLeft.Y + child.ActualHeight;
zones[i].X = (int)topLeft.X;
zones[i].Y = (int)topLeft.Y;
zones[i].Width = (int)child.ActualWidth;
zones[i].Height = (int)child.ActualHeight;
i++;
}
return zones;
}
public static EditorOverlay Current;
public EditorOverlay()
{
InitializeComponent();
Current = this;
// TODO: multimon
// Need to set Left and Top to the correct monitor based on the
// foreground window passed in the command line arguments
Rect workArea = System.Windows.SystemParameters.WorkArea;
Left = workArea.Left;
Top = workArea.Top;
Width = workArea.Width;
Height = workArea.Height;
}
void onLoad(object sender, RoutedEventArgs e)
{
ShowLayoutPicker();
}
public void ShowLayoutPicker()
{
DataContext = null;
_editor = null;
_layoutPreview = new LayoutPreview();
_layoutPreview.IsActualSize = true;
_layoutPreview.Opacity = 0.5;
Content = _layoutPreview;
MainWindow window = new MainWindow();
window.Owner = this;
window.Show();
}
// These event handlers are used to track the current state of the Shift and Ctrl keys on the keyboard
// They reflect that current state into properties on the Settings object, which the Zone view will listen to in editing mode
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
_settings.IsShiftKeyPressed = Keyboard.Modifiers.HasFlag(ModifierKeys.Shift);
_settings.IsCtrlKeyPressed = Keyboard.Modifiers.HasFlag(ModifierKeys.Control);
base.OnPreviewKeyDown(e);
}
protected override void OnPreviewKeyUp(KeyEventArgs e)
{
_settings.IsShiftKeyPressed = Keyboard.Modifiers.HasFlag(ModifierKeys.Shift);
_settings.IsCtrlKeyPressed = Keyboard.Modifiers.HasFlag(ModifierKeys.Control);
base.OnPreviewKeyUp(e);
}
public void Edit()
{
_layoutPreview = null;
if (DataContext is GridLayoutModel)
{
_editor = new GridEditor();
}
else if (DataContext is CanvasLayoutModel)
{
_editor = new CanvasEditor();
}
Content = _editor;
}
private Settings _settings = ((App)Application.Current).ZoneSettings;
private LayoutPreview _layoutPreview;
private UserControl _editor;
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using FancyZonesEditor.Models;
using MahApps.Metro.Controls;
namespace FancyZonesEditor
{
public class EditorWindow : MetroWindow
{
protected void OnSaveApplyTemplate(object sender, RoutedEventArgs e)
{
EditorOverlay mainEditor = EditorOverlay.Current;
LayoutModel model = mainEditor.DataContext as LayoutModel;
if (model != null)
{
model.Persist(mainEditor.GetZoneRects());
}
_choosing = true;
this.Close();
EditorOverlay.Current.Close();
}
protected void OnClosed(object sender, EventArgs e)
{
if (!_choosing)
{
EditorOverlay.Current.ShowLayoutPicker();
}
}
protected void OnCancel(object sender, RoutedEventArgs e)
{
_choosing = true;
this.Close();
EditorOverlay.Current.ShowLayoutPicker();
}
private bool _choosing = false;
}
}

View File

@@ -0,0 +1,193 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>FancyZonesEditor</RootNamespace>
<AssemblyName>FancyZonesEditor</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<TargetFrameworkProfile />
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
<PlatformTarget>x64</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\..\..\..\..\x64\Debug\modules\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
<PlatformTarget>x64</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\modules\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Configuration" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="CanvasEditor.xaml.cs">
<DependentUpon>CanvasEditor.xaml</DependentUpon>
</Compile>
<Compile Include="EditorWindow.cs" />
<Compile Include="GridEditor.xaml.cs">
<DependentUpon>GridEditor.xaml</DependentUpon>
</Compile>
<Compile Include="GridResizer.xaml.cs">
<DependentUpon>GridResizer.xaml</DependentUpon>
</Compile>
<Compile Include="LayoutPreview.xaml.cs">
<DependentUpon>LayoutPreview.xaml</DependentUpon>
</Compile>
<Compile Include="Models\CanvasLayoutModel.cs" />
<Compile Include="Models\GridLayoutModel.cs" />
<Compile Include="Models\LayoutModel.cs" />
<Compile Include="Models\Settings.cs" />
<Compile Include="EditorOverlay.xaml.cs">
<DependentUpon>EditorOverlay.xaml</DependentUpon>
</Compile>
<Compile Include="RowColInfo.cs" />
<Compile Include="GridEditorWindow.xaml.cs">
<DependentUpon>GridEditorWindow.xaml</DependentUpon>
</Compile>
<Compile Include="WindowLayout.xaml.cs">
<DependentUpon>WindowLayout.xaml</DependentUpon>
</Compile>
<Compile Include="CanvasEditorWindow.xaml.cs">
<DependentUpon>CanvasEditorWindow.xaml</DependentUpon>
</Compile>
<Compile Include="CanvasZone.xaml.cs">
<DependentUpon>CanvasZone.xaml</DependentUpon>
</Compile>
<Compile Include="GridZone.xaml.cs">
<DependentUpon>GridZone.xaml</DependentUpon>
</Compile>
<Page Include="CanvasEditor.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="GridEditor.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="GridResizer.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="LayoutPreview.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Page Include="EditorOverlay.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="GridEditorWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="WindowLayout.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="CanvasEditorWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="CanvasZone.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="GridZone.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MahApps.Metro">
<Version>2.0.0-alpha0455</Version>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Resource Include="images\ChromeClose.png" />
<Resource Include="images\Delete.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="images\Merge.png" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@@ -0,0 +1,35 @@
<UserControl x:Class="FancyZonesEditor.GridEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyZonesEditor"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<Style TargetType="Button">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="#F2F2F2"/>
<Setter Property="Width" Value="150"/>
</Style>
</UserControl.Resources>
<Grid>
<Canvas x:Name="Preview"/>
<Canvas x:Name="AdornerLayer"/>
<Canvas x:Name="MergePanel" Visibility="Collapsed" MouseUp="MergePanelMouseUp">
<StackPanel x:Name="MergeButtons" Background="Gray" Orientation="Horizontal">
<Button Click="MergeClick" Margin="0" Height="36" Width="134">
<StackPanel Orientation="Horizontal">
<Image Source="images/Merge.png" Margin="0,0,12,0" Height="16" HorizontalAlignment="Left" />
<TextBlock Text="Merge zones"/>
</StackPanel>
</Button>
</StackPanel>
</Canvas>
</Grid>
</UserControl>

View File

@@ -0,0 +1,911 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using FancyZonesEditor.Models;
namespace FancyZonesEditor
{
/// <summary>
/// GridEditor is how you tweak an initial GridLayoutModel before saving
/// </summary>
public partial class GridEditor : UserControl
{
public static readonly DependencyProperty ModelProperty = DependencyProperty.Register("Model", typeof(GridLayoutModel), typeof(GridEditor), new PropertyMetadata(null, OnGridDimensionsChanged));
public GridEditor()
{
InitializeComponent();
Loaded += GridEditor_Loaded;
((App)Application.Current).ZoneSettings.PropertyChanged += ZoneSettings_PropertyChanged;
}
private void GridEditor_Loaded(object sender, RoutedEventArgs e)
{
GridLayoutModel model = (GridLayoutModel)DataContext;
if (model != null)
{
int rows = model.Rows;
int cols = model.Columns;
_rowInfo = new RowColInfo[rows];
for (int row = 0; row < rows; row++)
{
_rowInfo[row] = new RowColInfo(model.RowPercents[row]);
}
_colInfo = new RowColInfo[cols];
for (int col = 0; col < cols; col++)
{
_colInfo[col] = new RowColInfo(model.ColumnPercents[col]);
}
int maxIndex = 0;
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
maxIndex = Math.Max(maxIndex, model.CellChildMap[row, col]);
}
}
for (int i = 0; i <= maxIndex; i++)
{
AddZone();
}
}
Model = model;
if (Model == null)
{
Model = new GridLayoutModel();
DataContext = Model;
}
Model.PropertyChanged += OnGridDimensionsChanged;
AddDragHandles();
}
private void ZoneSettings_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
Size actualSize = new Size(ActualWidth, ActualHeight);
if (actualSize.Width > 0)
{
ArrangeGridRects(actualSize);
}
}
public GridLayoutModel Model
{
get { return (GridLayoutModel)GetValue(ModelProperty); }
set { SetValue(ModelProperty, value); }
}
public Panel PreviewPanel { get { return Preview; } }
private void OnFullSplit(object o, SplitEventArgs e)
{
UIElementCollection previewChildren = Preview.Children;
UIElement splitee = (UIElement)o;
GridLayoutModel model = Model;
int spliteeIndex = previewChildren.IndexOf(splitee);
int rows = model.Rows;
int cols = model.Columns;
_startRow = -1;
_startCol = -1;
for (int row = rows - 1; row >= 0; row--)
{
for (int col = cols - 1; col >= 0; col--)
{
if (model.CellChildMap[row, col] == spliteeIndex)
{
RemoveDragHandles();
_startRow = _endRow = row;
_startCol = _endCol = col;
ExtendRangeToHaveEvenCellEdges();
for (row = _startRow; row <= _endRow; row++)
{
for (col = _startCol; col <= _endCol; col++)
{
if ((row != _startRow) || (col != _startCol))
{
model.CellChildMap[row, col] = AddZone();
}
}
}
OnGridDimensionsChanged();
return;
}
}
}
}
private void ExtendRangeToHaveEvenCellEdges()
{
// extend each edge of the [(_startCol, _startRow) - (_endCol, _endRow)] range based on merged cells until you have 4 straight edges with no "straddling cells"
GridLayoutModel model = Model;
while (_startRow > 0)
{
bool dirty = false;
for (int col = _startCol; col <= _endCol; col++)
{
if (model.CellChildMap[_startRow - 1, col] == model.CellChildMap[_startRow, col])
{
_startRow--;
dirty = true;
break;
}
}
if (!dirty)
{
break;
}
}
while (_endRow < model.Rows - 1)
{
bool dirty = false;
for (int col = _startCol; col <= _endCol; col++)
{
if (model.CellChildMap[_endRow + 1, col] == model.CellChildMap[_endRow, col])
{
_endRow++;
dirty = true;
break;
}
}
if (!dirty)
{
break;
}
}
while (_startCol > 0)
{
bool dirty = false;
for (int row = _startRow; row <= _endRow; row++)
{
if (model.CellChildMap[row, _startCol - 1] == model.CellChildMap[row, _startCol])
{
_startCol--;
dirty = true;
break;
}
}
if (!dirty)
{
break;
}
}
while (_endCol < model.Columns - 1)
{
bool dirty = false;
for (int row = _startRow; row <= _endRow; row++)
{
if (model.CellChildMap[row, _endCol + 1] == model.CellChildMap[row, _endCol])
{
_endCol++;
dirty = true;
break;
}
}
if (!dirty)
{
break;
}
}
}
private void OnSplit(object o, SplitEventArgs e)
{
UIElementCollection previewChildren = Preview.Children;
GridZone splitee = (GridZone)o;
int spliteeIndex = previewChildren.IndexOf(splitee);
GridLayoutModel model = Model;
int rows = model.Rows;
int cols = model.Columns;
int foundRow = -1;
int foundCol = -1;
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
if (model.CellChildMap[row, col] == spliteeIndex)
{
foundRow = row;
foundCol = col;
break;
}
}
if (foundRow != -1)
{
break;
}
}
int newChildIndex = AddZone();
double offset = e.Offset;
if (e.Orientation == Orientation.Vertical)
{
if (splitee.VerticalSnapPoints != null)
{
offset += Canvas.GetLeft(splitee);
int count = splitee.VerticalSnapPoints.Length;
bool foundExistingSplit = false;
for (int i = 0; i <= count; i++)
{
if (foundExistingSplit)
{
int walkRow = foundRow;
while ((walkRow < rows) && (model.CellChildMap[walkRow, foundCol + i] == spliteeIndex))
{
model.CellChildMap[walkRow++, foundCol + i] = newChildIndex;
}
}
if (_colInfo[foundCol + i].End == offset)
{
foundExistingSplit = true;
// use existing division
}
}
if (foundExistingSplit)
{
OnGridDimensionsChanged();
return;
}
while (_colInfo[foundCol].End < offset)
{
foundCol++;
}
offset -= _colInfo[foundCol].Start;
}
AddDragHandle(Orientation.Vertical, cols - 1);
cols++;
int[,] newCellChildMap = new int[rows, cols];
int[] newColPercents = new int[cols];
RowColInfo[] newColInfo = new RowColInfo[cols];
int sourceCol = 0;
for (int col = 0; col < cols; col++)
{
for (int row = 0; row < rows; row++)
{
if ((col > foundCol) && (model.CellChildMap[row, sourceCol] == spliteeIndex))
{
newCellChildMap[row, col] = newChildIndex;
}
else
{
newCellChildMap[row, col] = model.CellChildMap[row, sourceCol];
}
}
if (col != foundCol)
{
sourceCol++;
}
}
model.CellChildMap = newCellChildMap;
sourceCol = 0;
for (int col = 0; col < cols; col++)
{
if (col == foundCol)
{
RowColInfo[] split = _colInfo[col].Split(offset);
newColPercents[col] = split[0].Percent;
newColInfo[col++] = split[0];
newColPercents[col] = split[1].Percent;
newColInfo[col] = split[1];
sourceCol++;
}
else
{
newColPercents[col] = model.ColumnPercents[sourceCol];
newColInfo[col] = _colInfo[sourceCol++];
}
}
_colInfo = newColInfo;
model.ColumnPercents = newColPercents;
model.Columns++;
}
else // Horizontal
{
if (splitee.HorizontalSnapPoints != null)
{
offset += Canvas.GetTop(splitee);
int count = splitee.HorizontalSnapPoints.Length;
bool foundExistingSplit = false;
for (int i = 0; i <= count; i++)
{
if (foundExistingSplit)
{
int walkCol = foundCol;
while ((walkCol < cols) && (model.CellChildMap[foundRow + i, walkCol] == spliteeIndex))
{
model.CellChildMap[foundRow + i, walkCol] = newChildIndex;
}
}
if (_rowInfo[foundRow + i].End == offset)
{
foundExistingSplit = true;
// use existing division
}
}
if (foundExistingSplit)
{
OnGridDimensionsChanged();
return;
}
while (_rowInfo[foundRow].End < offset)
{
foundRow++;
}
offset -= _rowInfo[foundRow].Start;
}
AddDragHandle(Orientation.Horizontal, rows - 1);
rows++;
int[,] newCellChildMap = new int[rows, cols];
int[] newRowPercents = new int[rows];
RowColInfo[] newRowInfo = new RowColInfo[rows];
int sourceRow = 0;
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
if ((row > foundRow) && (model.CellChildMap[sourceRow, col] == spliteeIndex))
{
newCellChildMap[row, col] = newChildIndex;
}
else
{
newCellChildMap[row, col] = model.CellChildMap[sourceRow, col];
}
}
if (row != foundRow)
{
sourceRow++;
}
}
model.CellChildMap = newCellChildMap;
sourceRow = 0;
for (int row = 0; row < rows; row++)
{
if (row == foundRow)
{
RowColInfo[] split = _rowInfo[row].Split(offset);
newRowPercents[row] = split[0].Percent;
newRowInfo[row++] = split[0];
newRowPercents[row] = split[1].Percent;
newRowInfo[row] = split[1];
sourceRow++;
}
else
{
newRowPercents[row] = model.RowPercents[sourceRow];
newRowInfo[row] = _rowInfo[sourceRow++];
}
}
_rowInfo = newRowInfo;
model.RowPercents = newRowPercents;
model.Rows++;
}
}
private void RemoveDragHandles()
{
AdornerLayer.Children.Clear();
}
private void AddDragHandles()
{
if (AdornerLayer.Children.Count == 0)
{
int interiorRows = Model.Rows - 1;
int interiorCols = Model.Columns - 1;
for (int row = 0; row < interiorRows; row++)
{
AddDragHandle(Orientation.Horizontal, row);
}
for (int col = 0; col < interiorCols; col++)
{
AddDragHandle(Orientation.Vertical, col);
}
}
}
private void AddDragHandle(Orientation orientation, int index)
{
GridResizer resizer = new GridResizer();
resizer.Orientation = orientation;
resizer.Index = index;
resizer.Model = Model;
resizer.DragDelta += Resizer_DragDelta;
if (orientation == Orientation.Vertical)
{
index += (Model.Rows - 1);
}
AdornerLayer.Children.Insert(index, resizer);
}
private void DeleteZone(int index)
{
IList<int> freeZones = Model.FreeZones;
if (freeZones.Contains(index))
{
return;
}
freeZones.Add(index);
GridZone zone = (GridZone)Preview.Children[index];
zone.Visibility = Visibility.Hidden;
zone.MinHeight = 0;
zone.MinWidth = 0;
}
private int AddZone()
{
GridZone zone;
if (Model != null)
{
IList<int> freeZones = Model.FreeZones;
// first check free list
if (freeZones.Count > 0)
{
int freeIndex = freeZones[0];
freeZones.RemoveAt(0);
zone = (GridZone)Preview.Children[freeIndex];
zone.Visibility = Visibility.Visible;
return freeIndex;
}
}
zone = new GridZone();
zone.Split += OnSplit;
zone.MergeDrag += OnMergeDrag;
zone.MergeComplete += OnMergeComplete;
zone.FullSplit += OnFullSplit;
Preview.Children.Add(zone);
return Preview.Children.Count - 1;
}
private void OnGridDimensionsChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if ((e.PropertyName == "Rows") || (e.PropertyName == "Columns"))
{
OnGridDimensionsChanged();
}
}
private static void OnGridDimensionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((GridEditor)d).OnGridDimensionsChanged();
}
private void OnGridDimensionsChanged()
{
GridLayoutModel model = Model;
Size actualSize = new Size(ActualWidth, ActualHeight);
if (actualSize.Width > 0)
{
ArrangeGridRects(actualSize);
}
}
private void ArrangeGridRects(Size arrangeSize)
{
GridLayoutModel model = Model;
if (model == null)
{
return;
}
Settings settings = ((App)(Application.Current)).ZoneSettings;
int spacing = settings.Spacing;
int gutter = settings.Spacing;
int cols = model.Columns;
int rows = model.Rows;
double totalWidth = arrangeSize.Width - (gutter * 2) - (spacing * (cols - 1));
double totalHeight = arrangeSize.Height - (gutter * 2) - (spacing * (rows - 1));
double top = gutter;
for (int row = 0; row < rows; row++)
{
double cellHeight = _rowInfo[row].SetExtent(top, totalHeight);
top += cellHeight + spacing;
}
double left = gutter;
for (int col = 0; col < cols; col++)
{
double cellWidth = _colInfo[col].SetExtent(left, totalWidth);
left += cellWidth + spacing;
}
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
int i = model.CellChildMap[row, col];
if (((row == 0) || (model.CellChildMap[row - 1, col] != i)) &&
((col == 0) || (model.CellChildMap[row, col - 1] != i)))
{
// this is not a continuation of a span
GridZone zone = (GridZone)Preview.Children[i];
left = _colInfo[col].Start;
top = _rowInfo[row].Start;
Canvas.SetLeft(zone, left);
Canvas.SetTop(zone, top);
int maxRow = row;
while (((maxRow + 1) < rows) && (model.CellChildMap[maxRow + 1, col] == i))
{
maxRow++;
}
zone.HorizontalSnapPoints = null;
if (maxRow > row)
{
zone.HorizontalSnapPoints = new double[maxRow - row];
int pointsIndex = 0;
for (int walk = row; walk < maxRow; walk++)
{
zone.HorizontalSnapPoints[pointsIndex++] = _rowInfo[walk].End + spacing / 2 - top;
}
}
int maxCol = col;
while (((maxCol + 1) < cols) && (model.CellChildMap[row, maxCol + 1] == i))
{
maxCol++;
}
zone.VerticalSnapPoints = null;
if (maxCol > col)
{
zone.VerticalSnapPoints = new double[maxCol - col];
int pointsIndex = 0;
for (int walk = col; walk < maxCol; walk++)
{
zone.VerticalSnapPoints[pointsIndex++] = _colInfo[walk].End + spacing / 2 - left;
}
}
zone.MinWidth = _colInfo[maxCol].End - left;
zone.MinHeight = _rowInfo[maxRow].End - top;
}
}
}
AddDragHandles();
int childIndex = 0;
UIElementCollection adornerChildren = AdornerLayer.Children;
for (int row = 0; row < rows - 1; row++)
{
GridResizer resizer = (GridResizer)adornerChildren[childIndex++];
int startCol = -1;
int endCol = cols - 1;
for (int col = 0; col < cols; col++)
{
if ((startCol == -1) && (model.CellChildMap[row, col] != model.CellChildMap[row + 1, col]))
{
startCol = col;
}
else if ((startCol != -1) && (model.CellChildMap[row, col] == model.CellChildMap[row + 1, col]))
{
endCol = col - 1;
break;
}
}
if (startCol != -1)
{
Canvas.SetTop(resizer, _rowInfo[row].End + (spacing / 2) - 24); // hard coding this as (resizer.ActualHeight / 2) will still evaluate to 0 here ... a layout hasn't yet happened
Canvas.SetLeft(resizer, (_colInfo[endCol].End + _colInfo[startCol].Start) / 2);
}
else
{
resizer.Visibility = Visibility.Collapsed;
}
}
for (int col = 0; col < cols - 1; col++)
{
GridResizer resizer = (GridResizer)adornerChildren[childIndex++];
int startRow = -1;
int endRow = rows - 1;;
for (int row = 0; row < rows; row++)
{
if ((startRow == -1) && (model.CellChildMap[row, col] != model.CellChildMap[row, col + 1]))
{
startRow = row;
}
else if ((startRow != -1) && (model.CellChildMap[row, col] == model.CellChildMap[row, col + 1]))
{
endRow = row - 1;
break;
}
}
if (startRow != -1)
{
Canvas.SetLeft(resizer, _colInfo[col].End + (spacing / 2) - 24); // hard coding this as (resizer.ActualWidth / 2) will still evaluate to 0 here ... a layout hasn't yet happened
Canvas.SetTop(resizer, (_rowInfo[endRow].End + _rowInfo[startRow].Start) / 2);
}
else
{
resizer.Visibility = Visibility.Collapsed;
}
}
}
private void Resizer_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
GridResizer resizer = (GridResizer)sender;
int[] percents;
RowColInfo[] info;
int index = resizer.Index;
double delta;
if (resizer.Orientation == Orientation.Vertical)
{
percents = Model.ColumnPercents;
info = _colInfo;
delta = e.HorizontalChange;
}
else
{
percents = Model.RowPercents;
info = _rowInfo;
delta = e.VerticalChange;
}
double currentExtent = info[index].Extent;
double newExtent = currentExtent + delta;
int currentPercent = info[index].Percent;
int totalPercent = currentPercent + info[index + 1].Percent;
int newPercent = (int)(currentPercent * newExtent / currentExtent);
if ((newPercent > 0) && (newPercent < totalPercent))
{
percents[index] = info[index].Percent = newPercent;
percents[index + 1] = info[index + 1].Percent = totalPercent - newPercent;
Size actualSize = new Size(ActualWidth, ActualHeight);
ArrangeGridRects(actualSize);
}
}
private Point _startDragPos = new Point(-1, -1);
private void OnMergeComplete(object o, MouseButtonEventArgs e)
{
Point mousePoint = e.GetPosition(Preview);
_startDragPos = new Point(-1, -1);
int mergedIndex = Model.CellChildMap[_startRow, _startCol];
for (int row = _startRow; row <= _endRow; row++)
{
for (int col = _startCol; col <= _endCol; col++)
{
if (Model.CellChildMap[row, col] != mergedIndex)
{
// selection is more than one cell, merge is valid
MergePanel.Visibility = Visibility.Visible;
Canvas.SetTop(MergeButtons, mousePoint.Y);
Canvas.SetLeft(MergeButtons, mousePoint.X);
return;
}
}
}
// merge is only one zone. cancel merge;
ClearSelection();
}
private void OnMergeDrag(object o, MouseEventArgs e)
{
if (_startDragPos.X == -1)
{
_startDragPos = e.GetPosition(Preview);
}
GridLayoutModel model = Model;
if (_startDragPos.X != -1)
{
Point dragPos = e.GetPosition(Preview);
_startRow = -1;
_endRow = -1;
_startCol = -1;
_endCol = -1;
int rows = model.Rows;
int cols = model.Columns;
double minX, maxX;
if (dragPos.X < _startDragPos.X)
{
minX = dragPos.X;
maxX = _startDragPos.X;
}
else
{
minX = _startDragPos.X;
maxX = dragPos.X;
}
double minY, maxY;
if (dragPos.Y < _startDragPos.Y)
{
minY = dragPos.Y;
maxY = _startDragPos.Y;
}
else
{
minY = _startDragPos.Y;
maxY = dragPos.Y;
}
for (int row = 0; row < rows; row++)
{
if (_startRow == -1)
{
if (_rowInfo[row].End > minY)
{
_startRow = row;
}
}
else if (_rowInfo[row].Start > maxY)
{
_endRow = row - 1;
break;
}
}
if ((_startRow >= 0) && (_endRow == -1))
{
_endRow = rows - 1;
}
for (int col = 0; col < cols; col++)
{
if (_startCol == -1)
{
if (_colInfo[col].End > minX)
{
_startCol = col;
}
}
else if (_colInfo[col].Start > maxX)
{
_endCol = col - 1;
break;
}
}
if ((_startCol >= 0) && (_endCol == -1))
{
_endCol = cols - 1;
}
ExtendRangeToHaveEvenCellEdges();
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
((GridZone)Preview.Children[model.CellChildMap[row, col]]).IsSelected = (row >= _startRow) && (row <= _endRow) && (col >= _startCol) && (col <= _endCol);
}
}
e.Handled = true;
}
base.OnPreviewMouseMove(e);
}
private void ClearSelection()
{
foreach (UIElement zone in Preview.Children)
{
((GridZone) zone).IsSelected = false;
}
}
private void MergeClick(object sender, RoutedEventArgs e)
{
GridLayoutModel model = Model;
MergePanel.Visibility = Visibility.Collapsed;
int mergedIndex = model.CellChildMap[_startRow, _startCol];
for (int row = _startRow; row <= _endRow; row++)
{
for (int col = _startCol; col <= _endCol; col++)
{
int childIndex = model.CellChildMap[row, col];
if (childIndex != mergedIndex)
{
model.CellChildMap[row, col] = mergedIndex;
DeleteZone(childIndex);
}
}
}
OnGridDimensionsChanged();
ClearSelection();
}
private void CancelClick(object sender, RoutedEventArgs e)
{
MergePanel.Visibility = Visibility.Collapsed;
ClearSelection();
}
private void MergePanelMouseUp(object sender, MouseButtonEventArgs e)
{
CancelClick(null, null);
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
Size returnSize = base.ArrangeOverride(arrangeBounds);
ArrangeGridRects(arrangeBounds);
return returnSize;
}
Point _mouseDownPos = new Point(-1, -1);
private RowColInfo[] _rowInfo;
private RowColInfo[] _colInfo;
private int _startRow = -1;
private int _endRow = -1;
private int _startCol = -1;
private int _endCol = -1;
private const int c_multiplier = 10000;
}
}

View File

@@ -0,0 +1,184 @@
<local:EditorWindow x:Class="FancyZonesEditor.GridEditorWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:local="clr-namespace:FancyZonesEditor"
mc:Ignorable="d"
Title=""
Width="528"
SizeToContent="Height"
Background="White"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
Closed="OnClosed">
<Window.Resources>
<Style x:Key="titleText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="LineHeight" Value="24" />
<Setter Property="FontSize" Value="18"/>
<Setter Property="Margin" Value="16,20,0,12" />
</Style>
<Style x:Key="zoneCount" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="Regular" />
<Setter Property="FontSize" Value="24"/>
<Setter Property="LineHeight" Value="24" />
<Setter Property="Margin" Value="20,0,20,0" />
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="tabText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="#C4C4C4"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="LineHeight" Value="20" />
<Setter Property="Margin" Value="24,20,0,0" />
<Setter Property="TextAlignment" Value="Center" />
</Style>
<Style x:Key="settingText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="LineHeight" Value="20" />
<Setter Property="Margin" Value="5,10,0,0" />
<Setter Property="TextAlignment" Value="Left" />
</Style>
<Style x:Key="templateTitleText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="Regular" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="0,20,0,0" />
<Setter Property="TextAlignment" Value="Center" />
<Setter Property="Height" Value="30"/>
<Setter Property="Width" Value="250"/>
<Setter Property="VerticalAlignment" Value="Top"/>
</Style>
<Style x:Key="secondaryButton" TargetType="Button">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="0,5,0,5"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="#F2F2F2"/>
<Setter Property="Margin" Value="16,10,0,0" />
<Setter Property="Width" Value="239"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#BCBCBD"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="primaryButton" TargetType="Button">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="0,5,0,5"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="#0078D7"/>
<Setter Property="Margin" Value="16,10,0,0" />
<Setter Property="Width" Value="239"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#024D89"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="spinnerButton" TargetType="Button">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="24"/>
<Setter Property="Padding" Value="0,0,0,5"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="#F2F2F2"/>
<Setter Property="Margin" Value="0,0,0,0" />
</Style>
<Style x:Key="templateBackground" TargetType="Rectangle">
<Setter Property="Fill" Value="Black"/>
<Setter Property="Opacity" Value="0.05"/>
<Setter Property="RadiusX" Value="4"/>
<Setter Property="RadiusY" Value="4"/>
</Style>
<Style x:Key="templateBackgroundSelected" TargetType="Rectangle">
<Setter Property="Fill" Value="#F2F2F2"/>
<Setter Property="Opacity" Value="1"/>
<Setter Property="RadiusX" Value="4"/>
<Setter Property="RadiusY" Value="4"/>
<Setter Property="Stroke" Value="#0078D7"/>
<Setter Property="StrokeThickness" Value="2"/>
</Style>
<Style x:Key="newZoneButton" TargetType="Button">
<Setter Property="Background" Value="#f2f2f2"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontFamily" Value="SegoeUI"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="FontSize" Value="120"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="textLabel" TargetType="TextBlock">
<Setter Property="FontFamily" Value="SegoeUI"/>
<Setter Property="FontWeight" Value="Regular"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Margin" Value="16,12,0,0"/>
</Style>
<Style x:Key="textBox" TargetType="TextBox">
<Setter Property="BorderBrush" Value="#cccccc"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontFamily" Value="SegoeUI"/>
<Setter Property="FontWeight" Value="Regular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="0,5,0,0"/>
<Setter Property="Padding" Value="5,5,5,5"/>
</Style>
</Window.Resources>
<StackPanel>
<TextBlock Name="windowEditorDialogTitle" Text="Custom table layout creator" Style="{StaticResource titleText}" />
<TextBlock Text="Note: Hold down Shift Key to change orientation of splitter" Style="{StaticResource textLabel}" />
<TextBlock Text="Name" Style="{StaticResource textLabel}" />
<TextBox Text="{Binding Name}" Width="494" Style="{StaticResource textBox}" />
<!--
<StackPanel Orientation="Horizontal" Margin="0,8,0,0">
<CheckBox x:Name="showGridSetting" VerticalAlignment="Center" HorizontalAlignment="Center" IsChecked="True" Margin="21,4,0,0"/>
<TextBlock Text="Show snap grid" Style="{StaticResource settingText}" />
<TextBlock Text="Grid spacing" Style="{StaticResource settingText}" Margin="40,10,10,0" />
<TextBox x:Name="gridValue" Text="{Binding Path=Spacing,Mode=TwoWay}" Style="{StaticResource textBox}"/>
</StackPanel>
-->
<StackPanel Orientation="Horizontal" Margin="0,12,0,16">
<Button Content="Cancel" Style="{StaticResource secondaryButton}" Click="OnCancel" />
<Button Content="Save and apply" Style="{StaticResource primaryButton}" Click="OnSaveApplyTemplate" />
</StackPanel>
</StackPanel>
</local:EditorWindow>

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using FancyZonesEditor.Models;
using MahApps.Metro.Controls;
namespace FancyZonesEditor
{
/// <summary>
/// Interaction logic for Window2.xaml
/// </summary>
public partial class GridEditorWindow : EditorWindow
{
public GridEditorWindow()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,18 @@
<Thumb x:Class="FancyZonesEditor.GridResizer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyZonesEditor"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Thumb.Template>
<ControlTemplate>
<StackPanel x:Name="Body" Grid.Column="0" Width="48" Height="48">
<Ellipse Height="48" Width="48" Fill="#0078D7" />
<Rectangle Height="20" Width="2" Fill="White" Margin="5,-48,0,0"/>
<Rectangle Height="20" Width="2" Fill="White" Margin="-5,-48,0,0"/>
</StackPanel>
</ControlTemplate>
</Thumb.Template>
</Thumb>

View File

@@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using FancyZonesEditor.Models;
namespace FancyZonesEditor
{
/// <summary>
/// Interaction logic for GridResizer.xaml
/// </summary>
public partial class GridResizer : Thumb
{
public GridResizer()
{
InitializeComponent();
}
public Orientation Orientation
{
get
{
return _orientation;
}
set
{
_orientation = value;
ApplyTemplate();
StackPanel body = (StackPanel)Template.FindName("Body", this);
if (value == Orientation.Vertical)
{
body.RenderTransform = null;
body.Cursor = Cursors.SizeWE;
}
else
{
body.RenderTransform = c_rotateTransform;
body.Cursor = Cursors.SizeNS;
}
}
}
private static RotateTransform c_rotateTransform = new RotateTransform(90, 24, 24);
public int Index;
public LayoutModel Model;
private Orientation _orientation;
}
}

View File

@@ -0,0 +1,15 @@
<UserControl x:Class="FancyZonesEditor.GridZone"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyZonesEditor"
mc:Ignorable="d"
Background="LightGray"
Opacity="0.5"
d:DesignHeight="450" d:DesignWidth="800">
<Grid x:Name="Frame" Visibility="Collapsed">
<Canvas x:Name="Body" />
<!--<TextBlock Margin="2" Text="Shift Key switches direction&#13;Ctrl Key repeats"/>-->
</Grid>
</UserControl>

View File

@@ -0,0 +1,309 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace FancyZonesEditor
{
/// <summary>
/// Once you've "Committ"ed the starter grid, then the Zones within the grid come to life for you to be able to further subdivide them
/// using splitters
/// </summary>
public partial class GridZone : UserControl
{
public static readonly DependencyProperty IsSelectedProperty = DependencyProperty.Register("IsSelected", typeof(bool), typeof(GridZone), new PropertyMetadata(false, OnSelectionChanged));
private static void OnSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((GridZone)d).OnSelectionChanged();
}
private void OnSelectionChanged()
{
Background = IsSelected ? Brushes.SteelBlue : Brushes.LightGray;
}
public bool IsSelected
{
get { return (bool)GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}
public double[] VerticalSnapPoints;
public double[] HorizontalSnapPoints;
public GridZone()
{
InitializeComponent();
OnSelectionChanged();
_splitter = new Rectangle();
_splitter.Fill = Brushes.DarkGray;
Body.Children.Add(_splitter);
((App) Application.Current).ZoneSettings.PropertyChanged += ZoneSettings_PropertyChanged;
}
private void ZoneSettings_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsShiftKeyPressed")
{
_switchOrientation = ((App)Application.Current).ZoneSettings.IsShiftKeyPressed;
if (_lastPos.X != -1)
{
UpdateSplitter();
}
}
}
protected override Size ArrangeOverride(Size size)
{
_splitOrientation = (size.Width > size.Height) ? Orientation.Vertical : Orientation.Horizontal;
return base.ArrangeOverride(size);
}
private bool IsVerticalSplit
{
get
{
bool isVertical = _splitOrientation == Orientation.Vertical;
if (_switchOrientation)
{
isVertical = !isVertical;
}
return isVertical;
}
}
private int SplitterThickness { get { return Math.Max(((App)Application.Current).ZoneSettings.Spacing, 5); } }
private void UpdateSplitter()
{
int thickness = SplitterThickness;
if (IsVerticalSplit)
{
double bodyWidth = Body.ActualWidth;
double pos = _lastPos.X - thickness/2;
if (pos < 0)
{
pos = 0;
}
else if (pos > (bodyWidth - thickness))
{
pos = bodyWidth - thickness;
}
Canvas.SetLeft(_splitter, pos);
Canvas.SetTop(_splitter, 0);
_splitter.MinWidth = thickness;
_splitter.MinHeight = Body.ActualHeight;
}
else
{
double bodyHeight = Body.ActualHeight;
double pos = _lastPos.Y - thickness / 2;
if (pos < 0)
{
pos = 0;
}
else if (pos > (bodyHeight - thickness))
{
pos = bodyHeight - thickness;
}
Canvas.SetLeft(_splitter, 0);
Canvas.SetTop(_splitter, pos);
_splitter.MinWidth = Body.ActualWidth;
_splitter.MinHeight = thickness;
}
}
protected override void OnMouseEnter(MouseEventArgs e)
{
Frame.Visibility = Visibility.Visible;
base.OnMouseEnter(e);
}
protected override void OnMouseLeave(MouseEventArgs e)
{
Frame.Visibility = Visibility.Collapsed;
base.OnMouseLeave(e);
}
protected override void OnMouseDown(MouseButtonEventArgs e)
{
_mouseDownPos = _lastPos;
base.OnMouseDown(e);
}
protected override void OnMouseMove(MouseEventArgs e)
{
if (_inMergeDrag)
{
DoMergeDrag(e);
}
else
{
_lastPos = e.GetPosition(Body);
if (IsVerticalSplit)
{
if (VerticalSnapPoints != null)
{
int thickness = SplitterThickness;
foreach (double snapPoint in VerticalSnapPoints)
{
if (Math.Abs(_lastPos.X - snapPoint) <= (thickness * 2))
{
_lastPos.X = snapPoint;
break;
}
}
}
}
else // horizontal split
{
if (HorizontalSnapPoints != null)
{
int thickness = SplitterThickness;
foreach (double snapPoint in HorizontalSnapPoints)
{
if (Math.Abs(_lastPos.Y - snapPoint) <= (thickness * 2))
{
_lastPos.Y = snapPoint;
break;
}
}
}
}
if (_mouseDownPos.X == -1)
{
UpdateSplitter();
}
else
{
double threshold = SplitterThickness / 2;
if ((Math.Abs(_mouseDownPos.X - _lastPos.X) > threshold) || (Math.Abs(_mouseDownPos.Y - _lastPos.Y) > threshold))
{
// switch to merge (which is handled by parent GridEditor)
_inMergeDrag = true;
Mouse.Capture(this, CaptureMode.Element);
DoMergeDrag(e);
_splitter.Visibility = Visibility.Hidden;
}
}
}
base.OnMouseMove(e);
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
if (_inMergeDrag)
{
Mouse.Capture(this, CaptureMode.None);
DoMergeComplete(e);
_inMergeDrag = false;
_splitter.Visibility = Visibility.Visible;
}
else
{
int thickness = SplitterThickness;
double delta = IsVerticalSplit ? _mouseDownPos.X - _lastPos.X : _mouseDownPos.Y - _lastPos.Y;
if (Math.Abs(delta) <= thickness / 2)
{
if (IsVerticalSplit)
{
DoSplit(Orientation.Vertical, _lastPos.X - (thickness / 2));
}
else
{
DoSplit(Orientation.Horizontal, _lastPos.Y - (thickness / 2));
}
}
}
_mouseDownPos = new Point(-1, -1);
base.OnMouseUp(e);
}
public event SplitEventHandler Split;
public event SplitEventHandler FullSplit;
public event MouseEventHandler MergeDrag;
public event MouseButtonEventHandler MergeComplete;
private Rectangle _splitter;
private bool _switchOrientation = false;
private Point _lastPos = new Point(-1,-1);
private Point _mouseDownPos = new Point(-1,-1);
private bool _inMergeDrag = false;
private Orientation _splitOrientation;
private void DoMergeDrag(MouseEventArgs e)
{
if (MergeDrag != null)
{
MergeDrag(this, e);
}
}
private void DoMergeComplete(MouseButtonEventArgs e)
{
if (MergeComplete != null)
{
MergeComplete(this, e);
}
}
private void DoSplit(Orientation orientation, double offset)
{
if (Split != null)
{
Split(this, new SplitEventArgs(orientation, offset));
}
}
private void FullSplit_Click(object sender, RoutedEventArgs e)
{
DoFullSplit();
}
private void DoFullSplit()
{
if (FullSplit != null)
{
FullSplit(this, new SplitEventArgs());
}
}
}
public class SplitEventArgs : EventArgs
{
public SplitEventArgs() { }
public SplitEventArgs(Orientation orientation, double offset)
{
_orientation = orientation;
_offset = offset;
}
public Orientation Orientation { get { return _orientation; } }
public double Offset { get { return _offset; } }
private Orientation _orientation;
private double _offset;
}
public delegate void SplitEventHandler(object sender, SplitEventArgs args);
}

View File

@@ -0,0 +1,13 @@
<UserControl x:Class="FancyZonesEditor.LayoutPreview"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:FancyZonesEditor"
mc:Ignorable="d"
Loaded="OnLoaded"
d:DesignHeight="450" d:DesignWidth="800">
<Grid x:Name="Body">
</Grid>
</UserControl>

View File

@@ -0,0 +1,192 @@
using FancyZonesEditor.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace FancyZonesEditor
{
/// <summary>
/// Interaction logic for LayoutPreview.xaml
/// </summary>
public partial class LayoutPreview : UserControl
{
public static readonly DependencyProperty IsActualSizeProperty = DependencyProperty.Register("IsActualSize", typeof(bool), typeof(LayoutPreview), new PropertyMetadata(false));
public LayoutPreview()
{
InitializeComponent();
DataContextChanged += LayoutPreview_DataContextChanged;
((App)Application.Current).ZoneSettings.PropertyChanged += ZoneSettings_PropertyChanged;
}
private void LayoutPreview_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
_model = (LayoutModel)DataContext;
RenderPreview();
}
public bool IsActualSize
{
get { return (bool)GetValue(IsActualSizeProperty); }
set { SetValue(IsActualSizeProperty, value); }
}
private void ZoneSettings_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == "ZoneCount")
{
RenderPreview();
}
else if ((e.PropertyName == "ShowSpacing") || (e.PropertyName == "Spacing"))
{
if (IsActualSize)
{
Settings settings = ((App)Application.Current).ZoneSettings;
Body.Margin = new Thickness(settings.ShowSpacing ? settings.Spacing / 2 : 0);
}
else
{
Body.Margin = new Thickness(0);
}
if (_model is GridLayoutModel)
{
RenderPreview();
}
}
}
public Panel PreviewPanel { get { return Body; } }
private void OnLoaded(object sender, RoutedEventArgs e)
{
_model = (LayoutModel)DataContext;
RenderPreview();
}
private void RenderPreview()
{
if (_model == null)
{
return;
}
Body.Children.Clear();
GridLayoutModel gridModel = _model as GridLayoutModel;
if (gridModel != null)
{
RenderGridPreview(gridModel);
}
else
{
CanvasLayoutModel canvasModel = _model as CanvasLayoutModel;
if (canvasModel != null)
{
RenderCanvasPreview(canvasModel);
}
}
}
private void RenderGridPreview(GridLayoutModel grid)
{
Body.RowDefinitions.Clear();
foreach (int percent in grid.RowPercents)
{
RowDefinition def = new RowDefinition();
def.Height = new GridLength(percent, GridUnitType.Star);
Body.RowDefinitions.Add(def);
}
Body.ColumnDefinitions.Clear();
foreach (int percent in grid.ColumnPercents)
{
ColumnDefinition def = new ColumnDefinition();
def.Width = new GridLength(percent, GridUnitType.Star);
Body.ColumnDefinitions.Add(def);
}
Settings settings = ((App) Application.Current).ZoneSettings;
int divisor = IsActualSize ? 2 : 20;
Thickness margin = new Thickness(settings.ShowSpacing ? settings.Spacing / divisor : 0);
List<int> visited = new List<int>();
for (int row = 0; row < grid.Rows; row++)
{
for (int col = 0; col < grid.Columns; col++)
{
int childIndex = grid.CellChildMap[row,col];
if (!visited.Contains(childIndex))
{
visited.Add(childIndex);
Rectangle rect = new Rectangle();
Grid.SetRow(rect, row);
Grid.SetColumn(rect, col);
int span = 1;
int walk = row + 1;
while ((walk < grid.Rows) && grid.CellChildMap[walk,col] == childIndex)
{
span++;
walk++;
}
Grid.SetRowSpan(rect, span);
span = 1;
walk = col + 1;
while ((walk < grid.Columns) && grid.CellChildMap[row, walk] == childIndex)
{
span++;
walk++;
}
Grid.SetColumnSpan(rect, span);
rect.Margin = margin;
rect.StrokeThickness = 1;
rect.Stroke = Brushes.DarkGray;
rect.Fill = Brushes.LightGray;
Body.Children.Add(rect);
}
}
}
}
private void RenderCanvasPreview(CanvasLayoutModel canvas)
{
Body.RowDefinitions.Clear();
Body.ColumnDefinitions.Clear();
Viewbox viewbox = new Viewbox();
viewbox.Stretch = Stretch.Uniform;
Body.Children.Add(viewbox);
Canvas frame = new Canvas();
viewbox.Child = frame;
frame.Width = canvas.ReferenceWidth;
frame.Height = canvas.ReferenceHeight;
foreach (Int32Rect zone in canvas.Zones)
{
Rectangle rect = new Rectangle();
Canvas.SetTop(rect, zone.Y);
Canvas.SetLeft(rect, zone.X);
rect.MinWidth = zone.Width;
rect.MinHeight = zone.Height;
rect.StrokeThickness = 5;
rect.Stroke = Brushes.DarkGray;
rect.Fill = Brushes.LightGray;
frame.Children.Add(rect);
}
}
private LayoutModel _model;
}
}

View File

@@ -0,0 +1,277 @@
<Controls:MetroWindow x:Class="FancyZonesEditor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:local="clr-namespace:FancyZonesEditor"
mc:Ignorable="d"
Title="" Width="810"
SizeToContent="Height"
Background="White"
ResizeMode="NoResize"
WindowStartupLocation="CenterScreen"
Loaded="OnLoaded"
Closed="OnClosed">
<Window.Resources>
<local:BooleanToBrushConverter x:Key="BooleanToBrushConverter" />
<local:ModelToVisibilityConverter x:Key="ModelToVisibilityConverter" />
<Style x:Key="titleText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="LineHeight" Value="24" />
<Setter Property="FontSize" Value="18"/>
<Setter Property="Margin" Value="16,20,0,0" />
</Style>
<Style x:Key="zoneCount" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="Regular" />
<Setter Property="FontSize" Value="24"/>
<Setter Property="LineHeight" Value="24" />
<Setter Property="Margin" Value="20,0,20,0" />
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="tabText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="#C4C4C4"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="LineHeight" Value="20" />
<Setter Property="Margin" Value="24,20,0,0" />
<Setter Property="TextAlignment" Value="Center" />
</Style>
<Style x:Key="settingText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="LineHeight" Value="20" />
<Setter Property="Margin" Value="40,10,0,0" />
<Setter Property="TextAlignment" Value="Left" />
</Style>
<Style x:Key="settingCheckBoxText" TargetType="CheckBox">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="5,10,0,0" />
</Style>
<Style x:Key="templateTitleText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="TextAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="secondaryButton" TargetType="Button">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="0,5,0,5"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="#F2F2F2"/>
<Setter Property="Margin" Value="16,0,0,0" />
<Setter Property="Width" Value="378"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#BCBCBD"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="primaryButton" TargetType="Button">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Padding" Value="0,5,0,5"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="#0078D7"/>
<Setter Property="Margin" Value="16,0,16,0" />
<Setter Property="Width" Value="377"/>
<Setter Property="Height" Value="32"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#024D89"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="spinnerButton" TargetType="Button">
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="Black"/>
<Setter Property="FontSize" Value="24"/>
<Setter Property="Padding" Value="0,0,0,5"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="#F2F2F2"/>
<Setter Property="Margin" Value="0,0,0,0" />
</Style>
<Style x:Key="newLayoutButton" TargetType="Button">
<Setter Property="Background" Value="#f2f2f2"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontFamily" Value="SegoeUI"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="FontSize" Value="148"/>
</Style>
<Style x:Key="textBox" TargetType="TextBox">
<Setter Property="BorderBrush" Value="#cccccc"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontFamily" Value="SegoeUI"/>
<Setter Property="FontWeight" Value="Regular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Margin" Value="16,6,0,0"/>
<Setter Property="Padding" Value="5,5,5,5"/>
</Style>
<Style x:Key="templateBackground" TargetType="Border">
<Setter Property="Background" Value="#F2F2F2"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="BorderThickness" Value="2"/>
</Style>
<ControlTemplate x:Key="myTabs">
<Grid Width="404">
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="2"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Name="myTabText" Text="{TemplateBinding TabItem.Header}" Style="{StaticResource tabText}" />
<Rectangle Grid.Row="1" x:Name="myTabLine" Fill="#F2F2F2" Height="1"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="TabItem.IsSelected" Value="True">
<Setter TargetName="myTabText" Property="Foreground" Value="#2A79D7"/>
<Setter TargetName="myTabLine" Property="Fill" Value="#2A79D7" />
<Setter TargetName="myTabLine" Property="Height" Value="2"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Window.Resources>
<StackPanel>
<TextBlock Name="dialog_Title" Text="Choose your layout" Style="{StaticResource titleText}" />
<TabControl BorderThickness="0" x:Name="TemplateTab">
<TabItem Header="Templates" Template="{StaticResource myTabs}">
<StackPanel>
<StackPanel Margin="0,15,0,8" Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="decrementZones" Width="40" Height="40" Content="-" Style="{StaticResource spinnerButton}" Click="DecrementZones_Click"/>
<TextBlock x:Name="zoneCount" Text="{Binding ZoneCount}" Style="{StaticResource zoneCount}"/>
<Button x:Name="incrementZones" Width="40" Height="40" Content="+" Style="{StaticResource spinnerButton}" Click="IncrementZones_Click"/>
</StackPanel>
<ItemsControl ItemsSource="{Binding DefaultModels}" Margin="8,0,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" ItemWidth="262" ItemHeight="262" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="8"
BorderBrush="{Binding Path=IsSelected, Converter={StaticResource BooleanToBrushConverter}}"
Style="{StaticResource templateBackground}"
MouseDown="LayoutItem_Click">
<DockPanel Margin="0,20,0,0"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
LastChildFill="True">
<TextBlock Padding="8,0,8,0"
TextTrimming="CharacterEllipsis"
DockPanel.Dock="Top"
Text="{Binding Name}"
Style="{StaticResource templateTitleText}" />
<local:LayoutPreview Margin="16" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"/>
</DockPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Orientation="Horizontal" Margin="10,4,0,8">
<CheckBox x:Name="spaceAroundSetting" Content="Show space around zones" Style="{StaticResource settingCheckBoxText}" IsChecked="{Binding ShowSpacing}"/>
<TextBlock Text="Space around zones" Style="{StaticResource settingText}"/>
<TextBox x:Name="paddingValue" Text="{Binding Path=Spacing,Mode=TwoWay}" Style="{StaticResource textBox}"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,12,0,16">
<Button x:Name="EditTemplateBUtton" Padding="8" Content="Edit selected layout" Style="{StaticResource secondaryButton}" Click="EditLayout_Click"/>
<Button x:Name="ApplyTemplateButton" Padding="8" Content="Apply" Style="{StaticResource primaryButton}" Click="Apply_Click"/>
</StackPanel>
</StackPanel>
</TabItem>
<TabItem Header="Custom" Template="{StaticResource myTabs}">
<StackPanel>
<ItemsControl ItemsSource="{Binding CustomModels}" Margin="8,8,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" ItemWidth="262" ItemHeight="262" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="8"
BorderBrush="{Binding Path=IsSelected, Converter={StaticResource BooleanToBrushConverter}}"
Style="{StaticResource templateBackground}"
MouseDown="LayoutItem_Click">
<DockPanel Margin="0,20,0,0"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
LastChildFill="True">
<DockPanel DockPanel.Dock="Top" LastChildFill="True" >
<Button x:Name="DeleteButton" Visibility="{Binding Converter={StaticResource ModelToVisibilityConverter}}" DockPanel.Dock="Right" MaxHeight="10" Click="OnDelete" Padding="8,4,8,4" Margin="0,0,8,0" BorderThickness="0" Background="#f2f2f2">
<Image Source="images/Delete.png" />
</Button>
<TextBlock DockPanel.Dock="Top"
TextTrimming="CharacterEllipsis" Padding="8,0,8,0"
Text="{Binding Name}"
Style="{StaticResource templateTitleText}" />
</DockPanel>
<local:LayoutPreview Margin="16" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"/>
</DockPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Orientation="Horizontal" Margin="0,10,0,16">
<Button x:Name="EditCustomButton" Content="Edit selected layout" Padding="8" Style="{StaticResource secondaryButton}" Click="EditLayout_Click"/>
<Button x:Name="ApplyCustomButton" Content="Apply" Padding="8" Style="{StaticResource primaryButton}" Click="Apply_Click"/>
</StackPanel>
</StackPanel>
</TabItem>
</TabControl>
</StackPanel>
</Controls:MetroWindow>

View File

@@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using FancyZonesEditor.Models;
using MahApps.Metro.Controls;
namespace FancyZonesEditor
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : MetroWindow
{
public MainWindow()
{
InitializeComponent();
DataContext = _settings;
}
private void DecrementZones_Click(object sender, RoutedEventArgs e)
{
if (_settings.ZoneCount > 1)
{
_settings.ZoneCount--;
}
}
private void IncrementZones_Click(object sender, RoutedEventArgs e)
{
if (_settings.ZoneCount < 40)
{
_settings.ZoneCount++;
}
}
private Settings _settings = ((App)Application.Current).ZoneSettings;
private void NewCustomLayoutButton_Click(object sender, RoutedEventArgs e)
{
WindowLayout window = new WindowLayout();
window.Show();
this.Close();
}
private void LayoutItem_Click(object sender, MouseButtonEventArgs e)
{
Select(((Border)sender).DataContext as LayoutModel);
}
private void Select(LayoutModel newSelection)
{
LayoutModel currentSelection = EditorOverlay.Current.DataContext as LayoutModel;
if (currentSelection != null)
{
currentSelection.IsSelected = false;
}
newSelection.IsSelected = true;
EditorOverlay.Current.DataContext = newSelection;
}
private static string c_defaultNamePrefix = "Custom Layout ";
private bool _editing = false;
private void EditLayout_Click(object sender, RoutedEventArgs e)
{
_editing = true;
this.Close();
EditorOverlay mainEditor = EditorOverlay.Current;
LayoutModel model = mainEditor.DataContext as LayoutModel;
if (model == null)
{
mainEditor.Close();
return;
}
model.IsSelected = false;
bool isPredefinedLayout = Settings.IsPredefinedLayout(model);
if (!_settings.CustomModels.Contains(model) || isPredefinedLayout)
{
if (isPredefinedLayout)
{
// make a copy
model = model.Clone();
mainEditor.DataContext = model;
}
int maxCustomIndex = 0;
foreach (LayoutModel customModel in _settings.CustomModels)
{
string name = customModel.Name;
if (name.StartsWith(c_defaultNamePrefix))
{
int i;
if (Int32.TryParse(name.Substring(c_defaultNamePrefix.Length), out i))
{
if (maxCustomIndex < i)
{
maxCustomIndex = i;
}
}
}
}
model.Name = c_defaultNamePrefix + (++maxCustomIndex);
}
mainEditor.Edit();
EditorWindow window;
if (model is GridLayoutModel)
{
window = new GridEditorWindow();
}
else
{
window = new CanvasEditorWindow();
}
window.Owner = EditorOverlay.Current;
window.DataContext = model;
window.Show();
}
private void Apply_Click(object sender, RoutedEventArgs e)
{
EditorOverlay mainEditor = EditorOverlay.Current;
LayoutModel model = mainEditor.DataContext as LayoutModel;
if (model != null)
{
if (model is GridLayoutModel)
{
model.Apply(mainEditor.GetZoneRects());
}
else
{
model.Apply((model as CanvasLayoutModel).Zones.ToArray());
}
}
this.Close();
}
private void OnClosed(object sender, EventArgs e)
{
if (!_editing)
{
EditorOverlay.Current.Close();
}
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
foreach(LayoutModel model in _settings.CustomModels)
{
if (model.IsSelected)
{
TemplateTab.SelectedIndex = 1;
return;
}
}
}
private void OnDelete(object sender, RoutedEventArgs e)
{
LayoutModel model = ((FrameworkElement)sender).DataContext as LayoutModel;
if (model.IsSelected)
{
OnLoaded(null, null);
}
model.Delete();
}
}
public class BooleanToBrushConverter : IValueConverter
{
private static Brush c_selectedBrush = new SolidColorBrush(Color.FromRgb(0x00, 0x78, 0xD7));
private static Brush c_normalBrush = new SolidColorBrush(Color.FromRgb(0xF2, 0xF2, 0xF2));
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return ((bool)value) ? c_selectedBrush : c_normalBrush;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value == c_selectedBrush;
}
}
public class ModelToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return Settings.IsPredefinedLayout((LayoutModel)value) ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
}

View File

@@ -0,0 +1,164 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Converters;
using System.Windows.Documents;
namespace FancyZonesEditor.Models
{
// CanvasLayoutModel
// Free form Layout Model, which specifies independent zone rects
public class CanvasLayoutModel : LayoutModel
{
public CanvasLayoutModel(ushort version, string name, ushort id, byte[] data) : base(name, id)
{
if (version == c_latestVersion)
{
Load(data);
}
}
public CanvasLayoutModel(string name, ushort id, int referenceWidth, int referenceHeight) : base(name, id)
{
// Initialize Reference Size
_referenceWidth = referenceWidth;
_referenceHeight = referenceHeight;
}
public CanvasLayoutModel(string name, ushort id) : base(name, id) { }
public CanvasLayoutModel(string name) : base(name) { }
public CanvasLayoutModel() : base() { }
// ReferenceWidth - the reference width for the layout rect that all Zones are relative to
public int ReferenceWidth
{
get { return _referenceWidth; }
set
{
if (_referenceWidth != value)
{
_referenceWidth = value;
FirePropertyChanged("ReferenceWidth");
}
}
}
private int _referenceWidth;
// ReferenceHeight - the reference height for the layout rect that all Zones are relative to
public int ReferenceHeight
{
get { return _referenceHeight; }
set
{
if (_referenceHeight != value)
{
_referenceHeight = value;
FirePropertyChanged("ReferenceHeight");
}
}
}
private int _referenceHeight;
// Zones - the list of all zones in this layout, described as independent rectangles
public IList<Int32Rect> Zones { get { return _zones; } }
private IList<Int32Rect> _zones = new List<Int32Rect>();
// RemoveZoneAt
// Removes the specified index from the Zones list, and fires a property changed notification for the Zones property
public void RemoveZoneAt(int index)
{
Zones.RemoveAt(index);
FirePropertyChanged("Zones");
}
// AddZone
// Adds the specified Zone to the end of the Zones list, and fires a property changed notification for the Zones property
public void AddZone(Int32Rect zone)
{
Zones.Add(zone);
FirePropertyChanged("Zones");
}
private void Load(byte[] data)
{
// Initialize this CanvasLayoutModel based on the given persistence data
// Skip version (2 bytes), id (2 bytes), and type (1 bytes)
int i = 5;
_referenceWidth = data[i++] * 256 + data[i++];
_referenceHeight = data[i++] * 256 + data[i++];
int count = data[i++];
while (count-- > 0)
{
_zones.Add(new Int32Rect(
data[i++] * 256 + data[i++],
data[i++] * 256 + data[i++],
data[i++] * 256 + data[i++],
data[i++] * 256 + data[i++]));
}
}
// Clone
// Implements the LayoutModel.Clone abstract method
// Clones the data from this CanvasLayoutModel to a new CanvasLayoutModel
public override LayoutModel Clone()
{
CanvasLayoutModel layout = new CanvasLayoutModel(Name);
layout.ReferenceHeight = ReferenceHeight;
layout.ReferenceWidth = ReferenceWidth;
foreach(Int32Rect zone in Zones)
{
layout.Zones.Add(zone);
}
return layout;
}
// GetPersistData
// Implements the LayoutModel.GetPersistData abstract method
// Returns the state of this GridLayoutModel in persisted format
protected override byte[] GetPersistData()
{
byte[] data = new byte[10 + (_zones.Count * 8)];
int i = 0;
// Common persisted values between all layout types
data[i++] = (byte)(c_latestVersion / 256);
data[i++] = (byte)(c_latestVersion % 256);
data[i++] = 1; // LayoutModelType: 1 == CanvasLayoutModel
data[i++] = (byte)(Id / 256);
data[i++] = (byte)(Id % 256);
// End common
data[i++] = (byte)(_referenceWidth / 256);
data[i++] = (byte)(_referenceWidth % 256);
data[i++] = (byte)(_referenceHeight / 256);
data[i++] = (byte)(_referenceHeight % 256);
data[i++] = (byte)_zones.Count;
foreach (Int32Rect rect in _zones)
{
data[i++] = (byte)(rect.X / 256);
data[i++] = (byte)(rect.X % 256);
data[i++] = (byte)(rect.Y / 256);
data[i++] = (byte)(rect.Y % 256);
data[i++] = (byte)(rect.Width / 256);
data[i++] = (byte)(rect.Width % 256);
data[i++] = (byte)(rect.Height / 256);
data[i++] = (byte)(rect.Height % 256);
}
return data;
}
private static ushort c_latestVersion = 0;
}
}

View File

@@ -0,0 +1,224 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Documents;
namespace FancyZonesEditor.Models
{
// GridLayoutModel
// Grid-styled Layout Model, which specifies rows, columns, percentage sizes, and row/column spans
public class GridLayoutModel : LayoutModel
{
public GridLayoutModel() : base() { }
public GridLayoutModel(string name) : base(name) { }
public GridLayoutModel(string name, ushort id) : base(name, id) { }
public GridLayoutModel(ushort version, string name, ushort id, byte[] data) : base(name, id)
{
if (version == c_latestVersion)
{
Reload(data);
}
}
// Rows - number of rows in the Grid
public int Rows
{
get { return _rows; }
set
{
if (_rows != value)
{
_rows = value;
FirePropertyChanged("Rows");
}
}
}
private int _rows = 1;
// Columns - number of columns in the Grid
public int Columns
{
get { return _cols; }
set
{
if (_cols != value)
{
_cols = value;
FirePropertyChanged("Columns");
}
}
}
private int _cols = 1;
// CellChildMap - represents which "children" belong in which grid cells;
// shows spanning children by the same index appearing in adjacent cells
// TODO: ideally no setter here - this means moving logic like "split" over to model
public int[,] CellChildMap { get { return _cellChildMap; } set { _cellChildMap = value; } }
private int[,] _cellChildMap;
// RowPercents - represents the %age height of each row in the grid
public int[] RowPercents { get { return _rowPercents; } set { _rowPercents = value; } }
private int[] _rowPercents;
// ColumnPercents - represents the %age width of each column in the grid
public int[] ColumnPercents { get { return _colPercents; } set { _colPercents = value; } }
private int[] _colPercents;
// FreeZones (not persisted) - used to keep track of child indices that are no longer in use in the CellChildMap,
// making them candidates for re-use when it's needed to add another child
// TODO: do I need FreeZones on the data model? - I think I do
public IList<int> FreeZones { get { return _freeZones; } }
private IList<int> _freeZones = new List<int>();
public void Reload(byte[] data)
{
// Skip version (2 bytes), id (2 bytes), and type (1 bytes)
int i = 5;
Rows = data[i++];
Columns = data[i++];
_rowPercents = new int[Rows];
for (int row = 0; row < Rows; row++)
{
_rowPercents[row] = data[i++]*256 + data[i++];
}
_colPercents = new int[Columns];
for (int col = 0; col < Columns; col++)
{
_colPercents[col] = data[i++]*256 + data[i++];
}
_cellChildMap = new int[Rows, Columns];
for (int row = 0; row < Rows; row++)
{
for (int col = 0; col < Columns; col++)
{
_cellChildMap[row, col] = data[i++];
}
}
}
// Clone
// Implements the LayoutModel.Clone abstract method
// Clones the data from this GridLayoutModel to a new GridLayoutModel
public override LayoutModel Clone()
{
GridLayoutModel layout = new GridLayoutModel(Name);
int rows = Rows;
int cols = Columns;
layout.Rows = rows;
layout.Columns = cols;
int[,] cellChildMap = new int[rows, cols];
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
cellChildMap[row, col] = CellChildMap[row, col];
}
}
layout.CellChildMap = cellChildMap;
int[] rowPercents = new int[rows];
for (int row = 0; row < rows; row++)
{
rowPercents[row] = RowPercents[row];
}
layout.RowPercents = rowPercents;
int[] colPercents = new int[cols];
for (int col = 0; col < cols; col++)
{
colPercents[col] = ColumnPercents[col];
}
layout.ColumnPercents = colPercents;
return layout;
}
// GetPersistData
// Implements the LayoutModel.GetPersistData abstract method
// Returns the state of this GridLayoutModel in persisted format
protected override byte[] GetPersistData()
{
int rows = Rows;
int cols = Columns;
int[,] cellChildMap;
if (_freeZones.Count == 0)
{
// no unused indices -- so we can just use the _cellChildMap as is
cellChildMap = _cellChildMap;
}
else
{
// compress cellChildMap to not have gaps for unused child indices;
List<int> mapping = new List<int>();
cellChildMap = new int[rows, cols];
for (int row = 0; row < rows; row++)
{
for (int col = 0; col < cols; col++)
{
int source = _cellChildMap[row, col];
int index = mapping.IndexOf(source);
if (index == -1)
{
index = mapping.Count;
mapping.Add(source);
}
cellChildMap[row, col] = index;
}
}
}
byte[] data = new byte[7 + (Rows * 2) + (Columns * 2) + (Rows * Columns)];
int i = 0;
// Common persisted values between all layout types
data[i++] = (byte)(c_latestVersion / 256);
data[i++] = (byte)(c_latestVersion % 256);
data[i++] = 0; // LayoutModelType: 0 == GridLayoutModel
data[i++] = (byte)(Id / 256);
data[i++] = (byte)(Id % 256);
// End common
data[i++] = (byte)Rows;
data[i++] = (byte)Columns;
for (int row = 0; row < Rows; row++)
{
int rowPercent = _rowPercents[row];
data[i++] = (byte)(rowPercent / 256);
data[i++] = (byte)(rowPercent % 256);
}
for (int col = 0; col < Columns; col++)
{
int colPercent = _colPercents[col];
data[i++] = (byte)(colPercent / 256);
data[i++] = (byte)(colPercent % 256);
}
for (int row = 0; row < Rows; row++)
{
for (int col = 0; col < Columns; col++)
{
data[i++] = (byte)cellChildMap[row, col];
}
}
return data;
}
private static ushort c_latestVersion = 0;
}
}

View File

@@ -0,0 +1,226 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Win32;
namespace FancyZonesEditor.Models
{
// Base LayoutModel
// Manages common properties and base persistence
public abstract class LayoutModel : INotifyPropertyChanged
{
protected LayoutModel() { }
protected LayoutModel(string name) : this()
{
Name = name;
}
protected LayoutModel(string name, ushort id) : this(name)
{
_id = id;
}
// Name - the display name for this layout model - is also used as the key in the registry
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
FirePropertyChanged("Name");
}
}
}
private string _name;
// Id - the unique ID for this layout model - is used to connect fancy zones' ZonesSets with the editor's Layouts
// - note: 0 means this is a new layout, which means it will have its ID auto-assigned on persist
public ushort Id
{
get
{
if (_id == 0)
{
_id = ++s_maxId;
}
return _id;
}
}
private ushort _id = 0;
// IsSelected (not-persisted) - tracks whether or not this LayoutModel is selected in the picker
// TODO: once we switch to a picker per monitor, we need to move this state to the view
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
FirePropertyChanged("IsSelected");
}
}
}
private bool _isSelected;
// implementation of INotifyProeprtyChanged
public event PropertyChangedEventHandler PropertyChanged;
// FirePropertyChanged -- wrapper that calls INPC.PropertyChanged
protected virtual void FirePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
// Removes this Layout from the registry and the loaded CustomModels list
public void Delete()
{
RegistryKey key = Registry.CurrentUser.OpenSubKey(c_registryPath, true);
if (key != null)
{
key.DeleteValue(Name);
}
int i = s_customModels.IndexOf(this);
if (i != -1)
{
s_customModels.RemoveAt(i);
}
}
// Loads all the Layouts persisted under the Layouts key in the registry
public static ObservableCollection<LayoutModel> LoadCustomModels()
{
s_customModels = new ObservableCollection<LayoutModel>();
RegistryKey key = Registry.CurrentUser.OpenSubKey(c_registryPath);
if (key != null)
{
foreach (string name in key.GetValueNames())
{
LayoutModel model = null;
byte[] data = (byte[])Registry.GetValue(c_fullRegistryPath, name, null);
ushort version = (ushort) (data[0]*256 + data[1]);
byte type = data[2];
ushort id = (ushort) (data[3]*256 + data[4]);
switch (type)
{
case 0: model = new GridLayoutModel(version, name, id, data); break;
case 1: model = new CanvasLayoutModel(version, name, id, data); break;
}
if (model != null)
{
if (s_maxId < id)
{
s_maxId = id;
}
s_customModels.Add(model);
}
}
}
return s_customModels;
}
private static ObservableCollection<LayoutModel> s_customModels = null;
private static ushort s_maxId = 0;
// Callbacks that the base LayoutModel makes to derived types
protected abstract byte[] GetPersistData();
public abstract LayoutModel Clone();
// PInvokes to handshake with fancyzones backend
internal static class Native
{
[DllImport("kernel32", SetLastError = true, CharSet = CharSet.Ansi)]
public static extern IntPtr LoadLibrary([MarshalAs(UnmanagedType.LPStr)]string lpFileName);
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
internal delegate int PersistZoneSet(
[MarshalAs(UnmanagedType.LPWStr)] string activeKey,
[MarshalAs(UnmanagedType.LPWStr)] string key,
ushort layoutId,
int zoneCount,
[MarshalAs(UnmanagedType.LPArray)] int[] zoneArray);
}
public void Persist(System.Windows.Int32Rect[] zones)
{
// Persist the editor data
Registry.SetValue(c_fullRegistryPath, Name, GetPersistData(), Microsoft.Win32.RegistryValueKind.Binary);
Apply(zones);
}
public void Apply(System.Windows.Int32Rect[] zones)
{
// Persist the zone data back into FZ
var module = Native.LoadLibrary("fancyzones.dll");
if (module == IntPtr.Zero)
{
return;
}
var pfn = Native.GetProcAddress(module, "PersistZoneSet");
if (pfn == IntPtr.Zero)
{
return;
}
// Scale all the zones to the DPI and then pack them up to be marshalled.
int zoneCount = zones.Length;
var zoneArray = new int[zoneCount * 4];
var graphics = System.Drawing.Graphics.FromHwnd(IntPtr.Zero);
float dpi = graphics.DpiX / 96;
for (int i = 0; i < zones.Length; i++)
{
var left = (int)(zones[i].X * dpi);
var top = (int)(zones[i].Y * dpi);
var right = left + (int)(zones[i].Width * dpi);
var bottom = top + (int)(zones[i].Height * dpi);
var index = i * 4;
zoneArray[index] = left;
zoneArray[index+1] = top;
zoneArray[index+2] = right;
zoneArray[index+3] = bottom;
}
string[] args = Environment.GetCommandLineArgs();
if (args.Length > 1)
{
// args[1] = registry key value of currently active ZoneSet
// args[2] = id of layout to load at startup
string uniqueId = args[1];
// TODO: multimon
double height = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height;
double width = System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width;
var key = width.ToString() + "_" + height.ToString();
var persistZoneSet = Marshal.GetDelegateForFunctionPointer<Native.PersistZoneSet>(pfn);
persistZoneSet(uniqueId, key, _id, zoneCount, zoneArray);
}
}
private static readonly string c_registryPath = Settings.RegistryPath + "\\Layouts";
private static readonly string c_fullRegistryPath = Settings.FullRegistryPath + "\\Layouts";
}
}

View File

@@ -0,0 +1,304 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Collections;
using System.Collections.ObjectModel;
using FancyZonesEditor.Models;
using System.Windows.Documents;
using System.Windows;
using System.Windows.Controls;
using Microsoft.Win32;
namespace FancyZonesEditor
{
//
// Settings
// These are the configuration settings used by the rest of the editor
// Other UIs in the editor will subscribe to change events on the properties to stay up to date as these properties change
//
public class Settings : INotifyPropertyChanged
{
public Settings()
{
Rect workArea = System.Windows.SystemParameters.WorkArea;
// Initialize the five default layout models: Focus, Columns, Rows, Grid, and PriorityGrid
_defaultModels = new List<LayoutModel>(5);
_focusModel = new CanvasLayoutModel("Focus", c_focusModelId, (int)workArea.Width, (int)workArea.Height);
_defaultModels.Add(_focusModel);
_columnsModel = new GridLayoutModel("Columns", c_columnsModelId);
_columnsModel.Rows = 1;
_columnsModel.RowPercents = new int[1] { c_multiplier };
_defaultModels.Add(_columnsModel);
_rowsModel = new GridLayoutModel("Rows", c_rowsModelId);
_rowsModel.Columns = 1;
_rowsModel.ColumnPercents = new int[1] { c_multiplier };
_defaultModels.Add(_rowsModel);
_gridModel = new GridLayoutModel("Grid", c_gridModelId);
_defaultModels.Add(_gridModel);
_priorityGridModel = new GridLayoutModel("Priority Grid", c_priorityGridModelId);
_defaultModels.Add(_priorityGridModel);
_blankCustomModel = new CanvasLayoutModel("Create new custom", c_blankCustomModelId, (int)workArea.Width, (int)workArea.Height);
_zoneCount = (int)Registry.GetValue(FullRegistryPath, "ZoneCount", 3);
_spacing = (int)Registry.GetValue(FullRegistryPath, "Spacing", 16);
_showSpacing = (int)Registry.GetValue(FullRegistryPath, "ShowSpacing", 1) == 1;
UpdateLayoutModels();
}
// ZoneCount - number of zones selected in the picker window
public int ZoneCount
{
get { return _zoneCount; }
set
{
if (_zoneCount != value)
{
_zoneCount = value;
Registry.SetValue(FullRegistryPath, "ZoneCount", _zoneCount, RegistryValueKind.DWord);
UpdateLayoutModels();
FirePropertyChanged("ZoneCount");
}
}
}
private int _zoneCount;
// Spacing - how much space in between zones of the grid do you want
public int Spacing
{
get { return _spacing; }
set
{
if (_spacing != value)
{
_spacing = value;
Registry.SetValue(FullRegistryPath, "Spacing", _spacing, RegistryValueKind.DWord);
FirePropertyChanged("Spacing");
}
}
}
private int _spacing;
// ShowSpacing - is the Spacing value used or ignored?
public bool ShowSpacing
{
get { return _showSpacing; }
set
{
if (_showSpacing != value)
{
_showSpacing = value;
Registry.SetValue(FullRegistryPath, "ShowSpacing", _showSpacing, RegistryValueKind.DWord);
FirePropertyChanged("ShowSpacing");
}
}
}
private bool _showSpacing;
// IsShiftKeyPressed - is the shift key currently being held down
public bool IsShiftKeyPressed
{
get { return _isShiftKeyPressed; }
set
{
if (_isShiftKeyPressed != value)
{
_isShiftKeyPressed = value;
FirePropertyChanged("IsShiftKeyPressed");
}
}
}
private bool _isShiftKeyPressed;
// IsCtrlKeyPressed - is the ctrl key currently being held down
public bool IsCtrlKeyPressed
{
get { return _isCtrlKeyPressed; }
set
{
if (_isCtrlKeyPressed != value)
{
_isCtrlKeyPressed = value;
FirePropertyChanged("IsCtrlKeyPressed");
}
}
}
private bool _isCtrlKeyPressed;
// UpdateLayoutModels
// Update the five default layouts based on the new ZoneCount
private void UpdateLayoutModels()
{
int previousZoneCount = _focusModel.Zones.Count;
// Update the "Focus" Default Layout
_focusModel.Zones.Clear();
Int32Rect focusZoneRect = new Int32Rect((int)(_focusModel.ReferenceWidth * 0.1), (int)(_focusModel.ReferenceHeight * 0.1), (int)(_focusModel.ReferenceWidth * 0.6), (int)(_focusModel.ReferenceHeight * 0.6));
int focusRectXIncrement = (ZoneCount <= 1) ? 0 : (int)(_focusModel.ReferenceWidth * 0.2) / (ZoneCount - 1);
int focusRectYIncrement = (ZoneCount <= 1) ? 0 : (int)(_focusModel.ReferenceHeight * 0.2) / (ZoneCount - 1);
for (int i = 0; i < ZoneCount; i++)
{
_focusModel.Zones.Add(focusZoneRect);
focusZoneRect.X += focusRectXIncrement;
focusZoneRect.Y += focusRectYIncrement;
}
// Update the "Rows" and "Columns" Default Layouts
// They can share their model, just transposed
_rowsModel.CellChildMap = new int[ZoneCount, 1];
_columnsModel.CellChildMap = new int[1, ZoneCount];
_rowsModel.Rows = _columnsModel.Columns = ZoneCount;
_rowsModel.RowPercents = _columnsModel.ColumnPercents = new int[ZoneCount];
for (int i = 0; i < ZoneCount; i++)
{
_rowsModel.CellChildMap[i, 0] = i;
_columnsModel.CellChildMap[0, i] = i;
_rowsModel.RowPercents[i] = c_multiplier / ZoneCount; // _columnsModel is sharing the same array
}
// Update the "Grid" Default Layout
int rows = 1;
int cols = 1;
int mergeCount = 0;
while (ZoneCount / rows >= rows)
{
rows++;
}
rows--;
cols = ZoneCount / rows;
if (ZoneCount % rows == 0)
{
// even grid
}
else
{
cols++;
mergeCount = rows - (ZoneCount % rows);
}
_gridModel.Rows = rows;
_gridModel.Columns = cols;
_gridModel.RowPercents = new int[rows];
_gridModel.ColumnPercents = new int[cols];
_gridModel.CellChildMap = new int[rows, cols];
for (int row = 0; row < rows; row++)
{
_gridModel.RowPercents[row] = c_multiplier / rows;
}
for (int col = 0; col < cols; col++)
{
_gridModel.ColumnPercents[col] = c_multiplier / cols;
}
int index = 0;
for (int col = cols - 1; col >= 0; col--)
{
for (int row = rows - 1; row >= 0; row--)
{
_gridModel.CellChildMap[row, col] = index++;
if (index == ZoneCount)
{
index--;
}
}
}
// Update the "Priority Grid" Default Layout
if (ZoneCount <= s_priorityData.Length)
{
_priorityGridModel.Reload(s_priorityData[ZoneCount - 1]);
}
else
{
// same as grid;
_priorityGridModel.Rows = _gridModel.Rows;
_priorityGridModel.Columns = _gridModel.Columns;
_priorityGridModel.RowPercents = _gridModel.RowPercents;
_priorityGridModel.ColumnPercents = _gridModel.ColumnPercents;
_priorityGridModel.CellChildMap = _gridModel.CellChildMap;
}
}
public IList<LayoutModel> DefaultModels { get { return _defaultModels; } }
public ObservableCollection<LayoutModel> CustomModels
{
get
{
if (_customModels == null)
{
_customModels = LayoutModel.LoadCustomModels();
_customModels.Insert(0, _blankCustomModel);
}
return _customModels;
}
}
private ObservableCollection<LayoutModel> _customModels;
public static readonly string RegistryPath = "SOFTWARE\\SuperFancyZones";
public static readonly string FullRegistryPath = "HKEY_CURRENT_USER\\" + RegistryPath;
public static bool IsPredefinedLayout(LayoutModel model)
{
return (model.Id >= c_lastPrefinedId);
}
// implementation of INotifyProeprtyChanged
public event PropertyChangedEventHandler PropertyChanged;
// FirePropertyChanged -- wrapper that calls INPC.PropertyChanged
protected virtual void FirePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
// storage for Default Layout Models
private IList<LayoutModel> _defaultModels;
private CanvasLayoutModel _focusModel;
private GridLayoutModel _rowsModel;
private GridLayoutModel _columnsModel;
private GridLayoutModel _gridModel;
private GridLayoutModel _priorityGridModel;
private CanvasLayoutModel _blankCustomModel;
private static readonly ushort c_focusModelId = 0xFFFF;
private static readonly ushort c_rowsModelId = 0xFFFE;
private static readonly ushort c_columnsModelId = 0xFFFD;
private static readonly ushort c_gridModelId = 0xFFFC;
private static readonly ushort c_priorityGridModelId = 0xFFFB;
private static readonly ushort c_blankCustomModelId = 0xFFFA;
private static readonly ushort c_lastPrefinedId = c_blankCustomModelId;
// hard coded data for all the "Priority Grid" configurations that are unique to "Grid"
private static byte[][] s_priorityData = new byte[][]
{
new byte[] { 0, 0, 0, 0, 0, 1, 1, 39, 16, 39, 16, 0 },
new byte[] { 0, 0, 0, 0, 0, 1, 2, 39, 16, 26, 11, 13, 5, 0, 1 },
new byte[] { 0, 0, 0, 0, 0, 1, 3, 39, 16, 9, 196, 19, 136, 9, 196, 0, 1, 2 },
new byte[] { 0, 0, 0, 0, 0, 2, 3, 19, 136, 19, 136, 9, 196, 19, 136, 9, 196, 0, 1, 2, 0, 1, 3 },
new byte[] { 0, 0, 0, 0, 0, 2, 3, 19, 136, 19, 136, 9, 196, 19, 136, 9, 196, 0, 1, 2, 3, 1, 4 },
new byte[] { 0, 0, 0, 0, 0, 3, 3, 13, 5, 13, 6, 13, 5, 9, 196, 19, 136, 9, 196, 0, 1, 2, 0, 1, 3, 4, 1, 5 },
new byte[] { 0, 0, 0, 0, 0, 3, 3, 13, 5, 13, 6, 13, 5, 9, 196, 19, 136, 9, 196, 0, 1, 2, 3, 1, 4, 5, 1, 6 },
new byte[] { 0, 0, 0, 0, 0, 3, 4, 13, 5, 13, 6, 13, 5, 9, 196, 9, 196, 9, 196, 9, 196, 0, 1, 2, 3, 4, 1, 2, 5, 6, 1, 2, 7 },
new byte[] { 0, 0, 0, 0, 0, 3, 4, 13, 5, 13, 6, 13, 5, 9, 196, 9, 196, 9, 196, 9, 196, 0, 1, 2, 3, 4, 1, 2, 5, 6, 1, 7, 8 },
new byte[] { 0, 0, 0, 0, 0, 3, 4, 13, 5, 13, 6, 13, 5, 9, 196, 9, 196, 9, 196, 9, 196, 0, 1, 2, 3, 4, 1, 5, 6, 7, 1, 8, 9 },
new byte[] { 0, 0, 0, 0, 0, 3, 4, 13, 5, 13, 6, 13, 5, 9, 196, 9, 196, 9, 196, 9, 196, 0, 1, 2, 3, 4, 1, 5, 6, 7, 8, 9, 10 }
};
private const int c_multiplier = 10000;
}
}

View File

@@ -0,0 +1,55 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("FancyZonesEditor")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Microsoft Corp.")]
[assembly: AssemblyProduct("FancyZonesEditor")]
[assembly: AssemblyCopyright("Copyright (C) 2019 Microsoft Corp.")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
//In order to begin building localizable applications, set
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
//inside a <PropertyGroup>. For example, if you are using US english
//in your source files, set the <UICulture> to en-US. Then uncomment
//the NeutralResourceLanguage attribute below. Update the "en-US" in
//the line below to match the UICulture setting in the project file.
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.11.0.0")]
[assembly: AssemblyFileVersion("0.11.0.0")]

View File

@@ -0,0 +1,63 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace FancyZonesEditor.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FancyZonesEditor.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace FancyZonesEditor.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.1.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
public static Settings Default {
get {
return defaultInstance;
}
}
}
}

View File

@@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
<Settings />
</SettingsFile>

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FancyZonesEditor
{
public class RowColInfo
{
public RowColInfo(int percent)
{
Percent = percent;
}
public RowColInfo(int index, int count)
{
Percent = (c_multiplier / count) + ((index == 0) ? (c_multiplier % count) : 0);
}
private const int c_multiplier = 10000;
public double SetExtent(double start, double totalExtent)
{
Start = start;
Extent = totalExtent * Percent / c_multiplier;
End = Start + Extent;
return Extent;
}
public RowColInfo[] Split(double offset)
{
RowColInfo[] info = new RowColInfo[2];
int newPercent = (int)(Percent * offset / Extent);
info[0] = new RowColInfo(newPercent);
info[1] = new RowColInfo(Percent - newPercent);
return info;
}
public int Percent;
public double Extent;
public double Start;
public double End;
}
}

View File

@@ -0,0 +1,52 @@
<Window x:Class="FancyZonesEditor.WindowLayout"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:FancyZonesEditor"
mc:Ignorable="d"
Title="Window1" Height="450" Width="800"
WindowState="Maximized"
ShowInTaskbar="False"
ResizeMode="NoResize"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
Loaded="onLoad"
>
<Grid x:Name="Body">
<Rectangle Fill="White" Opacity="0.5"/>
<Grid x:Name="Window1" >
<Border BorderThickness="2" BorderBrush="Black">
<Grid>
<!--
<Thumb Background="Red" Height="36" VerticalAlignment="Top"/>
<Button Height="36" Width="36" VerticalAlignment="Top" Background="Red" FontFamily="Segoe UI Symbol" FontSize="16" Content="✖" HorizontalAlignment="Right"/>
-->
<Rectangle Fill="Black" Opacity="0.2"/>
<!--
<Grid Width="24" Height="24" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Rectangle Fill="White" Height="24" Width="2" Margin="-33,0,0,0">
<Rectangle.RenderTransform>
<RotateTransform Angle="-45"/>
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Fill="White" Height="24" Width="2" Margin="0,-3,0,0" VerticalAlignment="Center" HorizontalAlignment="Center">
<Rectangle.RenderTransform>
<RotateTransform Angle="45"/>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
-->
</Grid>
</Border>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace FancyZonesEditor
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class WindowLayout : Window
{
public WindowLayout()
{
InitializeComponent();
}
void onLoad(object sender, RoutedEventArgs e)
{
//WindowEditor window = new WindowEditor(); window.Show();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

View File

@@ -0,0 +1,704 @@
#include "pch.h"
struct FancyZones : public winrt::implements<FancyZones, IFancyZones, IFancyZonesCallback, IZoneWindowHost>
{
public:
FancyZones(HINSTANCE hinstance, IFancyZonesSettings* settings) noexcept
: m_hinstance(hinstance)
{
m_settings.attach(settings);
m_settings->SetCallback(this);
}
// IFancyZones
IFACEMETHODIMP_(void) Run() noexcept;
IFACEMETHODIMP_(void) Destroy() noexcept;
// IFancyZonesCallback
IFACEMETHODIMP_(bool) InMoveSize() noexcept { std::shared_lock readLock(m_lock); return m_inMoveSize; }
IFACEMETHODIMP_(void) MoveSizeStart(HWND window, HMONITOR monitor, POINT const& ptScreen) noexcept;
IFACEMETHODIMP_(void) MoveSizeUpdate(HMONITOR monitor, POINT const& ptScreen) noexcept;
IFACEMETHODIMP_(void) MoveSizeEnd(HWND window, POINT const& ptScreen) noexcept;
IFACEMETHODIMP_(void) VirtualDesktopChanged() noexcept;
IFACEMETHODIMP_(void) WindowCreated(HWND window) noexcept;
IFACEMETHODIMP_(bool) OnKeyDown(PKBDLLHOOKSTRUCT info) noexcept;
IFACEMETHODIMP_(void) ToggleEditor() noexcept;
// IZoneWindowHost
IFACEMETHODIMP_(void) ToggleZoneViewers() noexcept;
IFACEMETHODIMP_(void) MoveWindowsOnActiveZoneSetChange() noexcept;
IFACEMETHODIMP_(COLORREF) GetZoneHighlightColor() noexcept
{
// Skip the leading # and convert to long
const auto color = m_settings->GetSettings().zoneHightlightColor;
const auto tmp = std::stol(color.substr(1), nullptr, 16);
const auto nR = (tmp & 0xFF0000) >> 16;
const auto nG = (tmp & 0xFF00) >> 8;
const auto nB = (tmp & 0xFF);
return RGB(nR, nG, nB);
}
LRESULT WndProc(HWND, UINT, WPARAM, LPARAM) noexcept;
void OnDisplayChange(DisplayChangeType changeType) noexcept;
void ShowZoneEditorForMonitor(HMONITOR monitor) noexcept;
void AddZoneWindow(HMONITOR monitor, PCWSTR deviceId) noexcept;
void MoveWindowIntoZoneByIndex(HWND window, int index) noexcept;
protected:
static LRESULT CALLBACK s_WndProc(HWND, UINT, WPARAM, LPARAM) noexcept;
private:
struct require_read_lock
{
template<typename T>
require_read_lock(const std::shared_lock<T>& lock) { lock; }
template<typename T>
require_read_lock(const std::unique_lock<T>& lock) { lock; }
};
struct require_write_lock
{
template<typename T>
require_write_lock(const std::unique_lock<T>& lock) { lock; }
};
void UpdateZoneWindows() noexcept;
void MoveWindowsOnDisplayChange() noexcept;
void UpdateDragState(require_write_lock) noexcept;
void CycleActiveZoneSet(DWORD vkCode) noexcept;
void OnSnapHotkey(DWORD vkCode) noexcept;
void MoveSizeStartInternal(HWND window, HMONITOR monitor, POINT const& ptScreen, require_write_lock) noexcept;
void MoveSizeEndInternal(HWND window, POINT const& ptScreen, require_write_lock) noexcept;
void MoveSizeUpdateInternal(HMONITOR monitor, POINT const& ptScreen, require_write_lock) noexcept;
const HINSTANCE m_hinstance{};
mutable std::shared_mutex m_lock;
HWND m_window{};
HWND m_windowMoveSize{}; // The window that is being moved/sized
bool m_editorsVisible{}; // Are we showing the zone editors?
bool m_inMoveSize{}; // Whether or not a move/size operation is currently active
bool m_dragEnabled{}; // True if we should be showing zone hints while dragging
std::map<HMONITOR, winrt::com_ptr<IZoneWindow>> m_zoneWindowMap; // Map of monitor to ZoneWindow (one per monitor)
winrt::com_ptr<IZoneWindow> m_zoneWindowMoveSize; // "Active" ZoneWindow, where the move/size is happening. Will update as drag moves between monitors.
winrt::com_ptr<IFancyZonesSettings> m_settings;
GUID m_currentVirtualDesktopId{};
wil::unique_handle m_terminateEditorEvent;
static UINT WM_PRIV_VDCHANGED;
static UINT WM_PRIV_EDITOR;
enum class EditorExitKind : byte
{
Exit,
Terminate
};
};
UINT FancyZones::WM_PRIV_VDCHANGED = RegisterWindowMessage(L"{128c2cb0-6bdf-493e-abbe-f8705e04aa95}");
UINT FancyZones::WM_PRIV_EDITOR = RegisterWindowMessage(L"{87543824-7080-4e91-9d9c-0404642fc7b6}");
// IFancyZones
IFACEMETHODIMP_(void) FancyZones::Run() noexcept
{
std::unique_lock writeLock(m_lock);
WNDCLASSEXW wcex{};
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.lpfnWndProc = s_WndProc;
wcex.hInstance = m_hinstance;
wcex.lpszClassName = L"SuperFancyZones";
RegisterClassExW(&wcex);
BufferedPaintInit();
m_window = CreateWindowExW(WS_EX_TOOLWINDOW, L"SuperFancyZones", L"", WS_POPUP, 0, 0, 0, 0, nullptr, nullptr, m_hinstance, this);
if (!m_window) return;
RegisterHotKey(m_window, 1, MOD_WIN, VK_OEM_3);
VirtualDesktopChanged();
}
// IFancyZones
IFACEMETHODIMP_(void) FancyZones::Destroy() noexcept
{
std::unique_lock writeLock(m_lock);
BufferedPaintUnInit();
if (m_window)
{
DestroyWindow(m_window);
m_window = nullptr;
}
}
// IFancyZonesCallback
IFACEMETHODIMP_(void) FancyZones::MoveSizeStart(HWND window, HMONITOR monitor, POINT const& ptScreen) noexcept
{
std::unique_lock writeLock(m_lock);
MoveSizeStartInternal(window, monitor, ptScreen, writeLock);
}
// IFancyZonesCallback
IFACEMETHODIMP_(void) FancyZones::MoveSizeUpdate(HMONITOR monitor, POINT const& ptScreen) noexcept
{
std::unique_lock writeLock(m_lock);
MoveSizeUpdateInternal(monitor, ptScreen, writeLock);
}
// IFancyZonesCallback
IFACEMETHODIMP_(void) FancyZones::MoveSizeEnd(HWND window, POINT const& ptScreen) noexcept
{
std::unique_lock writeLock(m_lock);
MoveSizeEndInternal(window, ptScreen, writeLock);
}
// IFancyZonesCallback
IFACEMETHODIMP_(void) FancyZones::VirtualDesktopChanged() noexcept
{
// VirtualDesktopChanged is called from another thread but results in new windows being created.
// Jump over to the UI thread to handle it.
PostMessage(m_window, WM_PRIV_VDCHANGED, 0, 0);
}
// IFancyZonesCallback
IFACEMETHODIMP_(void) FancyZones::WindowCreated(HWND window) noexcept
{
if (m_settings->GetSettings().appLastZone_moveWindows)
{
wchar_t processPath[MAX_PATH] = { 0 };
DWORD modulePathSize = GetProcessPath(window, processPath, static_cast<DWORD>(MAX_PATH));
if (modulePathSize > 0)
{
INT zoneIndex = -1;
LRESULT res = RegistryHelpers::GetAppLastZone(window, processPath, &zoneIndex);
if ((res == ERROR_SUCCESS) && (zoneIndex != -1))
{
MoveWindowIntoZoneByIndex(window, zoneIndex);
}
}
}
}
// IFancyZonesCallback
IFACEMETHODIMP_(bool) FancyZones::OnKeyDown(PKBDLLHOOKSTRUCT info) noexcept
{
// Return true to swallow the keyboard event
bool const shift = GetAsyncKeyState(VK_SHIFT) & 0x8000;
bool const win = GetAsyncKeyState(VK_LWIN) & 0x8000;
if (win && !shift)
{
if (!m_settings->GetSettings().overrideSnapHotkeys)
{
return false;
}
bool const ctrl = GetAsyncKeyState(VK_CONTROL) & 0x8000;
if (ctrl)
{
if ((info->vkCode >= '0') && (info->vkCode <= '9'))
{
Trace::FancyZones::OnKeyDown(info->vkCode, win, ctrl, false /* inMoveSize */);
CycleActiveZoneSet(info->vkCode);
return true;
}
}
else if ((info->vkCode == VK_RIGHT) || (info->vkCode == VK_LEFT))
{
Trace::FancyZones::OnKeyDown(info->vkCode, win, ctrl, false /* inMoveSize */);
OnSnapHotkey(info->vkCode);
return true;
}
}
else if (m_inMoveSize && (info->vkCode >= '0') && (info->vkCode <= '9'))
{
Trace::FancyZones::OnKeyDown(info->vkCode, win, false /* control */, true/* inMoveSize */);
CycleActiveZoneSet(info->vkCode);
return true;
}
return false;
}
// IFancyZonesCallback
void FancyZones::ToggleEditor() noexcept
{
{
std::shared_lock readLock(m_lock);
if (m_terminateEditorEvent)
{
SetEvent(m_terminateEditorEvent.get());
return;
}
}
{
std::unique_lock writeLock(m_lock);
m_terminateEditorEvent.reset(CreateEvent(nullptr, true, false, nullptr));
}
// TODO: multimon support
// Pass in args so that the editor shows up on the correct monitor
// This can be an HWND, HMONITOR, or the X/Y/Width/Height of the monitor's work area, (whichever works best).
if (const HMONITOR monitor = MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY))
{
std::shared_lock readLock(m_lock);
auto iter = m_zoneWindowMap.find(monitor);
if (iter != m_zoneWindowMap.end())
{
// Pass command line args to the editor to tell it which layout it should pick by default
auto activeZoneSet = iter->second->ActiveZoneSet();
std::wstring params = iter->second->UniqueId() + L" " + std::to_wstring(activeZoneSet->LayoutId());
SHELLEXECUTEINFO sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
sei.lpFile = L"modules\\FancyZonesEditor.exe";
sei.lpParameters = params.c_str();
sei.nShow = SW_SHOWNORMAL;
ShellExecuteEx(&sei);
// Launch the editor on a background thread
// Wait for the editor's process to exit
// Post back to the main thread to update
std::thread waitForEditorThread([window = m_window, processHandle = sei.hProcess, terminateEditorEvent = m_terminateEditorEvent.get()]()
{
HANDLE waitEvents[2] = { processHandle, terminateEditorEvent };
auto result = WaitForMultipleObjects(2, waitEvents, false, INFINITE);
if (result == WAIT_OBJECT_0 + 0)
{
// Editor exited
// Update any changes it may have made
PostMessage(window, WM_PRIV_EDITOR, 0, static_cast<LPARAM>(EditorExitKind::Exit));
}
else if (result == WAIT_OBJECT_0 + 1)
{
// User hit Win+~ while editor is already running
// Shut it down
TerminateProcess(processHandle, 2);
PostMessage(window, WM_PRIV_EDITOR, 0, static_cast<LPARAM>(EditorExitKind::Terminate));
}
CloseHandle(processHandle);
});
waitForEditorThread.detach();
}
}
}
// IZoneWindowHost
IFACEMETHODIMP_(void) FancyZones::ToggleZoneViewers() noexcept
{
bool alreadyVisible{};
{
std::unique_lock writeLock(m_lock);
alreadyVisible = m_editorsVisible;
m_editorsVisible = !alreadyVisible;
}
Trace::FancyZones::ToggleZoneViewers(!alreadyVisible);
if (!alreadyVisible)
{
auto callback = [](HMONITOR monitor, HDC, RECT *, LPARAM data) -> BOOL
{
auto strongThis = reinterpret_cast<FancyZones*>(data);
strongThis->ShowZoneEditorForMonitor(monitor);
return TRUE;
};
EnumDisplayMonitors(nullptr, nullptr, callback, reinterpret_cast<LPARAM>(this));
}
else
{
std::shared_lock readLock(m_lock);
for (auto iter : m_zoneWindowMap)
{
iter.second->HideZoneWindow();
}
}
}
// IZoneWindowHost
IFACEMETHODIMP_(void) FancyZones::MoveWindowsOnActiveZoneSetChange() noexcept
{
if (m_settings->GetSettings().zoneSetChange_moveWindows)
{
MoveWindowsOnDisplayChange();
}
}
LRESULT FancyZones::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept
{
switch (message)
{
case WM_HOTKEY:
{
if (wparam == 1)
{
if (m_settings->GetSettings().use_standalone_editor)
{
ToggleEditor();
}
else
{
ToggleZoneViewers();
}
}
}
break;
case WM_SETTINGCHANGE:
{
if (wparam == SPI_SETWORKAREA)
{
OnDisplayChange(DisplayChangeType::WorkArea);
}
}
break;
case WM_DISPLAYCHANGE:
{
OnDisplayChange(DisplayChangeType::DisplayChange);
}
break;
default:
{
if (message == WM_PRIV_VDCHANGED)
{
OnDisplayChange(DisplayChangeType::VirtualDesktop);
}
else if (message == WM_PRIV_EDITOR)
{
if (lparam == static_cast<LPARAM>(EditorExitKind::Exit))
{
// Don't reload settings if we terminated the editor
OnDisplayChange(DisplayChangeType::Editor);
}
{
// Clean up the event either way
std::unique_lock writeLock(m_lock);
m_terminateEditorEvent.release();
}
}
else
{
return DefWindowProc(window, message, wparam, lparam);
}
}
break;
}
return 0;
}
void FancyZones::OnDisplayChange(DisplayChangeType changeType) noexcept
{
if (changeType == DisplayChangeType::VirtualDesktop)
{
// Explorer persists this value to the registry on a per session basis but only after
// the first virtual desktop switch happens. If the user hasn't switched virtual desktops in this session
// then this value will be empty. This means loading the first virtual desktop's configuration can be
// funky the first time we load up at boot since the user will not have switched virtual desktops yet.
std::shared_lock readLock(m_lock);
GUID currentVirtualDesktopId{};
if (SUCCEEDED(RegistryHelpers::GetCurrentVirtualDesktop(&currentVirtualDesktopId)))
{
m_currentVirtualDesktopId = currentVirtualDesktopId;
}
else
{
// TODO: Use the previous "Desktop 1" fallback
// Need to maintain a map of desktop name to virtual desktop uuid
}
}
UpdateZoneWindows();
if ((changeType == DisplayChangeType::WorkArea) || (changeType == DisplayChangeType::DisplayChange))
{
if (m_settings->GetSettings().displayChange_moveWindows)
{
MoveWindowsOnDisplayChange();
}
}
else if (changeType == DisplayChangeType::VirtualDesktop)
{
if (m_settings->GetSettings().virtualDesktopChange_moveWindows)
{
MoveWindowsOnDisplayChange();
}
}
else if (changeType == DisplayChangeType::Editor)
{
if (m_settings->GetSettings().zoneSetChange_moveWindows)
{
MoveWindowsOnDisplayChange();
}
}
}
void FancyZones::ShowZoneEditorForMonitor(HMONITOR monitor) noexcept
{
std::shared_lock readLock(m_lock);
auto iter = m_zoneWindowMap.find(monitor);
if (iter != m_zoneWindowMap.end())
{
bool const activate = MonitorFromPoint(POINT(), MONITOR_DEFAULTTOPRIMARY) == monitor;
iter->second->ShowZoneWindow(activate, false /*fadeIn*/);
}
}
void FancyZones::AddZoneWindow(HMONITOR monitor, PCWSTR deviceId) noexcept
{
std::unique_lock writeLock(m_lock);
wil::unique_cotaskmem_string virtualDesktopId;
if (SUCCEEDED_LOG(StringFromCLSID(m_currentVirtualDesktopId, &virtualDesktopId)))
{
const bool flash = m_settings->GetSettings().zoneSetChange_flashZones;
if (auto zoneWindow = MakeZoneWindow(this, m_hinstance, monitor, deviceId, virtualDesktopId.get(), flash))
{
m_zoneWindowMap[monitor] = std::move(zoneWindow);
}
}
}
void FancyZones::MoveWindowIntoZoneByIndex(HWND window, int index) noexcept
{
std::shared_lock readLock(m_lock);
if (window != m_windowMoveSize)
{
if (const HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL))
{
auto iter = m_zoneWindowMap.find(monitor);
if (iter != m_zoneWindowMap.end())
{
iter->second->MoveWindowIntoZoneByIndex(window, index);
}
}
}
}
LRESULT CALLBACK FancyZones::s_WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) noexcept
{
auto thisRef = reinterpret_cast<FancyZones*>(GetWindowLongPtr(window, GWLP_USERDATA));
if (!thisRef && (message == WM_CREATE))
{
const auto createStruct = reinterpret_cast<LPCREATESTRUCT>(lparam);
thisRef = reinterpret_cast<FancyZones*>(createStruct->lpCreateParams);
SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(thisRef));
}
return thisRef ? thisRef->WndProc(window, message, wparam, lparam) :
DefWindowProc(window, message, wparam, lparam);
}
void FancyZones::UpdateZoneWindows() noexcept
{
auto callback = [](HMONITOR monitor, HDC, RECT *, LPARAM data) -> BOOL
{
MONITORINFOEX mi;
mi.cbSize = sizeof(mi);
if (GetMonitorInfo(monitor, &mi))
{
DISPLAY_DEVICE displayDevice = { sizeof(displayDevice) };
PCWSTR deviceId = nullptr;
bool validMonitor = true;
if (EnumDisplayDevices(mi.szDevice, 0, &displayDevice, 1))
{
if (WI_IsFlagSet(displayDevice.StateFlags, DISPLAY_DEVICE_MIRRORING_DRIVER))
{
validMonitor = FALSE;
}
else if (displayDevice.DeviceID[0] != L'\0')
{
deviceId = displayDevice.DeviceID;
}
}
if (validMonitor)
{
if (!deviceId)
{
deviceId = GetSystemMetrics(SM_REMOTESESSION) ?
L"\\\\?\\DISPLAY#REMOTEDISPLAY#" :
L"\\\\?\\DISPLAY#LOCALDISPLAY#";
}
auto strongThis = reinterpret_cast<FancyZones*>(data);
strongThis->AddZoneWindow(monitor, deviceId);
}
}
return TRUE;
};
{
std::unique_lock writeLock(m_lock);
m_zoneWindowMap.clear();
}
EnumDisplayMonitors(nullptr, nullptr, callback, reinterpret_cast<LPARAM>(this));
}
void FancyZones::MoveWindowsOnDisplayChange() noexcept
{
auto callback = [](HWND window, LPARAM data) -> BOOL
{
int i = static_cast<int>(reinterpret_cast<UINT_PTR>(::GetProp(window, ZONE_STAMP)));
if (i != 0)
{
// i is off by 1 since 0 is special.
auto strongThis = reinterpret_cast<FancyZones*>(data);
strongThis->MoveWindowIntoZoneByIndex(window, i-1);
}
return TRUE;
};
EnumWindows(callback, reinterpret_cast<LPARAM>(this));
}
void FancyZones::UpdateDragState(require_write_lock) noexcept
{
const bool shift = GetAsyncKeyState(VK_SHIFT) & 0x8000;
m_dragEnabled = m_settings->GetSettings().shiftDrag ? shift : !shift;
}
void FancyZones::CycleActiveZoneSet(DWORD vkCode) noexcept
{
if (const HWND window = GetForegroundWindow())
{
if (const HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL))
{
std::shared_lock readLock(m_lock);
auto iter = m_zoneWindowMap.find(monitor);
if (iter != m_zoneWindowMap.end())
{
iter->second->CycleActiveZoneSet(vkCode);
}
}
}
}
void FancyZones::OnSnapHotkey(DWORD vkCode) noexcept
{
if (const HWND window = GetForegroundWindow())
{
if (const HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL))
{
std::shared_lock readLock(m_lock);
auto iter = m_zoneWindowMap.find(monitor);
if (iter != m_zoneWindowMap.end())
{
iter->second->MoveWindowIntoZoneByDirection(window, vkCode);
}
}
}
}
void FancyZones::MoveSizeStartInternal(HWND window, HMONITOR monitor, POINT const& ptScreen, require_write_lock writeLock) noexcept
{
// Only enter move/size if the cursor is in the titlebar.
// This prevents resize from triggering zones.
RECT windowRect{};
::GetWindowRect(window, &windowRect);
TITLEBARINFO titlebarInfo{ sizeof(titlebarInfo) };
::GetTitleBarInfo(window, &titlebarInfo);
// Titlebar height is weird and apps can do custom drag areas.
// Give it half of the height of the window to make sure.
titlebarInfo.rcTitleBar.bottom += ((windowRect.bottom - windowRect.top) / 2);
if (PtInRect(&titlebarInfo.rcTitleBar, ptScreen))
{
m_inMoveSize = true;
auto iter = m_zoneWindowMap.find(monitor);
if (iter != m_zoneWindowMap.end())
{
m_windowMoveSize = window;
// This updates m_dragEnabled depending on if the shift key is being held down.
UpdateDragState(writeLock);
if (m_dragEnabled)
{
m_zoneWindowMoveSize = iter->second;
m_zoneWindowMoveSize->MoveSizeEnter(window, m_dragEnabled);
}
else if (m_zoneWindowMoveSize)
{
m_zoneWindowMoveSize->MoveSizeCancel();
m_zoneWindowMoveSize = nullptr;
}
}
}
}
void FancyZones::MoveSizeEndInternal(HWND window, POINT const& ptScreen, require_write_lock) noexcept
{
m_inMoveSize = false;
m_dragEnabled = false;
m_windowMoveSize = nullptr;
if (m_zoneWindowMoveSize)
{
auto zoneWindow = std::move(m_zoneWindowMoveSize);
zoneWindow->MoveSizeEnd(window, ptScreen);
}
else
{
::RemoveProp(window, ZONE_STAMP);
wchar_t processPath[MAX_PATH]{};
DWORD processPathSize = GetProcessPath(window, processPath, static_cast<DWORD>(MAX_PATH));
if (processPathSize > 0)
{
RegistryHelpers::SaveAppLastZone(window, processPath, -1);
}
}
}
void FancyZones::MoveSizeUpdateInternal(HMONITOR monitor, POINT const& ptScreen, require_write_lock writeLock) noexcept
{
if (m_inMoveSize)
{
// This updates m_dragEnabled depending on if the shift key is being held down.
UpdateDragState(writeLock);
if (m_zoneWindowMoveSize)
{
// Update the ZoneWindow already handling move/size
if (!m_dragEnabled)
{
// Drag got disabled, tell it to cancel and clear out m_zoneWindowMoveSize
auto zoneWindow = std::move(m_zoneWindowMoveSize);
zoneWindow->MoveSizeCancel();
}
else
{
auto iter = m_zoneWindowMap.find(monitor);
if (iter != m_zoneWindowMap.end())
{
if (iter->second != m_zoneWindowMoveSize)
{
// The drag has moved to a different monitor.
auto const isDragEnabled = m_zoneWindowMoveSize->IsDragEnabled();
m_zoneWindowMoveSize->MoveSizeCancel();
m_zoneWindowMoveSize = iter->second;
m_zoneWindowMoveSize->MoveSizeEnter(m_windowMoveSize, isDragEnabled);
}
m_zoneWindowMoveSize->MoveSizeUpdate(ptScreen, m_dragEnabled);
}
}
}
else if (m_dragEnabled)
{
// We'll get here if the user presses/releases shift while dragging.
// Restart the drag on the ZoneWindow that m_windowMoveSize is on
MoveSizeStartInternal(m_windowMoveSize, monitor, ptScreen, writeLock);
MoveSizeUpdateInternal(monitor, ptScreen, writeLock);
}
}
}
winrt::com_ptr<IFancyZones> MakeFancyZones(HINSTANCE hinstance, IFancyZonesSettings* settings) noexcept
{
return winrt::make_self<FancyZones>(hinstance, settings);
}

Binary file not shown.

View File

@@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{F9C68EDF-AC74-4B77-9AF1-005D9C9F6A99}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>lib</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectName>FancyZonesLib</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalIncludeDirectories>..\;..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;..\..\..\..\deps\cpprestsdk\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpplatest</LanguageStandard>
<AdditionalIncludeDirectories>..\;..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;..\..\..\..\deps\cpprestsdk\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="FancyZones.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="RegistryHelpers.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="Settings.h" />
<ClInclude Include="trace.h" />
<ClInclude Include="util.h" />
<ClInclude Include="Zone.h" />
<ClInclude Include="ZoneSet.h" />
<ClInclude Include="ZoneWindow.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="FancyZones.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="Settings.cpp" />
<ClCompile Include="trace.cpp" />
<ClCompile Include="Zone.cpp" />
<ClCompile Include="ZoneSet.cpp" />
<ClCompile Include="ZoneWindow.cpp" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="fancyzones.rc" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.190716.2\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.190716.2\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.190716.2\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.190716.2\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Zone.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ZoneSet.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ZoneWindow.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="FancyZones.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="RegistryHelpers.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Settings.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="util.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Zone.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ZoneSet.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ZoneWindow.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="FancyZones.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Settings.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="fancyzones.rc">
<Filter>Source Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="fancyzones.def" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,164 @@
#pragma once
#include <shlwapi.h>
namespace RegistryHelpers
{
static PCWSTR REG_SETTINGS = L"Software\\SuperFancyZones";
static PCWSTR APP_ZONE_HISTORY_SUBKEY = L"AppZoneHistory";
inline PCWSTR GetKey(_In_opt_ PCWSTR monitorId, PWSTR key, size_t keyLength)
{
if (monitorId)
{
StringCchPrintf(key, keyLength, L"%s\\%s", REG_SETTINGS, monitorId);
}
else
{
StringCchPrintf(key, keyLength, L"%s", REG_SETTINGS);
}
return key;
}
inline HKEY OpenKey(_In_opt_ PCWSTR monitorId)
{
HKEY hkey;
wchar_t key[256];
GetKey(monitorId, key, ARRAYSIZE(key));
if (RegOpenKeyExW(HKEY_CURRENT_USER, key, 0, KEY_ALL_ACCESS, &hkey) == ERROR_SUCCESS)
{
return hkey;
}
return nullptr;
}
inline HKEY CreateKey(PCWSTR monitorId)
{
HKEY hkey;
wchar_t key[256]{};
GetKey(monitorId, key, ARRAYSIZE(key));
if (RegCreateKeyExW(HKEY_CURRENT_USER, key, 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, nullptr, &hkey, nullptr) == ERROR_SUCCESS)
{
return hkey;
}
return nullptr;
}
inline LSTATUS GetAppLastZone(HWND window, PCWSTR appPath, _Out_ PINT iZoneIndex)
{
*iZoneIndex = -1;
LSTATUS res{};
if (auto monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL))
{
wchar_t keyPath[256]{};
StringCchPrintf(keyPath, ARRAYSIZE(keyPath), L"%s\\%s\\%x", REG_SETTINGS, APP_ZONE_HISTORY_SUBKEY, monitor);
DWORD zoneIndex;
DWORD dataType = REG_DWORD;
DWORD dataSize = sizeof(DWORD);
res = SHRegGetUSValueW(keyPath, appPath, &dataType, &zoneIndex, &dataSize, FALSE, nullptr, 0);
if (res == ERROR_SUCCESS)
{
*iZoneIndex = static_cast<INT>(zoneIndex);
}
}
return res;
}
// Pass -1 for the zoneIndex to delete the entry from the registry
inline void SaveAppLastZone(HWND window, PCWSTR appPath, DWORD zoneIndex)
{
LSTATUS res{};
if (auto monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL))
{
wchar_t keyPath[256]{};
StringCchPrintf(keyPath, ARRAYSIZE(keyPath), L"%s\\%s\\%x", REG_SETTINGS, APP_ZONE_HISTORY_SUBKEY, monitor);
if (zoneIndex == -1)
{
SHDeleteValueW(HKEY_CURRENT_USER, keyPath, appPath);
}
else
{
SHRegSetUSValueW(keyPath, appPath, REG_DWORD, &zoneIndex, sizeof(zoneIndex), SHREGSET_FORCE_HKCU);
}
}
}
inline void GetString(PCWSTR uniqueId, PCWSTR setting, PWSTR value, DWORD cbValue)
{
wchar_t key[256]{};
GetKey(uniqueId, key, ARRAYSIZE(key));
SHRegGetUSValueW(key, setting, nullptr, value, &cbValue, FALSE, nullptr, 0);
}
inline void SetString(PCWSTR uniqueId, PCWSTR setting, PCWSTR value)
{
wchar_t key[256]{};
GetKey(uniqueId, key, ARRAYSIZE(key));
SHRegSetUSValueW(key, setting, REG_SZ, value, sizeof(value) * static_cast<DWORD>(wcslen(value)), SHREGSET_FORCE_HKCU);
}
template<typename t>
inline void GetValue(PCWSTR monitorId, PCWSTR setting, t* value, DWORD size)
{
wchar_t key[256]{};
GetKey(monitorId, key, ARRAYSIZE(key));
SHRegGetUSValueW(key, setting, nullptr, value, &size, FALSE, nullptr, 0);
}
template<typename t>
inline void SetValue(PCWSTR monitorId, PCWSTR setting, t value, DWORD size)
{
wchar_t key[256]{};
GetKey(monitorId, key, ARRAYSIZE(key));
SHRegSetUSValueW(key, setting, REG_BINARY, &value, size, SHREGSET_FORCE_HKCU);
}
inline void DeleteZoneSet(PCWSTR monitorId, GUID guid)
{
wil::unique_cotaskmem_string zoneSetId;
if (SUCCEEDED_LOG(StringFromCLSID(guid, &zoneSetId)))
{
wchar_t key[256]{};
GetKey(monitorId, key, ARRAYSIZE(key));
SHDeleteValueW(HKEY_CURRENT_USER, key, zoneSetId.get());
}
}
inline void DeleteAllZoneSets(PCWSTR monitorId)
{
wchar_t key[256]{};
GetKey(monitorId, key, ARRAYSIZE(key));
SHDeleteKey(HKEY_CURRENT_USER, key);
}
inline HRESULT GetCurrentVirtualDesktop(_Out_ GUID* id)
{
*id = GUID_NULL;
DWORD sessionId;
ProcessIdToSessionId(GetCurrentProcessId(), &sessionId);
wchar_t sessionKeyPath[256]{};
RETURN_IF_FAILED(
StringCchPrintfW(
sessionKeyPath,
ARRAYSIZE(sessionKeyPath),
L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\SessionInfo\\%d\\VirtualDesktops",
sessionId));
wil::unique_hkey key{};
GUID value{};
if (RegOpenKeyExW(HKEY_CURRENT_USER, sessionKeyPath, 0, KEY_ALL_ACCESS, &key) == ERROR_SUCCESS)
{
DWORD size = sizeof(value);
if (RegQueryValueExW(key.get(), L"CurrentVirtualDesktop", 0, nullptr, reinterpret_cast<BYTE*>(&value), &size) == ERROR_SUCCESS)
{
*id = value;
return S_OK;
}
}
return E_FAIL;
}
}

View File

@@ -0,0 +1,155 @@
#include "pch.h"
#include <common/settings_objects.h>
struct FancyZonesSettings : winrt::implements<FancyZonesSettings, IFancyZonesSettings>
{
public:
FancyZonesSettings(HINSTANCE hinstance, PCWSTR name)
: m_hinstance(hinstance)
, m_name(name)
{
LoadSettings(name, true /*fromFile*/);
}
IFACEMETHODIMP_(void) SetCallback(IFancyZonesCallback* callback) { m_callback.attach(callback); }
IFACEMETHODIMP_(bool) GetConfig(_Out_ PWSTR buffer, _Out_ int *buffer_sizeg) noexcept;
IFACEMETHODIMP_(void) SetConfig(PCWSTR config) noexcept;
IFACEMETHODIMP_(void) CallCustomAction(PCWSTR action) noexcept;
IFACEMETHODIMP_(Settings) GetSettings() noexcept { return m_settings; }
private:
void LoadSettings(PCWSTR config, bool fromFile) noexcept;
void SaveSettings() noexcept;
winrt::com_ptr<IFancyZonesCallback> m_callback;
const HINSTANCE m_hinstance;
PCWSTR m_name{};
Settings m_settings;
struct
{
PCWSTR name;
bool* value;
int resourceId;
} m_configBools[8] = {
{ L"fancyzones_shiftDrag", &m_settings.shiftDrag, IDS_SETTING_DESCRIPTION_SHIFTDRAG },
{ L"fancyzones_overrideSnapHotkeys", &m_settings.overrideSnapHotkeys, IDS_SETTING_DESCRIPTION_OVERRIDE_SNAP_HOTKEYS },
{ L"fancyzones_zoneSetChange_flashZones", &m_settings.zoneSetChange_flashZones, IDS_SETTING_DESCRIPTION_ZONESETCHANGE_FLASHZONES },
{ L"fancyzones_displayChange_moveWindows", &m_settings.displayChange_moveWindows, IDS_SETTING_DESCRIPTION_DISPLAYCHANGE_MOVEWINDOWS },
{ L"fancyzones_zoneSetChange_moveWindows", &m_settings.zoneSetChange_moveWindows, IDS_SETTING_DESCRIPTION_ZONESETCHANGE_MOVEWINDOWS },
{ L"fancyzones_virtualDesktopChange_moveWindows", &m_settings.virtualDesktopChange_moveWindows, IDS_SETTING_DESCRIPTION_VIRTUALDESKTOPCHANGE_MOVEWINDOWS },
{ L"fancyzones_appLastZone_moveWindows", &m_settings.appLastZone_moveWindows, IDS_SETTING_DESCRIPTION_APPLASTZONE_MOVEWINDOWS },
{ L"fancyzones_use_standalone_editor", &m_settings.use_standalone_editor, IDS_SETTING_DESCRIPTION_USE_STANDALONE_EDITOR },
};
struct
{
PCWSTR name;
std::wstring* value;
int resourceId;
} m_configStrings[1] = {
{ L"fancyzones_zoneHighlightColor", &m_settings.zoneHightlightColor, IDS_SETTING_DESCRIPTION_ZONEHIGHLIGHTCOLOR },
};
};
IFACEMETHODIMP_(bool) FancyZonesSettings::GetConfig(_Out_ PWSTR buffer, _Out_ int *buffer_size) noexcept
{
PowerToysSettings::Settings settings(m_hinstance, m_name);
// Pass a string literal or a resource id to Settings::set_description().
settings.set_description(IDS_SETTING_DESCRIPTION);
settings.set_icon_key(L"pt-fancy-zones");
settings.set_overview_link(L"https://github.com/microsoft/PowerToys/blob/master/src/modules/fancyzones/README.md");
settings.set_video_link(L"https://youtu.be/rTtGzZYAXgY");
// Add a custom action property. When using this settings type, the "PowertoyModuleIface::call_custom_action()"
// method should be overriden as well.
settings.add_custom_action(
L"ToggledFZEditor", // action name.
IDS_SETTING_LAUNCH_EDITOR_LABEL,
IDS_SETTING_LAUNCH_EDITOR_BUTTON,
IDS_SETTING_LAUNCH_EDITOR_DESCRIPTION
);
for (auto const& setting : m_configBools)
{
settings.add_bool_toogle(setting.name, setting.resourceId, *setting.value);
}
for (auto const& setting : m_configStrings)
{
settings.add_color_picker(setting.name, setting.resourceId, *setting.value);
}
return settings.serialize_to_buffer(buffer, buffer_size);
}
IFACEMETHODIMP_(void) FancyZonesSettings::SetConfig(PCWSTR config) noexcept try
{
LoadSettings(config, false /*fromFile*/);
SaveSettings();
Trace::SettingsChanged(m_settings);
}
CATCH_LOG();
IFACEMETHODIMP_(void) FancyZonesSettings::CallCustomAction(PCWSTR action) noexcept try
{
// Parse the action values, including name.
PowerToysSettings::CustomActionObject action_object =
PowerToysSettings::CustomActionObject::from_json_string(action);
if (action_object.get_name() == L"ToggledFZEditor")
{
m_callback->ToggleEditor();
}
}
CATCH_LOG();
void FancyZonesSettings::LoadSettings(PCWSTR config, bool fromFile) noexcept try
{
PowerToysSettings::PowerToyValues values = fromFile ?
PowerToysSettings::PowerToyValues::load_from_settings_file(m_name) :
PowerToysSettings::PowerToyValues::from_json_string(config);
for (auto const& setting : m_configBools)
{
if (values.is_bool_value(setting.name))
{
*setting.value = values.get_bool_value(setting.name);
}
}
for (auto const& setting : m_configStrings)
{
if (values.is_string_value(setting.name))
{
*setting.value = values.get_string_value(setting.name);
}
}
}
CATCH_LOG();
void FancyZonesSettings::SaveSettings() noexcept try
{
PowerToysSettings::PowerToyValues values(m_name);
for (auto const& setting : m_configBools)
{
values.add_property(setting.name, *setting.value);
}
for (auto const& setting : m_configStrings)
{
values.add_property(setting.name, *setting.value);
}
values.save_to_settings_file();
}
CATCH_LOG();
winrt::com_ptr<IFancyZonesSettings> MakeFancyZonesSettings(HINSTANCE hinstance, PCWSTR name) noexcept
{
return winrt::make_self<FancyZonesSettings>(hinstance, name);
}

View File

@@ -0,0 +1,28 @@
#pragma once
#define ZONE_STAMP L"FancyZones_zone"
struct Settings
{
// The values specified here are the defaults.
bool shiftDrag = true;
bool displayChange_moveWindows = false;
bool virtualDesktopChange_moveWindows = false;
bool zoneSetChange_flashZones = true;
bool zoneSetChange_moveWindows = false;
bool overrideSnapHotkeys = false;
bool appLastZone_moveWindows = false;
bool use_standalone_editor = true;
std::wstring zoneHightlightColor = L"#0078D7";
};
interface __declspec(uuid("{BA4E77C4-6F44-4C5D-93D3-CBDE880495C2}")) IFancyZonesSettings : public IUnknown
{
IFACEMETHOD_(void, SetCallback)(interface IFancyZonesCallback* callback) = 0;
IFACEMETHOD_(bool, GetConfig)(_Out_ PWSTR buffer, _Out_ int *buffer_size) = 0;
IFACEMETHOD_(void, SetConfig)(PCWSTR config) = 0;
IFACEMETHOD_(void, CallCustomAction)(PCWSTR action) = 0;
IFACEMETHOD_(Settings, GetSettings)() = 0;
};
winrt::com_ptr<IFancyZonesSettings> MakeFancyZonesSettings(HINSTANCE hinstance, PCWSTR config) noexcept;

View File

@@ -0,0 +1,95 @@
#include "pch.h"
struct Zone : winrt::implements<Zone, IZone>
{
public:
Zone(RECT zoneRect) :
m_zoneRect(zoneRect)
{
}
IFACEMETHODIMP_(RECT) GetZoneRect() noexcept { return m_zoneRect; }
IFACEMETHODIMP_(bool) IsEmpty() noexcept { return m_windows.empty(); };
IFACEMETHODIMP_(bool) ContainsWindow(HWND window) noexcept;
IFACEMETHODIMP_(void) AddWindowToZone(HWND window, HWND zoneWindow, bool stampZone) noexcept;
IFACEMETHODIMP_(void) RemoveWindowFromZone(HWND window, bool restoreSize) noexcept;
IFACEMETHODIMP_(void) SetId(size_t id) noexcept { m_id = id; }
IFACEMETHODIMP_(size_t) Id() noexcept { return m_id; }
private:
void SizeWindowToZone(HWND window, HWND zoneWindow) noexcept;
void StampZone(HWND window, bool stamp) noexcept;
RECT m_zoneRect{};
size_t m_id{};
std::map<HWND, RECT> m_windows{};
};
IFACEMETHODIMP_(bool) Zone::ContainsWindow(HWND window) noexcept
{
return (m_windows.find(window) != m_windows.end());
}
IFACEMETHODIMP_(void) Zone::AddWindowToZone(HWND window, HWND zoneWindow, bool stampZone) noexcept
{
WINDOWPLACEMENT placement;
::GetWindowPlacement(window, &placement);
::GetWindowRect(window, &placement.rcNormalPosition);
m_windows.emplace(std::pair<HWND, RECT>(window, placement.rcNormalPosition));
SizeWindowToZone(window, zoneWindow);
if (stampZone)
{
StampZone(window, true);
}
}
IFACEMETHODIMP_(void) Zone::RemoveWindowFromZone(HWND window, bool restoreSize) noexcept
{
auto iter = m_windows.find(window);
if (iter != m_windows.end())
{
m_windows.erase(iter);
StampZone(window, false);
}
}
void Zone::SizeWindowToZone(HWND window, HWND zoneWindow) noexcept
{
// Take care of 1px border
RECT zoneRect = m_zoneRect;
RECT windowRect{};
::GetWindowRect(window, &windowRect);
RECT frameRect{};
// Failure is expected on down level systems.
if (SUCCEEDED(DwmGetWindowAttribute(window, DWMWA_EXTENDED_FRAME_BOUNDS, &frameRect, sizeof(frameRect))))
{
zoneRect.bottom -= (frameRect.bottom - windowRect.bottom);
zoneRect.right -= (frameRect.right - windowRect.right);
zoneRect.left -= (frameRect.left - windowRect.left);
}
// Map to screen coords
MapWindowRect(zoneWindow, nullptr, &zoneRect);
::SetWindowPos(window, nullptr, zoneRect.left, zoneRect.top, zoneRect.right - zoneRect.left, zoneRect.bottom - zoneRect.top, SWP_NOZORDER | SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOSENDCHANGING);
}
void Zone::StampZone(HWND window, bool stamp) noexcept
{
if (stamp)
{
SetProp(window, ZONE_STAMP, reinterpret_cast<HANDLE>(m_id));
}
else
{
RemoveProp(window, ZONE_STAMP);
}
}
winrt::com_ptr<IZone> MakeZone(RECT zoneRect) noexcept
{
return winrt::make_self<Zone>(zoneRect);
}

View File

@@ -0,0 +1,14 @@
#pragma once
interface __declspec(uuid("{8228E934-B6EF-402A-9892-15A1441BF8B0}")) IZone : public IUnknown
{
IFACEMETHOD_(RECT, GetZoneRect)() = 0;
IFACEMETHOD_(bool, IsEmpty)() = 0;
IFACEMETHOD_(bool, ContainsWindow)(HWND window) = 0;
IFACEMETHOD_(void, AddWindowToZone)(HWND window, HWND zoneWindow, bool stampZone) = 0;
IFACEMETHOD_(void, RemoveWindowFromZone)(HWND window, bool restoreSize) = 0;
IFACEMETHOD_(void, SetId)(size_t id) = 0;
IFACEMETHOD_(size_t, Id)() = 0;
};
winrt::com_ptr<IZone> MakeZone(RECT zoneRect) noexcept;

View File

@@ -0,0 +1,385 @@
#include "pch.h"
struct ZoneSet : winrt::implements<ZoneSet, IZoneSet>
{
public:
ZoneSet(ZoneSetConfig const& config) : m_config(config)
{
if (config.ZoneCount > 0)
{
InitialPopulateZones();
}
}
ZoneSet(ZoneSetConfig const& config, std::vector<winrt::com_ptr<IZone>> zones) :
m_config(config),
m_zones(zones)
{
}
IFACEMETHODIMP_(GUID) Id() noexcept { return m_config.Id; }
IFACEMETHODIMP_(WORD) LayoutId() noexcept { return m_config.LayoutId; }
IFACEMETHODIMP AddZone(winrt::com_ptr<IZone> zone, bool front) noexcept;
IFACEMETHODIMP RemoveZone(winrt::com_ptr<IZone> zone) noexcept;
IFACEMETHODIMP_(winrt::com_ptr<IZone>) ZoneFromPoint(POINT pt) noexcept;
IFACEMETHODIMP_(winrt::com_ptr<IZone>) ZoneFromWindow(HWND window) noexcept;
IFACEMETHODIMP_(int) GetZoneIndexFromWindow(HWND window) noexcept;
IFACEMETHODIMP_(std::vector<winrt::com_ptr<IZone>>) GetZones() noexcept { return m_zones; }
IFACEMETHODIMP_(ZoneSetLayout) GetLayout() noexcept { return m_config.Layout; }
IFACEMETHODIMP_(int) GetInnerPadding() noexcept { return m_config.PaddingInner; }
IFACEMETHODIMP_(winrt::com_ptr<IZoneSet>) MakeCustomClone() noexcept;
IFACEMETHODIMP_(void) Save() noexcept;
IFACEMETHODIMP_(void) MoveZoneToFront(winrt::com_ptr<IZone> zone) noexcept;
IFACEMETHODIMP_(void) MoveZoneToBack(winrt::com_ptr<IZone> zone) noexcept;
IFACEMETHODIMP_(void) MoveWindowIntoZoneByIndex(HWND window, HWND zoneWindow, int index) noexcept;
IFACEMETHODIMP_(void) MoveWindowIntoZoneByDirection(HWND window, HWND zoneWindow, DWORD vkCode) noexcept;
IFACEMETHODIMP_(void) MoveSizeEnd(HWND window, HWND zoneWindow, POINT ptClient) noexcept;
private:
void InitialPopulateZones() noexcept;
void GenerateGridZones(MONITORINFO const& mi) noexcept;
void DoGridLayout(SIZE const& zoneArea, int numCols, int numRows) noexcept;
void GenerateFocusZones(MONITORINFO const& mi) noexcept;
void StampZone(HWND window, _In_opt_ winrt::com_ptr<IZone> zone) noexcept;
std::vector<winrt::com_ptr<IZone>> m_zones;
ZoneSetConfig m_config;
};
IFACEMETHODIMP ZoneSet::AddZone(winrt::com_ptr<IZone> zone, bool front) noexcept
{
// XXXX: need to reorder ids when inserting...
if (front)
{
m_zones.insert(m_zones.begin(), zone);
}
else
{
m_zones.emplace_back(zone);
}
// Important not to set Id 0 since we store it in the HWND using SetProp.
// SetProp(0) doesn't really work.
zone->SetId(m_zones.size());
return S_OK;
}
IFACEMETHODIMP ZoneSet::RemoveZone(winrt::com_ptr<IZone> zone) noexcept
{
auto iter = std::find(m_zones.begin(), m_zones.end(), zone);
if (iter != m_zones.end())
{
m_zones.erase(iter);
return S_OK;
}
return E_INVALIDARG;
}
IFACEMETHODIMP_(winrt::com_ptr<IZone>) ZoneSet::ZoneFromPoint(POINT pt) noexcept
{
for (auto iter = m_zones.begin(); iter != m_zones.end(); iter++)
{
if (winrt::com_ptr<IZone> zone = iter->try_as<IZone>())
{
if (PtInRect(&zone->GetZoneRect(), pt))
{
return zone;
}
}
}
return nullptr;
}
IFACEMETHODIMP_(winrt::com_ptr<IZone>) ZoneSet::ZoneFromWindow(HWND window) noexcept
{
for (auto iter = m_zones.begin(); iter != m_zones.end(); iter++)
{
if (winrt::com_ptr<IZone> zone = iter->try_as<IZone>())
{
if (zone->ContainsWindow(window))
{
return zone;
}
}
}
return nullptr;
}
IFACEMETHODIMP_(winrt::com_ptr<IZoneSet>) ZoneSet::MakeCustomClone() noexcept
{
if (SUCCEEDED_LOG(CoCreateGuid(&m_config.Id)))
{
m_config.IsCustom = true;
return winrt::make_self<ZoneSet>(m_config, m_zones);
}
return nullptr;
}
IFACEMETHODIMP_(void) ZoneSet::Save() noexcept
{
size_t const zoneCount = m_zones.size();
if (zoneCount == 0)
{
RegistryHelpers::DeleteZoneSet(m_config.ResolutionKey, m_config.Id);
}
else
{
ZoneSetPersistedData data{};
data.LayoutId = m_config.LayoutId;
data.ZoneCount = static_cast<DWORD>(zoneCount);
data.Layout = m_config.Layout;
data.PaddingInner = m_config.PaddingInner;
data.PaddingOuter = m_config.PaddingOuter;
int i = 0;
for (auto iter = m_zones.begin(); iter != m_zones.end(); iter++)
{
winrt::com_ptr<IZone> zone = iter->as<IZone>();
CopyRect(&data.Zones[i++], &zone->GetZoneRect());
}
wil::unique_cotaskmem_string guid;
if (SUCCEEDED_LOG(StringFromCLSID(m_config.Id, &guid)))
{
if (wil::unique_hkey hkey{ RegistryHelpers::CreateKey(m_config.ResolutionKey) })
{
RegSetValueExW(hkey.get(), guid.get(), 0, REG_BINARY, reinterpret_cast<BYTE*>(&data), sizeof(data));
}
}
}
}
IFACEMETHODIMP_(void) ZoneSet::MoveZoneToFront(winrt::com_ptr<IZone> zone) noexcept
{
auto iter = std::find(m_zones.begin(), m_zones.end(), zone);
if (iter != m_zones.end())
{
std::rotate(m_zones.begin(), iter, iter + 1);
}
}
IFACEMETHODIMP_(void) ZoneSet::MoveZoneToBack(winrt::com_ptr<IZone> zone) noexcept
{
auto iter = std::find(m_zones.begin(), m_zones.end(), zone);
if (iter != m_zones.end())
{
std::rotate(iter, iter + 1, m_zones.end());
}
}
IFACEMETHODIMP_(int) ZoneSet::GetZoneIndexFromWindow(HWND window) noexcept
{
int zoneIndex = 0;
for (auto iter = m_zones.begin(); iter != m_zones.end(); iter++, zoneIndex++)
{
if (winrt::com_ptr<IZone> zone = iter->try_as<IZone>())
{
if (zone->ContainsWindow(window))
{
return zoneIndex;
}
}
}
return -1;
}
IFACEMETHODIMP_(void) ZoneSet::MoveWindowIntoZoneByIndex(HWND window, HWND windowZone, int index) noexcept
{
if (index >= static_cast<int>(m_zones.size()))
{
index = 0;
}
if (index < m_zones.size())
{
if (auto zone = m_zones.at(index))
{
zone->AddWindowToZone(window, windowZone, false);
}
}
}
IFACEMETHODIMP_(void) ZoneSet::MoveWindowIntoZoneByDirection(HWND window, HWND windowZone, DWORD vkCode) noexcept
{
winrt::com_ptr<IZone> oldZone;
winrt::com_ptr<IZone> newZone;
auto iter = std::find(m_zones.begin(), m_zones.end(), ZoneFromWindow(window));
if (iter == m_zones.end())
{
iter = (vkCode == VK_RIGHT) ? m_zones.begin() : m_zones.end() - 1;
}
else if (oldZone = iter->as<IZone>())
{
if (vkCode == VK_LEFT)
{
if (iter == m_zones.begin())
{
iter = m_zones.end();
}
iter--;
}
else if (vkCode == VK_RIGHT)
{
iter++;
if (iter == m_zones.end())
{
iter = m_zones.begin();
}
}
}
if (newZone = iter->as<IZone>())
{
if (oldZone)
{
oldZone->RemoveWindowFromZone(window, false);
}
newZone->AddWindowToZone(window, windowZone, true);
}
}
IFACEMETHODIMP_(void) ZoneSet::MoveSizeEnd(HWND window, HWND zoneWindow, POINT ptClient) noexcept
{
if (auto zoneDrop = ZoneFromWindow(window))
{
zoneDrop->RemoveWindowFromZone(window, !IsZoomed(window));
}
if (auto zone = ZoneFromPoint(ptClient))
{
zone->AddWindowToZone(window, zoneWindow, true);
POINT pointAdjustedScreen = ptClient;
MapWindowPoints(zoneWindow, nullptr, &pointAdjustedScreen, 1);
SetCursorPos(pointAdjustedScreen.x, pointAdjustedScreen.y);
}
}
void ZoneSet::InitialPopulateZones() noexcept
{
// TODO: reconcile the pregenerated FZ layouts with the editor
MONITORINFO mi{};
mi.cbSize = sizeof(mi);
if (GetMonitorInfoW(m_config.Monitor, &mi))
{
if ((m_config.Layout == ZoneSetLayout::Grid) || (m_config.Layout == ZoneSetLayout::Row))
{
GenerateGridZones(mi);
}
else if (m_config.Layout == ZoneSetLayout::Focus)
{
GenerateFocusZones(mi);
}
Save();
}
}
void ZoneSet::GenerateGridZones(MONITORINFO const& mi) noexcept
{
Rect workArea(mi.rcWork);
int numCols, numRows;
if (m_config.Layout == ZoneSetLayout::Grid)
{
switch (m_config.ZoneCount)
{
case 1: numCols = 1; numRows = 1; break;
case 2: numCols = 2; numRows = 1; break;
case 3: numCols = 2; numRows = 2; break;
case 4: numCols = 2; numRows = 2; break;
case 5: numCols = 3; numRows = 3; break;
case 6: numCols = 3; numRows = 3; break;
case 7: numCols = 3; numRows = 3; break;
case 8: numCols = 3; numRows = 3; break;
case 9: numCols = 3; numRows = 3; break;
}
if ((m_config.ZoneCount == 2) && (workArea.height() > workArea.width()))
{
numCols = 1;
numRows = 2;
}
}
else if (m_config.Layout == ZoneSetLayout::Row)
{
numCols = m_config.ZoneCount;
numRows = 1;
}
SIZE const zoneArea = {
workArea.width() - ((m_config.PaddingOuter * 2) + (m_config.PaddingInner * (numCols - 1))),
workArea.height() - ((m_config.PaddingOuter * 2) + (m_config.PaddingInner * (numRows - 1)))
};
DoGridLayout(zoneArea, numCols, numRows);
}
void ZoneSet::DoGridLayout(SIZE const& zoneArea, int numCols, int numRows) noexcept
{
auto x = m_config.PaddingOuter;
auto y = m_config.PaddingOuter;
auto const zoneWidth = (zoneArea.cx / numCols);
auto const zoneHeight = (zoneArea.cy / numRows);
for (auto i = 1; i <= m_config.ZoneCount; i++)
{
auto col = numCols - (i % numCols);
RECT const zoneRect = { x, y, x + zoneWidth, y + zoneHeight };
AddZone(MakeZone(zoneRect), false);
x += zoneWidth + m_config.PaddingInner;
if (col == numCols)
{
x = m_config.PaddingOuter;
y += zoneHeight + m_config.PaddingInner;
}
}
}
void ZoneSet::GenerateFocusZones(MONITORINFO const& mi) noexcept
{
Rect const workArea(mi.rcWork);
SIZE const workHalf = { workArea.width() / 2, workArea.height() / 2 };
RECT const safeZone = {
m_config.PaddingOuter,
m_config.PaddingOuter,
workArea.width() - m_config.PaddingOuter,
workArea.height() - m_config.PaddingOuter
};
int const width = min(1920, workArea.width() * 60 / 100);
int const height = min(1200, workArea.height() * 75 / 100);
int const halfWidth = width / 2;
int const halfHeight = height / 2;
int x = workHalf.cx - halfWidth;
int y = workHalf.cy - halfHeight;
RECT const focusRect = { x, y, x + width, y + height };
AddZone(MakeZone(focusRect), false);
for (auto i = 2; i <= m_config.ZoneCount; i++)
{
switch (i)
{
case 2: x = focusRect.right - halfWidth; y = focusRect.top + m_config.PaddingInner; break; // right
case 3: x = focusRect.left - halfWidth; y = focusRect.top + (m_config.PaddingInner * 2); break; // left
case 4: x = focusRect.left + m_config.PaddingInner; y = focusRect.top - halfHeight; break; // up
case 5: x = focusRect.left - m_config.PaddingInner; y = focusRect.bottom - halfHeight; break; // down
}
// Bound into safe zone
x = min(safeZone.right - width, max(safeZone.left, x));
y = min(safeZone.bottom - height, max(safeZone.top, y));
RECT const zoneRect = { x, y, x + width, y + height };
AddZone(MakeZone(zoneRect), false);
}
}
winrt::com_ptr<IZoneSet> MakeZoneSet(ZoneSetConfig const& config) noexcept
{
return winrt::make_self<ZoneSet>(config);
}

View File

@@ -0,0 +1,79 @@
#pragma once
#include "Zone.h"
enum class ZoneSetLayout
{
Grid,
Row,
Focus,
Custom
};
interface __declspec(uuid("{E4839EB7-669D-49CF-84A9-71A2DFD851A3}")) IZoneSet : public IUnknown
{
IFACEMETHOD_(GUID, Id)() = 0;
IFACEMETHOD_(WORD, LayoutId)() = 0;
IFACEMETHOD(AddZone)(winrt::com_ptr<IZone> zone, bool front) = 0;
IFACEMETHOD(RemoveZone)(winrt::com_ptr<IZone> zone) = 0;
IFACEMETHOD_(winrt::com_ptr<IZone>, ZoneFromPoint)(POINT pt) = 0;
IFACEMETHOD_(winrt::com_ptr<IZone>, ZoneFromWindow)(HWND window) = 0;
IFACEMETHOD_(int, GetZoneIndexFromWindow)(HWND window) = 0;
IFACEMETHOD_(std::vector<winrt::com_ptr<IZone>>, GetZones)() = 0;
IFACEMETHOD_(ZoneSetLayout, GetLayout)() = 0;
IFACEMETHOD_(int, GetInnerPadding)() = 0;
IFACEMETHOD_(winrt::com_ptr<IZoneSet>, MakeCustomClone)() = 0;
IFACEMETHOD_(void, Save)() = 0;
IFACEMETHOD_(void, MoveZoneToFront)(winrt::com_ptr<IZone> zone) = 0;
IFACEMETHOD_(void, MoveZoneToBack)(winrt::com_ptr<IZone> zone) = 0;
IFACEMETHOD_(void, MoveWindowIntoZoneByIndex)(HWND window, HWND zoneWindow, int index) = 0;
IFACEMETHOD_(void, MoveWindowIntoZoneByDirection)(HWND window, HWND zoneWindow, DWORD vkCode) = 0;
IFACEMETHOD_(void, MoveSizeEnd)(HWND window, HWND zoneWindow, POINT ptClient) = 0;
};
#define VERSION_PERSISTEDDATA 0x0000F00D
struct ZoneSetPersistedData
{
DWORD Version{VERSION_PERSISTEDDATA};
WORD LayoutId{};
DWORD ZoneCount{};
ZoneSetLayout Layout{};
DWORD PaddingInner{};
DWORD PaddingOuter{};
RECT Zones[40]{};
};
struct ZoneSetConfig
{
ZoneSetConfig(
GUID id,
WORD layoutId,
HMONITOR monitor,
PCWSTR resolutionKey,
ZoneSetLayout layout,
int zoneCount,
int paddingOuter,
int paddingInner) noexcept :
Id(id),
LayoutId(layoutId),
Monitor(monitor),
ResolutionKey(resolutionKey),
Layout(layout),
ZoneCount(zoneCount),
PaddingOuter(paddingOuter),
PaddingInner(paddingInner)
{
}
GUID Id{};
WORD LayoutId{};
HMONITOR Monitor{};
PCWSTR ResolutionKey{};
ZoneSetLayout Layout{};
int ZoneCount{};
int PaddingOuter{};
int PaddingInner{};
bool IsCustom{};
};
winrt::com_ptr<IZoneSet> MakeZoneSet(ZoneSetConfig const& config) noexcept;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
#pragma once
#include "FancyZones.h"
interface __declspec(uuid("{7F017528-8110-4FB3-BE41-F472969C2560}")) IZoneWindow : public IUnknown
{
IFACEMETHOD(ShowZoneWindow)(bool activate, bool fadeIn) = 0;
IFACEMETHOD(HideZoneWindow)() = 0;
IFACEMETHOD(MoveSizeEnter)(HWND window, bool dragEnabled) = 0;
IFACEMETHOD(MoveSizeUpdate)(POINT const& ptScreen, bool dragEnabled) = 0;
IFACEMETHOD(MoveSizeEnd)(HWND window, POINT const& ptScreen) = 0;
IFACEMETHOD(MoveSizeCancel)() = 0;
IFACEMETHOD_(bool, IsDragEnabled)() = 0;
IFACEMETHOD_(void, MoveWindowIntoZoneByIndex)(HWND window, int index) = 0;
IFACEMETHOD_(void, MoveWindowIntoZoneByDirection)(HWND window, DWORD vkCode) = 0;
IFACEMETHOD_(void, CycleActiveZoneSet)(DWORD vkCode) = 0;
IFACEMETHOD_(void, SaveWindowProcessToZoneIndex)(HWND window) = 0;
IFACEMETHOD_(std::wstring, DeviceId)() = 0;
IFACEMETHOD_(std::wstring, UniqueId)() = 0;
IFACEMETHOD_(IZoneSet*, ActiveZoneSet)() = 0;
};
winrt::com_ptr<IZoneWindow> MakeZoneWindow(IZoneWindowHost* host, HINSTANCE hinstance, HMONITOR monitor,
PCWSTR deviceId, PCWSTR virtualDesktopId, bool flashZones) noexcept;

Binary file not shown.

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.190716.2" targetFramework="native" />
</packages>

View File

@@ -0,0 +1,5 @@
// pch.cpp: source file corresponding to the pre-compiled header
#include "pch.h"
// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.

View File

@@ -0,0 +1,33 @@
#pragma once
#include "resource.h"
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
#include <Unknwn.h>
#include <winrt/base.h>
#include <windows.h>
#include <windowsx.h>
#include <dwmapi.h>
#include <ProjectTelemetry.h>
#include <shellapi.h>
#include <strsafe.h>
#include <TraceLoggingActivity.h>
#include <wil\resource.h>
#include <wil\result.h>
#include <windows.foundation.h>
#include <psapi.h>
#include "trace.h"
#include "Settings.h"
#include "FancyZones.h"
#include "ZoneWindow.h"
#include "ZoneSet.h"
#include "Zone.h"
#include "util.h"
#include "RegistryHelpers.h"
#pragma comment(lib, "windowsapp")
namespace winrt
{
using namespace ::winrt;
}

View File

@@ -0,0 +1,13 @@
#define IDS_SETTING_DESCRIPTION_SHIFTDRAG 101
#define IDS_SETTING_DESCRIPTION_OVERRIDE_SNAP_HOTKEYS 102
#define IDS_SETTING_DESCRIPTION_DISPLAYCHANGE_MOVEWINDOWS 103
#define IDS_SETTING_DESCRIPTION_ZONESETCHANGE_MOVEWINDOWS 104
#define IDS_SETTING_DESCRIPTION_ZONESETCHANGE_FLASHZONES 105
#define IDS_SETTING_DESCRIPTION_VIRTUALDESKTOPCHANGE_MOVEWINDOWS 106
#define IDS_SETTING_DESCRIPTION_ZONEHIGHLIGHTCOLOR 107
#define IDS_SETTING_DESCRIPTION_APPLASTZONE_MOVEWINDOWS 108
#define IDS_SETTING_DESCRIPTION_USE_STANDALONE_EDITOR 109
#define IDS_SETTING_DESCRIPTION 110
#define IDS_SETTING_LAUNCH_EDITOR_LABEL 111
#define IDS_SETTING_LAUNCH_EDITOR_BUTTON 112
#define IDS_SETTING_LAUNCH_EDITOR_DESCRIPTION 113

View File

@@ -0,0 +1,164 @@
#include "pch.h"
#include "trace.h"
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
struct ZoneSetInfo
{
size_t NumberOfZones = 0;
size_t NumberOfWindows = 0;
ZoneSetLayout Layout = ZoneSetLayout::Custom;
};
ZoneSetInfo GetZoneSetInfo(_In_opt_ winrt::com_ptr<IZoneSet> set) noexcept
{
ZoneSetInfo info;
if (set)
{
auto zones = set->GetZones();
info.NumberOfZones = zones.size();
info.Layout = set->GetLayout();
info.NumberOfWindows = std::count_if(zones.cbegin(), zones.cend(), [&](winrt::com_ptr<IZone> zone)
{
return !zone->IsEmpty();
});
}
return info;
}
void Trace::RegisterProvider() noexcept
{
TraceLoggingRegister(g_hProvider);
}
void Trace::UnregisterProvider() noexcept
{
TraceLoggingUnregister(g_hProvider);
}
void Trace::FancyZones::EnableFancyZones(bool enabled) noexcept
{
TraceLoggingWrite(
g_hProvider,
"FancyZones::Event::EnableFancyZones",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(enabled, "Enabled"));
}
void Trace::FancyZones::ToggleZoneViewers(bool visible) noexcept
{
TraceLoggingWrite(
g_hProvider,
"FancyZones::Event::ToggleZoneViewers",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(visible, "Visible"));
}
void Trace::FancyZones::OnKeyDown(DWORD vk, bool win, bool control, bool inMoveSize) noexcept
{
TraceLoggingWrite(
g_hProvider,
"FancyZones::Event::OnKeyDown",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingValue(vk, "Hotkey"),
TraceLoggingBoolean(win, "WindowsKey"),
TraceLoggingBoolean(control, "ControlKey"),
TraceLoggingBoolean(inMoveSize, "InMoveSize"));
}
void Trace::SettingsChanged(const Settings& settings) noexcept
{
TraceLoggingWrite(
g_hProvider,
"FancyZones::Event::SettingsChanged",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(settings.shiftDrag, "Shift drag"),
TraceLoggingBoolean(settings.displayChange_moveWindows, "Move Windows On Display Change"),
TraceLoggingBoolean(settings.virtualDesktopChange_moveWindows, "Move Windows On Virtual Desktop Change"),
TraceLoggingBoolean(settings.zoneSetChange_flashZones, "Flash zones On Zone Set Change"),
TraceLoggingBoolean(settings.zoneSetChange_moveWindows, "Move Windows On Zone Set Change"),
TraceLoggingBoolean(settings.overrideSnapHotkeys, "Override snap hot keys"),
TraceLoggingWideString(settings.zoneHightlightColor.c_str(), "Zone highlight color"));
}
void Trace::VirtualDesktopChanged() noexcept
{
TraceLoggingWrite(
g_hProvider,
"FancyZones::Event::VirtualDesktopChanged",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
}
void Trace::ZoneWindow::KeyUp(WPARAM wParam, bool isEditorMode) noexcept
{
TraceLoggingWrite(
g_hProvider,
"FancyZones::Event::ZoneWindowKeyUp",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingValue(wParam, "Keyboard value"),
TraceLoggingBoolean(isEditorMode, "Editor Mode"));
}
void Trace::ZoneWindow::MoveSizeEnd(_In_opt_ winrt::com_ptr<IZoneSet> activeSet) noexcept
{
auto const zoneInfo = GetZoneSetInfo(activeSet);
TraceLoggingWrite(
g_hProvider,
"FancyZones::Event::MoveSizeEnd",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingValue(reinterpret_cast<void*>(activeSet.get()), "Active Set"),
TraceLoggingValue(zoneInfo.NumberOfZones, "NumberOfZones"),
TraceLoggingValue(zoneInfo.NumberOfWindows, "NumberOfWindows"),
TraceLoggingValue(static_cast<int>(zoneInfo.Layout), "LayoutKind"));
}
void Trace::ZoneWindow::CycleActiveZoneSet(_In_opt_ winrt::com_ptr<IZoneSet> activeSet, InputMode mode) noexcept
{
auto const zoneInfo = GetZoneSetInfo(activeSet);
TraceLoggingWrite(
g_hProvider,
"FancyZones::Event::CycleActiveZoneSet",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingValue(reinterpret_cast<void*>(activeSet.get()), "Active Set"),
TraceLoggingValue(zoneInfo.NumberOfZones, "NumberOfZones"),
TraceLoggingValue(zoneInfo.NumberOfWindows, "NumberOfWindows"),
TraceLoggingValue(static_cast<int>(zoneInfo.Layout), "LayoutKind"),
TraceLoggingValue(static_cast<int>(mode), "InputMode"));
}
void Trace::ZoneWindow::EditorModeActivity::Start() noexcept
{
m_activity = TraceLoggingActivity<g_hProvider, PROJECT_KEYWORD_MEASURE>();
TraceLoggingWriteStart(
m_activity.value(),
"FancyZones::Activity::EditorMode",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance));
}
void Trace::ZoneWindow::EditorModeActivity::Stop(_In_opt_ winrt::com_ptr<IZoneSet> activeSet) noexcept
{
auto const zoneInfo = GetZoneSetInfo(activeSet);
TraceLoggingWriteStop(
m_activity.value(),
"FancyZones::Activity::EditorMode",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingValue(reinterpret_cast<void*>(activeSet.get()), "Active Set"),
TraceLoggingValue(zoneInfo.NumberOfZones, "NumberOfZones"),
TraceLoggingValue(zoneInfo.NumberOfWindows, "NumberOfWindows"),
TraceLoggingValue(static_cast<int>(zoneInfo.Layout), "LayoutKind"));
m_activity.reset();
}

View File

@@ -0,0 +1,45 @@
#pragma once
struct Settings;
interface IZoneSet;
class Trace
{
public:
static void RegisterProvider() noexcept;
static void UnregisterProvider() noexcept;
class FancyZones
{
public:
static void EnableFancyZones(bool enabled) noexcept;
static void ToggleZoneViewers(bool visible) noexcept;
static void OnKeyDown(DWORD vkCode, bool win, bool control, bool inMoveSize) noexcept;
};
static void SettingsChanged(const Settings& settings) noexcept;
static void VirtualDesktopChanged() noexcept;
class ZoneWindow
{
public:
enum class InputMode
{
Keyboard,
Mouse
};
static void KeyUp(WPARAM wparam, bool isEditorMode) noexcept;
static void MoveSizeEnd(_In_opt_ winrt::com_ptr<IZoneSet> activeSet) noexcept;
static void CycleActiveZoneSet(_In_opt_ winrt::com_ptr<IZoneSet> activeSet, InputMode mode) noexcept;
class EditorModeActivity
{
public:
void Start() noexcept;
void Stop(_In_opt_ winrt::com_ptr<IZoneSet> activeSet) noexcept;
private:
std::optional<TraceLoggingActivity<g_hProvider, PROJECT_KEYWORD_MEASURE>> m_activity{};
};
};
};

View File

@@ -0,0 +1,152 @@
#pragma once
struct Rect
{
Rect() {}
Rect(RECT rect) : m_rect(rect)
{
}
Rect(RECT rect, UINT dpi) : m_rect(rect)
{
m_rect.right = m_rect.left + MulDiv(m_rect.right - m_rect.left, dpi, 96);
m_rect.bottom = m_rect.top + MulDiv(m_rect.bottom - m_rect.top, dpi, 96);
}
int x() const { return m_rect.left; }
int y() const { return m_rect.top; }
int width() const { return m_rect.right - m_rect.left; }
int height() const { return m_rect.bottom - m_rect.top; }
int left() const { return m_rect.left; }
int top() const { return m_rect.top; }
int right() const { return m_rect.right; }
int bottom() const { return m_rect.bottom; }
int aspectRatio() const { return MulDiv(m_rect.bottom - m_rect.top, 100, m_rect.right - m_rect.left); }
private:
RECT m_rect{};
};
inline void MakeWindowTransparent(HWND window)
{
int const pos = -GetSystemMetrics(SM_CXVIRTUALSCREEN) - 8;
if (wil::unique_hrgn hrgn{ CreateRectRgn(pos, 0, (pos + 1), 1) })
{
DWM_BLURBEHIND bh = { DWM_BB_ENABLE | DWM_BB_BLURREGION, TRUE, hrgn.get(), FALSE };
DwmEnableBlurBehindWindow(window, &bh);
}
}
inline void InitRGB(_Out_ RGBQUAD *quad, BYTE alpha, COLORREF color)
{
ZeroMemory(quad, sizeof(*quad));
quad->rgbReserved = alpha;
quad->rgbRed = GetRValue(color) * alpha / 255;
quad->rgbGreen = GetGValue(color) * alpha / 255;
quad->rgbBlue = GetBValue(color) * alpha / 255;
}
inline void FillRectARGB(wil::unique_hdc& hdc, RECT const *prcFill, BYTE alpha, COLORREF color, bool blendAlpha)
{
BITMAPINFO bi;
ZeroMemory(&bi, sizeof(bi));
bi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bi.bmiHeader.biWidth = 1;
bi.bmiHeader.biHeight = 1;
bi.bmiHeader.biPlanes = 1;
bi.bmiHeader.biBitCount = 32;
bi.bmiHeader.biCompression = BI_RGB;
RECT fillRect;
CopyRect(&fillRect, prcFill);
if ((alpha == 255) || !blendAlpha)
{
// Opaque or the caller does not want to blend the alpha
RGBQUAD bitmapBits;
InitRGB(&bitmapBits, alpha, color);
StretchDIBits(
hdc.get(),
fillRect.left,
fillRect.top,
fillRect.right - fillRect.left,
fillRect.bottom - fillRect.top,
0, 0, 1, 1, &bitmapBits, &bi, DIB_RGB_COLORS, SRCCOPY);
}
else
{
if (wil::unique_hdc hdcSrc{ CreateCompatibleDC(hdc.get()) })
{
void* pBitmapBits;
if (wil::unique_hbitmap bitmapSource{ CreateDIBSection(hdcSrc.get(), &bi, DIB_RGB_COLORS, &pBitmapBits, nullptr, 0) })
{
InitRGB(reinterpret_cast<RGBQUAD *>(pBitmapBits), alpha, color);
wil::unique_select_object bitmapOld{ SelectObject(hdcSrc.get(), bitmapSource.get()) };
BLENDFUNCTION bf = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA };
GdiAlphaBlend(
hdc.get(),
fillRect.left,
fillRect.top,
fillRect.right - fillRect.left,
fillRect.bottom - fillRect.top,
hdcSrc.get(), 0, 0, 1, 1, bf);
}
}
}
}
inline void FrameRectARGB(wil::unique_hdc& hdc, const RECT &rc, BYTE bAlpha, COLORREF clr, int thickness)
{
RECT sides[] = {
{ rc.left, rc.top, (rc.left + thickness), rc.bottom },
{ (rc.right - thickness), rc.top, rc.right, rc.bottom },
{ (rc.left + thickness), rc.top, (rc.right - thickness), (rc.top + thickness) },
{ (rc.left + thickness), (rc.bottom - thickness), (rc.right - thickness), rc.bottom }
};
for (UINT i = 0; i < ARRAYSIZE(sides); i++)
{
FillRectARGB(hdc, &(sides[i]), bAlpha, clr, false);
}
}
inline void ParseDeviceId(PCWSTR deviceId, PWSTR parsedId, size_t size)
{
// We're interested in the unique part between the first and last #'s
// Example input: \\?\DISPLAY#DELA026#5&10a58c63&0&UID16777488#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}
// Example output: DELA026#5&10a58c63&0&UID16777488
wchar_t buffer[256];
StringCchCopy(buffer, 256, deviceId);
PWSTR pszStart = wcschr(buffer, L'#');
PWSTR pszEnd = wcsrchr(buffer, L'#');
if (pszStart && pszEnd && (pszStart != pszEnd))
{
pszStart++; // skip past the first #
*pszEnd = '\0';
StringCchCopy(parsedId, size, pszStart);
}
else
{
StringCchCopy(parsedId, size, L"FallbackDevice");
}
}
inline DWORD GetProcessPath(HWND window, LPWSTR processPath, DWORD processPathMaxSize) noexcept
{
DWORD pid{};
GetWindowThreadProcessId(window, &pid);
DWORD numCopiedChars = 0;
wil::unique_handle windowProcessHandle(OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, TRUE, pid));
if (windowProcessHandle && (windowProcessHandle.get() != INVALID_HANDLE_VALUE))
{
// numCopiedChars first holds the size of processPath[], will then hold amount of characters returned by QueryFullProcessImageNameW
// if QueryFullProcessImageNameW fails, numCopiedChars will be zero.
numCopiedChars = processPathMaxSize;
QueryFullProcessImageNameW(windowProcessHandle.get(), 0, processPath, &numCopiedChars);
}
return numCopiedChars;
}

View File

@@ -0,0 +1,36 @@
#include "pch.h"
#include "lib\RegistryHelpers.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace FancyZonesUnitTests
{
TEST_CLASS(RegistryHelpersUnitTests)
{
public:
TEST_METHOD(GetDefaultKey)
{
// Test the path to the key is the same string.
wchar_t key[256];
Assert::AreEqual(0, wcscmp(RegistryHelpers::GetKey(nullptr, key, ARRAYSIZE(key)), L"Software\\SuperFancyZones"));
}
TEST_METHOD(GetKeyWithMonitor)
{
// Test the path to the key is the same string.
wchar_t key[256];
Assert::AreEqual(0, wcscmp(RegistryHelpers::GetKey(L"Monitor1", key, ARRAYSIZE(key)), L"Software\\SuperFancyZones\\Monitor1"));
}
TEST_METHOD(OpenKey)
{
// The default key should exist.
wil::unique_hkey key{ RegistryHelpers::OpenKey({}) };
Assert::IsNotNull(key.get());
// The Monitor1 key shouldn't exist.
wil::unique_hkey key2{ RegistryHelpers::OpenKey(L"Monitor1") };
Assert::IsNull(key2.get());
}
};
}

View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>UnitTests</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<ProjectSubType>NativeUnitTestProject</ProjectSubType>
<ProjectName>UnitTests-FancyZones</ProjectName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
<UseOfMfc>false</UseOfMfc>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
<UseOfMfc>false</UseOfMfc>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<SDLCheck>true</SDLCheck>
<AdditionalIncludeDirectories>..\..\..\..\common\Telemetry;..\..\..\..\;..\..\..\..\deps\cpprestsdk\include;..\..\;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<UseFullPaths>true</UseFullPaths>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpplatest</LanguageStandard>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>gdiplus.lib;dwmapi.lib;shlwapi.lib;uxtheme.lib;shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<AdditionalIncludeDirectories>..\..\..\..\common\Telemetry;..\..\..\..\;..\..\..\..\deps\cpprestsdk\include;..\..\;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<UseFullPaths>true</UseFullPaths>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<LanguageStandard>stdcpplatest</LanguageStandard>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalLibraryDirectories>$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>gdiplus.lib;dwmapi.lib;shlwapi.lib;uxtheme.lib;shcore.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="RegistryHelpers.Spec.cpp" />
<ClCompile Include="Util.Spec.cpp" />
<ClCompile Include="Zone.Spec.cpp" />
<ClCompile Include="ZoneSet.Spec.cpp" />
<ClCompile Include="ZoneWindow.Spec.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="Util.h" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\lib\FancyZonesLib.vcxproj">
<Project>{f9c68edf-ac74-4b77-9af1-005d9c9f6a99}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.190716.2\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.190716.2\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.190716.2\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\..\packages\Microsoft.Windows.ImplementationLibrary.1.0.190716.2\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ZoneSet.Spec.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Zone.Spec.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="RegistryHelpers.Spec.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Util.Spec.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ZoneWindow.Spec.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Util.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,33 @@
#include "pch.h"
#include "lib\util.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace FancyZonesUnitTests
{
TEST_CLASS(UtilUnitTests)
{
public:
TEST_METHOD(TestParseDeviceId)
{
// We're interested in the unique part between the first and last #'s
// Example input: \\?\DISPLAY#DELA026#5&10a58c63&0&UID16777488#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}
// Example output: DELA026#5&10a58c63&0&UID16777488
PCWSTR input = L"\\\\?\\DISPLAY#DELA026#5&10a58c63&0&UID16777488#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}";
wchar_t output[256]{};
ParseDeviceId(input, output, ARRAYSIZE(output));
Assert::AreEqual(0, wcscmp(output, L"DELA026#5&10a58c63&0&UID16777488"));
}
TEST_METHOD(TestParseInvalidDeviceId)
{
// We're interested in the unique part between the first and last #'s
// Example input: \\?\DISPLAY#DELA026#5&10a58c63&0&UID16777488#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}
// Example output: DELA026#5&10a58c63&0&UID16777488
PCWSTR input = L"AnInvalidDeviceId";
wchar_t output[256]{};
ParseDeviceId(input, output, ARRAYSIZE(output));
Assert::AreEqual(0, wcscmp(output, L"FallbackDevice"));
}
};
}

View File

@@ -0,0 +1,42 @@
#pragma once
namespace CustomAssert
{
static void AreEqual(const RECT& r1, const RECT& r2)
{
const bool equal = ((r1.left == r2.left) && (r1.right == r2.right) && (r1.top == r2.top) && (r1.bottom == r2.bottom));
Microsoft::VisualStudio::CppUnitTestFramework::Assert::IsTrue(equal);
}
static void AreEqual(GUID g1, GUID g2)
{
Microsoft::VisualStudio::CppUnitTestFramework::Assert::IsTrue(g1 == g2);
}
static void AreEqual(WORD w1, WORD w2)
{
Microsoft::VisualStudio::CppUnitTestFramework::Assert::IsTrue(w1 == w2);
}
}
namespace Mocks
{
static HWND Window()
{
static UINT_PTR s_nextWindow = 0;
return reinterpret_cast<HWND>(++s_nextWindow);
}
static HMONITOR Monitor()
{
static UINT_PTR s_nextMonitor = 0;
return reinterpret_cast<HMONITOR>(++s_nextMonitor);
}
static HINSTANCE Instance()
{
static UINT_PTR s_nextInstance = 0;
return reinterpret_cast<HINSTANCE>(++s_nextInstance);
}
}

View File

@@ -0,0 +1,54 @@
#include "pch.h"
#include "lib\Zone.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace FancyZonesUnitTests
{
TEST_CLASS(ZoneUnitTests)
{
public:
TEST_METHOD(TestCreateZone)
{
RECT zoneRect{ 10, 10, 200, 200 };
winrt::com_ptr<IZone> zone = MakeZone(zoneRect);
Assert::IsNotNull(&zone);
CustomAssert::AreEqual(zoneRect, zone->GetZoneRect());
constexpr size_t id = 10;
zone->SetId(id);
Assert::AreEqual(zone->Id(), id);
}
TEST_METHOD(ContainsWindow)
{
RECT zoneRect{ 10, 10, 200, 200 };
winrt::com_ptr<IZone> zone = MakeZone(zoneRect);
HWND newWindow = Mocks::Window();
Assert::IsFalse(zone->ContainsWindow(newWindow));
}
TEST_METHOD(TestAddRemoveWindow)
{
RECT zoneRect{ 10, 10, 200, 200 };
winrt::com_ptr<IZone> zone = MakeZone(zoneRect);
HWND newWindow = Mocks::Window();
Assert::IsFalse(zone->ContainsWindow(newWindow));
zone->AddWindowToZone(newWindow, Mocks::Window(), true);
Assert::IsTrue(zone->ContainsWindow(newWindow));
zone->RemoveWindowFromZone(newWindow, false);
Assert::IsFalse(zone->ContainsWindow(newWindow));
}
TEST_METHOD(TestRemoveInvalidWindow)
{
RECT zoneRect{ 10, 10, 200, 200 };
winrt::com_ptr<IZone> zone = MakeZone(zoneRect);
HWND newWindow = Mocks::Window();
zone->RemoveWindowFromZone(newWindow, false);
}
};
}

View File

@@ -0,0 +1,336 @@
#include "pch.h"
#include "lib\ZoneSet.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace FancyZonesUnitTests
{
TEST_CLASS(ZoneSetUnitTests)
{
public:
TEST_METHOD(TestCreateZoneSet)
{
GUID zoneSetId{};
CoCreateGuid(&zoneSetId);
constexpr size_t zoneCount = 0;
constexpr WORD layoutId = 0xFFFF;
constexpr int outerPadding = 3;
constexpr int innerPadding = 4;
ZoneSetConfig config(zoneSetId, layoutId, Mocks::Monitor(), L"WorkAreaIn", ZoneSetLayout::Grid, zoneCount, outerPadding, innerPadding);
winrt::com_ptr<IZoneSet> set = MakeZoneSet(config);
Assert::IsNotNull(&set);
CustomAssert::AreEqual(set->Id(), zoneSetId);
CustomAssert::AreEqual(set->LayoutId(), layoutId);
Assert::IsTrue(set->GetLayout() == ZoneSetLayout::Grid);
Assert::AreEqual(set->GetZones().size(), zoneCount);
Assert::AreEqual(set->GetInnerPadding(), innerPadding);
}
TEST_METHOD(TestAddZone)
{
ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn", ZoneSetLayout::Grid, 0, 3, 4);
winrt::com_ptr<IZoneSet> set = MakeZoneSet(config);
// Add a zone
{
winrt::com_ptr<IZone> zone = MakeZone({ 0, 0, 100, 100 });
set->AddZone(zone, false /*front*/);
auto zones = set->GetZones();
Assert::IsTrue(zones.size() == 1);
Assert::IsTrue(zones[0] == zone);
Assert::IsTrue(zone->Id() == 1);
}
// Add a second zone at the back.
{
winrt::com_ptr<IZone> zone = MakeZone({ 0, 0, 100, 100 });
set->AddZone(zone, false /*front*/);
auto zones = set->GetZones();
Assert::IsTrue(zones.size() == 2);
Assert::IsTrue(zones[1] == zone);
Assert::IsTrue(zone->Id() == 2);
}
}
TEST_METHOD(TestAddZoneFront)
{
ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn", ZoneSetLayout::Grid, 0, 3, 4);
winrt::com_ptr<IZoneSet> set = MakeZoneSet(config);
// Add a zone.
{
winrt::com_ptr<IZone> zone = MakeZone({ 0, 0, 100, 100 });
set->AddZone(zone, false /*front*/);
auto zones = set->GetZones();
Assert::IsTrue(zones.size() == 1);
Assert::IsTrue(zones[0] == zone);
Assert::IsTrue(zone->Id() == 1);
}
// Add a second zone at the front.
{
winrt::com_ptr<IZone> zone = MakeZone({ 0, 0, 100, 100 });
set->AddZone(zone, true /*front*/);
auto zones = set->GetZones();
Assert::IsTrue(zones.size() == 2);
Assert::IsTrue(zones[0] == zone);
Assert::IsTrue(zone->Id() == 2);
}
}
TEST_METHOD(TestRemoveZone)
{
ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn", ZoneSetLayout::Grid, 0, 3, 4);
winrt::com_ptr<IZoneSet> set = MakeZoneSet(config);
// Add a zone.
winrt::com_ptr<IZone> zone = MakeZone({ 0, 0, 100, 100 });
set->AddZone(zone, false /*front*/);
// And remove it.
set->RemoveZone(zone);
Assert::IsTrue(set->GetZones().size() == 0);
}
TEST_METHOD(TestRemoveInvalidZone)
{
ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn", ZoneSetLayout::Grid, 0, 3, 4);
winrt::com_ptr<IZoneSet> set = MakeZoneSet(config);
winrt::com_ptr<IZone> zone = MakeZone({ 0, 0, 100, 100 });
Assert::AreEqual(set->RemoveZone(zone), E_INVALIDARG);
}
TEST_METHOD(TestMoveZoneToFront)
{
ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn", ZoneSetLayout::Grid, 0, 3, 4);
winrt::com_ptr<IZoneSet> set = MakeZoneSet(config);
// Add a couple of zones.
winrt::com_ptr<IZone> zone1 = MakeZone({ 0, 0, 100, 100 });
winrt::com_ptr<IZone> zone2 = MakeZone({ 0, 0, 100, 100 });
winrt::com_ptr<IZone> zone3 = MakeZone({ 0, 0, 100, 100 });
set->AddZone(zone1, false /*front*/);
set->AddZone(zone2, false /*front*/);
set->AddZone(zone3, false /*front*/);
// And move it to the back.
set->MoveZoneToFront(zone3);
auto zones = set->GetZones();
Assert::IsTrue(zones.size() == 3);
Assert::IsTrue(zones[0] == zone3);
Assert::IsTrue(zones[1] == zone1);
Assert::IsTrue(zones[2] == zone2);
}
TEST_METHOD(TestMoveZoneToFrontWithInvalidZone)
{
ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn", ZoneSetLayout::Grid, 0, 3, 4);
winrt::com_ptr<IZoneSet> set = MakeZoneSet(config);
// Add a couple of zones.
winrt::com_ptr<IZone> zone1 = MakeZone({ 0, 0, 100, 100 });
winrt::com_ptr<IZone> zone2 = MakeZone({ 0, 0, 100, 100 });
winrt::com_ptr<IZone> zone3 = MakeZone({ 0, 0, 100, 100 });
set->AddZone(zone1, false /*front*/);
set->AddZone(zone2, false /*front*/);
set->AddZone(zone3, false /*front*/);
// Create an invalid zone and try to move it.
winrt::com_ptr<IZone> invalidZone = MakeZone({ 0, 0, 100, 100 });
set->MoveZoneToFront(invalidZone);
auto zones = set->GetZones();
Assert::IsTrue(zones.size() == 3);
Assert::IsTrue(zones[0] == zone1);
Assert::IsTrue(zones[1] == zone2);
Assert::IsTrue(zones[2] == zone3);
}
TEST_METHOD(TestMoveZoneToBack)
{
ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn", ZoneSetLayout::Grid, 0, 3, 4);
winrt::com_ptr<IZoneSet> set = MakeZoneSet(config);
// Add a couple of zones.
winrt::com_ptr<IZone> zone1 = MakeZone({ 0, 0, 100, 100 });
winrt::com_ptr<IZone> zone2 = MakeZone({ 0, 0, 100, 100 });
winrt::com_ptr<IZone> zone3 = MakeZone({ 0, 0, 100, 100 });
set->AddZone(zone1, false /*front*/);
set->AddZone(zone2, false /*front*/);
set->AddZone(zone3, false /*front*/);
// And move it to the back.
set->MoveZoneToBack(zone1);
auto zones = set->GetZones();
Assert::IsTrue(zones.size() == 3);
Assert::IsTrue(zones[0] == zone2);
Assert::IsTrue(zones[1] == zone3);
Assert::IsTrue(zones[2] == zone1);
}
TEST_METHOD(TestMoveZoneToBackWithInvalidZone)
{
ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn", ZoneSetLayout::Grid, 0, 3, 4);
winrt::com_ptr<IZoneSet> set = MakeZoneSet(config);
// Add a couple of zones.
winrt::com_ptr<IZone> zone1 = MakeZone({ 0, 0, 100, 100 });
winrt::com_ptr<IZone> zone2 = MakeZone({ 0, 0, 100, 100 });
winrt::com_ptr<IZone> zone3 = MakeZone({ 0, 0, 100, 100 });
set->AddZone(zone1, false /*front*/);
set->AddZone(zone2, false /*front*/);
set->AddZone(zone3, false /*front*/);
// Create an invalid zone and try to move it.
winrt::com_ptr<IZone> invalidZone = MakeZone({ 0, 0, 100, 100 });
set->MoveZoneToBack(invalidZone);
auto zones = set->GetZones();
Assert::IsTrue(zones.size() == 3);
Assert::IsTrue(zones[0] == zone1);
Assert::IsTrue(zones[1] == zone2);
Assert::IsTrue(zones[2] == zone3);
}
TEST_METHOD(TestMoveWindowIntoZoneByIndex)
{
ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn", ZoneSetLayout::Grid, 0, 3, 4);
winrt::com_ptr<IZoneSet> set = MakeZoneSet(config);
// Add a couple of zones.
winrt::com_ptr<IZone> zone1 = MakeZone({ 0, 0, 100, 100 });
winrt::com_ptr<IZone> zone2 = MakeZone({ 0, 0, 100, 100 });
winrt::com_ptr<IZone> zone3 = MakeZone({ 0, 0, 100, 100 });
set->AddZone(zone1, false /*front*/);
set->AddZone(zone2, false /*front*/);
set->AddZone(zone3, false /*front*/);
HWND window = Mocks::Window();
set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 1);
Assert::IsFalse(zone1->ContainsWindow(window));
Assert::IsTrue(zone2->ContainsWindow(window));
Assert::IsFalse(zone3->ContainsWindow(window));
}
TEST_METHOD(TestMoveWindowIntoZoneByIndexWithNoZones)
{
ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn", ZoneSetLayout::Grid, 0, 3, 4);
winrt::com_ptr<IZoneSet> set = MakeZoneSet(config);
// Add a couple of zones.
HWND window = Mocks::Window();
set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 0);
}
TEST_METHOD(TestMoveWindowIntoZoneByIndexWithInvalidIndex)
{
ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn", ZoneSetLayout::Grid, 0, 3, 4);
winrt::com_ptr<IZoneSet> set = MakeZoneSet(config);
// Add a couple of zones.
winrt::com_ptr<IZone> zone1 = MakeZone({ 0, 0, 100, 100 });
winrt::com_ptr<IZone> zone2 = MakeZone({ 0, 0, 100, 100 });
winrt::com_ptr<IZone> zone3 = MakeZone({ 0, 0, 100, 100 });
set->AddZone(zone1, false /*front*/);
set->AddZone(zone2, false /*front*/);
set->AddZone(zone3, false /*front*/);
HWND window = Mocks::Window();
set->MoveWindowIntoZoneByIndex(window, Mocks::Window(), 100);
Assert::IsTrue(zone1->ContainsWindow(window));
Assert::IsFalse(zone2->ContainsWindow(window));
Assert::IsFalse(zone3->ContainsWindow(window));
}
};
// MoveWindowIntoZoneByDirection is complicated enough to warrant it's own test class
TEST_CLASS(MoveWindowIntoZoneByDirectionUnitTests)
{
winrt::com_ptr<IZoneSet> set;
winrt::com_ptr<IZone> zone1;
winrt::com_ptr<IZone> zone2;
winrt::com_ptr<IZone> zone3;
TEST_METHOD_INITIALIZE(Initialize)
{
ZoneSetConfig config({}, 0xFFFF, Mocks::Monitor(), L"WorkAreaIn", ZoneSetLayout::Grid, 0, 3, 4);
set = MakeZoneSet(config);
// Add a couple of zones.
zone1 = MakeZone({ 0, 0, 100, 100 });
zone2 = MakeZone({ 0, 0, 100, 100 });
zone3 = MakeZone({ 0, 0, 100, 100 });
set->AddZone(zone1, false /*front*/);
set->AddZone(zone2, false /*front*/);
set->AddZone(zone3, false /*front*/);
}
TEST_METHOD(MoveWindowIntoZoneByDirectionRightNoZones)
{
HWND window = Mocks::Window();
set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT);
Assert::IsTrue(zone1->ContainsWindow(window));
Assert::IsFalse(zone2->ContainsWindow(window));
Assert::IsFalse(zone3->ContainsWindow(window));
}
TEST_METHOD(MoveWindowIntoZoneByDirectionLeftNoZones)
{
HWND window = Mocks::Window();
set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT);
Assert::IsFalse(zone1->ContainsWindow(window));
Assert::IsFalse(zone2->ContainsWindow(window));
Assert::IsTrue(zone3->ContainsWindow(window));
}
TEST_METHOD(MoveWindowIntoZoneByDirectionRight)
{
HWND window = Mocks::Window();
zone1->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/);
set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT);
Assert::IsFalse(zone1->ContainsWindow(window));
Assert::IsTrue(zone2->ContainsWindow(window));
Assert::IsFalse(zone3->ContainsWindow(window));
set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT);
Assert::IsFalse(zone1->ContainsWindow(window));
Assert::IsFalse(zone2->ContainsWindow(window));
Assert::IsTrue(zone3->ContainsWindow(window));
}
TEST_METHOD(MoveWindowIntoZoneByDirectionLeft)
{
HWND window = Mocks::Window();
zone3->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/);
set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT);
Assert::IsFalse(zone1->ContainsWindow(window));
Assert::IsTrue(zone2->ContainsWindow(window));
Assert::IsFalse(zone3->ContainsWindow(window));
set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT);
Assert::IsTrue(zone1->ContainsWindow(window));
Assert::IsFalse(zone2->ContainsWindow(window));
Assert::IsFalse(zone3->ContainsWindow(window));
}
TEST_METHOD(MoveWindowIntoZoneByDirectionWrapAroundRight)
{
HWND window = Mocks::Window();
zone3->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/);
set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_RIGHT);
Assert::IsTrue(zone1->ContainsWindow(window));
Assert::IsFalse(zone2->ContainsWindow(window));
Assert::IsFalse(zone3->ContainsWindow(window));
}
TEST_METHOD(MoveWindowIntoZoneByDirectionWrapAroundLeft)
{
HWND window = Mocks::Window();
zone1->AddWindowToZone(window, Mocks::Window(), false /*stampZone*/);
set->MoveWindowIntoZoneByDirection(window, Mocks::Window(), VK_LEFT);
Assert::IsFalse(zone1->ContainsWindow(window));
Assert::IsFalse(zone2->ContainsWindow(window));
Assert::IsTrue(zone3->ContainsWindow(window));
}
};
}

View File

@@ -0,0 +1,45 @@
#include "pch.h"
#include "lib\ZoneSet.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;
namespace FancyZonesUnitTests
{
TEST_CLASS(ZoneWindowUnitTests)
{
public:
TEST_METHOD(TestCreateZoneWindow)
{
winrt::com_ptr<IZoneWindow> zoneWindow = MakeZoneWindow(nullptr, Mocks::Instance(), Mocks::Monitor(), L"DeviceId", L"MyVirtualDesktopId", false);
Assert::IsNotNull(zoneWindow.get());
}
TEST_METHOD(TestDeviceId)
{
// Window initialization requires a valid HMONITOR - just use the primary for now.
HMONITOR pimaryMonitor = MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY);
winrt::com_ptr<IZoneWindow> zoneWindow = MakeZoneWindow(nullptr, Mocks::Instance(), pimaryMonitor, L"SomeRandomValue", L"MyVirtualDesktopId", false);
// We have no way to test the correctness, just do our best and check its not an empty string.
Assert::IsTrue(zoneWindow->DeviceId().size() > 0);
}
TEST_METHOD(TestUniqueId)
{
// Unique id of the format "ParsedMonitorDeviceId_MonitorWidth_MonitorHeight_VirtualDesktopId
// Example: "DELA026#5&10a58c63&0&UID16777488_1024_768_MyVirtualDesktopId"
std::wstring deviceId(L"\\\\?\\DISPLAY#DELA026#5&10a58c63&0&UID16777488#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}");
// Window initialization requires a valid HMONITOR - just use the primary for now.
HMONITOR pimaryMonitor = MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY);
MONITORINFO info;
info.cbSize = sizeof(info);
Assert::IsTrue(GetMonitorInfo(pimaryMonitor, &info));
Rect monitorRect = Rect(info.rcMonitor);
std::wstringstream ss;
ss << L"DELA026#5&10a58c63&0&UID16777488_" << monitorRect.width() << "_" << monitorRect.height() << "_MyVirtualDesktopId";
winrt::com_ptr<IZoneWindow> zoneWindow = MakeZoneWindow(nullptr, Mocks::Instance(), pimaryMonitor, deviceId.c_str(), L"MyVirtualDesktopId", false);
Assert::AreEqual(zoneWindow->UniqueId().compare(ss.str()), 0);
}
};
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.190716.2" targetFramework="native" />
</packages>

View File

@@ -0,0 +1,5 @@
// pch.cpp: source file corresponding to the pre-compiled header
#include "pch.h"
// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.

View File

@@ -0,0 +1,18 @@
// pch.h: This is a precompiled header file.
// Files listed below are compiled only once, improving build performance for future builds.
// This also affects IntelliSense performance, including code completion and many code browsing features.
// However, files listed here are ALL re-compiled if any one of them is updated between builds.
// Do not add files here that you will be updating frequently as this negates the performance advantage.
#ifndef PCH_H
#define PCH_H
// add headers that you want to pre-compile here
#include <Windows.h>
#include <winrt\base.h>
#include "lib\pch.h"
#include "CppUnitTest.h"
#include "Util.h"
#endif //PCH_H