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