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