Skip to content

Commit

Permalink
Windows snap hotkeys to move windows between screens (#1603)
Browse files Browse the repository at this point in the history
* When moving window into zones using arrow keys, support multi-monitor scenario

* Minor coding style adjustments

* Split implementation into separate functions because of readability

* Rename certain arguments

* Modify unit tests after API changes

* Address PR comments and add unit tests

* Return true from MoveWindowIntoZoneByDirection only if window is successfully added to new zone

* Improved monitor ordering (#1)

* Implemented improved monitor ordering v1

* Fixed some embarrassing bugs, added some tests

* Added one more test

* Extracted a value to a variable

* ASCII art in unit test comments describing monitor layouts

* Removed empty line for consistency

* Update comment to match the code

* Refactored tests, added tests for X,Y offsets

Co-authored-by: Ivan Stošić <ivan100sic@gmail.com>
  • Loading branch information
vldmr11080 and ivan100sic committed Mar 24, 2020
1 parent 7c0c75c commit 9e8faca
Show file tree
Hide file tree
Showing 11 changed files with 1,228 additions and 813 deletions.
88 changes: 79 additions & 9 deletions src/modules/fancyzones/lib/FancyZones.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ struct FancyZones : public winrt::implements<FancyZones, IFancyZones, IFancyZone

void OnEditorExitEvent() noexcept;

std::vector<std::pair<HMONITOR, RECT>> GetRawMonitorData() noexcept;
std::vector<HMONITOR> GetMonitorsSorted() noexcept;
bool MoveWindowIntoZoneByDirection(HMONITOR monitor, HWND window, DWORD vkCode, bool cycle);

const HINSTANCE m_hinstance{};

HKEY m_virtualDesktopsRegKey{ nullptr };
Expand Down Expand Up @@ -828,17 +832,43 @@ bool FancyZones::OnSnapHotkey(DWORD vkCode) noexcept
auto window = GetForegroundWindow();
if (IsInterestingWindow(window))
{
const HMONITOR monitor = MonitorFromWindow(window, MONITOR_DEFAULTTONULL);
if (monitor)
const HMONITOR current = MonitorFromWindow(window, MONITOR_DEFAULTTONULL);
if (current)
{
std::shared_lock readLock(m_lock);

auto iter = m_zoneWindowMap.find(monitor);
if (iter != m_zoneWindowMap.end())
std::vector<HMONITOR> monitorInfo = GetMonitorsSorted();
if (monitorInfo.size() > 1)
{
const auto& zoneWindowPtr = iter->second;
zoneWindowPtr->MoveWindowIntoZoneByDirection(window, vkCode);
return true;
// Multi monitor environment.
auto currMonitorInfo = std::find(std::begin(monitorInfo), std::end(monitorInfo), current);
do
{
if (MoveWindowIntoZoneByDirection(*currMonitorInfo, window, vkCode, false /* cycle through zones */))
{
return true;
}
// We iterated through all zones in current monitor zone layout, move on to next one (or previous depending on direction).
if (vkCode == VK_RIGHT)
{
currMonitorInfo = std::next(currMonitorInfo);
if (currMonitorInfo == std::end(monitorInfo))
{
currMonitorInfo = std::begin(monitorInfo);
}
}
else if (vkCode == VK_LEFT)
{
if (currMonitorInfo == std::begin(monitorInfo))
{
currMonitorInfo = std::end(monitorInfo);
}
currMonitorInfo = std::prev(currMonitorInfo);
}
} while (*currMonitorInfo != current);
}
else
{
// Single monitor environment.
return MoveWindowIntoZoneByDirection(current, window, vkCode, true /* cycle through zones */);
}
}
}
Expand Down Expand Up @@ -1111,6 +1141,46 @@ void FancyZones::OnEditorExitEvent() noexcept
JSONHelpers::FancyZonesDataInstance().SaveFancyZonesData();
}

std::vector<HMONITOR> FancyZones::GetMonitorsSorted() noexcept
{
std::shared_lock readLock(m_lock);

auto monitorInfo = GetRawMonitorData();
OrderMonitors(monitorInfo);
std::vector<HMONITOR> output;
std::transform(std::begin(monitorInfo), std::end(monitorInfo), std::back_inserter(output), [](const auto& info) { return info.first; });
return output;
}

std::vector<std::pair<HMONITOR, RECT>> FancyZones::GetRawMonitorData() noexcept
{
std::shared_lock readLock(m_lock);

std::vector<std::pair<HMONITOR, RECT>> monitorInfo;
for (const auto& [monitor, window] : m_zoneWindowMap)
{
if (window->ActiveZoneSet() != nullptr)
{
MONITORINFOEX mi;
mi.cbSize = sizeof(mi);
GetMonitorInfo(monitor, &mi);
monitorInfo.push_back({ monitor, mi.rcMonitor });
}
}
return monitorInfo;
}

bool FancyZones::MoveWindowIntoZoneByDirection(HMONITOR monitor, HWND window, DWORD vkCode, bool cycle)
{
auto iter = m_zoneWindowMap.find(monitor);
if (iter != std::end(m_zoneWindowMap))
{
const auto& zoneWindowPtr = iter->second;
return zoneWindowPtr->MoveWindowIntoZoneByDirection(window, vkCode, cycle);
}
return false;
}

winrt::com_ptr<IFancyZones> MakeFancyZones(HINSTANCE hinstance, const winrt::com_ptr<IFancyZonesSettings>& settings) noexcept
{
if (!settings)
Expand Down
22 changes: 17 additions & 5 deletions src/modules/fancyzones/lib/ZoneSet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ struct ZoneSet : winrt::implements<ZoneSet, IZoneSet>
GetZones() noexcept { return m_zones; }
IFACEMETHODIMP_(void)
MoveWindowIntoZoneByIndex(HWND window, HWND zoneWindow, int index) noexcept;
IFACEMETHODIMP_(void)
MoveWindowIntoZoneByDirection(HWND window, HWND zoneWindow, DWORD vkCode) noexcept;
IFACEMETHODIMP_(bool)
MoveWindowIntoZoneByDirection(HWND window, HWND zoneWindow, DWORD vkCode, bool cycle) noexcept;
IFACEMETHODIMP_(void)
MoveWindowIntoZoneByPoint(HWND window, HWND zoneWindow, POINT ptClient) noexcept;
IFACEMETHODIMP_(bool)
Expand Down Expand Up @@ -240,12 +240,12 @@ ZoneSet::MoveWindowIntoZoneByIndex(HWND window, HWND windowZone, int index) noex
}
}

IFACEMETHODIMP_(void)
ZoneSet::MoveWindowIntoZoneByDirection(HWND window, HWND windowZone, DWORD vkCode) noexcept
IFACEMETHODIMP_(bool)
ZoneSet::MoveWindowIntoZoneByDirection(HWND window, HWND windowZone, DWORD vkCode, bool cycle) noexcept
{
if (m_zones.empty())
{
return;
return false;
}

winrt::com_ptr<IZone> oldZone = nullptr;
Expand All @@ -262,6 +262,11 @@ ZoneSet::MoveWindowIntoZoneByDirection(HWND window, HWND windowZone, DWORD vkCod
{
if (iter == m_zones.begin())
{
if (!cycle)
{
oldZone->RemoveWindowFromZone(window, false);
return false;
}
iter = m_zones.end();
}
iter--;
Expand All @@ -271,6 +276,11 @@ ZoneSet::MoveWindowIntoZoneByDirection(HWND window, HWND windowZone, DWORD vkCod
iter++;
if (iter == m_zones.end())
{
if (!cycle)
{
oldZone->RemoveWindowFromZone(window, false);
return false;
}
iter = m_zones.begin();
}
}
Expand All @@ -283,7 +293,9 @@ ZoneSet::MoveWindowIntoZoneByDirection(HWND window, HWND windowZone, DWORD vkCod
oldZone->RemoveWindowFromZone(window, false);
}
newZone->AddWindowToZone(window, windowZone, true);
return true;
}
return false;
}

IFACEMETHODIMP_(void)
Expand Down
9 changes: 7 additions & 2 deletions src/modules/fancyzones/lib/ZoneSet.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ interface __declspec(uuid("{E4839EB7-669D-49CF-84A9-71A2DFD851A3}")) IZoneSet :
* @param zoneWindow The m_window of a ZoneWindow, it's a hidden window representing the
* current monitor desktop work area.
* @param vkCode Pressed arrow key.
* @param cycle Whether we should move window to the first zone if we reached last zone in layout.
*
* @returns Boolean which is always true if cycle argument is set, otherwise indicating if there is more
* zones left in the zone layout in which window can move.
*/
IFACEMETHOD_(void, MoveWindowIntoZoneByDirection)(HWND window, HWND zoneWindow, DWORD vkCode) = 0;
IFACEMETHOD_(bool, MoveWindowIntoZoneByDirection)(HWND window, HWND zoneWindow, DWORD vkCode, bool cycle) = 0;
/**
* Assign window to the zone based on cursor coordinates.
*
Expand All @@ -75,7 +79,8 @@ interface __declspec(uuid("{E4839EB7-669D-49CF-84A9-71A2DFD851A3}")) IZoneSet :
* @param monitorInfo Information about monitor on which zone layout is applied.
* @param zoneCount Number of zones inside zone layout.
* @param spacing Spacing between zones in pixels.
* @returns Boolean if calculation was successful.
*
* @returns Boolean indicating if calculation was successful.
*/
IFACEMETHOD_(bool, CalculateZones)(MONITORINFO monitorInfo, int zoneCount, int spacing) = 0;
};
Expand Down
16 changes: 10 additions & 6 deletions src/modules/fancyzones/lib/ZoneWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,8 @@ struct ZoneWindow : public winrt::implements<ZoneWindow, IZoneWindow>
IsDragEnabled() noexcept { return m_dragEnabled; }
IFACEMETHODIMP_(void)
MoveWindowIntoZoneByIndex(HWND window, int index) noexcept;
IFACEMETHODIMP_(void)
MoveWindowIntoZoneByDirection(HWND window, DWORD vkCode) noexcept;
IFACEMETHODIMP_(bool)
MoveWindowIntoZoneByDirection(HWND window, DWORD vkCode, bool cycle) noexcept;
IFACEMETHODIMP_(void)
CycleActiveZoneSet(DWORD vkCode) noexcept;
IFACEMETHODIMP_(std::wstring)
Expand Down Expand Up @@ -466,14 +466,18 @@ ZoneWindow::MoveWindowIntoZoneByIndex(HWND window, int index) noexcept
}
}

IFACEMETHODIMP_(void)
ZoneWindow::MoveWindowIntoZoneByDirection(HWND window, DWORD vkCode) noexcept
IFACEMETHODIMP_(bool)
ZoneWindow::MoveWindowIntoZoneByDirection(HWND window, DWORD vkCode, bool cycle) noexcept
{
if (m_activeZoneSet)
{
m_activeZoneSet->MoveWindowIntoZoneByDirection(window, m_window.get(), vkCode);
SaveWindowProcessToZoneIndex(window);
if (m_activeZoneSet->MoveWindowIntoZoneByDirection(window, m_window.get(), vkCode, cycle))
{
SaveWindowProcessToZoneIndex(window);
return true;
}
}
return false;
}

IFACEMETHODIMP_(void)
Expand Down
6 changes: 5 additions & 1 deletion src/modules/fancyzones/lib/ZoneWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,12 @@ interface __declspec(uuid("{7F017528-8110-4FB3-BE41-F472969C2560}")) IZoneWindow
*
* @param window Handle of window which should be assigned to zone.
* @param vkCode Pressed arrow key.
* @param cycle Whether we should move window to the first zone if we reached last zone in layout.
*
* @returns Boolean which is always true if cycle argument is set, otherwise indicating if there is more
* zones left in the zone layout in which window can move.
*/
IFACEMETHOD_(void, MoveWindowIntoZoneByDirection)(HWND window, DWORD vkCode) = 0;
IFACEMETHOD_(bool, MoveWindowIntoZoneByDirection)(HWND window, DWORD vkCode, bool cycle) = 0;
/**
* Cycle through active zone layouts (giving hints about each layout).
*
Expand Down
83 changes: 83 additions & 0 deletions src/modules/fancyzones/lib/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,86 @@ UINT GetDpiForMonitor(HMONITOR monitor) noexcept

return (dpi == 0) ? DPIAware::DEFAULT_DPI : dpi;
}

void OrderMonitors(std::vector<std::pair<HMONITOR, RECT>>& monitorInfo)
{
const size_t nMonitors = monitorInfo.size();
// blocking[i][j] - whether monitor i blocks monitor j in the ordering, i.e. monitor i should go before monitor j
std::vector<std::vector<bool>> blocking(nMonitors, std::vector<bool>(nMonitors, false));

// blockingCount[j] - the number of monitors which block monitor j
std::vector<size_t> blockingCount(nMonitors, 0);

for (size_t i = 0; i < nMonitors; i++)
{
RECT rectI = monitorInfo[i].second;
for (size_t j = 0; j < nMonitors; j++)
{
RECT rectJ = monitorInfo[j].second;
blocking[i][j] = rectI.top < rectJ.bottom && rectI.left < rectJ.right && i != j;
if (blocking[i][j])
{
blockingCount[j]++;
}
}
}

// used[i] - whether the sorting algorithm has used monitor i so far
std::vector<bool> used(nMonitors, false);

// the sorted sequence of monitors
std::vector<std::pair<HMONITOR, RECT>> sortedMonitorInfo;

for (size_t iteration = 0; iteration < nMonitors; iteration++)
{
// Indices of candidates to become the next monitor in the sequence
std::vector<size_t> candidates;

// First, find indices of all unblocked monitors
for (size_t i = 0; i < nMonitors; i++)
{
if (blockingCount[i] == 0 && !used[i])
{
candidates.push_back(i);
}
}

// In the unlikely event that there are no unblocked monitors, declare all unused monitors as candidates.
if (candidates.empty())
{
for (size_t i = 0; i < nMonitors; i++)
{
if (!used[i])
{
candidates.push_back(i);
}
}
}

// Pick the lexicographically smallest monitor as the next one
size_t smallest = candidates[0];
for (size_t j = 1; j < candidates.size(); j++)
{
size_t current = candidates[j];

// Compare (top, left) lexicographically
if (std::tie(monitorInfo[current].second.top, monitorInfo[current].second.left)
< std::tie(monitorInfo[smallest].second.top, monitorInfo[smallest].second.left))
{
smallest = current;
}
}

used[smallest] = true;
sortedMonitorInfo.push_back(monitorInfo[smallest]);
for (size_t i = 0; i < nMonitors; i++)
{
if (blocking[smallest][i])
{
blockingCount[i]--;
}
}
}

monitorInfo = std::move(sortedMonitorInfo);
}
3 changes: 2 additions & 1 deletion src/modules/fancyzones/lib/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,5 @@ inline unsigned char OpacitySettingToAlpha(int opacity)
return static_cast<unsigned char>(opacity * 2.55);
}

UINT GetDpiForMonitor(HMONITOR monitor) noexcept;
UINT GetDpiForMonitor(HMONITOR monitor) noexcept;
void OrderMonitors(std::vector<std::pair<HMONITOR, RECT>>& monitorInfo);

0 comments on commit 9e8faca

Please sign in to comment.