XULResizerElement.cpp (12030B)
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 "mozilla/dom/XULResizerElement.h" 8 9 #include "mozilla/EventDispatcher.h" 10 #include "mozilla/MouseEvents.h" 11 #include "mozilla/PresShell.h" 12 #include "mozilla/dom/Document.h" 13 #include "mozilla/dom/DocumentInlines.h" 14 #include "mozilla/dom/XULResizerElementBinding.h" 15 #include "nsContentUtils.h" 16 #include "nsDOMCSSDeclaration.h" 17 #include "nsIFrame.h" 18 #include "nsLayoutUtils.h" 19 #include "nsPresContext.h" 20 #include "nsStyledElement.h" 21 22 namespace mozilla::dom { 23 24 nsXULElement* NS_NewXULResizerElement( 25 already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) { 26 RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); 27 auto* nim = nodeInfo->NodeInfoManager(); 28 return new (nim) XULResizerElement(nodeInfo.forget()); 29 } 30 31 static bool GetEventPoint(const WidgetGUIEvent* aEvent, 32 LayoutDeviceIntPoint& aPoint) { 33 NS_ENSURE_TRUE(aEvent, false); 34 35 const WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); 36 if (touchEvent) { 37 // return false if there is more than one touch on the page, or if 38 // we can't find a touch point 39 if (touchEvent->mTouches.Length() != 1) { 40 return false; 41 } 42 43 const dom::Touch* touch = touchEvent->mTouches.SafeElementAt(0); 44 if (!touch) { 45 return false; 46 } 47 aPoint = touch->mRefPoint; 48 } else { 49 aPoint = aEvent->mRefPoint; 50 } 51 return true; 52 } 53 54 JSObject* XULResizerElement::WrapNode(JSContext* aCx, 55 JS::Handle<JSObject*> aGivenProto) { 56 return XULResizerElement_Binding::Wrap(aCx, this, aGivenProto); 57 } 58 59 XULResizerElement::Direction XULResizerElement::GetDirection() { 60 static const mozilla::dom::Element::AttrValuesArray strings[] = { 61 // clang-format off 62 nsGkAtoms::topleft, nsGkAtoms::top, nsGkAtoms::topright, 63 nsGkAtoms::left, nsGkAtoms::right, 64 nsGkAtoms::bottomleft, nsGkAtoms::bottom, nsGkAtoms::bottomright, 65 nsGkAtoms::bottomstart, nsGkAtoms::bottomend, 66 nullptr 67 // clang-format on 68 }; 69 70 static const Direction directions[] = { 71 // clang-format off 72 {-1, -1}, {0, -1}, {1, -1}, 73 {-1, 0}, {1, 0}, 74 {-1, 1}, {0, 1}, {1, 1}, 75 {-1, 1}, {1, 1} // clang-format on 76 }; 77 78 const auto* frame = GetPrimaryFrame(); 79 if (!frame) { 80 return directions[0]; // default: topleft 81 } 82 83 int32_t index = 84 FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::dir, strings, eCaseMatters); 85 if (index < 0) { 86 return directions[0]; // default: topleft 87 } 88 89 if (index >= 8) { 90 // Directions 8 and higher are RTL-aware directions and should reverse the 91 // horizontal component if RTL. 92 auto wm = frame->GetWritingMode(); 93 if (wm.IsPhysicalRTL()) { 94 Direction direction = directions[index]; 95 direction.mHorizontal *= -1; 96 return direction; 97 } 98 } 99 100 return directions[index]; 101 } 102 103 nsresult XULResizerElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { 104 if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) { 105 PostHandleEventInternal(aVisitor); 106 } 107 return nsXULElement::PostHandleEvent(aVisitor); 108 } 109 110 Maybe<nsSize> XULResizerElement::GetCurrentSize() const { 111 nsIContent* contentToResize = GetContentToResize(); 112 if (!contentToResize) { 113 return Nothing(); 114 } 115 nsIFrame* frame = contentToResize->GetPrimaryFrame(); 116 if (!frame) { 117 return Nothing(); 118 } 119 return Some(frame->StylePosition()->mBoxSizing == StyleBoxSizing::Content 120 ? frame->GetContentRect().Size() 121 : frame->GetRect().Size()); 122 } 123 124 void XULResizerElement::PostHandleEventInternal( 125 EventChainPostVisitor& aVisitor) { 126 bool doDefault = true; 127 const WidgetEvent& event = *aVisitor.mEvent; 128 switch (event.mMessage) { 129 case eTouchStart: 130 case eMouseDown: { 131 if (event.mClass == eTouchEventClass || 132 (event.mClass == eMouseEventClass && 133 event.AsMouseEvent()->mButton == MouseButton::ePrimary)) { 134 auto size = GetCurrentSize(); 135 if (!size) { 136 break; // don't do anything if there's nothing to resize 137 } 138 // cache the content rectangle for the frame to resize 139 mMouseDownSize = *size; 140 141 // remember current mouse coordinates 142 auto* guiEvent = event.AsGUIEvent(); 143 if (!GetEventPoint(guiEvent, mMouseDownPoint)) { 144 break; 145 } 146 mTrackingMouseMove = true; 147 PresShell::SetCapturingContent(this, CaptureFlags::IgnoreAllowedState); 148 doDefault = false; 149 } 150 } break; 151 152 case eTouchMove: 153 case eMouseMove: { 154 if (mTrackingMouseMove) { 155 nsCOMPtr<nsIContent> contentToResize = GetContentToResize(); 156 if (!contentToResize) { 157 break; // don't do anything if there's nothing to resize 158 } 159 nsIFrame* frame = contentToResize->GetPrimaryFrame(); 160 if (!frame) { 161 break; 162 } 163 164 // both MouseMove and direction are negative when pointing to the 165 // top and left, and positive when pointing to the bottom and right 166 167 // retrieve the offset of the mousemove event relative to the mousedown. 168 // The difference is how much the resize needs to be 169 LayoutDeviceIntPoint refPoint; 170 auto* guiEvent = event.AsGUIEvent(); 171 if (!GetEventPoint(guiEvent, refPoint)) { 172 break; 173 } 174 175 const nsPoint oldPos = nsLayoutUtils::GetEventCoordinatesRelativeTo( 176 guiEvent->mWidget, mMouseDownPoint, RelativeTo{frame}); 177 const nsPoint newPos = nsLayoutUtils::GetEventCoordinatesRelativeTo( 178 guiEvent->mWidget, refPoint, RelativeTo{frame}); 179 180 nsPoint mouseMove(newPos - oldPos); 181 182 // Determine which direction to resize by checking the dir attribute. 183 // For windows and menus, ensure that it can be resized in that 184 // direction. 185 Direction direction = GetDirection(); 186 187 const CSSIntSize newSize = [&] { 188 nsSize newAuSize = mMouseDownSize; 189 // Check if there are any size constraints on this window. 190 newAuSize.width += direction.mHorizontal * mouseMove.x; 191 newAuSize.height += direction.mVertical * mouseMove.y; 192 if (newAuSize.width < AppUnitsPerCSSPixel() && mouseMove.x != 0) { 193 newAuSize.width = AppUnitsPerCSSPixel(); 194 } 195 if (newAuSize.height < AppUnitsPerCSSPixel() && mouseMove.y != 0) { 196 newAuSize.height = AppUnitsPerCSSPixel(); 197 } 198 199 // When changing the size in a direction, don't allow the new size to 200 // be less that the resizer's size. This ensures that content isn't 201 // resized too small as to make the resizer invisible. 202 if (auto* resizerFrame = GetPrimaryFrame()) { 203 nsRect resizerRect = resizerFrame->GetRect(); 204 if (newAuSize.width < resizerRect.width && mouseMove.x != 0) { 205 newAuSize.width = resizerRect.width; 206 } 207 if (newAuSize.height < resizerRect.height && mouseMove.y != 0) { 208 newAuSize.height = resizerRect.height; 209 } 210 } 211 212 // Convert the rectangle into css pixels. 213 return CSSIntSize::FromAppUnitsRounded(newAuSize); 214 }(); 215 216 // Only resize in a given direction if the new size doesn't match the 217 // current size. 218 if (auto currentSize = GetCurrentSize()) { 219 auto newAuSize = CSSIntSize::ToAppUnits(newSize); 220 if (newAuSize.width == currentSize->width) { 221 direction.mHorizontal = 0; 222 } 223 if (newAuSize.height == currentSize->height) { 224 direction.mVertical = 0; 225 } 226 } 227 228 SizeInfo sizeInfo, originalSizeInfo; 229 sizeInfo.width.AppendInt(newSize.width); 230 sizeInfo.height.AppendInt(newSize.height); 231 ResizeContent(contentToResize, direction, sizeInfo, &originalSizeInfo); 232 MaybePersistOriginalSize(contentToResize, originalSizeInfo); 233 234 doDefault = false; 235 } 236 } break; 237 238 case ePointerClick: { 239 auto* mouseEvent = event.AsMouseEvent(); 240 if (mouseEvent->IsLeftClickEvent()) { 241 // Execute the oncommand event handler. 242 nsContentUtils::DispatchXULCommand( 243 this, false, nullptr, nullptr, mouseEvent->IsControl(), 244 mouseEvent->IsAlt(), mouseEvent->IsShift(), mouseEvent->IsMeta(), 245 mouseEvent->mInputSource, mouseEvent->mButton); 246 } 247 } break; 248 249 case eTouchEnd: 250 case eMouseUp: { 251 if (event.mClass == eTouchEventClass || 252 (event.mClass == eMouseEventClass && 253 event.AsMouseEvent()->mButton == MouseButton::ePrimary)) { 254 mTrackingMouseMove = false; 255 PresShell::ReleaseCapturingContent(); 256 doDefault = false; 257 } 258 } break; 259 260 case eMouseDoubleClick: { 261 if (event.AsMouseEvent()->mButton == MouseButton::ePrimary) { 262 if (nsIContent* contentToResize = GetContentToResize()) { 263 RestoreOriginalSize(contentToResize); 264 } 265 } 266 } break; 267 268 default: 269 break; 270 } 271 272 if (!doDefault) { 273 aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; 274 } 275 } 276 277 nsIContent* XULResizerElement::GetContentToResize() const { 278 if (!IsInComposedDoc()) { 279 return nullptr; 280 } 281 // Return the parent, but skip over native anonymous content 282 nsIContent* parent = GetParent(); 283 return parent ? parent->FindFirstNonChromeOnlyAccessContent() : nullptr; 284 } 285 286 /* static */ 287 void XULResizerElement::ResizeContent(nsIContent* aContent, 288 const Direction& aDirection, 289 const SizeInfo& aSizeInfo, 290 SizeInfo* aOriginalSizeInfo) { 291 RefPtr inlineStyleContent = nsStyledElement::FromNode(aContent); 292 if (!inlineStyleContent) { 293 return; 294 } 295 nsCOMPtr<nsDOMCSSDeclaration> decl = inlineStyleContent->Style(); 296 if (aOriginalSizeInfo) { 297 decl->GetPropertyValue("width"_ns, aOriginalSizeInfo->width); 298 decl->GetPropertyValue("height"_ns, aOriginalSizeInfo->height); 299 } 300 301 // only set the property if the element could have changed in that 302 // direction 303 if (aDirection.mHorizontal) { 304 nsAutoCString widthstr(aSizeInfo.width); 305 if (!widthstr.IsEmpty() && !StringEndsWith(widthstr, "px"_ns)) { 306 widthstr.AppendLiteral("px"); 307 } 308 decl->SetProperty("width"_ns, widthstr, ""_ns, IgnoreErrors()); 309 } 310 311 if (aDirection.mVertical) { 312 nsAutoCString heightstr(aSizeInfo.height); 313 if (!heightstr.IsEmpty() && !StringEndsWith(heightstr, "px"_ns)) { 314 heightstr.AppendLiteral("px"); 315 } 316 decl->SetProperty("height"_ns, heightstr, ""_ns, IgnoreErrors()); 317 } 318 } 319 320 /* static */ 321 void XULResizerElement::MaybePersistOriginalSize(nsIContent* aContent, 322 const SizeInfo& aSizeInfo) { 323 nsresult rv; 324 aContent->GetProperty(nsGkAtoms::_moz_original_size, &rv); 325 if (rv != NS_PROPTABLE_PROP_NOT_THERE) { 326 return; 327 } 328 329 UniquePtr<SizeInfo> sizeInfo(new SizeInfo(aSizeInfo)); 330 rv = aContent->SetProperty( 331 nsGkAtoms::_moz_original_size, sizeInfo.get(), 332 nsINode::DeleteProperty<XULResizerElement::SizeInfo>); 333 if (NS_SUCCEEDED(rv)) { 334 (void)sizeInfo.release(); 335 } 336 } 337 338 /* static */ 339 void XULResizerElement::RestoreOriginalSize(nsIContent* aContent) { 340 nsresult rv; 341 SizeInfo* sizeInfo = static_cast<SizeInfo*>( 342 aContent->GetProperty(nsGkAtoms::_moz_original_size, &rv)); 343 if (NS_FAILED(rv)) { 344 return; 345 } 346 347 NS_ASSERTION(sizeInfo, "We set a null sizeInfo!?"); 348 Direction direction = {1, 1}; 349 ResizeContent(aContent, direction, *sizeInfo, nullptr); 350 aContent->RemoveProperty(nsGkAtoms::_moz_original_size); 351 } 352 353 } // namespace mozilla::dom