1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "nsCOMPtr.h"
8 #include "nsResizerFrame.h"
9 #include "nsIContent.h"
10 #include "mozilla/PresShell.h"
11 #include "mozilla/dom/Document.h"
12 #include "mozilla/UniquePtr.h"
13 #include "nsGkAtoms.h"
14 #include "nsNameSpaceManager.h"
15
16 #include "nsPresContext.h"
17 #include "nsFrameManager.h"
18 #include "nsDocShell.h"
19 #include "nsIDocShellTreeOwner.h"
20 #include "nsIBaseWindow.h"
21 #include "nsPIDOMWindow.h"
22 #include "mozilla/MouseEvents.h"
23 #include "nsContentUtils.h"
24 #include "nsMenuPopupFrame.h"
25 #include "nsServiceManagerUtils.h"
26 #include "nsIScreenManager.h"
27 #include "mozilla/dom/Element.h"
28 #include "mozilla/dom/MouseEventBinding.h"
29 #include "nsError.h"
30 #include "nsICSSDeclaration.h"
31 #include "nsStyledElement.h"
32 #include <algorithm>
33
34 using namespace mozilla;
35
36 //
37 // NS_NewResizerFrame
38 //
39 // Creates a new Resizer frame and returns it
40 //
NS_NewResizerFrame(PresShell * aPresShell,ComputedStyle * aStyle)41 nsIFrame* NS_NewResizerFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
42 return new (aPresShell) nsResizerFrame(aStyle, aPresShell->GetPresContext());
43 }
44
NS_IMPL_FRAMEARENA_HELPERS(nsResizerFrame)45 NS_IMPL_FRAMEARENA_HELPERS(nsResizerFrame)
46
47 nsResizerFrame::nsResizerFrame(ComputedStyle* aStyle,
48 nsPresContext* aPresContext)
49 : nsTitleBarFrame(aStyle, aPresContext, kClassID) {}
50
HandleEvent(nsPresContext * aPresContext,WidgetGUIEvent * aEvent,nsEventStatus * aEventStatus)51 nsresult nsResizerFrame::HandleEvent(nsPresContext* aPresContext,
52 WidgetGUIEvent* aEvent,
53 nsEventStatus* aEventStatus) {
54 NS_ENSURE_ARG_POINTER(aEventStatus);
55 if (nsEventStatus_eConsumeNoDefault == *aEventStatus) {
56 return NS_OK;
57 }
58
59 AutoWeakFrame weakFrame(this);
60 bool doDefault = true;
61
62 switch (aEvent->mMessage) {
63 case eTouchStart:
64 case eMouseDown: {
65 if (aEvent->mClass == eTouchEventClass ||
66 (aEvent->mClass == eMouseEventClass &&
67 aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary)) {
68 nsCOMPtr<nsIBaseWindow> window;
69 mozilla::PresShell* presShell = aPresContext->GetPresShell();
70 nsIContent* contentToResize =
71 GetContentToResize(presShell, getter_AddRefs(window));
72 if (contentToResize) {
73 nsIFrame* frameToResize = contentToResize->GetPrimaryFrame();
74 if (!frameToResize) break;
75
76 // cache the content rectangle for the frame to resize
77 // GetScreenRectInAppUnits returns the border box rectangle, so
78 // adjust to get the desired content rectangle.
79 nsRect rect = frameToResize->GetScreenRectInAppUnits();
80 if (frameToResize->StylePosition()->mBoxSizing ==
81 StyleBoxSizing::Content) {
82 rect.Deflate(frameToResize->GetUsedBorderAndPadding());
83 }
84
85 mMouseDownRect = LayoutDeviceIntRect::FromAppUnitsToNearest(
86 rect, aPresContext->AppUnitsPerDevPixel());
87 doDefault = false;
88 } else {
89 // If there is no window, then resizing isn't allowed.
90 if (!window) break;
91
92 doDefault = false;
93
94 // ask the widget implementation to begin a resize drag if it can
95 Direction direction = GetDirection();
96 nsresult rv = aEvent->mWidget->BeginResizeDrag(
97 aEvent, direction.mHorizontal, direction.mVertical);
98 // for native drags, don't set the fields below
99 if (rv != NS_ERROR_NOT_IMPLEMENTED) break;
100
101 // if there's no native resize support, we need to do window
102 // resizing ourselves
103 window->GetPositionAndSize(&mMouseDownRect.x, &mMouseDownRect.y,
104 &mMouseDownRect.width,
105 &mMouseDownRect.height);
106 }
107
108 // remember current mouse coordinates
109 LayoutDeviceIntPoint refPoint;
110 if (!GetEventPoint(aEvent, refPoint)) return NS_OK;
111 mMouseDownPoint = refPoint + aEvent->mWidget->WidgetToScreenOffset();
112
113 // we're tracking
114 mTrackingMouseMove = true;
115
116 PresShell::SetCapturingContent(GetContent(),
117 CaptureFlags::IgnoreAllowedState);
118 }
119 } break;
120
121 case eTouchEnd:
122 case eMouseUp: {
123 if (aEvent->mClass == eTouchEventClass ||
124 (aEvent->mClass == eMouseEventClass &&
125 aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary)) {
126 // we're done tracking.
127 mTrackingMouseMove = false;
128
129 PresShell::ReleaseCapturingContent();
130
131 doDefault = false;
132 }
133 } break;
134
135 case eTouchMove:
136 case eMouseMove: {
137 if (mTrackingMouseMove) {
138 nsCOMPtr<nsIBaseWindow> window;
139 mozilla::PresShell* presShell = aPresContext->GetPresShell();
140 nsCOMPtr<nsIContent> contentToResize =
141 GetContentToResize(presShell, getter_AddRefs(window));
142
143 // check if the returned content really is a menupopup
144 nsMenuPopupFrame* menuPopupFrame = nullptr;
145 if (contentToResize) {
146 menuPopupFrame = do_QueryFrame(contentToResize->GetPrimaryFrame());
147 }
148
149 // both MouseMove and direction are negative when pointing to the
150 // top and left, and positive when pointing to the bottom and right
151
152 // retrieve the offset of the mousemove event relative to the mousedown.
153 // The difference is how much the resize needs to be
154 LayoutDeviceIntPoint refPoint;
155 if (!GetEventPoint(aEvent, refPoint)) return NS_OK;
156 LayoutDeviceIntPoint screenPoint =
157 refPoint + aEvent->mWidget->WidgetToScreenOffset();
158 LayoutDeviceIntPoint mouseMove(screenPoint - mMouseDownPoint);
159
160 // Determine which direction to resize by checking the dir attribute.
161 // For windows and menus, ensure that it can be resized in that
162 // direction.
163 Direction direction = GetDirection();
164 if (window || menuPopupFrame) {
165 if (menuPopupFrame) {
166 menuPopupFrame->CanAdjustEdges(
167 (direction.mHorizontal == -1) ? eSideLeft : eSideRight,
168 (direction.mVertical == -1) ? eSideTop : eSideBottom,
169 mouseMove);
170 }
171 } else if (!contentToResize) {
172 break; // don't do anything if there's nothing to resize
173 }
174
175 LayoutDeviceIntRect rect = mMouseDownRect;
176
177 // Check if there are any size constraints on this window.
178 widget::SizeConstraints sizeConstraints;
179 if (window) {
180 nsCOMPtr<nsIWidget> widget;
181 window->GetMainWidget(getter_AddRefs(widget));
182 sizeConstraints = widget->GetSizeConstraints();
183 }
184
185 AdjustDimensions(&rect.x, &rect.width, sizeConstraints.mMinSize.width,
186 sizeConstraints.mMaxSize.width, mouseMove.x,
187 direction.mHorizontal);
188 AdjustDimensions(&rect.y, &rect.height, sizeConstraints.mMinSize.height,
189 sizeConstraints.mMaxSize.height, mouseMove.y,
190 direction.mVertical);
191
192 // Don't allow resizing a window or a popup past the edge of the screen,
193 // so adjust the rectangle to fit within the available screen area.
194 if (window) {
195 nsCOMPtr<nsIScreen> screen;
196 nsCOMPtr<nsIScreenManager> sm(
197 do_GetService("@mozilla.org/gfx/screenmanager;1"));
198 if (sm) {
199 CSSIntRect frameRect = GetScreenRect();
200 // ScreenForRect requires display pixels, so scale from device pix
201 double scale;
202 window->GetUnscaledDevicePixelsPerCSSPixel(&scale);
203 sm->ScreenForRect(NSToIntRound(frameRect.x / scale),
204 NSToIntRound(frameRect.y / scale), 1, 1,
205 getter_AddRefs(screen));
206 if (screen) {
207 LayoutDeviceIntRect screenRect;
208 screen->GetRect(&screenRect.x, &screenRect.y, &screenRect.width,
209 &screenRect.height);
210 rect.IntersectRect(rect, screenRect);
211 }
212 }
213 } else if (menuPopupFrame) {
214 nsRect frameRect = menuPopupFrame->GetScreenRectInAppUnits();
215 nsIFrame* rootFrame = aPresContext->PresShell()->GetRootFrame();
216 nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits();
217
218 nsPopupLevel popupLevel = menuPopupFrame->PopupLevel();
219 int32_t appPerDev = aPresContext->AppUnitsPerDevPixel();
220 LayoutDeviceIntRect screenRect = menuPopupFrame->GetConstraintRect(
221 LayoutDeviceIntRect::FromAppUnitsToNearest(frameRect, appPerDev),
222 // round using ...ToInside as it's better to be a pixel too small
223 // than be too large. If the popup is too large it could get
224 // flipped to the opposite side of the anchor point while
225 // resizing.
226 LayoutDeviceIntRect::FromAppUnitsToInside(rootScreenRect,
227 appPerDev),
228 popupLevel);
229 rect.IntersectRect(rect, screenRect);
230 }
231
232 if (contentToResize) {
233 // convert the rectangle into css pixels. When changing the size in a
234 // direction, don't allow the new size to be less that the resizer's
235 // size. This ensures that content isn't resized too small as to make
236 // the resizer invisible.
237 nsRect appUnitsRect = ToAppUnits(rect.ToUnknownRect(),
238 aPresContext->AppUnitsPerDevPixel());
239 if (appUnitsRect.width < mRect.width && mouseMove.x)
240 appUnitsRect.width = mRect.width;
241 if (appUnitsRect.height < mRect.height && mouseMove.y)
242 appUnitsRect.height = mRect.height;
243 nsIntRect cssRect =
244 appUnitsRect.ToInsidePixels(AppUnitsPerCSSPixel());
245
246 LayoutDeviceIntRect oldRect;
247 AutoWeakFrame weakFrame(menuPopupFrame);
248 if (menuPopupFrame) {
249 nsCOMPtr<nsIWidget> widget = menuPopupFrame->GetWidget();
250 if (widget) oldRect = widget->GetScreenBounds();
251
252 // convert the new rectangle into outer window coordinates
253 LayoutDeviceIntPoint clientOffset = widget->GetClientOffset();
254 rect.x -= clientOffset.x;
255 rect.y -= clientOffset.y;
256 }
257
258 SizeInfo sizeInfo, originalSizeInfo;
259 sizeInfo.width.AppendInt(cssRect.width);
260 sizeInfo.height.AppendInt(cssRect.height);
261 ResizeContent(contentToResize, direction, sizeInfo,
262 &originalSizeInfo);
263 MaybePersistOriginalSize(contentToResize, originalSizeInfo);
264
265 // Move the popup to the new location unless it is anchored, since
266 // the position shouldn't change. nsMenuPopupFrame::SetPopupPosition
267 // will instead ensure that the popup's position is anchored at the
268 // right place.
269 if (weakFrame.IsAlive() &&
270 (oldRect.x != rect.x || oldRect.y != rect.y) &&
271 (!menuPopupFrame->IsAnchored() ||
272 menuPopupFrame->PopupLevel() != ePopupLevelParent)) {
273 CSSPoint cssPos =
274 rect.TopLeft() / aPresContext->CSSToDevPixelScale();
275 menuPopupFrame->MoveTo(RoundedToInt(cssPos), true);
276 }
277 } else {
278 window->SetPositionAndSize(
279 rect.x, rect.y, rect.width, rect.height,
280 nsIBaseWindow::eRepaint); // do the repaint.
281 }
282
283 doDefault = false;
284 }
285 } break;
286
287 case eMouseClick: {
288 WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent();
289 if (mouseEvent->IsLeftClickEvent()) {
290 MouseClicked(mouseEvent);
291 }
292 break;
293 }
294 case eMouseDoubleClick:
295 if (aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary) {
296 nsCOMPtr<nsIBaseWindow> window;
297 mozilla::PresShell* presShell = aPresContext->GetPresShell();
298 nsIContent* contentToResize =
299 GetContentToResize(presShell, getter_AddRefs(window));
300 if (contentToResize) {
301 nsMenuPopupFrame* menuPopupFrame =
302 do_QueryFrame(contentToResize->GetPrimaryFrame());
303 if (menuPopupFrame)
304 break; // Don't restore original sizing for menupopup frames until
305 // we handle screen constraints here. (Bug 357725)
306
307 RestoreOriginalSize(contentToResize);
308 }
309 }
310 break;
311
312 default:
313 break;
314 }
315
316 if (!doDefault) *aEventStatus = nsEventStatus_eConsumeNoDefault;
317
318 if (doDefault && weakFrame.IsAlive())
319 return nsTitleBarFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
320
321 return NS_OK;
322 }
323
GetContentToResize(mozilla::PresShell * aPresShell,nsIBaseWindow ** aWindow)324 nsIContent* nsResizerFrame::GetContentToResize(mozilla::PresShell* aPresShell,
325 nsIBaseWindow** aWindow) {
326 *aWindow = nullptr;
327
328 nsAutoString elementid;
329 mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::element,
330 elementid);
331 if (elementid.IsEmpty()) {
332 // If the resizer is in a popup, resize the popup's widget, otherwise
333 // resize the widget associated with the window.
334 nsIFrame* popup = GetParent();
335 while (popup) {
336 nsMenuPopupFrame* popupFrame = do_QueryFrame(popup);
337 if (popupFrame) {
338 return popupFrame->GetContent();
339 }
340 popup = popup->GetParent();
341 }
342
343 // don't allow resizing windows in content shells
344 if (!aPresShell->GetPresContext()->IsChrome()) {
345 // don't allow resizers in content shells, except for the viewport
346 // scrollbar which doesn't have a parent
347 nsIContent* nonNativeAnon =
348 mContent->FindFirstNonChromeOnlyAccessContent();
349 if (!nonNativeAnon || nonNativeAnon->GetParent()) {
350 return nullptr;
351 }
352 }
353
354 // get the document and the window - should this be cached?
355 if (nsPIDOMWindowOuter* domWindow =
356 aPresShell->GetDocument()->GetWindow()) {
357 nsCOMPtr<nsIDocShell> docShell = domWindow->GetDocShell();
358 if (docShell) {
359 nsCOMPtr<nsIDocShellTreeOwner> treeOwner;
360 docShell->GetTreeOwner(getter_AddRefs(treeOwner));
361 if (treeOwner) {
362 CallQueryInterface(treeOwner, aWindow);
363 }
364 }
365 }
366
367 return nullptr;
368 }
369
370 if (elementid.EqualsLiteral("_parent")) {
371 // return the parent, but skip over native anonymous content
372 nsIContent* parent = mContent->GetParent();
373 return parent ? parent->FindFirstNonChromeOnlyAccessContent() : nullptr;
374 }
375
376 return aPresShell->GetDocument()->GetElementById(elementid);
377 }
378
AdjustDimensions(int32_t * aPos,int32_t * aSize,int32_t aMinSize,int32_t aMaxSize,int32_t aMovement,int8_t aResizerDirection)379 void nsResizerFrame::AdjustDimensions(int32_t* aPos, int32_t* aSize,
380 int32_t aMinSize, int32_t aMaxSize,
381 int32_t aMovement,
382 int8_t aResizerDirection) {
383 int32_t oldSize = *aSize;
384
385 *aSize += aResizerDirection * aMovement;
386 // use one as a minimum size or the element could disappear
387 if (*aSize < 1) *aSize = 1;
388
389 // Constrain the size within the minimum and maximum size.
390 *aSize = std::max(aMinSize, std::min(aMaxSize, *aSize));
391
392 // For left and top resizers, the window must be moved left by the same
393 // amount that the window was resized.
394 if (aResizerDirection == -1) *aPos += oldSize - *aSize;
395 }
396
397 /* static */
ResizeContent(nsIContent * aContent,const Direction & aDirection,const SizeInfo & aSizeInfo,SizeInfo * aOriginalSizeInfo)398 void nsResizerFrame::ResizeContent(nsIContent* aContent,
399 const Direction& aDirection,
400 const SizeInfo& aSizeInfo,
401 SizeInfo* aOriginalSizeInfo) {
402 if (RefPtr<nsStyledElement> inlineStyleContent =
403 nsStyledElement::FromNode(aContent)) {
404 nsICSSDeclaration* decl = inlineStyleContent->Style();
405
406 if (aOriginalSizeInfo) {
407 decl->GetPropertyValue("width"_ns, aOriginalSizeInfo->width);
408 decl->GetPropertyValue("height"_ns, aOriginalSizeInfo->height);
409 }
410
411 // only set the property if the element could have changed in that
412 // direction
413 if (aDirection.mHorizontal) {
414 nsAutoCString widthstr(aSizeInfo.width);
415 if (!widthstr.IsEmpty() &&
416 !Substring(widthstr, widthstr.Length() - 2, 2).EqualsLiteral("px"))
417 widthstr.AppendLiteral("px");
418 decl->SetProperty("width"_ns, widthstr, ""_ns, IgnoreErrors());
419 }
420 if (aDirection.mVertical) {
421 nsAutoCString heightstr(aSizeInfo.height);
422 if (!heightstr.IsEmpty() &&
423 !Substring(heightstr, heightstr.Length() - 2, 2).EqualsLiteral("px"))
424 heightstr.AppendLiteral("px");
425 decl->SetProperty("height"_ns, heightstr, ""_ns, IgnoreErrors());
426 }
427 }
428 }
429
430 /* static */
MaybePersistOriginalSize(nsIContent * aContent,const SizeInfo & aSizeInfo)431 void nsResizerFrame::MaybePersistOriginalSize(nsIContent* aContent,
432 const SizeInfo& aSizeInfo) {
433 nsresult rv;
434
435 aContent->GetProperty(nsGkAtoms::_moz_original_size, &rv);
436 if (rv != NS_PROPTABLE_PROP_NOT_THERE) return;
437
438 UniquePtr<SizeInfo> sizeInfo(new SizeInfo(aSizeInfo));
439 rv = aContent->SetProperty(nsGkAtoms::_moz_original_size, sizeInfo.get(),
440 nsINode::DeleteProperty<nsResizerFrame::SizeInfo>);
441 if (NS_SUCCEEDED(rv)) {
442 Unused << sizeInfo.release();
443 }
444 }
445
446 /* static */
RestoreOriginalSize(nsIContent * aContent)447 void nsResizerFrame::RestoreOriginalSize(nsIContent* aContent) {
448 nsresult rv;
449 SizeInfo* sizeInfo = static_cast<SizeInfo*>(
450 aContent->GetProperty(nsGkAtoms::_moz_original_size, &rv));
451 if (NS_FAILED(rv)) return;
452
453 NS_ASSERTION(sizeInfo, "We set a null sizeInfo!?");
454 Direction direction = {1, 1};
455 ResizeContent(aContent, direction, *sizeInfo, nullptr);
456 aContent->RemoveProperty(nsGkAtoms::_moz_original_size);
457 }
458
459 /* returns a Direction struct containing the horizontal and vertical direction
460 */
GetDirection()461 nsResizerFrame::Direction nsResizerFrame::GetDirection() {
462 static const mozilla::dom::Element::AttrValuesArray strings[] = {
463 // clang-format off
464 nsGkAtoms::topleft, nsGkAtoms::top, nsGkAtoms::topright,
465 nsGkAtoms::left, nsGkAtoms::right,
466 nsGkAtoms::bottomleft, nsGkAtoms::bottom, nsGkAtoms::bottomright,
467 nsGkAtoms::bottomstart, nsGkAtoms::bottomend,
468 nullptr
469 // clang-format on
470 };
471
472 static const Direction directions[] = {
473 // clang-format off
474 {-1, -1}, {0, -1}, {1, -1},
475 {-1, 0}, {1, 0},
476 {-1, 1}, {0, 1}, {1, 1},
477 {-1, 1}, {1, 1}
478 // clang-format on
479 };
480
481 if (!GetContent()) {
482 return directions[0]; // default: topleft
483 }
484
485 int32_t index = mContent->AsElement()->FindAttrValueIn(
486 kNameSpaceID_None, nsGkAtoms::dir, strings, eCaseMatters);
487 if (index < 0) {
488 return directions[0]; // default: topleft
489 }
490
491 if (index >= 8) {
492 // Directions 8 and higher are RTL-aware directions and should reverse the
493 // horizontal component if RTL.
494 WritingMode wm = GetWritingMode();
495 if (wm.IsPhysicalRTL()) {
496 Direction direction = directions[index];
497 direction.mHorizontal *= -1;
498 return direction;
499 }
500 }
501
502 return directions[index];
503 }
504
MouseClicked(WidgetMouseEvent * aEvent)505 void nsResizerFrame::MouseClicked(WidgetMouseEvent* aEvent) {
506 // Execute the oncommand event handler.
507 nsCOMPtr<nsIContent> content = mContent;
508 nsContentUtils::DispatchXULCommand(content, false, nullptr, nullptr,
509 aEvent->IsControl(), aEvent->IsAlt(),
510 aEvent->IsShift(), aEvent->IsMeta(),
511 aEvent->mInputSource, aEvent->mButton);
512 }
513