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