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) ¶m);
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