1 /*
2  *  Sticky Window Snapper Class
3  *  Copyright (C) 2021 Pedro López-Cabanillas <plcl@users.sourceforge.net>
4  *  Copyright (C) 2014 mmbob (Nicholas Cook)
5  *
6  *  This program is free software: you can redistribute it and/or modify
7  *  it under the terms of the GNU General Public License as published by
8  *  the Free Software Foundation, either version 3 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU General Public License for more details.
15  *
16  *  You should have received a copy of the GNU General Public License
17  *  along with this program.  If not, see <https://www.gnu.org/licenses/>.
18  */
19 
20 #include <algorithm>
21 #include <dwmapi.h>
22 #include "winsnap.h"
23 
Edge(int position,int start,int end)24 Edge::Edge(int position, int start, int end) : Position(position), Start(start), End(end)
25 { }
26 
operator ==(const Edge & other) const27 bool Edge::operator ==(const Edge& other) const
28 {
29 	return Position == other.Position && Start == other.Start && End == other.End;
30 }
31 
HandleMessage(void * message)32 bool WinSnap::HandleMessage(void *message)
33 {
34     MSG* msg = static_cast<MSG*>(message);
35     if (this->m_enabled) {
36         this->m_window = msg->hwnd;
37         switch (msg->message)
38         {
39         case WM_SIZING:
40             return HandleSizing(*reinterpret_cast<RECT*>(msg->lParam), static_cast<int>(msg->wParam));
41 
42         case WM_MOVING:
43             return HandleMoving(*reinterpret_cast<RECT*>(msg->lParam));
44 
45         case WM_ENTERSIZEMOVE:
46             return HandleEnterSizeMove();
47 
48         case WM_EXITSIZEMOVE:
49             return HandleExitSizeMove();
50 
51         default:
52             break;
53         }
54     }
55     return false;
56 }
57 
IsEnabled() const58 bool WinSnap::IsEnabled() const
59 {
60     return m_enabled;
61 }
62 
SetEnabled(const bool enabled)63 void WinSnap::SetEnabled(const bool enabled)
64 {
65     m_enabled = enabled;
66 }
67 
HandleEnterSizeMove()68 bool WinSnap::HandleEnterSizeMove()
69 {
70     for(auto& edgeList : m_edges)
71 		edgeList.clear();
72 
73 	// Pass "this" as the parameter
74 	EnumDisplayMonitors(nullptr, nullptr, [](HMONITOR monitorHandle, HDC, LPRECT, LPARAM param) -> BOOL
75 	{
76 		MONITORINFOEX monitorInfo;
77 		monitorInfo.cbSize = sizeof(monitorInfo);
78 		if (GetMonitorInfo(monitorHandle, &monitorInfo) == 0)
79             return false;
80 
81 		// AddRectToEdges adds edges for the outside of a rectangle, which works well
82 		// for windows.  For monitors, however, we want to snap to the inside, so we
83 		// swap the opposite edges.
84 		std::swap(monitorInfo.rcWork.left, monitorInfo.rcWork.right);
85 		std::swap(monitorInfo.rcWork.top, monitorInfo.rcWork.bottom);
86         reinterpret_cast<WinSnap*>(param)->AddRectToEdges(monitorInfo.rcWork);
87 
88         return true;
89 	}, (LPARAM) this);
90 
91 
92 	struct Param
93 	{
94         WinSnap* _this;
95 		std::list<HRGN> windowRegions;
96 	};
97 
98 	Param param;
99 	param._this = this;
100 
101 	EnumWindows([](HWND windowHandle, LPARAM _param) -> BOOL
102 	{
103 		// We don't want non-application windows or application windows the user can't see.
104 		auto param = reinterpret_cast<Param*>(_param);
105         if (windowHandle == param->_this->m_window)
106             return true;
107 		if (!IsWindowVisible(windowHandle))
108             return true;
109 		if (IsIconic(windowHandle))
110             return true;
111 
112 		int styles = (int) GetWindowLongPtr(windowHandle, GWL_STYLE);
113 		if ((styles & WS_CHILD) != 0 || (styles & WS_CAPTION) == 0)
114             return true;
115 		int extendedStyles = (int) GetWindowLongPtr(windowHandle, GWL_EXSTYLE);
116         if (/*(extendedStyles & WS_EX_TOOLWINDOW) != 0 ||*/ (extendedStyles & WS_EX_NOACTIVATE) != 0)
117             return true;
118 		// Ignore the window class 'ApplicationFrameWindow'
119 		if (GetClassWord(windowHandle, GCW_ATOM) == 0xC194)
120             return true;
121 
122 		RECT thisRect;
123 		GetWindowRect(windowHandle, &thisRect);
124 		HRGN thisRegion = CreateRectRgnIndirect(&thisRect);
125 
126 		// Since EnumWindows enumerates from the highest z-order to the lowest, we
127 		// can just check to see if this window is covered by any previous ones.
128 		bool isUserVisible = true;
129         for(HRGN region : param->windowRegions)
130 		{
131 			if (CombineRgn(thisRegion, thisRegion, region, RGN_DIFF) == NULLREGION)
132 			{
133 				isUserVisible = false;
134 				break;
135 			}
136 		}
137 
138 		if (isUserVisible)
139 		{
140 			param->windowRegions.push_back(thisRegion);
141 
142 			// Maximized windows by definition cover the whole work area, so the
143 			// only snap edges they would have are on the outside of the monitor.
144 			if (!IsZoomed(windowHandle)) {
145 				RECT frame, border;
146                 if (S_OK == DwmGetWindowAttribute(windowHandle, DWMWA_EXTENDED_FRAME_BOUNDS, &frame, sizeof(RECT))) {
147                     border.left = frame.left - thisRect.left;
148                     border.top = frame.top - thisRect.top;
149                     border.right = thisRect.right - frame.right;
150                     border.bottom = thisRect.bottom - frame.bottom;
151                     thisRect.left += border.left;
152                     thisRect.top += border.top;
153                     thisRect.right -= border.right;
154                     thisRect.bottom -= border.bottom;
155                 }
156 				param->_this->AddRectToEdges(thisRect);
157 			}
158 		}
159 		else
160 			DeleteObject(thisRegion);
161 
162         return true;
163 	}, (LPARAM) &param);
164 
165     for (HRGN region : param.windowRegions)
166 	{
167 		DeleteObject(region);
168 	}
169 
170     for (auto& edgeList : m_edges)
171 	{
172 		edgeList.sort([](const Edge& _1, const Edge& _2) -> bool { return _1.Position < _2.Position; });
173 		edgeList.erase(std::unique(edgeList.begin(), edgeList.end()), edgeList.end());
174 	}
175 
176     RECT bounds, frame;
177     GetWindowRect(m_window, &bounds);
178 	GetCursorPos(&m_originalCursorOffset);
179     m_originalCursorOffset.x -= bounds.left;
180     m_originalCursorOffset.y -= bounds.top;
181 
182     if (S_OK == DwmGetWindowAttribute(m_window, DWMWA_EXTENDED_FRAME_BOUNDS, &frame, sizeof(RECT))) {
183         m_border.left = frame.left - bounds.left;
184         m_border.top = frame.top - bounds.top;
185         m_border.right = bounds.right - frame.right;
186         m_border.bottom = bounds.bottom - frame.bottom;
187     }
188 	return true;
189 }
190 
HandleExitSizeMove()191 bool WinSnap::HandleExitSizeMove()
192 {
193 	m_inProgress = false;
194 
195 	return true;
196 }
197 
HandleMoving(RECT & bounds)198 bool WinSnap::HandleMoving(RECT& bounds)
199 {
200 	// The difference between the cursor position and the top-left corner of the dragged window.
201 	// This is normally constant while dragging a window, but when near an edge that we snap to,
202 	// this changes.
203     POINT cursorOffset;
204 	GetCursorPos(&cursorOffset);
205 	cursorOffset.x -= bounds.left;
206 	cursorOffset.y -= bounds.top;
207 
208 	// While we are snapping a window, the window displayed to the user is not the "real" location
209 	// of the window, i.e. where it would be if we hadn't snapped it.
210     RECT realBounds;
211 	if (m_inProgress)
212 	{
213         POINT offsetDiff{ cursorOffset.x - m_originalCursorOffset.x, cursorOffset.y - m_originalCursorOffset.y };
214 		SetRect(&realBounds, bounds.left + offsetDiff.x, bounds.top + offsetDiff.y,
215 				bounds.right + offsetDiff.x, bounds.bottom + offsetDiff.y);
216 	}
217 	else
218 		realBounds = bounds;
219 
220     int boundsEdges[Side::Count]{ realBounds.left, realBounds.top, realBounds.right, realBounds.bottom };
221     int snapEdges[Side::Count]{ SHRT_MIN, SHRT_MIN, SHRT_MAX, SHRT_MAX };
222     bool snapDirections[Side::Count]{ false, false, false, false };
223 
224 	for (int i = 0; i < Side::Count; ++i)
225 	{
226 		int snapPosition;
227         if (CanSnapEdge(boundsEdges, (Side) i, &snapPosition))
228 		{
229 			snapDirections[i] = true;
230 			snapEdges[i] = snapPosition;
231 		}
232 	}
233 
234 	if ((GetKeyState(VK_SHIFT) & 0x8000) == 0 && (snapDirections[0] || snapDirections[1] || snapDirections[2] || snapDirections[3]))
235 	{
236 		if (!m_inProgress)
237 			m_inProgress = true;
238 
239 		RECT snapRect = { snapEdges[0] - m_border.left, snapEdges[1] - m_border.top, snapEdges[2] + m_border.right, snapEdges[3] + m_border.bottom };
240 		SnapToRect(&bounds, snapRect, true, snapDirections[0], snapDirections[1], snapDirections[2], snapDirections[3]);
241 
242 		return true;
243 	}
244 	else if (m_inProgress)
245 	{
246 		m_inProgress = false;
247 		bounds = realBounds;
248 		return true;
249 	}
250 
251 	return false;
252 }
253 
HandleSizing(RECT & bounds,int which)254 bool WinSnap::HandleSizing(RECT& bounds, int which)
255 {
256     bool allowSnap[Side::Count]{ true, true, true, true };
257 	allowSnap[Side::Left] = (which == WMSZ_LEFT || which == WMSZ_TOPLEFT || which == WMSZ_BOTTOMLEFT);
258 	allowSnap[Side::Top] = (which == WMSZ_TOP || which == WMSZ_TOPLEFT || which == WMSZ_TOPRIGHT);
259 	allowSnap[Side::Right] = (which == WMSZ_RIGHT || which == WMSZ_TOPRIGHT || which == WMSZ_BOTTOMRIGHT);
260 	allowSnap[Side::Bottom] = (which == WMSZ_BOTTOM || which == WMSZ_BOTTOMLEFT || which == WMSZ_BOTTOMRIGHT);
261 
262 	// The difference between the cursor position and the top-left corner of the dragged window.
263 	// This is normally constant while dragging a window, but when near an edge that we snap to,
264 	// this changes.
265     POINT cursorOffset;
266 	GetCursorPos(&cursorOffset);
267 	cursorOffset.x -= bounds.left;
268 	cursorOffset.y -= bounds.top;
269 
270     int boundsEdges[Side::Count]{ bounds.left, bounds.top, bounds.right, bounds.bottom };
271     int snapEdges[Side::Count]{ SHRT_MIN, SHRT_MIN, SHRT_MAX, SHRT_MAX };
272     bool snapDirections[Side::Count]{ false, false, false, false };
273 
274 	for (int i = 0; i < Side::Count; ++i)
275 	{
276 		if (!allowSnap[i])
277 			continue;
278 		int snapPosition;
279         if (CanSnapEdge(boundsEdges, (Side) i, &snapPosition))
280 		{
281 			snapDirections[i] = true;
282 			snapEdges[i] = snapPosition;
283 		}
284 	}
285 
286 	if ((GetKeyState(VK_SHIFT) & 0x8000) == 0 && (snapDirections[0] || snapDirections[1] || snapDirections[2] || snapDirections[3]))
287 	{
288 		if (!m_inProgress)
289 			m_inProgress = true;
290 
291 		RECT snapRect = { snapEdges[0] - m_border.left, snapEdges[1] - m_border.top, snapEdges[2] + m_border.right, snapEdges[3] + m_border.bottom };
292 		SnapToRect(&bounds, snapRect, false, snapDirections[0], snapDirections[1], snapDirections[2], snapDirections[3]);
293 		return true;
294 	}
295 	else if (m_inProgress)
296 	{
297 		m_inProgress = false;
298 
299 		return true;
300 	}
301 
302 	return false;
303 }
304 
305 // Breaks down a rect into 4 edges which are added to the global list of edges to snap to.
AddRectToEdges(const RECT & rect)306 void WinSnap::AddRectToEdges(const RECT& rect)
307 {
308 	int startX = std::min<>(rect.left, rect.right);
309 	int endX = std::max<>(rect.left, rect.right);
310 	int startY = std::min<>(rect.top, rect.bottom);
311 	int endY = std::max<>(rect.top, rect.bottom);
312 
313 	m_edges[Side::Left].push_front(Edge(rect.right, startY, endY));
314 	m_edges[Side::Right].push_front(Edge(rect.left, startY, endY));
315 	m_edges[Side::Top].push_front(Edge(rect.bottom, startX, endX));
316 	m_edges[Side::Bottom].push_front(Edge(rect.top, startX, endX));
317 }
318 
SnapToRect(RECT * bounds,const RECT & rect,bool retainSize,bool left,bool top,bool right,bool bottom) const319 void WinSnap::SnapToRect(RECT* bounds, const RECT& rect, bool retainSize, bool left, bool top, bool right, bool bottom) const
320 {
321 	if (left && right)
322 	{
323 		bounds->left = rect.left;
324 		bounds->right = rect.right;
325 	}
326 	else if (left)
327 	{
328 		if (retainSize)
329 			bounds->right += rect.left - bounds->left;
330 		bounds->left = rect.left;
331 	}
332 	else if (right)
333 	{
334 		if (retainSize)
335 			bounds->left += rect.right - bounds->right;
336 		bounds->right = rect.right;
337 	}
338 
339 	if (top && bottom)
340 	{
341 		bounds->top = rect.top;
342 		bounds->bottom = rect.bottom;
343 	}
344 	else if (top)
345 	{
346 		if (retainSize)
347 			bounds->bottom += rect.top - bounds->top;
348 		bounds->top = rect.top;
349 	}
350 	else if (bottom)
351 	{
352 		if (retainSize)
353 			bounds->top += rect.bottom - bounds->bottom;
354 		bounds->bottom = rect.bottom;
355 	}
356 }
357 
CanSnapEdge(int boundsEdges[Side::Count],Side which,int * snapPosition) const358 bool WinSnap::CanSnapEdge(int boundsEdges[Side::Count], Side which, int* snapPosition) const
359 {
360     for (const Edge& edge : m_edges[which])
361 	{
362 		int edgeDistance = edge.Position - boundsEdges[which];
363 		// Since each edge list is sorted, if the snap edge is past our bound's edge, we know that none of the other edges will work.
364 		if (edgeDistance >= SNAP_DISTANCE)
365 			break;
366 		else if (edgeDistance > -SNAP_DISTANCE)
367 		{
368 			// The modulos get the position of the edges perpendicular to the bound edge we are working on.
369 			int min = std::min<>(boundsEdges[(which + 1) % Side::Count], boundsEdges[(which + 3) % Side::Count]);
370 			int max = std::max<>(boundsEdges[(which + 1) % Side::Count], boundsEdges[(which + 3) % Side::Count]);
371 			if (max > edge.Start && min < edge.End)
372 			{
373 				*snapPosition = edge.Position;
374 				return true;
375 			}
376 		}
377 	}
378 	return false;
379 }
380