From 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 03:47:29 +0200 Subject: Adding upstream version 115.8.0esr. Signed-off-by: Daniel Baumann --- layout/xul/MiddleCroppingLabelFrame.cpp | 38 + layout/xul/MiddleCroppingLabelFrame.h | 29 + layout/xul/SimpleXULLeafFrame.cpp | 37 + layout/xul/SimpleXULLeafFrame.h | 46 + layout/xul/crashtests/131008-1.xhtml | 11 + layout/xul/crashtests/137216-1.xhtml | 4 + layout/xul/crashtests/1379332-2.xhtml | 9 + layout/xul/crashtests/140218-1.xml | 4 + layout/xul/crashtests/151826-1.xhtml | 27 + layout/xul/crashtests/168724-1.xhtml | 18 + layout/xul/crashtests/289410-1.xhtml | 14 + layout/xul/crashtests/291702-1.xhtml | 11 + layout/xul/crashtests/291702-2.xhtml | 11 + layout/xul/crashtests/291702-3.xhtml | 137 + layout/xul/crashtests/294371-1.xhtml | 53 + layout/xul/crashtests/322786-1.xhtml | 6 + layout/xul/crashtests/325377.xhtml | 16 + layout/xul/crashtests/326879-1.xhtml | 31 + layout/xul/crashtests/329327-1.xhtml | 2 + layout/xul/crashtests/329407-1.xml | 14 + layout/xul/crashtests/336962-1.xhtml | 18 + layout/xul/crashtests/344228-1.xhtml | 27 + layout/xul/crashtests/365151.xhtml | 39 + layout/xul/crashtests/366112-1.xhtml | 9 + layout/xul/crashtests/366203-1.xhtml | 40 + layout/xul/crashtests/367185-1.xhtml | 11 + layout/xul/crashtests/369942-1.xhtml | 36 + layout/xul/crashtests/376137-1.html | 18 + layout/xul/crashtests/376137-2.html | 11 + layout/xul/crashtests/378961.html | 9 + layout/xul/crashtests/381862.html | 23 + layout/xul/crashtests/382746-1.xhtml | 15 + layout/xul/crashtests/382899-1.xhtml | 9 + layout/xul/crashtests/384037-1.xhtml | 9 + layout/xul/crashtests/384105-1-inner.xhtml | 21 + layout/xul/crashtests/384105-1.html | 9 + layout/xul/crashtests/384373-1.xhtml | 10 + layout/xul/crashtests/384373-2.xhtml | 4 + layout/xul/crashtests/384373.html | 23 + layout/xul/crashtests/384871-1-inner.xhtml | 9 + layout/xul/crashtests/384871-1.html | 9 + layout/xul/crashtests/386642.xhtml | 31 + layout/xul/crashtests/387080-1.xhtml | 6 + layout/xul/crashtests/391974-1-inner.xhtml | 19 + layout/xul/crashtests/391974-1.html | 9 + layout/xul/crashtests/402912-1.xhtml | 5 + layout/xul/crashtests/404192.xhtml | 12 + layout/xul/crashtests/408904-1.xhtml | 1 + layout/xul/crashtests/412479-1.xhtml | 4 + layout/xul/crashtests/417509.xhtml | 7 + layout/xul/crashtests/430356-1.xhtml | 5 + layout/xul/crashtests/464407-1.xhtml | 9 + layout/xul/crashtests/470063-1.html | 15 + layout/xul/crashtests/470272.html | 21 + layout/xul/crashtests/538308-1.xhtml | 32 + layout/xul/crashtests/557174-1.xml | 1 + layout/xul/crashtests/564705-1.xhtml | 6 + layout/xul/crashtests/583957-1.html | 20 + layout/xul/crashtests/617089.html | 9 + layout/xul/crashtests/716503.html | 11 + layout/xul/crashtests/crashtests.list | 52 + layout/xul/crashtests/menulist-focused.xhtml | 5 + layout/xul/moz.build | 50 + layout/xul/nsIPopupContainer.h | 29 + layout/xul/nsIScrollbarMediator.h | 101 + layout/xul/nsMenuPopupFrame.cpp | 2433 +++++++++++ layout/xul/nsMenuPopupFrame.h | 643 +++ layout/xul/nsRepeatService.cpp | 93 + layout/xul/nsRepeatService.h | 73 + layout/xul/nsScrollbarButtonFrame.cpp | 272 ++ layout/xul/nsScrollbarButtonFrame.h | 85 + layout/xul/nsScrollbarFrame.cpp | 595 +++ layout/xul/nsScrollbarFrame.h | 150 + layout/xul/nsSliderFrame.cpp | 1578 +++++++ layout/xul/nsSliderFrame.h | 235 ++ layout/xul/nsSplitterFrame.cpp | 964 +++++ layout/xul/nsSplitterFrame.h | 84 + layout/xul/nsXULPopupManager.cpp | 2923 +++++++++++++ layout/xul/nsXULPopupManager.h | 909 ++++ layout/xul/nsXULTooltipListener.cpp | 664 +++ layout/xul/nsXULTooltipListener.h | 99 + .../xul/reftest/checkbox-dynamic-change-ref.xhtml | 6 + layout/xul/reftest/checkbox-dynamic-change.xhtml | 17 + .../reftest/image-scaling-min-height-1-ref.xhtml | 14 + .../xul/reftest/image-scaling-min-height-1.xhtml | 14 + layout/xul/reftest/image-size-ref.xhtml | 102 + layout/xul/reftest/image-size.xhtml | 104 + layout/xul/reftest/image4x3.png | Bin 0 -> 176 bytes layout/xul/reftest/popup-explicit-size-ref.xhtml | 6 + layout/xul/reftest/popup-explicit-size.xhtml | 7 + layout/xul/reftest/radio-dynamic-change-ref.xhtml | 6 + layout/xul/reftest/radio-dynamic-change.xhtml | 17 + layout/xul/reftest/reftest.list | 14 + .../xul/reftest/scrollbar-marks-overlay-ref.html | 64 + layout/xul/reftest/scrollbar-marks-overlay.html | 18 + layout/xul/reftest/scrollbar-marks-ref.html | 13 + layout/xul/reftest/scrollbar-marks.html | 18 + layout/xul/reftest/scrollbar-marks2.html | 19 + .../xul/reftest/textbox-text-transform-ref.xhtml | 6 + layout/xul/reftest/textbox-text-transform.xhtml | 6 + layout/xul/test/browser.ini | 11 + layout/xul/test/browser_bug1163304.js | 83 + layout/xul/test/browser_bug1754298.js | 35 + layout/xul/test/browser_bug685470.js | 38 + layout/xul/test/browser_bug703210.js | 56 + layout/xul/test/browser_bug706743.js | 158 + layout/xul/test/chrome.ini | 38 + layout/xul/test/file_bug386386.sjs | 14 + layout/xul/test/mochitest.ini | 17 + layout/xul/test/test_bug1197913.xhtml | 63 + layout/xul/test/test_bug159346.xhtml | 143 + layout/xul/test/test_bug381167.xhtml | 52 + layout/xul/test/test_bug386386.html | 34 + layout/xul/test/test_bug394800.xhtml | 39 + layout/xul/test/test_bug398982-1.xhtml | 31 + layout/xul/test/test_bug398982-2.xhtml | 33 + layout/xul/test/test_bug467442.xhtml | 53 + layout/xul/test/test_bug477754.xhtml | 51 + layout/xul/test/test_bug511075.html | 121 + layout/xul/test/test_bug563416.html | 53 + layout/xul/test/test_bug703150.xhtml | 74 + layout/xul/test/test_bug987230.xhtml | 109 + layout/xul/test/test_drag_thumb_in_link.html | 76 + layout/xul/test/test_menuitem_ctrl_click.xhtml | 80 + layout/xul/test/test_popupReflowPos.xhtml | 77 + layout/xul/test/test_popupSizeTo.xhtml | 55 + layout/xul/test/test_popupZoom.xhtml | 53 + layout/xul/test/test_resizer_ctrl_click.xhtml | 51 + layout/xul/test/test_resizer_incontent.xhtml | 42 + layout/xul/test/test_splitter.xhtml | 117 + layout/xul/test/test_splitter_sibling.xhtml | 88 + layout/xul/test/test_submenuClose.xhtml | 91 + .../xul/test/test_toolbarbutton_ctrl_click.xhtml | 51 + layout/xul/test/test_windowminmaxsize.xhtml | 193 + layout/xul/test/titledpanelwindow.xhtml | 5 + layout/xul/test/windowminmaxsize1.xhtml | 4 + layout/xul/test/windowminmaxsize10.xhtml | 4 + layout/xul/test/windowminmaxsize2.xhtml | 4 + layout/xul/test/windowminmaxsize3.xhtml | 4 + layout/xul/test/windowminmaxsize4.xhtml | 4 + layout/xul/test/windowminmaxsize5.xhtml | 4 + layout/xul/test/windowminmaxsize6.xhtml | 4 + layout/xul/test/windowminmaxsize7.xhtml | 4 + layout/xul/test/windowminmaxsize8.xhtml | 4 + layout/xul/test/windowminmaxsize9.xhtml | 4 + layout/xul/tree/crashtests/307298-1.xhtml | 21 + layout/xul/tree/crashtests/309732-1.xhtml | 30 + layout/xul/tree/crashtests/309732-2.xhtml | 31 + layout/xul/tree/crashtests/366583-1.xhtml | 43 + layout/xul/tree/crashtests/380217-1.xhtml | 31 + layout/xul/tree/crashtests/382444-1-inner.html | 15 + layout/xul/tree/crashtests/382444-1.html | 9 + layout/xul/tree/crashtests/391178-1.xhtml | 41 + layout/xul/tree/crashtests/391178-2.xhtml | 20 + layout/xul/tree/crashtests/393665-1.xhtml | 3 + layout/xul/tree/crashtests/399227-1.xhtml | 44 + layout/xul/tree/crashtests/399692-1.xhtml | 10 + layout/xul/tree/crashtests/399715-1.xhtml | 9 + layout/xul/tree/crashtests/409807-1.xhtml | 25 + layout/xul/tree/crashtests/414170-1.xhtml | 20 + layout/xul/tree/crashtests/479931-1.xhtml | 19 + layout/xul/tree/crashtests/585815-iframe.xhtml | 72 + layout/xul/tree/crashtests/585815.html | 18 + layout/xul/tree/crashtests/601427.html | 30 + layout/xul/tree/crashtests/730441-3.xhtml | 38 + layout/xul/tree/crashtests/crashtests.list | 18 + layout/xul/tree/moz.build | 44 + layout/xul/tree/nsITreeSelection.idl | 119 + layout/xul/tree/nsITreeView.idl | 173 + layout/xul/tree/nsTreeBodyFrame.cpp | 4363 ++++++++++++++++++++ layout/xul/tree/nsTreeBodyFrame.h | 607 +++ layout/xul/tree/nsTreeColumns.cpp | 462 +++ layout/xul/tree/nsTreeColumns.h | 214 + layout/xul/tree/nsTreeContentView.cpp | 1269 ++++++ layout/xul/tree/nsTreeContentView.h | 164 + layout/xul/tree/nsTreeImageListener.cpp | 115 + layout/xul/tree/nsTreeImageListener.h | 67 + layout/xul/tree/nsTreeSelection.cpp | 724 ++++ layout/xul/tree/nsTreeSelection.h | 56 + layout/xul/tree/nsTreeStyleCache.cpp | 103 + layout/xul/tree/nsTreeStyleCache.h | 82 + layout/xul/tree/nsTreeUtils.cpp | 135 + layout/xul/tree/nsTreeUtils.h | 43 + 183 files changed, 25200 insertions(+) create mode 100644 layout/xul/MiddleCroppingLabelFrame.cpp create mode 100644 layout/xul/MiddleCroppingLabelFrame.h create mode 100644 layout/xul/SimpleXULLeafFrame.cpp create mode 100644 layout/xul/SimpleXULLeafFrame.h create mode 100644 layout/xul/crashtests/131008-1.xhtml create mode 100644 layout/xul/crashtests/137216-1.xhtml create mode 100644 layout/xul/crashtests/1379332-2.xhtml create mode 100644 layout/xul/crashtests/140218-1.xml create mode 100644 layout/xul/crashtests/151826-1.xhtml create mode 100644 layout/xul/crashtests/168724-1.xhtml create mode 100644 layout/xul/crashtests/289410-1.xhtml create mode 100644 layout/xul/crashtests/291702-1.xhtml create mode 100644 layout/xul/crashtests/291702-2.xhtml create mode 100644 layout/xul/crashtests/291702-3.xhtml create mode 100644 layout/xul/crashtests/294371-1.xhtml create mode 100644 layout/xul/crashtests/322786-1.xhtml create mode 100644 layout/xul/crashtests/325377.xhtml create mode 100644 layout/xul/crashtests/326879-1.xhtml create mode 100644 layout/xul/crashtests/329327-1.xhtml create mode 100644 layout/xul/crashtests/329407-1.xml create mode 100644 layout/xul/crashtests/336962-1.xhtml create mode 100644 layout/xul/crashtests/344228-1.xhtml create mode 100644 layout/xul/crashtests/365151.xhtml create mode 100644 layout/xul/crashtests/366112-1.xhtml create mode 100644 layout/xul/crashtests/366203-1.xhtml create mode 100644 layout/xul/crashtests/367185-1.xhtml create mode 100644 layout/xul/crashtests/369942-1.xhtml create mode 100644 layout/xul/crashtests/376137-1.html create mode 100644 layout/xul/crashtests/376137-2.html create mode 100644 layout/xul/crashtests/378961.html create mode 100644 layout/xul/crashtests/381862.html create mode 100644 layout/xul/crashtests/382746-1.xhtml create mode 100644 layout/xul/crashtests/382899-1.xhtml create mode 100644 layout/xul/crashtests/384037-1.xhtml create mode 100644 layout/xul/crashtests/384105-1-inner.xhtml create mode 100644 layout/xul/crashtests/384105-1.html create mode 100644 layout/xul/crashtests/384373-1.xhtml create mode 100644 layout/xul/crashtests/384373-2.xhtml create mode 100644 layout/xul/crashtests/384373.html create mode 100644 layout/xul/crashtests/384871-1-inner.xhtml create mode 100644 layout/xul/crashtests/384871-1.html create mode 100644 layout/xul/crashtests/386642.xhtml create mode 100644 layout/xul/crashtests/387080-1.xhtml create mode 100644 layout/xul/crashtests/391974-1-inner.xhtml create mode 100644 layout/xul/crashtests/391974-1.html create mode 100644 layout/xul/crashtests/402912-1.xhtml create mode 100644 layout/xul/crashtests/404192.xhtml create mode 100644 layout/xul/crashtests/408904-1.xhtml create mode 100644 layout/xul/crashtests/412479-1.xhtml create mode 100644 layout/xul/crashtests/417509.xhtml create mode 100644 layout/xul/crashtests/430356-1.xhtml create mode 100644 layout/xul/crashtests/464407-1.xhtml create mode 100644 layout/xul/crashtests/470063-1.html create mode 100644 layout/xul/crashtests/470272.html create mode 100644 layout/xul/crashtests/538308-1.xhtml create mode 100644 layout/xul/crashtests/557174-1.xml create mode 100644 layout/xul/crashtests/564705-1.xhtml create mode 100644 layout/xul/crashtests/583957-1.html create mode 100644 layout/xul/crashtests/617089.html create mode 100644 layout/xul/crashtests/716503.html create mode 100644 layout/xul/crashtests/crashtests.list create mode 100644 layout/xul/crashtests/menulist-focused.xhtml create mode 100644 layout/xul/moz.build create mode 100644 layout/xul/nsIPopupContainer.h create mode 100644 layout/xul/nsIScrollbarMediator.h create mode 100644 layout/xul/nsMenuPopupFrame.cpp create mode 100644 layout/xul/nsMenuPopupFrame.h create mode 100644 layout/xul/nsRepeatService.cpp create mode 100644 layout/xul/nsRepeatService.h create mode 100644 layout/xul/nsScrollbarButtonFrame.cpp create mode 100644 layout/xul/nsScrollbarButtonFrame.h create mode 100644 layout/xul/nsScrollbarFrame.cpp create mode 100644 layout/xul/nsScrollbarFrame.h create mode 100644 layout/xul/nsSliderFrame.cpp create mode 100644 layout/xul/nsSliderFrame.h create mode 100644 layout/xul/nsSplitterFrame.cpp create mode 100644 layout/xul/nsSplitterFrame.h create mode 100644 layout/xul/nsXULPopupManager.cpp create mode 100644 layout/xul/nsXULPopupManager.h create mode 100644 layout/xul/nsXULTooltipListener.cpp create mode 100644 layout/xul/nsXULTooltipListener.h create mode 100644 layout/xul/reftest/checkbox-dynamic-change-ref.xhtml create mode 100644 layout/xul/reftest/checkbox-dynamic-change.xhtml create mode 100644 layout/xul/reftest/image-scaling-min-height-1-ref.xhtml create mode 100644 layout/xul/reftest/image-scaling-min-height-1.xhtml create mode 100644 layout/xul/reftest/image-size-ref.xhtml create mode 100644 layout/xul/reftest/image-size.xhtml create mode 100644 layout/xul/reftest/image4x3.png create mode 100644 layout/xul/reftest/popup-explicit-size-ref.xhtml create mode 100644 layout/xul/reftest/popup-explicit-size.xhtml create mode 100644 layout/xul/reftest/radio-dynamic-change-ref.xhtml create mode 100644 layout/xul/reftest/radio-dynamic-change.xhtml create mode 100644 layout/xul/reftest/reftest.list create mode 100644 layout/xul/reftest/scrollbar-marks-overlay-ref.html create mode 100644 layout/xul/reftest/scrollbar-marks-overlay.html create mode 100644 layout/xul/reftest/scrollbar-marks-ref.html create mode 100644 layout/xul/reftest/scrollbar-marks.html create mode 100644 layout/xul/reftest/scrollbar-marks2.html create mode 100644 layout/xul/reftest/textbox-text-transform-ref.xhtml create mode 100644 layout/xul/reftest/textbox-text-transform.xhtml create mode 100644 layout/xul/test/browser.ini create mode 100644 layout/xul/test/browser_bug1163304.js create mode 100644 layout/xul/test/browser_bug1754298.js create mode 100644 layout/xul/test/browser_bug685470.js create mode 100644 layout/xul/test/browser_bug703210.js create mode 100644 layout/xul/test/browser_bug706743.js create mode 100644 layout/xul/test/chrome.ini create mode 100644 layout/xul/test/file_bug386386.sjs create mode 100644 layout/xul/test/mochitest.ini create mode 100644 layout/xul/test/test_bug1197913.xhtml create mode 100644 layout/xul/test/test_bug159346.xhtml create mode 100644 layout/xul/test/test_bug381167.xhtml create mode 100644 layout/xul/test/test_bug386386.html create mode 100644 layout/xul/test/test_bug394800.xhtml create mode 100644 layout/xul/test/test_bug398982-1.xhtml create mode 100644 layout/xul/test/test_bug398982-2.xhtml create mode 100644 layout/xul/test/test_bug467442.xhtml create mode 100644 layout/xul/test/test_bug477754.xhtml create mode 100644 layout/xul/test/test_bug511075.html create mode 100644 layout/xul/test/test_bug563416.html create mode 100644 layout/xul/test/test_bug703150.xhtml create mode 100644 layout/xul/test/test_bug987230.xhtml create mode 100644 layout/xul/test/test_drag_thumb_in_link.html create mode 100644 layout/xul/test/test_menuitem_ctrl_click.xhtml create mode 100644 layout/xul/test/test_popupReflowPos.xhtml create mode 100644 layout/xul/test/test_popupSizeTo.xhtml create mode 100644 layout/xul/test/test_popupZoom.xhtml create mode 100644 layout/xul/test/test_resizer_ctrl_click.xhtml create mode 100644 layout/xul/test/test_resizer_incontent.xhtml create mode 100644 layout/xul/test/test_splitter.xhtml create mode 100644 layout/xul/test/test_splitter_sibling.xhtml create mode 100644 layout/xul/test/test_submenuClose.xhtml create mode 100644 layout/xul/test/test_toolbarbutton_ctrl_click.xhtml create mode 100644 layout/xul/test/test_windowminmaxsize.xhtml create mode 100644 layout/xul/test/titledpanelwindow.xhtml create mode 100644 layout/xul/test/windowminmaxsize1.xhtml create mode 100644 layout/xul/test/windowminmaxsize10.xhtml create mode 100644 layout/xul/test/windowminmaxsize2.xhtml create mode 100644 layout/xul/test/windowminmaxsize3.xhtml create mode 100644 layout/xul/test/windowminmaxsize4.xhtml create mode 100644 layout/xul/test/windowminmaxsize5.xhtml create mode 100644 layout/xul/test/windowminmaxsize6.xhtml create mode 100644 layout/xul/test/windowminmaxsize7.xhtml create mode 100644 layout/xul/test/windowminmaxsize8.xhtml create mode 100644 layout/xul/test/windowminmaxsize9.xhtml create mode 100644 layout/xul/tree/crashtests/307298-1.xhtml create mode 100644 layout/xul/tree/crashtests/309732-1.xhtml create mode 100644 layout/xul/tree/crashtests/309732-2.xhtml create mode 100644 layout/xul/tree/crashtests/366583-1.xhtml create mode 100644 layout/xul/tree/crashtests/380217-1.xhtml create mode 100644 layout/xul/tree/crashtests/382444-1-inner.html create mode 100644 layout/xul/tree/crashtests/382444-1.html create mode 100644 layout/xul/tree/crashtests/391178-1.xhtml create mode 100644 layout/xul/tree/crashtests/391178-2.xhtml create mode 100644 layout/xul/tree/crashtests/393665-1.xhtml create mode 100644 layout/xul/tree/crashtests/399227-1.xhtml create mode 100644 layout/xul/tree/crashtests/399692-1.xhtml create mode 100644 layout/xul/tree/crashtests/399715-1.xhtml create mode 100644 layout/xul/tree/crashtests/409807-1.xhtml create mode 100644 layout/xul/tree/crashtests/414170-1.xhtml create mode 100644 layout/xul/tree/crashtests/479931-1.xhtml create mode 100644 layout/xul/tree/crashtests/585815-iframe.xhtml create mode 100644 layout/xul/tree/crashtests/585815.html create mode 100644 layout/xul/tree/crashtests/601427.html create mode 100644 layout/xul/tree/crashtests/730441-3.xhtml create mode 100644 layout/xul/tree/crashtests/crashtests.list create mode 100644 layout/xul/tree/moz.build create mode 100644 layout/xul/tree/nsITreeSelection.idl create mode 100644 layout/xul/tree/nsITreeView.idl create mode 100644 layout/xul/tree/nsTreeBodyFrame.cpp create mode 100644 layout/xul/tree/nsTreeBodyFrame.h create mode 100644 layout/xul/tree/nsTreeColumns.cpp create mode 100644 layout/xul/tree/nsTreeColumns.h create mode 100644 layout/xul/tree/nsTreeContentView.cpp create mode 100644 layout/xul/tree/nsTreeContentView.h create mode 100644 layout/xul/tree/nsTreeImageListener.cpp create mode 100644 layout/xul/tree/nsTreeImageListener.h create mode 100644 layout/xul/tree/nsTreeSelection.cpp create mode 100644 layout/xul/tree/nsTreeSelection.h create mode 100644 layout/xul/tree/nsTreeStyleCache.cpp create mode 100644 layout/xul/tree/nsTreeStyleCache.h create mode 100644 layout/xul/tree/nsTreeUtils.cpp create mode 100644 layout/xul/tree/nsTreeUtils.h (limited to 'layout/xul') diff --git a/layout/xul/MiddleCroppingLabelFrame.cpp b/layout/xul/MiddleCroppingLabelFrame.cpp new file mode 100644 index 0000000000..73cb15be74 --- /dev/null +++ b/layout/xul/MiddleCroppingLabelFrame.cpp @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MiddleCroppingLabelFrame.h" +#include "MiddleCroppingBlockFrame.h" +#include "mozilla/dom/Element.h" +#include "mozilla/PresShell.h" + +nsIFrame* NS_NewMiddleCroppingLabelFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::MiddleCroppingLabelFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +void MiddleCroppingLabelFrame::GetUncroppedValue(nsAString& aValue) { + mContent->AsElement()->GetAttr(nsGkAtoms::value, aValue); +} + +nsresult MiddleCroppingLabelFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::value) { + UpdateDisplayedValueToUncroppedValue(true); + } + return NS_OK; +} + +NS_QUERYFRAME_HEAD(MiddleCroppingLabelFrame) + NS_QUERYFRAME_ENTRY(MiddleCroppingLabelFrame) +NS_QUERYFRAME_TAIL_INHERITING(MiddleCroppingBlockFrame) +NS_IMPL_FRAMEARENA_HELPERS(MiddleCroppingLabelFrame) + +} // namespace mozilla diff --git a/layout/xul/MiddleCroppingLabelFrame.h b/layout/xul/MiddleCroppingLabelFrame.h new file mode 100644 index 0000000000..2d0bdf8686 --- /dev/null +++ b/layout/xul/MiddleCroppingLabelFrame.h @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_MiddleCroppingLabelFrame_h +#define mozilla_MiddleCroppingLabelFrame_h + +#include "MiddleCroppingBlockFrame.h" +namespace mozilla { + +// A frame for a or with crop="center" +class MiddleCroppingLabelFrame final : public MiddleCroppingBlockFrame { + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(MiddleCroppingLabelFrame) + + MiddleCroppingLabelFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : MiddleCroppingBlockFrame(aStyle, aPresContext, kClassID) {} + + void GetUncroppedValue(nsAString& aValue) override; + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; +}; + +} // namespace mozilla + +#endif diff --git a/layout/xul/SimpleXULLeafFrame.cpp b/layout/xul/SimpleXULLeafFrame.cpp new file mode 100644 index 0000000000..96b3d81762 --- /dev/null +++ b/layout/xul/SimpleXULLeafFrame.cpp @@ -0,0 +1,37 @@ + +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SimpleXULLeafFrame.h" +#include "mozilla/PresShell.h" + +nsIFrame* NS_NewSimpleXULLeafFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SimpleXULLeafFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SimpleXULLeafFrame) + +void SimpleXULLeafFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + const auto wm = GetWritingMode(); + const auto& bp = aReflowInput.ComputedLogicalBorderPadding(wm); + aDesiredSize.ISize(wm) = bp.IStartEnd(wm) + aReflowInput.ComputedISize(); + aDesiredSize.BSize(wm) = + bp.BStartEnd(wm) + (aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE + ? aReflowInput.ComputedMinBSize() + : aReflowInput.ComputedBSize()); + aDesiredSize.SetOverflowAreasToDesiredBounds(); +} + +} // namespace mozilla diff --git a/layout/xul/SimpleXULLeafFrame.h b/layout/xul/SimpleXULLeafFrame.h new file mode 100644 index 0000000000..468b1f3d2a --- /dev/null +++ b/layout/xul/SimpleXULLeafFrame.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// A simple frame class for XUL frames that are leafs on the tree but need +// background / border painting, and for some reason or another need special +// code (like event handling code) which we haven't ported to the DOM. +// +// This should generally not be used for new frame classes. + +#ifndef mozilla_SimpleXULLeafFrame_h +#define mozilla_SimpleXULLeafFrame_h + +#include "nsLeafFrame.h" + +namespace mozilla { + +// Shared class for thumb and scrollbar buttons. +class SimpleXULLeafFrame : public nsLeafFrame { + public: + NS_DECL_FRAMEARENA_HELPERS(SimpleXULLeafFrame) + + // TODO: Look at appearance instead maybe? + nscoord GetIntrinsicISize() override { return 0; } + nscoord GetIntrinsicBSize() override { return 0; } + + void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + explicit SimpleXULLeafFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext, ClassID aClassID) + : nsLeafFrame(aStyle, aPresContext, aClassID) {} + + explicit SimpleXULLeafFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : SimpleXULLeafFrame(aStyle, aPresContext, kClassID) {} + + friend nsIFrame* NS_NewSimpleXULLeafFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); +}; + +} // namespace mozilla + +#endif diff --git a/layout/xul/crashtests/131008-1.xhtml b/layout/xul/crashtests/131008-1.xhtml new file mode 100644 index 0000000000..fe15a46aa6 --- /dev/null +++ b/layout/xul/crashtests/131008-1.xhtml @@ -0,0 +1,11 @@ + + + +
abc
+ + +
diff --git a/layout/xul/crashtests/137216-1.xhtml b/layout/xul/crashtests/137216-1.xhtml new file mode 100644 index 0000000000..e01541c622 --- /dev/null +++ b/layout/xul/crashtests/137216-1.xhtml @@ -0,0 +1,4 @@ + + + + + + diff --git a/layout/xul/crashtests/381862.html b/layout/xul/crashtests/381862.html new file mode 100644 index 0000000000..65721d1a3f --- /dev/null +++ b/layout/xul/crashtests/381862.html @@ -0,0 +1,23 @@ + +Testcase bug - Crash [@ nsBoxFrame::BuildDisplayListForChildren] with tree stuff in iframe toggling display + + + + + + + diff --git a/layout/xul/crashtests/382746-1.xhtml b/layout/xul/crashtests/382746-1.xhtml new file mode 100644 index 0000000000..c76a1531cd --- /dev/null +++ b/layout/xul/crashtests/382746-1.xhtml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/layout/xul/crashtests/382899-1.xhtml b/layout/xul/crashtests/382899-1.xhtml new file mode 100644 index 0000000000..4b48eac240 --- /dev/null +++ b/layout/xul/crashtests/382899-1.xhtml @@ -0,0 +1,9 @@ + + + + + + +x + + diff --git a/layout/xul/crashtests/384037-1.xhtml b/layout/xul/crashtests/384037-1.xhtml new file mode 100644 index 0000000000..04bac671cc --- /dev/null +++ b/layout/xul/crashtests/384037-1.xhtml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/layout/xul/crashtests/384105-1-inner.xhtml b/layout/xul/crashtests/384105-1-inner.xhtml new file mode 100644 index 0000000000..ea9c0be8ad --- /dev/null +++ b/layout/xul/crashtests/384105-1-inner.xhtml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/layout/xul/crashtests/384105-1.html b/layout/xul/crashtests/384105-1.html new file mode 100644 index 0000000000..8161342ec8 --- /dev/null +++ b/layout/xul/crashtests/384105-1.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/layout/xul/crashtests/384373-1.xhtml b/layout/xul/crashtests/384373-1.xhtml new file mode 100644 index 0000000000..603b53cdea --- /dev/null +++ b/layout/xul/crashtests/384373-1.xhtml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/layout/xul/crashtests/384373-2.xhtml b/layout/xul/crashtests/384373-2.xhtml new file mode 100644 index 0000000000..1d56394e31 --- /dev/null +++ b/layout/xul/crashtests/384373-2.xhtml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/layout/xul/crashtests/384373.html b/layout/xul/crashtests/384373.html new file mode 100644 index 0000000000..a3658b86f8 --- /dev/null +++ b/layout/xul/crashtests/384373.html @@ -0,0 +1,23 @@ + + + + Testcase for bug 384373 + + + + + + + + + diff --git a/layout/xul/crashtests/384871-1-inner.xhtml b/layout/xul/crashtests/384871-1-inner.xhtml new file mode 100644 index 0000000000..62efdb2608 --- /dev/null +++ b/layout/xul/crashtests/384871-1-inner.xhtml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/layout/xul/crashtests/384871-1.html b/layout/xul/crashtests/384871-1.html new file mode 100644 index 0000000000..bcd9f98bc8 --- /dev/null +++ b/layout/xul/crashtests/384871-1.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/layout/xul/crashtests/386642.xhtml b/layout/xul/crashtests/386642.xhtml new file mode 100644 index 0000000000..50db21a095 --- /dev/null +++ b/layout/xul/crashtests/386642.xhtml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/layout/xul/crashtests/387080-1.xhtml b/layout/xul/crashtests/387080-1.xhtml new file mode 100644 index 0000000000..4eb9bd784b --- /dev/null +++ b/layout/xul/crashtests/387080-1.xhtml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/layout/xul/crashtests/391974-1-inner.xhtml b/layout/xul/crashtests/391974-1-inner.xhtml new file mode 100644 index 0000000000..f13aa2110f --- /dev/null +++ b/layout/xul/crashtests/391974-1-inner.xhtml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/layout/xul/crashtests/391974-1.html b/layout/xul/crashtests/391974-1.html new file mode 100644 index 0000000000..6946d66182 --- /dev/null +++ b/layout/xul/crashtests/391974-1.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/layout/xul/crashtests/402912-1.xhtml b/layout/xul/crashtests/402912-1.xhtml new file mode 100644 index 0000000000..b2cb98dc5a --- /dev/null +++ b/layout/xul/crashtests/402912-1.xhtml @@ -0,0 +1,5 @@ + + + + + diff --git a/layout/xul/crashtests/404192.xhtml b/layout/xul/crashtests/404192.xhtml new file mode 100644 index 0000000000..4ad5af348b --- /dev/null +++ b/layout/xul/crashtests/404192.xhtml @@ -0,0 +1,12 @@ + + + + + diff --git a/layout/xul/crashtests/408904-1.xhtml b/layout/xul/crashtests/408904-1.xhtml new file mode 100644 index 0000000000..59f215c73b --- /dev/null +++ b/layout/xul/crashtests/408904-1.xhtml @@ -0,0 +1 @@ + diff --git a/layout/xul/crashtests/412479-1.xhtml b/layout/xul/crashtests/412479-1.xhtml new file mode 100644 index 0000000000..b1086a816e --- /dev/null +++ b/layout/xul/crashtests/412479-1.xhtml @@ -0,0 +1,4 @@ + + + + diff --git a/layout/xul/crashtests/417509.xhtml b/layout/xul/crashtests/417509.xhtml new file mode 100644 index 0000000000..81703ada37 --- /dev/null +++ b/layout/xul/crashtests/417509.xhtml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/layout/xul/crashtests/430356-1.xhtml b/layout/xul/crashtests/430356-1.xhtml new file mode 100644 index 0000000000..8e7858904f --- /dev/null +++ b/layout/xul/crashtests/430356-1.xhtml @@ -0,0 +1,5 @@ + + + + + diff --git a/layout/xul/crashtests/464407-1.xhtml b/layout/xul/crashtests/464407-1.xhtml new file mode 100644 index 0000000000..83666a6a46 --- /dev/null +++ b/layout/xul/crashtests/464407-1.xhtml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/layout/xul/crashtests/470063-1.html b/layout/xul/crashtests/470063-1.html new file mode 100644 index 0000000000..11c01b30e4 --- /dev/null +++ b/layout/xul/crashtests/470063-1.html @@ -0,0 +1,15 @@ + + + + + + + diff --git a/layout/xul/crashtests/470272.html b/layout/xul/crashtests/470272.html new file mode 100644 index 0000000000..5caf12d636 --- /dev/null +++ b/layout/xul/crashtests/470272.html @@ -0,0 +1,21 @@ + + + + + +
+
    + +
      + + + +
      + + diff --git a/layout/xul/crashtests/538308-1.xhtml b/layout/xul/crashtests/538308-1.xhtml new file mode 100644 index 0000000000..477c725ed1 --- /dev/null +++ b/layout/xul/crashtests/538308-1.xhtml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + diff --git a/layout/xul/crashtests/557174-1.xml b/layout/xul/crashtests/557174-1.xml new file mode 100644 index 0000000000..02850a2db9 --- /dev/null +++ b/layout/xul/crashtests/557174-1.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/layout/xul/crashtests/564705-1.xhtml b/layout/xul/crashtests/564705-1.xhtml new file mode 100644 index 0000000000..b0f29bef7a --- /dev/null +++ b/layout/xul/crashtests/564705-1.xhtml @@ -0,0 +1,6 @@ + + + + + + diff --git a/layout/xul/crashtests/583957-1.html b/layout/xul/crashtests/583957-1.html new file mode 100644 index 0000000000..48d29fc1c6 --- /dev/null +++ b/layout/xul/crashtests/583957-1.html @@ -0,0 +1,20 @@ + + + + + + + diff --git a/layout/xul/crashtests/617089.html b/layout/xul/crashtests/617089.html new file mode 100644 index 0000000000..22e5f6d535 --- /dev/null +++ b/layout/xul/crashtests/617089.html @@ -0,0 +1,9 @@ + + + +
      +
      +
      +
      + + diff --git a/layout/xul/crashtests/716503.html b/layout/xul/crashtests/716503.html new file mode 100644 index 0000000000..250ad2ba40 --- /dev/null +++ b/layout/xul/crashtests/716503.html @@ -0,0 +1,11 @@ + + + + +
      + diff --git a/layout/xul/crashtests/crashtests.list b/layout/xul/crashtests/crashtests.list new file mode 100644 index 0000000000..3154435451 --- /dev/null +++ b/layout/xul/crashtests/crashtests.list @@ -0,0 +1,52 @@ +load chrome://reftest/content/crashtests/layout/xul/crashtests/131008-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/crashtests/137216-1.xhtml +load 140218-1.xml +load chrome://reftest/content/crashtests/layout/xul/crashtests/151826-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/crashtests/168724-1.xhtml +skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/289410-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/crashtests/291702-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/crashtests/291702-2.xhtml +load chrome://reftest/content/crashtests/layout/xul/crashtests/291702-3.xhtml +load chrome://reftest/content/crashtests/layout/xul/crashtests/294371-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/crashtests/322786-1.xhtml +skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/325377.xhtml +skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/326879-1.xhtml +skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/329327-1.xhtml +load 329407-1.xml +load chrome://reftest/content/crashtests/layout/xul/crashtests/336962-1.xhtml +skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/344228-1.xhtml +skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/365151.xhtml +load chrome://reftest/content/crashtests/layout/xul/crashtests/366112-1.xhtml +skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/366203-1.xhtml +load 367185-1.xhtml +load 369942-1.xhtml +load 376137-1.html +load 376137-2.html +load 378961.html +load 381862.html +load chrome://reftest/content/crashtests/layout/xul/crashtests/382746-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/crashtests/382899-1.xhtml +load 384037-1.xhtml +load 384105-1.html +load 384373.html +load 384871-1.html +load chrome://reftest/content/crashtests/layout/xul/crashtests/386642.xhtml +load chrome://reftest/content/crashtests/layout/xul/crashtests/387080-1.xhtml +load 391974-1.html +load 402912-1.xhtml +load 404192.xhtml +load chrome://reftest/content/crashtests/layout/xul/crashtests/408904-1.xhtml +load 412479-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/crashtests/417509.xhtml +load 430356-1.xhtml +asserts(0-1) load 464407-1.xhtml # Bugs 450974, 1267054, 718883 +load 470063-1.html +load 470272.html +skip-if(Android) load chrome://reftest/content/crashtests/layout/xul/crashtests/538308-1.xhtml +load 557174-1.xml +load chrome://reftest/content/crashtests/layout/xul/crashtests/564705-1.xhtml +load 583957-1.html +load 617089.html +load menulist-focused.xhtml +load 716503.html +load chrome://reftest/content/crashtests/layout/xul/crashtests/1379332-2.xhtml diff --git a/layout/xul/crashtests/menulist-focused.xhtml b/layout/xul/crashtests/menulist-focused.xhtml new file mode 100644 index 0000000000..7a09a838d7 --- /dev/null +++ b/layout/xul/crashtests/menulist-focused.xhtml @@ -0,0 +1,5 @@ + + + + + diff --git a/layout/xul/moz.build b/layout/xul/moz.build new file mode 100644 index 0000000000..6942bff353 --- /dev/null +++ b/layout/xul/moz.build @@ -0,0 +1,50 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "XUL") + +if CONFIG["ENABLE_TESTS"]: + MOCHITEST_MANIFESTS += ["test/mochitest.ini"] + MOCHITEST_CHROME_MANIFESTS += ["test/chrome.ini"] + BROWSER_CHROME_MANIFESTS += ["test/browser.ini"] + +EXPORTS += [ + "nsIScrollbarMediator.h", + "nsXULPopupManager.h", + "nsXULTooltipListener.h", +] + +UNIFIED_SOURCES += [ + "MiddleCroppingLabelFrame.cpp", + "nsMenuPopupFrame.cpp", + "nsRepeatService.cpp", + "nsScrollbarButtonFrame.cpp", + "nsScrollbarFrame.cpp", + "nsSliderFrame.cpp", + "nsSplitterFrame.cpp", + "nsXULPopupManager.cpp", + "nsXULTooltipListener.cpp", + "SimpleXULLeafFrame.cpp", +] + +DIRS += ["tree"] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + CFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "../base", + "../generic", + "../painting", + "../style", + "/dom/base", + "/dom/xul", +] diff --git a/layout/xul/nsIPopupContainer.h b/layout/xul/nsIPopupContainer.h new file mode 100644 index 0000000000..4870863781 --- /dev/null +++ b/layout/xul/nsIPopupContainer.h @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsIPopupContainer_h___ +#define nsIPopupContainer_h___ + +#include "nsQueryFrame.h" +class nsIContent; + +namespace mozilla { +class PresShell; +namespace dom { +class Element; +} +} // namespace mozilla + +class nsIPopupContainer { + public: + NS_DECL_QUERYFRAME_TARGET(nsIPopupContainer) + + virtual mozilla::dom::Element* GetDefaultTooltip() = 0; + + static nsIPopupContainer* GetPopupContainer(mozilla::PresShell* aShell); +}; + +#endif diff --git a/layout/xul/nsIScrollbarMediator.h b/layout/xul/nsIScrollbarMediator.h new file mode 100644 index 0000000000..ce26289fd9 --- /dev/null +++ b/layout/xul/nsIScrollbarMediator.h @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsIScrollbarMediator_h___ +#define nsIScrollbarMediator_h___ + +#include "mozilla/ScrollTypes.h" +#include "nsQueryFrame.h" +#include "nsCoord.h" + +class nsScrollbarFrame; +class nsIFrame; + +class nsIScrollbarMediator : public nsQueryFrame { + public: + NS_DECL_QUERYFRAME_TARGET(nsIScrollbarMediator) + + /** + * The aScrollbar argument denotes the scrollbar that's firing the + * notification. aScrollbar is never null. aDirection is either -1, 0, or 1. + */ + + /** + * When set to ENABLE_SNAP, additional scrolling will be performed after the + * scroll operation to maintain the constraints set by CSS Scroll snapping. + * The additional scrolling may include asynchronous smooth scrolls that + * continue to animate after the initial scroll position has been set. + * In case of DEFAULT, it means ENABLE_SNAP for CSS scroll snap v1, + * DISABLE_SNAP for the old scroll snap. + */ + + /** + * One of the following three methods is called when the scrollbar's button is + * clicked. + * @note These methods might destroy the frame, pres shell, and other objects. + */ + virtual void ScrollByPage(nsScrollbarFrame* aScrollbar, int32_t aDirection, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) = 0; + virtual void ScrollByWhole(nsScrollbarFrame* aScrollbar, int32_t aDirection, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) = 0; + virtual void ScrollByLine(nsScrollbarFrame* aScrollbar, int32_t aDirection, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) = 0; + + // Only implemented for nsGfxScrollFrame, not nsTreeBodyFrame. + virtual void ScrollByUnit(nsScrollbarFrame* aScrollbar, + mozilla::ScrollMode aMode, int32_t aDirection, + mozilla::ScrollUnit aUnit, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) = 0; + + /** + * RepeatButtonScroll is called when the scrollbar's button is held down. When + * the button is first clicked the increment is set; RepeatButtonScroll adds + * this increment to the current position. + * @note This method might destroy the frame, pres shell, and other objects. + */ + virtual void RepeatButtonScroll(nsScrollbarFrame* aScrollbar) = 0; + /** + * aOldPos and aNewPos are scroll positions. + * The scroll positions start with zero at the left edge; implementors that + * want zero at the right edge for RTL content will need to adjust + * accordingly. (See nsHTMLScrollFrame::ThumbMoved in nsGfxScrollFrame.cpp.) + * @note This method might destroy the frame, pres shell, and other objects. + */ + virtual void ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos, + nscoord aNewPos) = 0; + /** + * Called when the scroll bar thumb, slider, or any other component is + * released. + */ + virtual void ScrollbarReleased(nsScrollbarFrame* aScrollbar) = 0; + virtual void VisibilityChanged(bool aVisible) = 0; + + /** + * Obtain the frame for the horizontal or vertical scrollbar, or null + * if there is no such box. + */ + virtual nsScrollbarFrame* GetScrollbarBox(bool aVertical) = 0; + /** + * Show or hide scrollbars on 2 fingers touch. + * Subclasses should call their ScrollbarActivity's corresponding methods. + */ + virtual void ScrollbarActivityStarted() const = 0; + virtual void ScrollbarActivityStopped() const = 0; + + virtual bool IsScrollbarOnRight() const = 0; + + /** + * Returns true if the mediator is asking the scrollbar to suppress + * repainting itself on changes. + */ + virtual bool ShouldSuppressScrollbarRepaints() const = 0; +}; + +#endif diff --git a/layout/xul/nsMenuPopupFrame.cpp b/layout/xul/nsMenuPopupFrame.cpp new file mode 100644 index 0000000000..c82411285f --- /dev/null +++ b/layout/xul/nsMenuPopupFrame.cpp @@ -0,0 +1,2433 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMenuPopupFrame.h" +#include "LayoutConstants.h" +#include "XULButtonElement.h" +#include "XULPopupElement.h" +#include "mozilla/dom/XULPopupElement.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsIFrameInlines.h" +#include "nsAtom.h" +#include "nsPresContext.h" +#include "mozilla/ComputedStyle.h" +#include "nsCSSRendering.h" +#include "nsNameSpaceManager.h" +#include "nsIFrameInlines.h" +#include "nsViewManager.h" +#include "nsWidgetsCID.h" +#include "nsPIDOMWindow.h" +#include "nsFrameManager.h" +#include "mozilla/dom/Document.h" +#include "nsRect.h" +#include "nsIScrollableFrame.h" +#include "nsIPopupContainer.h" +#include "nsIDocShell.h" +#include "nsReadableUtils.h" +#include "nsUnicharUtils.h" +#include "nsLayoutUtils.h" +#include "nsContentUtils.h" +#include "nsCSSFrameConstructor.h" +#include "nsPIWindowRoot.h" +#include "nsIReflowCallback.h" +#include "nsIDocShellTreeOwner.h" +#include "nsIBaseWindow.h" +#include "nsISound.h" +#include "nsIScreenManager.h" +#include "nsServiceManagerUtils.h" +#include "nsStyleConsts.h" +#include "nsStyleStructInlines.h" +#include "nsTransitionManager.h" +#include "nsDisplayList.h" +#include "nsIDOMXULSelectCntrlEl.h" +#include "mozilla/widget/ScreenManager.h" +#include "mozilla/AnimationUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/Preferences.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/Services.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/KeyboardEvent.h" +#include "mozilla/dom/KeyboardEventBinding.h" +#include + +#include "X11UndefineNone.h" +#include "nsXULPopupManager.h" + +using namespace mozilla; +using mozilla::dom::Document; +using mozilla::dom::Element; +using mozilla::dom::Event; +using mozilla::dom::XULButtonElement; + +int8_t nsMenuPopupFrame::sDefaultLevelIsTop = -1; + +TimeStamp nsMenuPopupFrame::sLastKeyTime; + +#ifdef MOZ_WAYLAND +# include "mozilla/WidgetUtilsGtk.h" +# define IS_WAYLAND_DISPLAY() mozilla::widget::GdkIsWaylandDisplay() +extern mozilla::LazyLogModule gWidgetPopupLog; +# define LOG_WAYLAND(...) \ + MOZ_LOG(gWidgetPopupLog, mozilla::LogLevel::Debug, (__VA_ARGS__)) +#else +# define IS_WAYLAND_DISPLAY() false +# define LOG_WAYLAND(...) +#endif + +// NS_NewMenuPopupFrame +// +// Wrapper for creating a new menu popup container +// +nsIFrame* NS_NewMenuPopupFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) + nsMenuPopupFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsMenuPopupFrame) + +NS_QUERYFRAME_HEAD(nsMenuPopupFrame) + NS_QUERYFRAME_ENTRY(nsMenuPopupFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame) + +// +// nsMenuPopupFrame ctor +// +nsMenuPopupFrame::nsMenuPopupFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsBlockFrame(aStyle, aPresContext, kClassID) { + // the preference name is backwards here. True means that the 'top' level is + // the default, and false means that the 'parent' level is the default. + if (sDefaultLevelIsTop >= 0) return; + sDefaultLevelIsTop = + Preferences::GetBool("ui.panel.default_level_parent", false); +} // ctor + +nsMenuPopupFrame::~nsMenuPopupFrame() = default; + +static bool IsMouseTransparent(const ComputedStyle& aStyle) { + // If pointer-events: none; is set on the popup, then the widget should + // ignore mouse events, passing them through to the content behind. + return aStyle.PointerEvents() == StylePointerEvents::None; +} + +static nsIWidget::InputRegion ComputeInputRegion(const ComputedStyle& aStyle, + const nsPresContext& aPc) { + return {IsMouseTransparent(aStyle), + (aStyle.StyleUIReset()->mMozWindowInputRegionMargin.ToCSSPixels() * + aPc.CSSToDevPixelScale()) + .Truncated()}; +} + +bool nsMenuPopupFrame::ShouldCreateWidgetUpfront() const { + if (mPopupType != PopupType::Menu) { + // Any panel with a type attribute, such as the autocomplete popup, is + // always generated right away. + return mContent->AsElement()->HasAttr(nsGkAtoms::type); + } + + // Generate the widget up-front if the parent menu is a unless its + // sizetopopup is set to "none". + return ShouldExpandToInflowParentOrAnchor(); +} + +void nsMenuPopupFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsBlockFrame::Init(aContent, aParent, aPrevInFlow); + + CreatePopupView(); + + // XXX Hack. The popup's view should float above all other views, + // so we use the nsView::SetFloating() to tell the view manager + // about that constraint. + nsView* ourView = GetView(); + nsViewManager* viewManager = ourView->GetViewManager(); + viewManager->SetViewFloating(ourView, true); + + const auto& el = PopupElement(); + mPopupType = PopupType::Panel; + if (el.IsMenu()) { + mPopupType = PopupType::Menu; + } else if (el.IsXULElement(nsGkAtoms::tooltip)) { + mPopupType = PopupType::Tooltip; + } + + if (PresContext()->IsChrome()) { + mInContentShell = false; + } + + // Support incontentshell=false attribute to allow popups to be displayed + // outside of the content shell. Chrome only. + if (el.NodePrincipal()->IsSystemPrincipal()) { + if (el.GetXULBoolAttr(nsGkAtoms::incontentshell)) { + mInContentShell = true; + } else if (el.AttrValueIs(kNameSpaceID_None, nsGkAtoms::incontentshell, + nsGkAtoms::_false, eCaseMatters)) { + mInContentShell = false; + } + } + + // To improve performance, create the widget for the popup if needed. Popups + // such as menus will create their widgets later when the popup opens. + // + // FIXME(emilio): Doing this up-front for all menupopups causes a bunch of + // assertions, while it's supposed to be just an optimization. + if (!ourView->HasWidget() && ShouldCreateWidgetUpfront()) { + CreateWidgetForView(ourView); + } + + AddStateBits(NS_FRAME_IN_POPUP); +} + +bool nsMenuPopupFrame::HasRemoteContent() const { + return !mInContentShell && mPopupType == PopupType::Panel && + mContent->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::remote, nsGkAtoms::_true, + eIgnoreCase); +} + +bool nsMenuPopupFrame::IsNoAutoHide() const { + // Panels with noautohide="true" don't hide when the mouse is clicked + // outside of them, or when another application is made active. Non-autohide + // panels cannot be used in content windows. + return !mInContentShell && mPopupType == PopupType::Panel && + mContent->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::noautohide, + nsGkAtoms::_true, eIgnoreCase); +} + +widget::PopupLevel nsMenuPopupFrame::GetPopupLevel(bool aIsNoAutoHide) const { + // The popup level is determined as follows, in this order: + // 1. non-panels (menus and tooltips) are always topmost + // 2. any specified level attribute + // 3. if a titlebar attribute is set, use the 'floating' level + // 4. if this is a noautohide panel, use the 'parent' level + // 5. use the platform-specific default level + + // If this is not a panel, this is always a top-most popup. + if (mPopupType != PopupType::Panel) { + return PopupLevel::Top; + } + + // If the level attribute has been set, use that. + static Element::AttrValuesArray strings[] = { + nsGkAtoms::top, nsGkAtoms::parent, nsGkAtoms::floating, nullptr}; + switch (mContent->AsElement()->FindAttrValueIn( + kNameSpaceID_None, nsGkAtoms::level, strings, eCaseMatters)) { + case 0: + return PopupLevel::Top; + case 1: + return PopupLevel::Parent; + case 2: + return PopupLevel::Floating; + } + + // Panels with titlebars most likely want to be floating popups. + if (mContent->AsElement()->HasAttr(nsGkAtoms::titlebar)) { + return PopupLevel::Floating; + } + + // If this panel is a noautohide panel, the default is the parent level. + if (aIsNoAutoHide) { + return PopupLevel::Parent; + } + + // Otherwise, the result depends on the platform. + return sDefaultLevelIsTop ? PopupLevel::Top : PopupLevel::Parent; +} + +void nsMenuPopupFrame::PrepareWidget(bool aRecreate) { + nsView* ourView = GetView(); + if (aRecreate) { + if (auto* widget = GetWidget()) { + // Widget's WebRender resources needs to be cleared before creating new + // widget. + widget->ClearCachedWebrenderResources(); + } + ourView->DestroyWidget(); + } + if (!ourView->HasWidget()) { + CreateWidgetForView(ourView); + } else { + PropagateStyleToWidget(); + } +} + +nsresult nsMenuPopupFrame::CreateWidgetForView(nsView* aView) { + // Create a widget for ourselves. + widget::InitData widgetData; + widgetData.mWindowType = widget::WindowType::Popup; + widgetData.mBorderStyle = widget::BorderStyle::Default; + widgetData.mForMenupopupFrame = true; + widgetData.mClipSiblings = true; + widgetData.mPopupHint = mPopupType; + widgetData.mNoAutoHide = IsNoAutoHide(); + + if (!mInContentShell) { + // A drag popup may be used for non-static translucent drag feedback + if (mPopupType == PopupType::Panel && + mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, + nsGkAtoms::drag, eIgnoreCase)) { + widgetData.mIsDragPopup = true; + } + } + + nsAutoString title; + if (widgetData.mNoAutoHide && + mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::titlebar, + nsGkAtoms::normal, eCaseMatters)) { + widgetData.mBorderStyle = widget::BorderStyle::Title; + + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::label, title); + if (mContent->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::close, + nsGkAtoms::_true, eCaseMatters)) { + widgetData.mBorderStyle = + widgetData.mBorderStyle | widget::BorderStyle::Close; + } + } + + bool remote = HasRemoteContent(); + + const auto mode = nsLayoutUtils::GetFrameTransparency(this, this); + widgetData.mHasRemoteContent = remote; + widgetData.mTransparencyMode = mode; + widgetData.mPopupLevel = GetPopupLevel(widgetData.mNoAutoHide); + + // Panels which have a parent level need a parent widget. This allows them to + // always appear in front of the parent window but behind other windows that + // should be in front of it. + nsCOMPtr parentWidget; + if (widgetData.mPopupLevel != PopupLevel::Top) { + nsCOMPtr dsti = PresContext()->GetDocShell(); + if (!dsti) return NS_ERROR_FAILURE; + + nsCOMPtr treeOwner; + dsti->GetTreeOwner(getter_AddRefs(treeOwner)); + if (!treeOwner) return NS_ERROR_FAILURE; + + nsCOMPtr baseWindow(do_QueryInterface(treeOwner)); + if (baseWindow) baseWindow->GetMainWidget(getter_AddRefs(parentWidget)); + } + + nsresult rv = aView->CreateWidgetForPopup(&widgetData, parentWidget); + if (NS_FAILED(rv)) { + return rv; + } + + nsIWidget* widget = aView->GetWidget(); + widget->SetTransparencyMode(mode); + + PropagateStyleToWidget(); + + // most popups don't have a title so avoid setting the title if there isn't + // one + if (!title.IsEmpty()) { + widget->SetTitle(title); + } + + return NS_OK; +} + +void nsMenuPopupFrame::PropagateStyleToWidget(WidgetStyleFlags aFlags) const { + if (aFlags.isEmpty()) { + return; + } + + nsIWidget* widget = GetWidget(); + if (!widget) { + return; + } + + if (aFlags.contains(WidgetStyle::ColorScheme)) { + widget->SetColorScheme(Some(LookAndFeel::ColorSchemeForFrame(this))); + } + if (aFlags.contains(WidgetStyle::InputRegion)) { + widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext())); + } + if (aFlags.contains(WidgetStyle::Opacity)) { + widget->SetWindowOpacity(StyleUIReset()->mWindowOpacity); + } + if (aFlags.contains(WidgetStyle::Shadow)) { + widget->SetWindowShadowStyle(GetShadowStyle()); + } + if (aFlags.contains(WidgetStyle::Transform)) { + widget->SetWindowTransform(ComputeWidgetTransform()); + } +} + +bool nsMenuPopupFrame::IsMouseTransparent() const { + return ::IsMouseTransparent(*Style()); +} + +StyleWindowShadow nsMenuPopupFrame::GetShadowStyle() const { + StyleWindowShadow shadow = StyleUIReset()->mWindowShadow; + if (shadow != StyleWindowShadow::Default) { + return shadow; + } + + switch (StyleDisplay()->EffectiveAppearance()) { + case StyleAppearance::Tooltip: + return StyleWindowShadow::Tooltip; + case StyleAppearance::Menupopup: + return StyleWindowShadow::Menu; + default: + return StyleWindowShadow::Default; + } +} + +void nsMenuPopupFrame::SetPopupState(nsPopupState aState) { + mPopupState = aState; + + // Work around https://gitlab.gnome.org/GNOME/gtk/-/issues/4166 + if (aState == ePopupShown && IS_WAYLAND_DISPLAY()) { + if (nsIWidget* widget = GetWidget()) { + widget->SetInputRegion(ComputeInputRegion(*Style(), *PresContext())); + } + } +} + +// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsXULPopupShownEvent::Run() { + nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame()); + // Set the state to visible if the popup is still open. + if (popup && popup->IsOpen()) { + popup->SetPopupState(ePopupShown); + } + + if (!mPopup->IsXULElement(nsGkAtoms::tooltip)) { + nsCOMPtr obsService = + mozilla::services::GetObserverService(); + if (obsService) { + obsService->NotifyObservers(mPopup, "popup-shown", nullptr); + } + } + WidgetMouseEvent event(true, eXULPopupShown, nullptr, + WidgetMouseEvent::eReal); + return EventDispatcher::Dispatch(mPopup, mPresContext, &event); +} + +NS_IMETHODIMP nsXULPopupShownEvent::HandleEvent(Event* aEvent) { + nsMenuPopupFrame* popup = do_QueryFrame(mPopup->GetPrimaryFrame()); + // Ignore events not targeted at the popup itself (ie targeted at + // descendants): + if (mPopup != aEvent->GetTarget()) { + return NS_OK; + } + if (popup) { + // ResetPopupShownDispatcher will delete the reference to this, so keep + // another one until Run is finished. + RefPtr event = this; + // Only call Run if it the dispatcher was assigned. This avoids calling the + // Run method if the transitionend event fires multiple times. + if (popup->ClearPopupShownDispatcher()) { + return Run(); + } + } + + CancelListener(); + return NS_OK; +} + +void nsXULPopupShownEvent::CancelListener() { + mPopup->RemoveSystemEventListener(u"transitionend"_ns, this, false); +} + +NS_IMPL_ISUPPORTS_INHERITED(nsXULPopupShownEvent, Runnable, + nsIDOMEventListener); + +void nsMenuPopupFrame::DidSetComputedStyle(ComputedStyle* aOldStyle) { + nsBlockFrame::DidSetComputedStyle(aOldStyle); + + if (!aOldStyle) { + return; + } + + WidgetStyleFlags flags; + + if (aOldStyle->StyleUI()->mColorScheme != StyleUI()->mColorScheme) { + flags += WidgetStyle::ColorScheme; + } + + auto& newUI = *StyleUIReset(); + auto& oldUI = *aOldStyle->StyleUIReset(); + if (newUI.mWindowOpacity != oldUI.mWindowOpacity) { + flags += WidgetStyle::Opacity; + } + + if (newUI.mMozWindowTransform != oldUI.mMozWindowTransform) { + flags += WidgetStyle::Transform; + } + + if (newUI.mWindowShadow != oldUI.mWindowShadow) { + flags += WidgetStyle::Shadow; + } + + const auto& pc = *PresContext(); + auto oldRegion = ComputeInputRegion(*aOldStyle, pc); + auto newRegion = ComputeInputRegion(*Style(), pc); + if (oldRegion.mFullyTransparent != newRegion.mFullyTransparent || + oldRegion.mMargin != newRegion.mMargin) { + flags += WidgetStyle::InputRegion; + } + + PropagateStyleToWidget(flags); +} + +void nsMenuPopupFrame::TweakMinPrefISize(nscoord& aSize) { + if (!ShouldExpandToInflowParentOrAnchor()) { + return; + } + // Make sure to accommodate for our scrollbar if needed. Do it only for + // menulists to match previous behavior. + // + // NOTE(emilio): This is somewhat hacky. The "right" fix (which would be + // using scrollbar-gutter: stable on the scroller) isn't great, because even + // though we want a stable gutter, we want to draw on top of the gutter when + // there's no scrollbar, otherwise it looks rather weird. + // + // Automatically accommodating for the scrollbar otherwise would be bug + // 764076, but that has its own set of problems. + if (nsIScrollableFrame* sf = GetScrollFrame()) { + aSize += sf->GetDesiredScrollbarSizes().LeftRight(); + } + + nscoord menuListOrAnchorWidth = 0; + if (nsIFrame* menuList = GetInFlowParent()) { + menuListOrAnchorWidth = menuList->GetRect().width; + } + if (mAnchorType == MenuPopupAnchorType_Rect) { + menuListOrAnchorWidth = std::max(menuListOrAnchorWidth, mScreenRect.width); + } + // Input margin doesn't have contents, so account for it for popup sizing + // purposes. + menuListOrAnchorWidth += + 2 * StyleUIReset()->mMozWindowInputRegionMargin.ToAppUnits(); + aSize = std::max(aSize, menuListOrAnchorWidth); +} + +nscoord nsMenuPopupFrame::GetMinISize(gfxContext* aRC) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + + result = nsBlockFrame::GetMinISize(aRC); + TweakMinPrefISize(result); + return result; +} + +nscoord nsMenuPopupFrame::GetPrefISize(gfxContext* aRC) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + + result = nsBlockFrame::GetPrefISize(aRC); + TweakMinPrefISize(result); + return result; +} + +void nsMenuPopupFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("nsMenuPopupFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + + const auto wm = GetWritingMode(); + // Default to preserving our bounds. + aDesiredSize.SetSize(wm, GetLogicalSize(wm)); + + LayoutPopup(aPresContext, aDesiredSize, aReflowInput, aStatus); + + aDesiredSize.SetBlockStartAscent(aDesiredSize.BSize(wm)); + aDesiredSize.SetOverflowAreasToDesiredBounds(); + FinishAndStoreOverflow(&aDesiredSize, aReflowInput.mStyleDisplay); +} + +void nsMenuPopupFrame::EnsureActiveMenuListItemIsVisible() { + if (!IsMenuList() || !IsOpen()) { + return; + } + nsIFrame* frame = GetCurrentMenuItemFrame(); + if (!frame) { + return; + } + RefPtr presShell = PresShell(); + presShell->ScrollFrameIntoView( + frame, Nothing(), ScrollAxis(), ScrollAxis(), + ScrollFlags::ScrollOverflowHidden | ScrollFlags::ScrollFirstAncestorOnly); +} + +void nsMenuPopupFrame::LayoutPopup(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + if (IsNativeMenu()) { + return; + } + + SchedulePaint(); + + const bool isOpen = IsOpen(); + if (!isOpen) { + // If the popup is not open, only do layout while showing or if we're a + // menulist. + // + // This is needed because the SelectParent code wants to limit the height of + // the popup before opening it. + // + // TODO(emilio): We should consider adding a way to do that more reliably + // instead, but this preserves existing behavior. + const bool needsLayout = mPopupState == ePopupShowing || + mPopupState == ePopupPositioning || IsMenuList(); + if (!needsLayout) { + RemoveStateBits(NS_FRAME_FIRST_REFLOW); + return; + } + } + + // Do a first reflow, with all our content, in order to find our preferred + // size. Then, we do a second reflow with the updated dimensions. + const bool needsPrefSize = mPrefSize == nsSize(-1, -1) || IsSubtreeDirty(); + if (needsPrefSize) { + // Get the preferred, minimum and maximum size. If the menu is sized to the + // popup, then the popup's width is the menu's width. + ReflowOutput preferredSize(aReflowInput); + nsBlockFrame::Reflow(aPresContext, preferredSize, aReflowInput, aStatus); + mPrefSize = preferredSize.PhysicalSize(); + } + + // Get our desired position and final size, now that we have a preferred size. + auto constraints = GetRects(mPrefSize); + const auto finalSize = constraints.mUsedRect.Size(); + + // We need to do an extra reflow if we haven't reflowed, our size doesn't + // match with our final intended size, or our bsize is unconstrained (in which + // case we need to specify the final size so that percentages work). + const bool needDefiniteReflow = + aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE || !needsPrefSize || + finalSize != mPrefSize; + + if (needDefiniteReflow) { + ReflowInput constrainedReflowInput(aReflowInput); + const auto& bp = aReflowInput.ComputedPhysicalBorderPadding(); + // TODO: writing-mode handling not terribly correct, but it doesn't matter. + const nsSize finalContentSize(finalSize.width - bp.LeftRight(), + finalSize.height - bp.TopBottom()); + constrainedReflowInput.SetComputedISize(finalContentSize.width); + constrainedReflowInput.SetComputedBSize(finalContentSize.height); + constrainedReflowInput.SetIResize(finalSize.width != mPrefSize.width); + constrainedReflowInput.SetBResize([&] { + if (finalSize.height != mPrefSize.height) { + return true; + } + if (needsPrefSize && + aReflowInput.ComputedBSize() == NS_UNCONSTRAINEDSIZE && + aReflowInput.ComputedMaxBSize() == finalContentSize.height) { + // If we have measured, and maybe clamped our children via max-height, + // they might need to get percentages in the block axis re-resolved. + return true; + } + return false; + }()); + + aStatus.Reset(); + nsBlockFrame::Reflow(aPresContext, aDesiredSize, constrainedReflowInput, + aStatus); + } + + // Set our size, since nsAbsoluteContainingBlock won't. + SetRect(constraints.mUsedRect); + + nsView* view = GetView(); + if (isOpen) { + nsViewManager* viewManager = view->GetViewManager(); + viewManager->ResizeView(view, + nsRect(nsPoint(), constraints.mUsedRect.Size())); + if (mPopupState == ePopupOpening) { + mPopupState = ePopupVisible; + } + + viewManager->SetViewVisibility(view, ViewVisibility::Show); + SyncFrameViewProperties(view); + } + + // Perform our move now. That will position the view and so on. + PerformMove(constraints); + + // finally, if the popup just opened, send a popupshown event + bool openChanged = mIsOpenChanged; + if (openChanged) { + mIsOpenChanged = false; + + // Make sure the current selection in a menulist is visible. + EnsureActiveMenuListItemIsVisible(); + + // If the animate attribute is set to open, check for a transition and wait + // for it to finish before firing the popupshown event. + if (LookAndFeel::GetInt(LookAndFeel::IntID::PanelAnimations) && + mContent->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::animate, nsGkAtoms::open, + eCaseMatters) && + AnimationUtils::HasCurrentTransitions(mContent->AsElement(), + PseudoStyleType::NotPseudo)) { + mPopupShownDispatcher = new nsXULPopupShownEvent(mContent, aPresContext); + mContent->AddSystemEventListener(u"transitionend"_ns, + mPopupShownDispatcher, false, false); + return; + } + + // If there are no transitions, fire the popupshown event right away. + nsCOMPtr event = + new nsXULPopupShownEvent(GetContent(), aPresContext); + mContent->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget()); + } +} + +bool nsMenuPopupFrame::IsMenuList() const { + return PopupElement().IsInMenuList(); +} + +bool nsMenuPopupFrame::ShouldExpandToInflowParentOrAnchor() const { + return IsMenuList() && !mContent->GetParent()->AsElement()->AttrValueIs( + kNameSpaceID_None, nsGkAtoms::sizetopopup, + nsGkAtoms::none, eCaseMatters); +} + +nsIContent* nsMenuPopupFrame::GetTriggerContent( + nsMenuPopupFrame* aMenuPopupFrame) { + while (aMenuPopupFrame) { + if (aMenuPopupFrame->mTriggerContent) { + return aMenuPopupFrame->mTriggerContent; + } + + auto* button = XULButtonElement::FromNodeOrNull( + aMenuPopupFrame->GetContent()->GetParent()); + if (!button || !button->IsMenu()) { + break; + } + + auto* popup = button->GetContainingPopupElement(); + if (!popup) { + break; + } + + // check up the menu hierarchy until a popup with a trigger node is found + aMenuPopupFrame = do_QueryFrame(popup->GetPrimaryFrame()); + } + + return nullptr; +} + +void nsMenuPopupFrame::InitPositionFromAnchorAlign(const nsAString& aAnchor, + const nsAString& aAlign) { + mTriggerContent = nullptr; + + if (aAnchor.EqualsLiteral("topleft")) + mPopupAnchor = POPUPALIGNMENT_TOPLEFT; + else if (aAnchor.EqualsLiteral("topright")) + mPopupAnchor = POPUPALIGNMENT_TOPRIGHT; + else if (aAnchor.EqualsLiteral("bottomleft")) + mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT; + else if (aAnchor.EqualsLiteral("bottomright")) + mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT; + else if (aAnchor.EqualsLiteral("leftcenter")) + mPopupAnchor = POPUPALIGNMENT_LEFTCENTER; + else if (aAnchor.EqualsLiteral("rightcenter")) + mPopupAnchor = POPUPALIGNMENT_RIGHTCENTER; + else if (aAnchor.EqualsLiteral("topcenter")) + mPopupAnchor = POPUPALIGNMENT_TOPCENTER; + else if (aAnchor.EqualsLiteral("bottomcenter")) + mPopupAnchor = POPUPALIGNMENT_BOTTOMCENTER; + else + mPopupAnchor = POPUPALIGNMENT_NONE; + + if (aAlign.EqualsLiteral("topleft")) + mPopupAlignment = POPUPALIGNMENT_TOPLEFT; + else if (aAlign.EqualsLiteral("topright")) + mPopupAlignment = POPUPALIGNMENT_TOPRIGHT; + else if (aAlign.EqualsLiteral("bottomleft")) + mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT; + else if (aAlign.EqualsLiteral("bottomright")) + mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT; + else + mPopupAlignment = POPUPALIGNMENT_NONE; + + mPosition = POPUPPOSITION_UNKNOWN; +} + +void nsMenuPopupFrame::InitializePopup(nsIContent* aAnchorContent, + nsIContent* aTriggerContent, + const nsAString& aPosition, + int32_t aXPos, int32_t aYPos, + MenuPopupAnchorType aAnchorType, + bool aAttributesOverride) { + auto* widget = GetWidget(); + bool recreateWidget = widget && widget->NeedsRecreateToReshow(); + PrepareWidget(recreateWidget); + + mPopupState = ePopupShowing; + mAnchorContent = aAnchorContent; + mTriggerContent = aTriggerContent; + mXPos = aXPos; + mYPos = aYPos; + mIsNativeMenu = false; + mIsTopLevelContextMenu = false; + mVFlip = false; + mHFlip = false; + mAlignmentOffset = 0; + mPositionedOffset = 0; + mPositionedByMoveToRect = false; + + mAnchorType = aAnchorType; + + // if aAttributesOverride is true, then the popupanchor, popupalign and + // position attributes on the override those values passed in. + // If false, those attributes are only used if the values passed in are empty + if (aAnchorContent || aAnchorType == MenuPopupAnchorType_Rect) { + nsAutoString anchor, align, position, flip; + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::popupanchor, + anchor); + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::popupalign, + align); + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::position, + position); + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::flip, flip); + + if (aAttributesOverride) { + // if the attributes are set, clear the offset position. Otherwise, + // the offset is used to adjust the position from the anchor point + if (anchor.IsEmpty() && align.IsEmpty() && position.IsEmpty()) + position.Assign(aPosition); + else + mXPos = mYPos = 0; + } else if (!aPosition.IsEmpty()) { + position.Assign(aPosition); + } + + if (flip.EqualsLiteral("none")) { + mFlip = FlipType_None; + } else if (flip.EqualsLiteral("both")) { + mFlip = FlipType_Both; + } else if (flip.EqualsLiteral("slide")) { + mFlip = FlipType_Slide; + } + + position.CompressWhitespace(); + int32_t spaceIdx = position.FindChar(' '); + // if there is a space in the position, assume it is the anchor and + // alignment as two separate tokens. + if (spaceIdx >= 0) { + InitPositionFromAnchorAlign(Substring(position, 0, spaceIdx), + Substring(position, spaceIdx + 1)); + } else if (position.EqualsLiteral("before_start")) { + mPopupAnchor = POPUPALIGNMENT_TOPLEFT; + mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT; + mPosition = POPUPPOSITION_BEFORESTART; + } else if (position.EqualsLiteral("before_end")) { + mPopupAnchor = POPUPALIGNMENT_TOPRIGHT; + mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT; + mPosition = POPUPPOSITION_BEFOREEND; + } else if (position.EqualsLiteral("after_start")) { + mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT; + mPopupAlignment = POPUPALIGNMENT_TOPLEFT; + mPosition = POPUPPOSITION_AFTERSTART; + } else if (position.EqualsLiteral("after_end")) { + mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT; + mPopupAlignment = POPUPALIGNMENT_TOPRIGHT; + mPosition = POPUPPOSITION_AFTEREND; + } else if (position.EqualsLiteral("start_before")) { + mPopupAnchor = POPUPALIGNMENT_TOPLEFT; + mPopupAlignment = POPUPALIGNMENT_TOPRIGHT; + mPosition = POPUPPOSITION_STARTBEFORE; + } else if (position.EqualsLiteral("start_after")) { + mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT; + mPopupAlignment = POPUPALIGNMENT_BOTTOMRIGHT; + mPosition = POPUPPOSITION_STARTAFTER; + } else if (position.EqualsLiteral("end_before")) { + mPopupAnchor = POPUPALIGNMENT_TOPRIGHT; + mPopupAlignment = POPUPALIGNMENT_TOPLEFT; + mPosition = POPUPPOSITION_ENDBEFORE; + } else if (position.EqualsLiteral("end_after")) { + mPopupAnchor = POPUPALIGNMENT_BOTTOMRIGHT; + mPopupAlignment = POPUPALIGNMENT_BOTTOMLEFT; + mPosition = POPUPPOSITION_ENDAFTER; + } else if (position.EqualsLiteral("overlap")) { + mPopupAnchor = POPUPALIGNMENT_TOPLEFT; + mPopupAlignment = POPUPALIGNMENT_TOPLEFT; + mPosition = POPUPPOSITION_OVERLAP; + } else if (position.EqualsLiteral("after_pointer")) { + mPopupAnchor = POPUPALIGNMENT_TOPLEFT; + mPopupAlignment = POPUPALIGNMENT_TOPLEFT; + mPosition = POPUPPOSITION_AFTERPOINTER; + // XXXndeakin this is supposed to anchor vertically after, but with the + // horizontal position as the mouse pointer. + mYPos += 21; + } else if (position.EqualsLiteral("selection")) { + mPopupAnchor = POPUPALIGNMENT_BOTTOMLEFT; + mPopupAlignment = POPUPALIGNMENT_TOPLEFT; + mPosition = POPUPPOSITION_SELECTION; + } else { + InitPositionFromAnchorAlign(anchor, align); + } + } + // When converted back to CSSIntRect it is (-1, -1, 0, 0) - as expected in + // nsXULPopupManager::Rollup + mScreenRect = nsRect(-AppUnitsPerCSSPixel(), -AppUnitsPerCSSPixel(), 0, 0); + + if (aAttributesOverride) { + // Use |left| and |top| dimension attributes to position the popup if + // present, as they may have been persisted. + nsAutoString left, top; + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::left, left); + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::top, top); + + nsresult err; + if (!left.IsEmpty()) { + int32_t x = left.ToInteger(&err); + if (NS_SUCCEEDED(err)) { + mScreenRect.x = CSSPixel::ToAppUnits(x); + } + } + if (!top.IsEmpty()) { + int32_t y = top.ToInteger(&err); + if (NS_SUCCEEDED(err)) { + mScreenRect.y = CSSPixel::ToAppUnits(y); + } + } + } +} + +void nsMenuPopupFrame::InitializePopupAtScreen(nsIContent* aTriggerContent, + int32_t aXPos, int32_t aYPos, + bool aIsContextMenu) { + auto* widget = GetWidget(); + bool recreateWidget = widget && widget->NeedsRecreateToReshow(); + PrepareWidget(recreateWidget); + + mPopupState = ePopupShowing; + mAnchorContent = nullptr; + mTriggerContent = aTriggerContent; + mScreenRect = + nsRect(CSSPixel::ToAppUnits(aXPos), CSSPixel::ToAppUnits(aYPos), 0, 0); + mXPos = 0; + mYPos = 0; + mFlip = FlipType_Default; + mPopupAnchor = POPUPALIGNMENT_NONE; + mPopupAlignment = POPUPALIGNMENT_NONE; + mPosition = POPUPPOSITION_UNKNOWN; + mIsContextMenu = aIsContextMenu; + mIsTopLevelContextMenu = aIsContextMenu; + mIsNativeMenu = false; + mAnchorType = MenuPopupAnchorType_Point; + mPositionedOffset = 0; + mPositionedByMoveToRect = false; +} + +void nsMenuPopupFrame::InitializePopupAsNativeContextMenu( + nsIContent* aTriggerContent, int32_t aXPos, int32_t aYPos) { + mTriggerContent = aTriggerContent; + mPopupState = ePopupShowing; + mAnchorContent = nullptr; + mScreenRect = + nsRect(CSSPixel::ToAppUnits(aXPos), CSSPixel::ToAppUnits(aYPos), 0, 0); + mXPos = 0; + mYPos = 0; + mFlip = FlipType_Default; + mPopupAnchor = POPUPALIGNMENT_NONE; + mPopupAlignment = POPUPALIGNMENT_NONE; + mPosition = POPUPPOSITION_UNKNOWN; + mIsContextMenu = true; + mIsTopLevelContextMenu = true; + mIsNativeMenu = true; + mAnchorType = MenuPopupAnchorType_Point; + mPositionedOffset = 0; + mPositionedByMoveToRect = false; +} + +void nsMenuPopupFrame::InitializePopupAtRect(nsIContent* aTriggerContent, + const nsAString& aPosition, + const nsIntRect& aRect, + bool aAttributesOverride) { + InitializePopup(nullptr, aTriggerContent, aPosition, 0, 0, + MenuPopupAnchorType_Rect, aAttributesOverride); + mScreenRect = ToAppUnits(aRect, AppUnitsPerCSSPixel()); +} + +void nsMenuPopupFrame::ShowPopup(bool aIsContextMenu) { + mIsContextMenu = aIsContextMenu; + + InvalidateFrameSubtree(); + + if (mPopupState == ePopupShowing || mPopupState == ePopupPositioning) { + mPopupState = ePopupOpening; + mIsOpenChanged = true; + + // Clear mouse capture when a popup is opened. + if (mPopupType == PopupType::Menu) { + if (auto* activeESM = EventStateManager::GetActiveEventStateManager()) { + EventStateManager::ClearGlobalActiveContent(activeESM); + } + + PresShell::ReleaseCapturingContent(); + } + + if (RefPtr menu = PopupElement().GetContainingMenu()) { + menu->PopupOpened(); + } + + // do we need an actual reflow here? + // is SetPopupPosition all that is needed? + PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors, + NS_FRAME_IS_DIRTY); + + if (mPopupType == PopupType::Menu) { + nsCOMPtr sound(do_GetService("@mozilla.org/sound;1")); + if (sound) sound->PlayEventSound(nsISound::EVENT_MENU_POPUP); + } + } +} + +void nsMenuPopupFrame::ClearTriggerContentIncludingDocument() { + // clear the trigger content if the popup is being closed. But don't clear + // it if the popup is just being made invisible as a popuphiding or command + if (mTriggerContent) { + // if the popup had a trigger node set, clear the global window popup node + // as well + Document* doc = mContent->GetUncomposedDoc(); + if (doc) { + if (nsPIDOMWindowOuter* win = doc->GetWindow()) { + nsCOMPtr root = win->GetTopWindowRoot(); + if (root) { + root->SetPopupNode(nullptr); + } + } + } + } + mTriggerContent = nullptr; +} + +void nsMenuPopupFrame::HidePopup(bool aDeselectMenu, nsPopupState aNewState, + bool aFromFrameDestruction) { + NS_ASSERTION(aNewState == ePopupClosed || aNewState == ePopupInvisible, + "popup being set to unexpected state"); + + ClearPopupShownDispatcher(); + + // don't hide the popup when it isn't open + if (mPopupState == ePopupClosed || mPopupState == ePopupShowing || + mPopupState == ePopupPositioning) { + return; + } + + if (aNewState == ePopupClosed) { + // clear the trigger content if the popup is being closed. But don't clear + // it if the popup is just being made invisible as a popuphiding or command + // event may want to retrieve it. + ClearTriggerContentIncludingDocument(); + mAnchorContent = nullptr; + } + + // when invisible and about to be closed, HidePopup has already been called, + // so just set the new state to closed and return + if (mPopupState == ePopupInvisible) { + if (aNewState == ePopupClosed) { + mPopupState = ePopupClosed; + } + return; + } + + mPopupState = aNewState; + + mIncrementalString.Truncate(); + + mIsOpenChanged = false; + mHFlip = mVFlip = false; + + if (auto* widget = GetWidget()) { + // Ideally we should call ClearCachedWebrenderResources but there are + // intermittent failures (see bug 1748788), so we currently call + // ClearWebrenderAnimationResources instead. + widget->ClearWebrenderAnimationResources(); + } + + nsView* view = GetView(); + nsViewManager* viewManager = view->GetViewManager(); + viewManager->SetViewVisibility(view, ViewVisibility::Hide); + + RefPtr popup = &PopupElement(); + // XXX, bug 137033, In Windows, if mouse is outside the window when the + // menupopup closes, no mouse_enter/mouse_exit event will be fired to clear + // current hover state, we should clear it manually. This code may not the + // best solution, but we can leave it here until we find the better approach. + if (!aFromFrameDestruction && + popup->State().HasState(dom::ElementState::HOVER)) { + EventStateManager* esm = PresContext()->EventStateManager(); + esm->SetContentState(nullptr, dom::ElementState::HOVER); + } + popup->PopupClosed(aDeselectMenu); +} + +nsPoint nsMenuPopupFrame::AdjustPositionForAnchorAlign( + nsRect& anchorRect, const nsSize& aPrefSize, FlipStyle& aHFlip, + FlipStyle& aVFlip) const { + // flip the anchor and alignment for right-to-left + int8_t popupAnchor(mPopupAnchor); + int8_t popupAlign(mPopupAlignment); + if (IsDirectionRTL()) { + // no need to flip the centered anchor types vertically + if (popupAnchor <= POPUPALIGNMENT_LEFTCENTER) { + popupAnchor = -popupAnchor; + } + popupAlign = -popupAlign; + } + + nsRect originalAnchorRect(anchorRect); + + // first, determine at which corner of the anchor the popup should appear + nsPoint pnt; + switch (popupAnchor) { + case POPUPALIGNMENT_LEFTCENTER: + pnt = nsPoint(anchorRect.x, anchorRect.y + anchorRect.height / 2); + anchorRect.y = pnt.y; + anchorRect.height = 0; + break; + case POPUPALIGNMENT_RIGHTCENTER: + pnt = nsPoint(anchorRect.XMost(), anchorRect.y + anchorRect.height / 2); + anchorRect.y = pnt.y; + anchorRect.height = 0; + break; + case POPUPALIGNMENT_TOPCENTER: + pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.y); + anchorRect.x = pnt.x; + anchorRect.width = 0; + break; + case POPUPALIGNMENT_BOTTOMCENTER: + pnt = nsPoint(anchorRect.x + anchorRect.width / 2, anchorRect.YMost()); + anchorRect.x = pnt.x; + anchorRect.width = 0; + break; + case POPUPALIGNMENT_TOPRIGHT: + pnt = anchorRect.TopRight(); + break; + case POPUPALIGNMENT_BOTTOMLEFT: + pnt = anchorRect.BottomLeft(); + break; + case POPUPALIGNMENT_BOTTOMRIGHT: + pnt = anchorRect.BottomRight(); + break; + case POPUPALIGNMENT_TOPLEFT: + default: + pnt = anchorRect.TopLeft(); + break; + } + + // If the alignment is on the right edge of the popup, move the popup left + // by the width. Similarly, if the alignment is on the bottom edge of the + // popup, move the popup up by the height. In addition, account for the + // margins of the popup on the edge on which it is aligned. + nsMargin margin = GetMargin(); + switch (popupAlign) { + case POPUPALIGNMENT_TOPRIGHT: + pnt.MoveBy(-aPrefSize.width - margin.right, margin.top); + break; + case POPUPALIGNMENT_BOTTOMLEFT: + pnt.MoveBy(margin.left, -aPrefSize.height - margin.bottom); + break; + case POPUPALIGNMENT_BOTTOMRIGHT: + pnt.MoveBy(-aPrefSize.width - margin.right, + -aPrefSize.height - margin.bottom); + break; + case POPUPALIGNMENT_TOPLEFT: + default: + pnt.MoveBy(margin.left, margin.top); + break; + } + + // If we aligning to the selected item in the popup, adjust the vertical + // position by the height of the menulist label and the selected item's + // position. + if (mPosition == POPUPPOSITION_SELECTION) { + MOZ_ASSERT(popupAnchor == POPUPALIGNMENT_BOTTOMLEFT || + popupAnchor == POPUPALIGNMENT_BOTTOMRIGHT); + MOZ_ASSERT(popupAlign == POPUPALIGNMENT_TOPLEFT || + popupAlign == POPUPALIGNMENT_TOPRIGHT); + + // Only adjust the popup if it just opened, otherwise the popup will move + // around if its gets resized or the selection changed. Cache the value in + // mPositionedOffset and use that instead for any future calculations. + if (mIsOpenChanged) { + if (nsIFrame* selectedItemFrame = GetSelectedItemForAlignment()) { + const nscoord itemHeight = selectedItemFrame->GetRect().height; + const nscoord itemOffset = + selectedItemFrame->GetOffsetToIgnoringScrolling(this).y; + // We want to line-up the anchor rect with the selected item, but if the + // selected item is outside of our bounds, we don't want to shift the + // popup up in a way that our box would no longer intersect with the + // anchor. + nscoord maxOffset = aPrefSize.height - itemHeight; + if (const nsIScrollableFrame* sf = GetScrollFrame()) { + // HACK: We ideally would want to use the offset from the bottom + // bottom of our scroll-frame to the bottom of our frame (so as to + // ensure that the bottom of the scrollport is inside the anchor + // rect). + // + // But at this point of the code, the scroll frame may not be laid out + // with a definite size (might be overflowing us). + // + // So, we assume the offset from the bottom is symmetric to the offset + // from the top. This holds for all the popups where this matters + // (menulists on macOS, effectively), and seems better than somehow + // moving the popup after the fact as we used to do. + const nsIFrame* f = do_QueryFrame(sf); + maxOffset -= f->GetOffsetTo(this).y; + } + mPositionedOffset = + originalAnchorRect.height + std::min(itemOffset, maxOffset); + } + } + + pnt.y -= mPositionedOffset; + } + + // Flipping horizontally is allowed as long as the popup is above or below + // the anchor. This will happen if both the anchor and alignment are top or + // both are bottom, but different values. Similarly, flipping vertically is + // allowed if the popup is to the left or right of the anchor. In this case, + // the values of the constants are such that both must be positive or both + // must be negative. A special case, used for overlap, allows flipping + // vertically as well. + // If we are flipping in both directions, we want to set a flip style both + // horizontally and vertically. However, we want to flip on the inside edge + // of the anchor. Consider the example of a typical dropdown menu. + // Vertically, we flip the popup on the outside edges of the anchor menu, + // however horizontally, we want to to use the inside edges so the popup + // still appears underneath the anchor menu instead of floating off the + // side of the menu. + switch (popupAnchor) { + case POPUPALIGNMENT_LEFTCENTER: + case POPUPALIGNMENT_RIGHTCENTER: + aHFlip = FlipStyle_Outside; + aVFlip = FlipStyle_Inside; + break; + case POPUPALIGNMENT_TOPCENTER: + case POPUPALIGNMENT_BOTTOMCENTER: + aHFlip = FlipStyle_Inside; + aVFlip = FlipStyle_Outside; + break; + default: { + FlipStyle anchorEdge = + mFlip == FlipType_Both ? FlipStyle_Inside : FlipStyle_None; + aHFlip = (popupAnchor == -popupAlign) ? FlipStyle_Outside : anchorEdge; + if (((popupAnchor > 0) == (popupAlign > 0)) || + (popupAnchor == POPUPALIGNMENT_TOPLEFT && + popupAlign == POPUPALIGNMENT_TOPLEFT)) + aVFlip = FlipStyle_Outside; + else + aVFlip = anchorEdge; + break; + } + } + + return pnt; +} + +nsIFrame* nsMenuPopupFrame::GetSelectedItemForAlignment() const { + // This method adjusts a menulist's popup such that the selected item is under + // the cursor, aligned with the menulist label. + nsCOMPtr select; + if (mAnchorContent) { + select = mAnchorContent->AsElement()->AsXULSelectControl(); + } + + if (!select) { + // If there isn't an anchor, then try just getting the parent of the popup. + select = mContent->GetParent()->AsElement()->AsXULSelectControl(); + if (!select) { + return nullptr; + } + } + + nsCOMPtr selectedElement; + select->GetSelectedItem(getter_AddRefs(selectedElement)); + return selectedElement ? selectedElement->GetPrimaryFrame() : nullptr; +} + +nscoord nsMenuPopupFrame::SlideOrResize(nscoord& aScreenPoint, nscoord aSize, + nscoord aScreenBegin, + nscoord aScreenEnd, + nscoord* aOffset) const { + // The popup may be positioned such that either the left/top or bottom/right + // is outside the screen - but never both. + nscoord newPos = + std::max(aScreenBegin, std::min(aScreenEnd - aSize, aScreenPoint)); + *aOffset = newPos - aScreenPoint; + aScreenPoint = newPos; + return std::min(aSize, aScreenEnd - aScreenPoint); +} + +nscoord nsMenuPopupFrame::FlipOrResize(nscoord& aScreenPoint, nscoord aSize, + nscoord aScreenBegin, nscoord aScreenEnd, + nscoord aAnchorBegin, nscoord aAnchorEnd, + nscoord aMarginBegin, nscoord aMarginEnd, + FlipStyle aFlip, bool aEndAligned, + bool* aFlipSide) const { + // The flip side argument will be set to true if there wasn't room and we + // flipped to the opposite side. + *aFlipSide = false; + + // all of the coordinates used here are in app units relative to the screen + nscoord popupSize = aSize; + if (aScreenPoint < aScreenBegin) { + // at its current position, the popup would extend past the left or top + // edge of the screen, so it will have to be moved or resized. + if (aFlip) { + // for inside flips, we flip on the opposite side of the anchor + nscoord startpos = aFlip == FlipStyle_Outside ? aAnchorBegin : aAnchorEnd; + nscoord endpos = aFlip == FlipStyle_Outside ? aAnchorEnd : aAnchorBegin; + + // check whether there is more room to the left and right (or top and + // bottom) of the anchor and put the popup on the side with more room. + if (startpos - aScreenBegin >= aScreenEnd - endpos) { + aScreenPoint = aScreenBegin; + popupSize = startpos - aScreenPoint - aMarginEnd; + *aFlipSide = !aEndAligned; + } else { + // If the newly calculated position is different than the existing + // position, flip such that the popup is to the right or bottom of the + // anchor point instead . However, when flipping use the same margin + // size. + nscoord newScreenPoint = endpos + aMarginEnd; + if (newScreenPoint != aScreenPoint) { + *aFlipSide = aEndAligned; + aScreenPoint = newScreenPoint; + // check if the new position is still off the right or bottom edge of + // the screen. If so, resize the popup. + if (aScreenPoint + aSize > aScreenEnd) { + popupSize = aScreenEnd - aScreenPoint; + } + } + } + } else { + aScreenPoint = aScreenBegin; + } + } else if (aScreenPoint + aSize > aScreenEnd) { + // at its current position, the popup would extend past the right or + // bottom edge of the screen, so it will have to be moved or resized. + if (aFlip) { + // for inside flips, we flip on the opposite side of the anchor + nscoord startpos = aFlip == FlipStyle_Outside ? aAnchorBegin : aAnchorEnd; + nscoord endpos = aFlip == FlipStyle_Outside ? aAnchorEnd : aAnchorBegin; + + // check whether there is more room to the left and right (or top and + // bottom) of the anchor and put the popup on the side with more room. + if (aScreenEnd - endpos >= startpos - aScreenBegin) { + *aFlipSide = aEndAligned; + if (mIsContextMenu) { + aScreenPoint = aScreenEnd - aSize; + } else { + aScreenPoint = endpos + aMarginBegin; + popupSize = aScreenEnd - aScreenPoint; + } + } else { + // if the newly calculated position is different than the existing + // position, we flip such that the popup is to the left or top of the + // anchor point instead. + nscoord newScreenPoint = startpos - aSize - aMarginBegin; + if (newScreenPoint != aScreenPoint) { + *aFlipSide = !aEndAligned; + aScreenPoint = newScreenPoint; + + // check if the new position is still off the left or top edge of the + // screen. If so, resize the popup. + if (aScreenPoint < aScreenBegin) { + aScreenPoint = aScreenBegin; + if (!mIsContextMenu) { + popupSize = startpos - aScreenPoint - aMarginBegin; + } + } + } + } + } else { + aScreenPoint = aScreenEnd - aSize; + } + } + + // Make sure that the point is within the screen boundaries and that the + // size isn't off the edge of the screen. This can happen when a large + // positive or negative margin is used. + if (aScreenPoint < aScreenBegin) { + aScreenPoint = aScreenBegin; + } + if (aScreenPoint > aScreenEnd) { + aScreenPoint = aScreenEnd - aSize; + } + + // If popupSize ended up being negative, or the original size was actually + // smaller than the calculated popup size, just use the original size instead. + if (popupSize <= 0 || aSize < popupSize) { + popupSize = aSize; + } + + return std::min(popupSize, aScreenEnd - aScreenPoint); +} + +nsRect nsMenuPopupFrame::ComputeAnchorRect(nsPresContext* aRootPresContext, + nsIFrame* aAnchorFrame) const { + // Get the root frame for a reference + nsIFrame* rootFrame = aRootPresContext->PresShell()->GetRootFrame(); + + // The dimensions of the anchor + nsRect anchorRect = aAnchorFrame->GetRectRelativeToSelf(); + + // Relative to the root + anchorRect = nsLayoutUtils::TransformFrameRectToAncestor( + aAnchorFrame, anchorRect, rootFrame); + // Relative to the screen + anchorRect.MoveBy(rootFrame->GetScreenRectInAppUnits().TopLeft()); + + // In its own app units + return anchorRect.ScaleToOtherAppUnitsRoundOut( + aRootPresContext->AppUnitsPerDevPixel(), + PresContext()->AppUnitsPerDevPixel()); +} + +static nsIFrame* MaybeDelegatedAnchorFrame(nsIFrame* aFrame) { + if (!aFrame) { + return nullptr; + } + if (auto* element = Element::FromNodeOrNull(aFrame->GetContent())) { + if (element->HasAttr(nsGkAtoms::delegatesanchor)) { + for (nsIFrame* f : aFrame->PrincipalChildList()) { + if (!f->IsPlaceholderFrame()) { + return f; + } + } + } + } + return aFrame; +} + +auto nsMenuPopupFrame::GetRects(const nsSize& aPrefSize) const -> Rects { + if (NS_WARN_IF(aPrefSize == nsSize(-1, -1))) { + // Return early if the popup hasn't been laid out yet. On Windows, this can + // happen when using a drag popup before it opens. + return {}; + } + + nsPresContext* pc = PresContext(); + nsIFrame* rootFrame = pc->PresShell()->GetRootFrame(); + NS_ASSERTION(rootFrame->GetView() && GetView() && + rootFrame->GetView() == GetView()->GetParent(), + "rootFrame's view is not our view's parent???"); + + // Indicators of whether the popup should be flipped or resized. + FlipStyle hFlip = FlipStyle_None, vFlip = FlipStyle_None; + + const nsMargin margin = GetMargin(); + + // the screen rectangle of the root frame, in dev pixels. + const nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits(); + + const bool isNoAutoHide = IsNoAutoHide(); + const PopupLevel popupLevel = GetPopupLevel(isNoAutoHide); + + Rects result; + + // Set the popup's size to the preferred size. Below, this size will be + // adjusted to fit on the screen or within the content area. If the anchor is + // sized to the popup, use the anchor's width instead of the preferred width. + result.mUsedRect = nsRect(nsPoint(), aPrefSize); + + const bool anchored = IsAnchored(); + if (anchored) { + // In order to deal with transforms, we need the root prescontext: + nsPresContext* rootPc = pc->GetRootPresContext(); + if (NS_WARN_IF(!rootPc)) { + // If we can't reach a root pres context, don't bother continuing. + return result; + } + + result.mAnchorRect = result.mUntransformedAnchorRect = [&] { + // If anchored to a rectangle, use that rectangle. Otherwise, determine + // the rectangle from the anchor. + if (mAnchorType == MenuPopupAnchorType_Rect) { + return mScreenRect; + } + // if the frame is not specified, use the anchor node passed to OpenPopup. + // If that wasn't specified either, use the root frame. Note that + // mAnchorContent might be a different document so its presshell must be + // used. + nsIFrame* anchorFrame = GetAnchorFrame(); + if (!anchorFrame) { + return rootScreenRect; + } + return ComputeAnchorRect(rootPc, anchorFrame); + }(); + + // if we are anchored, there are certain things we don't want to do when + // repositioning the popup to fit on the screen, such as end up positioned + // over the anchor, for instance a popup appearing over the menu label. + // When doing this reposition, we want to move the popup to the side with + // the most room. The combination of anchor and alignment dictate if we + // readjust above/below or to the left/right. + if (mAnchorContent || mAnchorType == MenuPopupAnchorType_Rect) { + // move the popup according to the anchor and alignment. This will also + // tell us which axis the popup is flush against in case we have to move + // it around later. The AdjustPositionForAnchorAlign method accounts for + // the popup's margin. + result.mUsedRect.MoveTo(AdjustPositionForAnchorAlign( + result.mAnchorRect, aPrefSize, hFlip, vFlip)); + } else { + // With no anchor, the popup is positioned relative to the root frame. + result.mUsedRect.MoveTo(result.mAnchorRect.TopLeft() + + nsPoint(margin.left, margin.top)); + } + + // mXPos and mYPos specify an additional offset passed to OpenPopup that + // should be added to the position. We also add the offset to the anchor + // pos so a later flip/resize takes the offset into account. + // FIXME(emilio): Wayland doesn't seem to be accounting for this offset + // anywhere, and it probably should. + { + nsPoint offset(CSSPixel::ToAppUnits(mXPos), CSSPixel::ToAppUnits(mYPos)); + if (IsDirectionRTL()) { + offset.x = -offset.x; + } + result.mUsedRect.MoveBy(offset); + result.mAnchorRect.MoveBy(offset); + } + } else { + // Not anchored, use mScreenRect + result.mUsedRect.MoveTo(mScreenRect.TopLeft()); + result.mAnchorRect = result.mUntransformedAnchorRect = + nsRect(mScreenRect.TopLeft(), nsSize()); + + // Right-align RTL context menus, and apply margin and offsets as per the + // platform conventions. + if (mIsContextMenu && IsDirectionRTL()) { + result.mUsedRect.x -= aPrefSize.Width(); + result.mUsedRect.MoveBy(-margin.right, margin.top); + } else { + result.mUsedRect.MoveBy(margin.left, margin.top); + } +#ifdef XP_MACOSX + // OSX tooltips follow standard flip rule but other popups flip horizontally + // not vertically + if (mPopupType == PopupType::Tooltip) { + vFlip = FlipStyle_Outside; + } else { + hFlip = FlipStyle_Outside; + } +#else + // Other OS screen positioned popups can be flipped vertically but never + // horizontally + vFlip = FlipStyle_Outside; +#endif // #ifdef XP_MACOSX + } + + const int32_t a2d = pc->AppUnitsPerDevPixel(); + + nsView* view = GetView(); + NS_ASSERTION(view, "popup with no view"); + + nsIWidget* widget = view->GetWidget(); + + // If a panel has flip="none", don't constrain or flip it. + // Also, always do this for content shells, so that the popup doesn't extend + // outside the containing frame. + if (mInContentShell || mFlip != FlipType_None) { + const Maybe constraintRect = + GetConstraintRect(result.mAnchorRect, rootScreenRect, popupLevel); + + if (constraintRect) { + // Ensure that anchorRect is on the constraint rect. + result.mAnchorRect = result.mAnchorRect.Intersect(*constraintRect); + // Shrink the popup down if it is larger than the constraint size + if (result.mUsedRect.width > constraintRect->width) { + result.mUsedRect.width = constraintRect->width; + } + if (result.mUsedRect.height > constraintRect->height) { + result.mUsedRect.height = constraintRect->height; + } + } + + if (IS_WAYLAND_DISPLAY() && widget) { + // Shrink the popup down if it's larger than popup size received from + // Wayland compositor. We don't know screen size on Wayland so this is the + // only info we have there. + const nsSize waylandSize = LayoutDeviceIntRect::ToAppUnits( + widget->GetMoveToRectPopupSize(), a2d); + if (waylandSize.width > 0 && result.mUsedRect.width > waylandSize.width) { + LOG_WAYLAND("Wayland constraint width [%p]: %d to %d", widget, + result.mUsedRect.width, waylandSize.width); + result.mUsedRect.width = waylandSize.width; + } + if (waylandSize.height > 0 && + result.mUsedRect.height > waylandSize.height) { + LOG_WAYLAND("Wayland constraint height [%p]: %d to %d", widget, + result.mUsedRect.height, waylandSize.height); + result.mUsedRect.height = waylandSize.height; + } + } + + // At this point the anchor (anchorRect) is within the available screen + // area (constraintRect) and the popup is known to be no larger than the + // screen. + if (constraintRect) { + // We might want to "slide" an arrow if the panel is of the correct type - + // but we can only slide on one axis - the other axis must be "flipped or + // resized" as normal. + bool slideHorizontal = false, slideVertical = false; + if (mFlip == FlipType_Slide) { + int8_t position = GetAlignmentPosition(); + slideHorizontal = position >= POPUPPOSITION_BEFORESTART && + position <= POPUPPOSITION_AFTEREND; + slideVertical = position >= POPUPPOSITION_STARTBEFORE && + position <= POPUPPOSITION_ENDAFTER; + } + + // Next, check if there is enough space to show the popup at full size + // when positioned at screenPoint. If not, flip the popups to the opposite + // side of their anchor point, or resize them as necessary. + if (slideHorizontal) { + result.mUsedRect.width = SlideOrResize( + result.mUsedRect.x, result.mUsedRect.width, constraintRect->x, + constraintRect->XMost(), &result.mAlignmentOffset); + } else { + const bool endAligned = + IsDirectionRTL() + ? mPopupAlignment == POPUPALIGNMENT_TOPLEFT || + mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT + : mPopupAlignment == POPUPALIGNMENT_TOPRIGHT || + mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT; + result.mUsedRect.width = FlipOrResize( + result.mUsedRect.x, result.mUsedRect.width, constraintRect->x, + constraintRect->XMost(), result.mAnchorRect.x, + result.mAnchorRect.XMost(), margin.left, margin.right, hFlip, + endAligned, &result.mHFlip); + } + if (slideVertical) { + result.mUsedRect.height = SlideOrResize( + result.mUsedRect.y, result.mUsedRect.height, constraintRect->y, + constraintRect->YMost(), &result.mAlignmentOffset); + } else { + bool endAligned = mPopupAlignment == POPUPALIGNMENT_BOTTOMLEFT || + mPopupAlignment == POPUPALIGNMENT_BOTTOMRIGHT; + result.mUsedRect.height = FlipOrResize( + result.mUsedRect.y, result.mUsedRect.height, constraintRect->y, + constraintRect->YMost(), result.mAnchorRect.y, + result.mAnchorRect.YMost(), margin.top, margin.bottom, vFlip, + endAligned, &result.mVFlip); + } + +#ifdef DEBUG + NS_ASSERTION(constraintRect->Contains(result.mUsedRect), + "Popup is offscreen"); + if (!constraintRect->Contains(result.mUsedRect)) { + NS_WARNING(nsPrintfCString("Popup is offscreen (%s vs. %s)", + ToString(constraintRect).c_str(), + ToString(result.mUsedRect).c_str()) + .get()); + } +#endif + } + } + // snap the popup's position in screen coordinates to device pixels, see + // bug 622507, bug 961431 + result.mUsedRect.x = pc->RoundAppUnitsToNearestDevPixels(result.mUsedRect.x); + result.mUsedRect.y = pc->RoundAppUnitsToNearestDevPixels(result.mUsedRect.y); + + // determine the x and y position of the view by subtracting the desired + // screen position from the screen position of the root frame. + result.mViewPoint = result.mUsedRect.TopLeft() - rootScreenRect.TopLeft(); + + // Offset the position by the width and height of the borders and titlebar. + // Even though GetClientOffset should return (0, 0) when there is no titlebar + // or borders, we skip these calculations anyway for non-panels to save time + // since they will never have a titlebar. + if (mPopupType == PopupType::Panel && widget) { + result.mClientOffset = widget->GetClientOffset(); + result.mViewPoint += + LayoutDeviceIntPoint::ToAppUnits(result.mClientOffset, a2d); + } + + return result; +} + +void nsMenuPopupFrame::SetPopupPosition(bool aIsMove) { + if (aIsMove && (mPrefSize.width == -1 || mPrefSize.height == -1)) { + return; + } + + auto rects = GetRects(mPrefSize); + if (rects.mUsedRect.Size() != mRect.Size()) { + MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IN_REFLOW)); + // We need to resize on top of moving, trigger an actual reflow. + PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors, + NS_FRAME_IS_DIRTY); + return; + } + PerformMove(rects); +} + +void nsMenuPopupFrame::PerformMove(const Rects& aRects) { + auto* ps = PresShell(); + + // We're just moving, sync frame position and offset as needed. + ps->GetViewManager()->MoveViewTo(GetView(), aRects.mViewPoint.x, + aRects.mViewPoint.y); + + // Now that we've positioned the view, sync up the frame's origin. + nsBlockFrame::SetPosition(aRects.mViewPoint - + GetParent()->GetOffsetTo(ps->GetRootFrame())); + + // If the popup is in the positioned state or if it is shown and the position + // or size changed, dispatch a popuppositioned event if the popup wants it. + if (mPopupState == ePopupPositioning || + (mPopupState == ePopupShown && + !aRects.mUsedRect.IsEqualEdges(mUsedScreenRect)) || + (mPopupState == ePopupShown && + aRects.mAlignmentOffset != mAlignmentOffset)) { + mUsedScreenRect = aRects.mUsedRect; + if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && !mPendingPositionedEvent) { + mPendingPositionedEvent = + nsXULPopupPositionedEvent::DispatchIfNeeded(mContent->AsElement()); + } + } + + if (!mPositionedByMoveToRect) { + mUntransformedAnchorRect = aRects.mUntransformedAnchorRect; + } + + mAlignmentOffset = aRects.mAlignmentOffset; + mLastClientOffset = aRects.mClientOffset; + mHFlip = aRects.mHFlip; + mVFlip = aRects.mVFlip; + + // If this is a noautohide popup, set the screen coordinates of the popup. + // This way, the popup stays at the location where it was opened even when the + // window is moved. Popups at the parent level follow the parent window as it + // is moved and remained anchored, so we want to maintain the anchoring + // instead. + // + // FIXME: This suffers from issues like bug 1823552, where constraints imposed + // by the anchor are lost, but this is super-old behavior. + const bool fixPositionToPoint = + IsNoAutoHide() && (GetPopupLevel() != PopupLevel::Parent || + mAnchorType == MenuPopupAnchorType_Rect); + if (fixPositionToPoint) { + // Account for the margin that will end up being added to the screen + // coordinate the next time SetPopupPosition is called. + const auto& margin = GetMargin(); + mAnchorType = MenuPopupAnchorType_Point; + mScreenRect.x = aRects.mUsedRect.x - margin.left; + mScreenRect.y = aRects.mUsedRect.y - margin.top; + } + + // For anchored popups that shouldn't follow the anchor, fix the original + // anchor rect. + if (IsAnchored() && !ShouldFollowAnchor() && !mUsedScreenRect.IsEmpty() && + mAnchorType != MenuPopupAnchorType_Rect) { + mAnchorType = MenuPopupAnchorType_Rect; + mScreenRect = aRects.mUntransformedAnchorRect; + } + + // NOTE(emilio): This call below is kind of a workaround, but we need to do + // this here because some position changes don't go through the + // view system -> popup manager, like: + // + // https://searchfox.org/mozilla-central/rev/477950cf9ca9c9bb5ff6f34e0d0f6ca4718ea798/widget/gtk/nsWindow.cpp#3847 + // + // So this might be the last chance we have to set the remote browser's + // position. + // + // Ultimately this probably wants to get fixed in the widget size of things, + // but given this is worst-case a redundant DOM traversal, and that popups + // usually don't have all that much content, this is probably an ok + // workaround. + WidgetPositionOrSizeDidChange(); +} + +void nsMenuPopupFrame::WidgetPositionOrSizeDidChange() { + // In the case this popup has remote contents having OOP iframes, it's + // possible that OOP iframe's nsSubDocumentFrame has been already reflowed + // thus, we will never have a chance to tell this parent browser's position + // update to the OOP documents without notifying it explicitly. + if (!HasRemoteContent()) { + return; + } + for (nsIContent* content = mContent->GetFirstChild(); content; + content = content->GetNextNode(mContent)) { + if (content->IsXULElement(nsGkAtoms::browser) && + content->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::remote, + nsGkAtoms::_true, eIgnoreCase)) { + if (auto* browserParent = dom::BrowserParent::GetFrom(content)) { + browserParent->NotifyPositionUpdatedForContentsInPopup(); + } + } + } +} + +Maybe nsMenuPopupFrame::GetConstraintRect( + const nsRect& aAnchorRect, const nsRect& aRootScreenRect, + PopupLevel aPopupLevel) const { + const nsPresContext* pc = PresContext(); + const int32_t a2d = PresContext()->AppUnitsPerDevPixel(); + Maybe result; + + auto AddConstraint = [&result](const nsRect& aConstraint) { + if (result) { + *result = result->Intersect(aConstraint); + } else { + result.emplace(aConstraint); + } + }; + + // Determine the available screen space. It will be reduced by the OS chrome + // such as menubars. It addition, for content shells, it will be the area of + // the content rather than the screen. + // In Wayland we can't use the screen rect because we can't know absolute + // window position. + if (!IS_WAYLAND_DISPLAY()) { + const DesktopToLayoutDeviceScale scale = + pc->DeviceContext()->GetDesktopToDeviceScale(); + // For content shells, get the screen where the root frame is located. This + // is because we need to constrain the content to this content area, so we + // should use the same screen. Otherwise, use the screen where the anchor is + // located. + const nsRect& rect = mInContentShell ? aRootScreenRect : aAnchorRect; + auto desktopRect = DesktopIntRect::RoundOut( + LayoutDeviceRect::FromAppUnits(rect, a2d) / scale); + desktopRect.width = std::max(1, desktopRect.width); + desktopRect.height = std::max(1, desktopRect.height); + + RefPtr screen = + widget::ScreenManager::GetSingleton().ScreenForRect(desktopRect); + MOZ_ASSERT(screen, "We always fall back to the primary screen"); + // Non-top-level popups (which will always be panels) should never overlap + // the OS bar. + const bool canOverlapOSBar = + aPopupLevel == PopupLevel::Top && + LookAndFeel::GetInt(LookAndFeel::IntID::MenusCanOverlapOSBar) && + !mInContentShell; + // Get the total screen area if the popup is allowed to overlap it. + const auto screenRect = + canOverlapOSBar ? screen->GetRect() : screen->GetAvailRect(); + AddConstraint(LayoutDeviceRect::ToAppUnits(screenRect, a2d)); + } + + if (mInContentShell) { + // For content shells, clip to the client area rather than the screen area + AddConstraint(aRootScreenRect); + } else if (!mOverrideConstraintRect.IsEmpty()) { + AddConstraint(mOverrideConstraintRect); + // This is currently only used for element, since we have + // some UA style sheet rules that depend on the + + + + + +`; + const url = "data:text/html," + encodeURI(PAGE); + await BrowserTestUtils.withNewTab( + { + gBrowser, + url, + }, + async function (browser) { + let popupShownPromise = BrowserTestUtils.waitForSelectPopupShown(window); + await BrowserTestUtils.synthesizeMouseAtCenter("select", {}, browser); + let popup = await popupShownPromise; + EventUtils.sendString("C", window); + EventUtils.sendKey("RETURN", window); + ok( + await TestUtils.waitForCondition(() => { + return SpecialPowers.spawn( + browser, + [], + () => content.document.querySelector("select").value + ).then(value => value == 3); + }), + "Unexpected value for select element (expected 3)!" + ); + } + ); +}); diff --git a/layout/xul/test/browser_bug685470.js b/layout/xul/test/browser_bug685470.js new file mode 100644 index 0000000000..46997b2e3b --- /dev/null +++ b/layout/xul/test/browser_bug685470.js @@ -0,0 +1,38 @@ +add_task(async function () { + const html = + '

      This paragraph has a tooltip.

      '; + await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html," + html + ); + + await new Promise(resolve => { + SpecialPowers.pushPrefEnv({ set: [["ui.tooltipDelay", 0]] }, resolve); + }); + + await BrowserTestUtils.synthesizeMouseAtCenter( + "#p1", + { type: "mousemove" }, + gBrowser.selectedBrowser + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#p1", + {}, + gBrowser.selectedBrowser + ); + + // Wait until the tooltip timeout triggers that would normally have opened the popup. + await new Promise(resolve => setTimeout(resolve, 0)); + is( + document.getElementById("aHTMLTooltip").state, + "closed", + "local tooltip is closed" + ); + is( + document.getElementById("remoteBrowserTooltip").state, + "closed", + "remote tooltip is closed" + ); + + gBrowser.removeCurrentTab(); +}); diff --git a/layout/xul/test/browser_bug703210.js b/layout/xul/test/browser_bug703210.js new file mode 100644 index 0000000000..5026875310 --- /dev/null +++ b/layout/xul/test/browser_bug703210.js @@ -0,0 +1,56 @@ +add_task(async function () { + const url = + "data:text/html," + + "" + + '

      This paragraph has a tooltip.

      ' + + '

      This paragraph doesn\'t have tooltip.

      '; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + let browser = gBrowser.selectedBrowser; + + await new Promise(resolve => { + SpecialPowers.pushPrefEnv({ set: [["ui.tooltipDelay", 0]] }, resolve); + }); + + let popupShownPromise = BrowserTestUtils.waitForEvent( + document, + "popupshown", + false, + event => { + is(event.originalTarget.localName, "tooltip", "tooltip is showing"); + return true; + } + ); + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + document, + "popuphidden", + false, + event => { + is(event.originalTarget.localName, "tooltip", "tooltip is hidden"); + return true; + } + ); + + // Send a mousemove at a known position to start the test. + await BrowserTestUtils.synthesizeMouseAtCenter( + "#p2", + { type: "mousemove" }, + browser + ); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#p1", + { type: "mousemove" }, + browser + ); + await popupShownPromise; + await BrowserTestUtils.synthesizeMouseAtCenter( + "#p2", + { type: "mousemove" }, + browser + ); + await popupHiddenPromise; + + gBrowser.removeCurrentTab(); +}); diff --git a/layout/xul/test/browser_bug706743.js b/layout/xul/test/browser_bug706743.js new file mode 100644 index 0000000000..c28721e831 --- /dev/null +++ b/layout/xul/test/browser_bug706743.js @@ -0,0 +1,158 @@ +add_task(async function () { + const url = + "data:text/html," + + 'here is an anchor element'; + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url); + let browser = gBrowser.selectedBrowser; + + await new Promise(resolve => { + SpecialPowers.pushPrefEnv({ set: [["ui.tooltipDelay", 0]] }, resolve); + }); + + // Send a mousemove at a known position to start the test. + await BrowserTestUtils.synthesizeMouse( + "#target", + -5, + -5, + { type: "mousemove" }, + browser + ); + + // show tooltip by mousemove into target. + let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown"); + await BrowserTestUtils.synthesizeMouse( + "#target", + 5, + 15, + { type: "mousemove" }, + browser + ); + await popupShownPromise; + + // hide tooltip by mousemove to outside. + let popupHiddenPromise = BrowserTestUtils.waitForEvent( + document, + "popuphidden" + ); + await BrowserTestUtils.synthesizeMouse( + "#target", + -5, + 15, + { type: "mousemove" }, + browser + ); + await popupHiddenPromise; + + // mousemove into the target and start drag by emulation via nsIDragService. + // Note that on some platforms, we cannot actually start the drag by + // synthesized events. E.g., Windows waits an actual mousemove event after + // dragstart. + + // Emulate a buggy mousemove event. widget might dispatch mousemove event + // during drag. + + function tooltipNotExpected() { + ok(false, "tooltip is shown during drag"); + } + addEventListener("popupshown", tooltipNotExpected, true); + + let dragService = Cc["@mozilla.org/widget/dragservice;1"].getService( + Ci.nsIDragService + ); + dragService.startDragSessionForTests( + Ci.nsIDragService.DRAGDROP_ACTION_MOVE | + Ci.nsIDragService.DRAGDROP_ACTION_COPY | + Ci.nsIDragService.DRAGDROP_ACTION_LINK + ); + try { + await BrowserTestUtils.synthesizeMouse( + "#target", + 5, + 15, + { type: "mousemove" }, + browser + ); + + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 100)); + } finally { + removeEventListener("popupshown", tooltipNotExpected, true); + dragService.endDragSession(true); + } + + await BrowserTestUtils.synthesizeMouse( + "#target", + -5, + -5, + { type: "mousemove" }, + browser + ); + + // If tooltip listener used a flag for managing D&D state, we would need + // to test if the tooltip is shown after drag. + + // show tooltip by mousemove into target. + popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown"); + await BrowserTestUtils.synthesizeMouse( + "#target", + 5, + 15, + { type: "mousemove" }, + browser + ); + await popupShownPromise; + + // hide tooltip by mousemove to outside. + popupHiddenPromise = BrowserTestUtils.waitForEvent(document, "popuphidden"); + await BrowserTestUtils.synthesizeMouse( + "#target", + -5, + 15, + { type: "mousemove" }, + browser + ); + await popupHiddenPromise; + + // Show tooltip after mousedown + popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown"); + await BrowserTestUtils.synthesizeMouse( + "#target", + 5, + 15, + { type: "mousemove" }, + browser + ); + await popupShownPromise; + + popupHiddenPromise = BrowserTestUtils.waitForEvent(document, "popuphidden"); + await BrowserTestUtils.synthesizeMouse( + "#target", + 5, + 15, + { type: "mousedown" }, + browser + ); + await popupHiddenPromise; + + await BrowserTestUtils.synthesizeMouse( + "#target", + 5, + 15, + { type: "mouseup" }, + browser + ); + await BrowserTestUtils.synthesizeMouse( + "#target", + -5, + 15, + { type: "mousemove" }, + browser + ); + + ok(true, "tooltips appear properly"); + + gBrowser.removeCurrentTab(); +}); diff --git a/layout/xul/test/chrome.ini b/layout/xul/test/chrome.ini new file mode 100644 index 0000000000..67be92b0ce --- /dev/null +++ b/layout/xul/test/chrome.ini @@ -0,0 +1,38 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + windowminmaxsize1.xhtml + windowminmaxsize2.xhtml + windowminmaxsize3.xhtml + windowminmaxsize4.xhtml + windowminmaxsize5.xhtml + windowminmaxsize6.xhtml + windowminmaxsize7.xhtml + windowminmaxsize8.xhtml + windowminmaxsize9.xhtml + windowminmaxsize10.xhtml + titledpanelwindow.xhtml + +[test_bug159346.xhtml] +[test_bug381167.xhtml] +[test_bug398982-1.xhtml] +[test_bug398982-2.xhtml] +[test_bug467442.xhtml] +[test_bug477754.xhtml] +[test_bug703150.xhtml] +[test_bug987230.xhtml] +skip-if = os == 'linux' # No native mousedown event on Linux +[test_bug1197913.xhtml] +[test_popupReflowPos.xhtml] +[test_popupSizeTo.xhtml] +[test_popupZoom.xhtml] +[test_submenuClose.xhtml] +[test_windowminmaxsize.xhtml] +[test_resizer_ctrl_click.xhtml] +[test_resizer_incontent.xhtml] +[test_splitter.xhtml] +skip-if = toolkit == 'android' # no XUL theme +[test_splitter_sibling.xhtml] +skip-if = toolkit == 'android' # no XUL theme +[test_toolbarbutton_ctrl_click.xhtml] +[test_menuitem_ctrl_click.xhtml] diff --git a/layout/xul/test/file_bug386386.sjs b/layout/xul/test/file_bug386386.sjs new file mode 100644 index 0000000000..4cd23a7909 --- /dev/null +++ b/layout/xul/test/file_bug386386.sjs @@ -0,0 +1,14 @@ +// SJS file for test_bug386386.html +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + response.setHeader( + "Content-Type", + "application/xhtml+xml;charset=utf-8", + false + ); + response.write( + "%3C%3Fxml%20version%3D%221.0%22%3F%3E%0A%3Cwindow%3E%3C/window%3E" + ); +} diff --git a/layout/xul/test/mochitest.ini b/layout/xul/test/mochitest.ini new file mode 100644 index 0000000000..1db641a777 --- /dev/null +++ b/layout/xul/test/mochitest.ini @@ -0,0 +1,17 @@ +[DEFAULT] +support-files = + file_bug386386.sjs +[test_bug386386.html] +allow_xul_xbl = true +skip-if = + http3 +[test_bug394800.xhtml] +allow_xul_xbl = true +skip-if = + http3 +[test_bug511075.html] +skip-if = toolkit == 'android' #bug 798806 +[test_bug563416.html] +skip-if = toolkit == 'android' +[test_drag_thumb_in_link.html] +skip-if = toolkit == 'android' diff --git a/layout/xul/test/test_bug1197913.xhtml b/layout/xul/test/test_bug1197913.xhtml new file mode 100644 index 0000000000..539f02128f --- /dev/null +++ b/layout/xul/test/test_bug1197913.xhtml @@ -0,0 +1,63 @@ + + + + + + + diff --git a/layout/xul/test/test_bug159346.xhtml b/layout/xul/test/test_bug159346.xhtml new file mode 100644 index 0000000000..c33823f755 --- /dev/null +++ b/layout/xul/test/test_bug159346.xhtml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + +Mozilla Bug 159346 +

      + +
      +
      + + + + +
      diff --git a/layout/xul/test/test_bug381167.xhtml b/layout/xul/test/test_bug381167.xhtml new file mode 100644 index 0000000000..750dabae33 --- /dev/null +++ b/layout/xul/test/test_bug381167.xhtml @@ -0,0 +1,52 @@ + + + + Test for Bug 381167 + + + + +Mozilla Bug 381167 +

      + + + + + + + +
      +
      +
      + + diff --git a/layout/xul/test/test_bug386386.html b/layout/xul/test/test_bug386386.html new file mode 100644 index 0000000000..d3187c9142 --- /dev/null +++ b/layout/xul/test/test_bug386386.html @@ -0,0 +1,34 @@ + +Testcase for bug 386386 + + + + + + + + + + + diff --git a/layout/xul/test/test_bug394800.xhtml b/layout/xul/test/test_bug394800.xhtml new file mode 100644 index 0000000000..26fc50f771 --- /dev/null +++ b/layout/xul/test/test_bug394800.xhtml @@ -0,0 +1,39 @@ + + + + Test Mozilla bug 394800 + + + + + + + +
      + +Mozilla Bug 394800 +

      + +
      +
      + + + + + diff --git a/layout/xul/test/test_bug398982-1.xhtml b/layout/xul/test/test_bug398982-1.xhtml new file mode 100644 index 0000000000..da6598b70d --- /dev/null +++ b/layout/xul/test/test_bug398982-1.xhtml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/layout/xul/test/test_bug398982-2.xhtml b/layout/xul/test/test_bug398982-2.xhtml new file mode 100644 index 0000000000..865e688ea3 --- /dev/null +++ b/layout/xul/test/test_bug398982-2.xhtml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + diff --git a/layout/xul/test/test_bug467442.xhtml b/layout/xul/test/test_bug467442.xhtml new file mode 100644 index 0000000000..f0f84c3f86 --- /dev/null +++ b/layout/xul/test/test_bug467442.xhtml @@ -0,0 +1,53 @@ + + + + + + + + + + Mozilla Bug 467442 + + diff --git a/layout/xul/test/test_bug477754.xhtml b/layout/xul/test/test_bug477754.xhtml new file mode 100644 index 0000000000..338f95c62e --- /dev/null +++ b/layout/xul/test/test_bug477754.xhtml @@ -0,0 +1,51 @@ + + + + + + + diff --git a/layout/xul/test/test_bug511075.html b/layout/xul/test/test_bug511075.html new file mode 100644 index 0000000000..34e784ba56 --- /dev/null +++ b/layout/xul/test/test_bug511075.html @@ -0,0 +1,121 @@ + + + + + Test for Bug 511075 + + + + + + +Mozilla Bug 511075 +

      + +
      +
      +
      + + + diff --git a/layout/xul/test/test_bug563416.html b/layout/xul/test/test_bug563416.html new file mode 100644 index 0000000000..22abb5bdc3 --- /dev/null +++ b/layout/xul/test/test_bug563416.html @@ -0,0 +1,53 @@ + + + + + Test for Bug 563416 + + + + +Mozilla Bug 563416 +

      + +
      +
      +
      + + diff --git a/layout/xul/test/test_bug703150.xhtml b/layout/xul/test/test_bug703150.xhtml new file mode 100644 index 0000000000..4a7230bd49 --- /dev/null +++ b/layout/xul/test/test_bug703150.xhtml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + +Mozilla Bug 703150 +

      + +
      +
      + + + + +
      diff --git a/layout/xul/test/test_bug987230.xhtml b/layout/xul/test/test_bug987230.xhtml new file mode 100644 index 0000000000..3161ad9d0e --- /dev/null +++ b/layout/xul/test/test_bug987230.xhtml @@ -0,0 +1,109 @@ + + + + + + + diff --git a/layout/xul/test/test_drag_thumb_in_link.html b/layout/xul/test/test_drag_thumb_in_link.html new file mode 100644 index 0000000000..7c39fd0f28 --- /dev/null +++ b/layout/xul/test/test_drag_thumb_in_link.html @@ -0,0 +1,76 @@ + + + + +Test for Bug 367028 + + + + + + +Mozilla Bug 367028 +

      + + + block anchorbigger block + + + + diff --git a/layout/xul/test/test_menuitem_ctrl_click.xhtml b/layout/xul/test/test_menuitem_ctrl_click.xhtml new file mode 100644 index 0000000000..af0b82a6ed --- /dev/null +++ b/layout/xul/test/test_menuitem_ctrl_click.xhtml @@ -0,0 +1,80 @@ + + + + + + + diff --git a/layout/xul/test/test_popupReflowPos.xhtml b/layout/xul/test/test_popupReflowPos.xhtml new file mode 100644 index 0000000000..a26a833d13 --- /dev/null +++ b/layout/xul/test/test_popupReflowPos.xhtml @@ -0,0 +1,77 @@ + + + + + + + + .mbox { + display: inline-block; + width: 33%; + height: 50px; + background: green; + vertical-align: middle; + } + .orange { + background: orange; + } + .change > .mbox { + width: 60px; + } + + + + + + + + + + + + + + + + diff --git a/layout/xul/test/test_popupSizeTo.xhtml b/layout/xul/test/test_popupSizeTo.xhtml new file mode 100644 index 0000000000..6e60f28e0a --- /dev/null +++ b/layout/xul/test/test_popupSizeTo.xhtml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + diff --git a/layout/xul/test/test_popupZoom.xhtml b/layout/xul/test/test_popupZoom.xhtml new file mode 100644 index 0000000000..641ed0756c --- /dev/null +++ b/layout/xul/test/test_popupZoom.xhtml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + diff --git a/layout/xul/test/test_resizer_ctrl_click.xhtml b/layout/xul/test/test_resizer_ctrl_click.xhtml new file mode 100644 index 0000000000..225d3c6518 --- /dev/null +++ b/layout/xul/test/test_resizer_ctrl_click.xhtml @@ -0,0 +1,51 @@ + + + + + + + + diff --git a/layout/xul/test/test_resizer_incontent.xhtml b/layout/xul/test/test_resizer_incontent.xhtml new file mode 100644 index 0000000000..2d29dd3f8d --- /dev/null +++ b/layout/xul/test/test_resizer_incontent.xhtml @@ -0,0 +1,42 @@ + + + + + + + + + + diff --git a/layout/xul/test/test_splitter.xhtml b/layout/xul/test/test_splitter.xhtml new file mode 100644 index 0000000000..32c4118c8c --- /dev/null +++ b/layout/xul/test/test_splitter.xhtml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/layout/xul/test/test_splitter_sibling.xhtml b/layout/xul/test/test_splitter_sibling.xhtml new file mode 100644 index 0000000000..a2e00890c5 --- /dev/null +++ b/layout/xul/test/test_splitter_sibling.xhtml @@ -0,0 +1,88 @@ + + + + + + + diff --git a/layout/xul/test/test_submenuClose.xhtml b/layout/xul/test/test_submenuClose.xhtml new file mode 100644 index 0000000000..47337e61b9 --- /dev/null +++ b/layout/xul/test/test_submenuClose.xhtml @@ -0,0 +1,91 @@ + + + + + + + diff --git a/layout/xul/test/test_toolbarbutton_ctrl_click.xhtml b/layout/xul/test/test_toolbarbutton_ctrl_click.xhtml new file mode 100644 index 0000000000..6ad5f18ae7 --- /dev/null +++ b/layout/xul/test/test_toolbarbutton_ctrl_click.xhtml @@ -0,0 +1,51 @@ + + + + + + + + diff --git a/layout/xul/test/test_windowminmaxsize.xhtml b/layout/xul/test/test_windowminmaxsize.xhtml new file mode 100644 index 0000000000..187732dd3d --- /dev/null +++ b/layout/xul/test/test_windowminmaxsize.xhtml @@ -0,0 +1,193 @@ + + + + + + + + + +

      +

      + +
      +
      + + +
      diff --git a/layout/xul/test/titledpanelwindow.xhtml b/layout/xul/test/titledpanelwindow.xhtml new file mode 100644 index 0000000000..4289f8deab --- /dev/null +++ b/layout/xul/test/titledpanelwindow.xhtml @@ -0,0 +1,5 @@ + + + + diff --git a/layout/xul/test/windowminmaxsize1.xhtml b/layout/xul/test/windowminmaxsize1.xhtml new file mode 100644 index 0000000000..59f361aced --- /dev/null +++ b/layout/xul/test/windowminmaxsize1.xhtml @@ -0,0 +1,4 @@ + + + + diff --git a/layout/xul/test/windowminmaxsize10.xhtml b/layout/xul/test/windowminmaxsize10.xhtml new file mode 100644 index 0000000000..8b568d986f --- /dev/null +++ b/layout/xul/test/windowminmaxsize10.xhtml @@ -0,0 +1,4 @@ + + + + diff --git a/layout/xul/test/windowminmaxsize2.xhtml b/layout/xul/test/windowminmaxsize2.xhtml new file mode 100644 index 0000000000..fb72903dbd --- /dev/null +++ b/layout/xul/test/windowminmaxsize2.xhtml @@ -0,0 +1,4 @@ + + + + diff --git a/layout/xul/test/windowminmaxsize3.xhtml b/layout/xul/test/windowminmaxsize3.xhtml new file mode 100644 index 0000000000..ed6acbe2be --- /dev/null +++ b/layout/xul/test/windowminmaxsize3.xhtml @@ -0,0 +1,4 @@ + + + + diff --git a/layout/xul/test/windowminmaxsize4.xhtml b/layout/xul/test/windowminmaxsize4.xhtml new file mode 100644 index 0000000000..e29a48016e --- /dev/null +++ b/layout/xul/test/windowminmaxsize4.xhtml @@ -0,0 +1,4 @@ + + + + diff --git a/layout/xul/test/windowminmaxsize5.xhtml b/layout/xul/test/windowminmaxsize5.xhtml new file mode 100644 index 0000000000..7cbce93cc1 --- /dev/null +++ b/layout/xul/test/windowminmaxsize5.xhtml @@ -0,0 +1,4 @@ + + + + diff --git a/layout/xul/test/windowminmaxsize6.xhtml b/layout/xul/test/windowminmaxsize6.xhtml new file mode 100644 index 0000000000..abba98027a --- /dev/null +++ b/layout/xul/test/windowminmaxsize6.xhtml @@ -0,0 +1,4 @@ + + + + diff --git a/layout/xul/test/windowminmaxsize7.xhtml b/layout/xul/test/windowminmaxsize7.xhtml new file mode 100644 index 0000000000..f14e2ca4f8 --- /dev/null +++ b/layout/xul/test/windowminmaxsize7.xhtml @@ -0,0 +1,4 @@ + + + + diff --git a/layout/xul/test/windowminmaxsize8.xhtml b/layout/xul/test/windowminmaxsize8.xhtml new file mode 100644 index 0000000000..8beff9d32a --- /dev/null +++ b/layout/xul/test/windowminmaxsize8.xhtml @@ -0,0 +1,4 @@ + + + + diff --git a/layout/xul/test/windowminmaxsize9.xhtml b/layout/xul/test/windowminmaxsize9.xhtml new file mode 100644 index 0000000000..b4a06d4ff2 --- /dev/null +++ b/layout/xul/test/windowminmaxsize9.xhtml @@ -0,0 +1,4 @@ + + + + diff --git a/layout/xul/tree/crashtests/307298-1.xhtml b/layout/xul/tree/crashtests/307298-1.xhtml new file mode 100644 index 0000000000..6c04a01321 --- /dev/null +++ b/layout/xul/tree/crashtests/307298-1.xhtml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/layout/xul/tree/crashtests/309732-1.xhtml b/layout/xul/tree/crashtests/309732-1.xhtml new file mode 100644 index 0000000000..a7e40b75b9 --- /dev/null +++ b/layout/xul/tree/crashtests/309732-1.xhtml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/layout/xul/tree/crashtests/309732-2.xhtml b/layout/xul/tree/crashtests/309732-2.xhtml new file mode 100644 index 0000000000..354c58dacf --- /dev/null +++ b/layout/xul/tree/crashtests/309732-2.xhtml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/layout/xul/tree/crashtests/366583-1.xhtml b/layout/xul/tree/crashtests/366583-1.xhtml new file mode 100644 index 0000000000..fd12709905 --- /dev/null +++ b/layout/xul/tree/crashtests/366583-1.xhtml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/layout/xul/tree/crashtests/380217-1.xhtml b/layout/xul/tree/crashtests/380217-1.xhtml new file mode 100644 index 0000000000..251b3c450d --- /dev/null +++ b/layout/xul/tree/crashtests/380217-1.xhtml @@ -0,0 +1,31 @@ + + + + + +* { position: fixed; } +*:not(style) { + /* At the time this testcase was added, the above `float` styling would + have automatically forced "display:block" for these elements, so we + should preserve that styling to preserve the integrity of the crashtest + since blockification behavior for -moz-box is changing. */ + display: block; +} + + + + + + + + + + + + + + + + diff --git a/layout/xul/tree/crashtests/382444-1-inner.html b/layout/xul/tree/crashtests/382444-1-inner.html new file mode 100644 index 0000000000..01805e6b34 --- /dev/null +++ b/layout/xul/tree/crashtests/382444-1-inner.html @@ -0,0 +1,15 @@ + + +Testcase bug - Crash [@ nsINodeInfo::Equals] with underflow event, tree stuff and removing window + + + + + + + diff --git a/layout/xul/tree/crashtests/382444-1.html b/layout/xul/tree/crashtests/382444-1.html new file mode 100644 index 0000000000..8926cf16d7 --- /dev/null +++ b/layout/xul/tree/crashtests/382444-1.html @@ -0,0 +1,9 @@ + + + + + + + diff --git a/layout/xul/tree/crashtests/391178-1.xhtml b/layout/xul/tree/crashtests/391178-1.xhtml new file mode 100644 index 0000000000..0f4b16cd99 --- /dev/null +++ b/layout/xul/tree/crashtests/391178-1.xhtml @@ -0,0 +1,41 @@ + + + + + + + +
      +
      + + + diff --git a/layout/xul/tree/crashtests/391178-2.xhtml b/layout/xul/tree/crashtests/391178-2.xhtml new file mode 100644 index 0000000000..423b5d1bfe --- /dev/null +++ b/layout/xul/tree/crashtests/391178-2.xhtml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/layout/xul/tree/crashtests/393665-1.xhtml b/layout/xul/tree/crashtests/393665-1.xhtml new file mode 100644 index 0000000000..6fb5ec0c9e --- /dev/null +++ b/layout/xul/tree/crashtests/393665-1.xhtml @@ -0,0 +1,3 @@ + + + diff --git a/layout/xul/tree/crashtests/399227-1.xhtml b/layout/xul/tree/crashtests/399227-1.xhtml new file mode 100644 index 0000000000..3ae4dfa764 --- /dev/null +++ b/layout/xul/tree/crashtests/399227-1.xhtml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/layout/xul/tree/crashtests/399692-1.xhtml b/layout/xul/tree/crashtests/399692-1.xhtml new file mode 100644 index 0000000000..97eec26742 --- /dev/null +++ b/layout/xul/tree/crashtests/399692-1.xhtml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/layout/xul/tree/crashtests/399715-1.xhtml b/layout/xul/tree/crashtests/399715-1.xhtml new file mode 100644 index 0000000000..ea0a20cfa2 --- /dev/null +++ b/layout/xul/tree/crashtests/399715-1.xhtml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/layout/xul/tree/crashtests/409807-1.xhtml b/layout/xul/tree/crashtests/409807-1.xhtml new file mode 100644 index 0000000000..a3af3da41b --- /dev/null +++ b/layout/xul/tree/crashtests/409807-1.xhtml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/layout/xul/tree/crashtests/414170-1.xhtml b/layout/xul/tree/crashtests/414170-1.xhtml new file mode 100644 index 0000000000..82ea63bcfd --- /dev/null +++ b/layout/xul/tree/crashtests/414170-1.xhtml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/layout/xul/tree/crashtests/479931-1.xhtml b/layout/xul/tree/crashtests/479931-1.xhtml new file mode 100644 index 0000000000..458a192501 --- /dev/null +++ b/layout/xul/tree/crashtests/479931-1.xhtml @@ -0,0 +1,19 @@ + + + + + + +
      + + + diff --git a/layout/xul/tree/crashtests/585815-iframe.xhtml b/layout/xul/tree/crashtests/585815-iframe.xhtml new file mode 100644 index 0000000000..90c20fca80 --- /dev/null +++ b/layout/xul/tree/crashtests/585815-iframe.xhtml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/layout/xul/tree/crashtests/585815.html b/layout/xul/tree/crashtests/585815.html new file mode 100644 index 0000000000..7c3b27f6aa --- /dev/null +++ b/layout/xul/tree/crashtests/585815.html @@ -0,0 +1,18 @@ + + + + Testcase for bug 585815 + + + + + + + + + diff --git a/layout/xul/tree/crashtests/601427.html b/layout/xul/tree/crashtests/601427.html new file mode 100644 index 0000000000..2a2999052e --- /dev/null +++ b/layout/xul/tree/crashtests/601427.html @@ -0,0 +1,30 @@ + + + + diff --git a/layout/xul/tree/crashtests/730441-3.xhtml b/layout/xul/tree/crashtests/730441-3.xhtml new file mode 100644 index 0000000000..c3fe199a83 --- /dev/null +++ b/layout/xul/tree/crashtests/730441-3.xhtml @@ -0,0 +1,38 @@ + + + + + + + + diff --git a/layout/xul/tree/crashtests/crashtests.list b/layout/xul/tree/crashtests/crashtests.list new file mode 100644 index 0000000000..81f6ab7e6c --- /dev/null +++ b/layout/xul/tree/crashtests/crashtests.list @@ -0,0 +1,18 @@ +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/307298-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/309732-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/309732-2.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/366583-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/380217-1.xhtml +load 382444-1.html +load 391178-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/391178-2.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/393665-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/399227-1.xhtml +load 399692-1.xhtml +load 399715-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/409807-1.xhtml +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/414170-1.xhtml +load 479931-1.xhtml +load 585815.html +pref(widget.windows.window_occlusion_tracking.enabled,false) load 601427.html # Bug 1819154 +load chrome://reftest/content/crashtests/layout/xul/tree/crashtests/730441-3.xhtml diff --git a/layout/xul/tree/moz.build b/layout/xul/tree/moz.build new file mode 100644 index 0000000000..f99b7bd41c --- /dev/null +++ b/layout/xul/tree/moz.build @@ -0,0 +1,44 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "XUL") + +XPIDL_SOURCES += [ + "nsITreeSelection.idl", + "nsITreeView.idl", +] + +XPIDL_MODULE = "layout_xul_tree" + +EXPORTS += [ + "nsTreeColumns.h", + "nsTreeUtils.h", +] + +UNIFIED_SOURCES += [ + "nsTreeBodyFrame.cpp", + "nsTreeColumns.cpp", + "nsTreeContentView.cpp", + "nsTreeImageListener.cpp", + "nsTreeSelection.cpp", + "nsTreeStyleCache.cpp", + "nsTreeUtils.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "..", + "../../base", + "../../forms", + "../../generic", + "../../painting", + "../../style", + "/dom/base", + "/dom/xul", +] diff --git a/layout/xul/tree/nsITreeSelection.idl b/layout/xul/tree/nsITreeSelection.idl new file mode 100644 index 0000000000..d265b639ee --- /dev/null +++ b/layout/xul/tree/nsITreeSelection.idl @@ -0,0 +1,119 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +webidl XULTreeElement; + +[scriptable, uuid(ab6fe746-300b-4ab4-abb9-1c0e3977874c)] +interface nsITreeSelection : nsISupports +{ + /** + * The tree widget for this selection. + */ + attribute XULTreeElement tree; + + /** + * This attribute is a boolean indicating single selection. + */ + readonly attribute boolean single; + + /** + * The number of rows currently selected in this tree. + */ + readonly attribute long count; + + /** + * Indicates whether or not the row at the specified index is + * part of the selection. + */ + boolean isSelected(in long index); + + /** + * Deselect all rows and select the row at the specified index. + */ + void select(in long index); + + /** + * Perform a timed select. + */ + void timedSelect(in long index, in long delay); + + /** + * Toggle the selection state of the row at the specified index. + */ + void toggleSelect(in long index); + + /** + * Select the range specified by the indices. If augment is true, + * then we add the range to the selection without clearing out anything + * else. If augment is false, everything is cleared except for the specified range. + */ + void rangedSelect(in long startIndex, in long endIndex, in boolean augment); + + /** + * Clears the range. + */ + void clearRange(in long startIndex, in long endIndex); + + /** + * Clears the selection. + */ + void clearSelection(); + + /** + * Selects all rows. + */ + void selectAll(); + + /** + * Iterate the selection using these methods. + */ + long getRangeCount(); + void getRangeAt(in long i, out long min, out long max); + + /** + * Can be used to invalidate the selection. + */ + void invalidateSelection(); + + /** + * Called when the row count changes to adjust selection indices. + */ + void adjustSelection(in long index, in long count); + + /** + * This attribute is a boolean indicating whether or not the + * "select" event should fire when the selection is changed using + * one of our methods. A view can use this to temporarily suppress + * the selection while manipulating all of the indices, e.g., on + * a sort. + * Note: setting this attribute to false will fire a select event. + */ + attribute boolean selectEventsSuppressed; + + /** + * The current item (the one that gets a focus rect in addition to being + * selected). + */ + attribute long currentIndex; + + /** + * The selection "pivot". This is the first item the user selected as + * part of a ranged select. + */ + readonly attribute long shiftSelectPivot; +}; + +/** + * The following interface is not scriptable and MUST NEVER BE MADE scriptable. + * Native treeselections implement it, and we use this to check whether a + * treeselection is native (and therefore suitable for use by untrusted content). + */ +[uuid(1bd59678-5cb3-4316-b246-31a91b19aabe)] +interface nsINativeTreeSelection : nsITreeSelection +{ + [noscript] void ensureNative(); +}; diff --git a/layout/xul/tree/nsITreeView.idl b/layout/xul/tree/nsITreeView.idl new file mode 100644 index 0000000000..7f2480ceaf --- /dev/null +++ b/layout/xul/tree/nsITreeView.idl @@ -0,0 +1,173 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsITreeSelection; + +webidl DataTransfer; +webidl TreeColumn; +webidl XULTreeElement; + +[scriptable, uuid(091116f0-0bdc-4b32-b9c8-c8d5a37cb088)] +interface nsITreeView : nsISupports +{ + /** + * The total number of rows in the tree (including the offscreen rows). + */ + readonly attribute long rowCount; + + /** + * The selection for this view. + */ + attribute nsITreeSelection selection; + + /** + * A whitespace delimited list of properties. For each property X the view + * gives back will cause the pseudoclasses ::-moz-tree-cell(x), + * ::-moz-tree-row(x), ::-moz-tree-twisty(x), ::-moz-tree-image(x), + * ::-moz-tree-cell-text(x). to be matched on the pseudoelement + * ::moz-tree-row. + */ + AString getRowProperties(in long index); + + /** + * A whitespace delimited list of properties for a given cell. Each + * property, x, that the view gives back will cause the pseudoclasses + * ::-moz-tree-cell(x), ::-moz-tree-row(x), ::-moz-tree-twisty(x), + * ::-moz-tree-image(x), ::-moz-tree-cell-text(x). to be matched on the + * cell. + */ + AString getCellProperties(in long row, in TreeColumn col); + + /** + * Called to get properties to paint a column background. For shading the sort + * column, etc. + */ + AString getColumnProperties(in TreeColumn col); + + /** + * Methods that can be used to test whether or not a twisty should be drawn, + * and if so, whether an open or closed twisty should be used. + */ + boolean isContainer(in long index); + boolean isContainerOpen(in long index); + boolean isContainerEmpty(in long index); + + /** + * isSeparator is used to determine if the row at index is a separator. + * A value of true will result in the tree drawing a horizontal separator. + * The tree uses the ::moz-tree-separator pseudoclass to draw the separator. + */ + boolean isSeparator(in long index); + + /** + * Specifies if there is currently a sort on any column. Used mostly by dragdrop + * to affect drop feedback. + */ + boolean isSorted(); + + const short DROP_BEFORE = -1; + const short DROP_ON = 0; + const short DROP_AFTER = 1; + /** + * Methods used by the drag feedback code to determine if a drag is allowable at + * the current location. To get the behavior where drops are only allowed on + * items, such as the mailNews folder pane, always return false when + * the orientation is not DROP_ON. + */ + boolean canDrop(in long index, in long orientation, in DataTransfer dataTransfer); + + /** + * Called when the user drops something on this view. The |orientation| param + * specifies before/on/after the given |row|. + */ + void drop(in long row, in long orientation, in DataTransfer dataTransfer); + + /** + * Methods used by the tree to draw thread lines in the tree. + * getParentIndex is used to obtain the index of a parent row. + * If there is no parent row, getParentIndex returns -1. + */ + long getParentIndex(in long rowIndex); + + /** + * hasNextSibling is used to determine if the row at rowIndex has a nextSibling + * that occurs *after* the index specified by afterIndex. Code that is forced + * to march down the view looking at levels can optimize the march by starting + * at afterIndex+1. + */ + boolean hasNextSibling(in long rowIndex, in long afterIndex); + + /** + * The level is an integer value that represents + * the level of indentation. It is multiplied by the width specified in the + * :moz-tree-indentation pseudoelement to compute the exact indendation. + */ + long getLevel(in long index); + + /** + * The image path for a given cell. For defining an icon for a cell. + * If the empty string is returned, the :moz-tree-image pseudoelement + * will be used. + */ + AString getImageSrc(in long row, in TreeColumn col); + + /** + * The value for a given cell. This method is only called for columns + * of type other than |text|. + */ + AString getCellValue(in long row, in TreeColumn col); + + /** + * The text for a given cell. If a column consists only of an image, then + * the empty string is returned. + */ + AString getCellText(in long row, in TreeColumn col); + + /** + * Called during initialization to link the view to the front end box object. + */ + void setTree(in XULTreeElement tree); + + /** + * Called on the view when an item is opened or closed. + */ + void toggleOpenState(in long index); + + /** + * Called on the view when a header is clicked. + */ + void cycleHeader(in TreeColumn col); + + /** + * Should be called from a XUL onselect handler whenever the selection changes. + */ + [binaryname(SelectionChangedXPCOM)] + void selectionChanged(); + + /** + * Called on the view when a cell in a non-selectable cycling column (e.g., unread/flag/etc.) is clicked. + */ + void cycleCell(in long row, in TreeColumn col); + + /** + * isEditable is called to ask the view if the cell contents are editable. + * A value of true will result in the tree popping up a text field when + * the user tries to inline edit the cell. + */ + boolean isEditable(in long row, in TreeColumn col); + + /** + * setCellValue is called when the value of the cell has been set by the user. + * This method is only called for columns of type other than |text|. + */ + void setCellValue(in long row, in TreeColumn col, in AString value); + + /** + * setCellText is called when the contents of the cell have been edited by the user. + */ + void setCellText(in long row, in TreeColumn col, in AString value); +}; diff --git a/layout/xul/tree/nsTreeBodyFrame.cpp b/layout/xul/tree/nsTreeBodyFrame.cpp new file mode 100644 index 0000000000..c7e8272aa0 --- /dev/null +++ b/layout/xul/tree/nsTreeBodyFrame.cpp @@ -0,0 +1,4363 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SimpleXULLeafFrame.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/PathHelpers.h" +#include "mozilla/Likely.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/intl/Segmenter.h" + +#include "gfxUtils.h" +#include "nsAlgorithm.h" +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsFontMetrics.h" +#include "nsITreeView.h" +#include "nsPresContext.h" +#include "nsNameSpaceManager.h" + +#include "nsTreeBodyFrame.h" +#include "nsTreeSelection.h" +#include "nsTreeImageListener.h" + +#include "nsGkAtoms.h" +#include "nsCSSAnonBoxes.h" + +#include "gfxContext.h" +#include "nsIContent.h" +#include "mozilla/ComputedStyle.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/intl/Segmenter.h" +#include "nsCSSRendering.h" +#include "nsString.h" +#include "nsContainerFrame.h" +#include "nsView.h" +#include "nsViewManager.h" +#include "nsVariant.h" +#include "nsWidgetsCID.h" +#include "nsIFrameInlines.h" +#include "nsTreeContentView.h" +#include "nsTreeUtils.h" +#include "nsStyleConsts.h" +#include "nsITheme.h" +#include "imgIRequest.h" +#include "imgIContainer.h" +#include "mozilla/dom/NodeInfo.h" +#include "nsContentUtils.h" +#include "nsLayoutUtils.h" +#include "nsIScrollableFrame.h" +#include "nsDisplayList.h" +#include "mozilla/dom/CustomEvent.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/TreeColumnBinding.h" +#include +#include "ScrollbarActivity.h" + +#ifdef ACCESSIBILITY +# include "nsAccessibilityService.h" +# include "nsIWritablePropertyBag2.h" +#endif +#include "nsBidiUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::gfx; +using namespace mozilla::image; +using namespace mozilla::layout; + +enum CroppingStyle { CropNone, CropLeft, CropRight, CropCenter, CropAuto }; + +// FIXME: Maybe unify with MiddleCroppingBlockFrame? +static void CropStringForWidth(nsAString& aText, gfxContext& aRenderingContext, + nsFontMetrics& aFontMetrics, nscoord aWidth, + CroppingStyle aCropType) { + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + + // See if the width is even smaller than the ellipsis + // If so, clear the text completely. + const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis(); + aFontMetrics.SetTextRunRTL(false); + nscoord ellipsisWidth = + nsLayoutUtils::AppUnitWidthOfString(kEllipsis, aFontMetrics, drawTarget); + + if (ellipsisWidth > aWidth) { + aText.Truncate(0); + return; + } + if (ellipsisWidth == aWidth) { + aText.Assign(kEllipsis); + return; + } + + // We will be drawing an ellipsis, thank you very much. + // Subtract out the required width of the ellipsis. + // This is the total remaining width we have to play with. + aWidth -= ellipsisWidth; + + using mozilla::intl::GraphemeClusterBreakIteratorUtf16; + using mozilla::intl::GraphemeClusterBreakReverseIteratorUtf16; + + // Now we crop. This is quite basic: it will not be really accurate in the + // presence of complex scripts with contextual shaping, etc., as it measures + // each grapheme cluster in isolation, not in its proper context. + switch (aCropType) { + case CropAuto: + case CropNone: + case CropRight: { + const Span text(aText); + GraphemeClusterBreakIteratorUtf16 iter(text); + uint32_t pos = 0; + nscoord totalWidth = 0; + + while (Maybe nextPos = iter.Next()) { + const nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString( + text.FromTo(pos, *nextPos), aFontMetrics, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + pos = *nextPos; + totalWidth += charWidth; + } + + if (pos < aText.Length()) { + aText.Replace(pos, aText.Length() - pos, kEllipsis); + } + } break; + + case CropLeft: { + const Span text(aText); + GraphemeClusterBreakReverseIteratorUtf16 iter(text); + uint32_t pos = text.Length(); + nscoord totalWidth = 0; + + // nextPos is decreasing since we use a reverse iterator. + while (Maybe nextPos = iter.Next()) { + const nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString( + text.FromTo(*nextPos, pos), aFontMetrics, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + + pos = *nextPos; + totalWidth += charWidth; + } + + if (pos > 0) { + aText.Replace(0, pos, kEllipsis); + } + } break; + + case CropCenter: { + const Span text(aText); + nscoord totalWidth = 0; + GraphemeClusterBreakIteratorUtf16 leftIter(text); + GraphemeClusterBreakReverseIteratorUtf16 rightIter(text); + uint32_t leftPos = 0; + uint32_t rightPos = text.Length(); + + while (leftPos < rightPos) { + Maybe nextPos = leftIter.Next(); + nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString( + text.FromTo(leftPos, *nextPos), aFontMetrics, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + + leftPos = *nextPos; + totalWidth += charWidth; + + if (leftPos >= rightPos) { + break; + } + + nextPos = rightIter.Next(); + charWidth = nsLayoutUtils::AppUnitWidthOfString( + text.FromTo(*nextPos, rightPos), aFontMetrics, drawTarget); + if (totalWidth + charWidth > aWidth) { + break; + } + + rightPos = *nextPos; + totalWidth += charWidth; + } + + if (leftPos < rightPos) { + aText.Replace(leftPos, rightPos - leftPos, kEllipsis); + } + } break; + } +} + +// Function that cancels all the image requests in our cache. +void nsTreeBodyFrame::CancelImageRequests() { + for (nsTreeImageCacheEntry entry : mImageCache.Values()) { + // If our imgIRequest object was registered with the refresh driver + // then we need to deregister it. + nsLayoutUtils::DeregisterImageRequest(PresContext(), entry.request, + nullptr); + entry.request->UnlockImage(); + entry.request->CancelAndForgetObserver(NS_BINDING_ABORTED); + } +} + +// +// NS_NewTreeFrame +// +// Creates a new tree frame +// +nsIFrame* NS_NewTreeBodyFrame(PresShell* aPresShell, ComputedStyle* aStyle) { + return new (aPresShell) nsTreeBodyFrame(aStyle, aPresShell->GetPresContext()); +} + +NS_IMPL_FRAMEARENA_HELPERS(nsTreeBodyFrame) + +NS_QUERYFRAME_HEAD(nsTreeBodyFrame) + NS_QUERYFRAME_ENTRY(nsIScrollbarMediator) + NS_QUERYFRAME_ENTRY(nsTreeBodyFrame) +NS_QUERYFRAME_TAIL_INHERITING(SimpleXULLeafFrame) + +// Constructor +nsTreeBodyFrame::nsTreeBodyFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : SimpleXULLeafFrame(aStyle, aPresContext, kClassID), + mImageCache(), + mTopRowIndex(0), + mPageLength(0), + mHorzPosition(0), + mOriginalHorzWidth(-1), + mHorzWidth(0), + mAdjustWidth(0), + mRowHeight(0), + mIndentation(0), + mUpdateBatchNest(0), + mRowCount(0), + mMouseOverRow(-1), + mFocused(false), + mHasFixedRowCount(false), + mVerticalOverflow(false), + mHorizontalOverflow(false), + mReflowCallbackPosted(false), + mCheckingOverflow(false) { + mColumns = new nsTreeColumns(this); +} + +// Destructor +nsTreeBodyFrame::~nsTreeBodyFrame() { + CancelImageRequests(); + DetachImageListeners(); +} + +static void GetBorderPadding(ComputedStyle* aStyle, nsMargin& aMargin) { + aMargin.SizeTo(0, 0, 0, 0); + aStyle->StylePadding()->GetPadding(aMargin); + aMargin += aStyle->StyleBorder()->GetComputedBorder(); +} + +static void AdjustForBorderPadding(ComputedStyle* aStyle, nsRect& aRect) { + nsMargin borderPadding(0, 0, 0, 0); + GetBorderPadding(aStyle, borderPadding); + aRect.Deflate(borderPadding); +} + +void nsTreeBodyFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + SimpleXULLeafFrame::Init(aContent, aParent, aPrevInFlow); + + mIndentation = GetIndentation(); + mRowHeight = GetRowHeight(); + + // Call GetBaseElement so that mTree is assigned. + RefPtr tree(GetBaseElement()); + if (MOZ_LIKELY(tree)) { + nsAutoString rows; + if (tree->GetAttr(nsGkAtoms::rows, rows)) { + nsresult err; + mPageLength = rows.ToInteger(&err); + mHasFixedRowCount = true; + } + } + + if (PresContext()->UseOverlayScrollbars()) { + mScrollbarActivity = + new ScrollbarActivity(static_cast(this)); + } +} + +void nsTreeBodyFrame::Destroy(DestroyContext& aContext) { + if (mScrollbarActivity) { + mScrollbarActivity->Destroy(); + mScrollbarActivity = nullptr; + } + + mScrollEvent.Revoke(); + // Make sure we cancel any posted callbacks. + if (mReflowCallbackPosted) { + PresShell()->CancelReflowCallback(this); + mReflowCallbackPosted = false; + } + + if (mColumns) mColumns->SetTree(nullptr); + + RefPtr tree = mTree; + + if (nsCOMPtr view = std::move(mView)) { + nsCOMPtr sel; + view->GetSelection(getter_AddRefs(sel)); + if (sel) { + sel->SetTree(nullptr); + } + view->SetTree(nullptr); + } + + // Make this call now because view->SetTree can run js which can undo this + // call. + if (tree) { + tree->BodyDestroyed(mTopRowIndex); + } + if (mTree && mTree != tree) { + mTree->BodyDestroyed(mTopRowIndex); + } + + SimpleXULLeafFrame::Destroy(aContext); +} + +void nsTreeBodyFrame::EnsureView() { + if (mView) { + return; + } + + if (PresShell()->IsReflowLocked()) { + if (!mReflowCallbackPosted) { + mReflowCallbackPosted = true; + PresShell()->PostReflowCallback(this); + } + return; + } + + AutoWeakFrame weakFrame(this); + + RefPtr tree = GetBaseElement(); + if (!tree) { + return; + } + nsCOMPtr treeView = tree->GetView(); + if (!treeView || !weakFrame.IsAlive()) { + return; + } + int32_t rowIndex = tree->GetCachedTopVisibleRow(); + + // Set our view. + SetView(treeView); + NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); + + // Scroll to the given row. + // XXX is this optimal if we haven't laid out yet? + ScrollToRow(rowIndex); + NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); +} + +void nsTreeBodyFrame::ManageReflowCallback() { + const nscoord horzWidth = CalcHorzWidth(GetScrollParts()); + if (!mReflowCallbackPosted) { + if (!mLastReflowRect || !mLastReflowRect->IsEqualEdges(mRect) || + mHorzWidth != horzWidth) { + PresShell()->PostReflowCallback(this); + mReflowCallbackPosted = true; + mOriginalHorzWidth = mHorzWidth; + } + } else if (mHorzWidth != horzWidth && mOriginalHorzWidth == horzWidth) { + // FIXME(emilio): This doesn't seem sound to me, if the rect changes in the + // block axis. + PresShell()->CancelReflowCallback(this); + mReflowCallbackPosted = false; + mOriginalHorzWidth = -1; + } + mLastReflowRect = Some(mRect); + mHorzWidth = horzWidth; +} + +nscoord nsTreeBodyFrame::GetIntrinsicBSize() { + return mHasFixedRowCount ? mRowHeight * mPageLength : 0; +} + +void nsTreeBodyFrame::DidReflow(nsPresContext* aPresContext, + const ReflowInput* aReflowInput) { + ManageReflowCallback(); + SimpleXULLeafFrame::DidReflow(aPresContext, aReflowInput); +} + +bool nsTreeBodyFrame::ReflowFinished() { + if (!mView) { + AutoWeakFrame weakFrame(this); + EnsureView(); + NS_ENSURE_TRUE(weakFrame.IsAlive(), false); + } + if (mView) { + CalcInnerBox(); + ScrollParts parts = GetScrollParts(); + mHorzWidth = CalcHorzWidth(parts); + if (!mHasFixedRowCount) { + mPageLength = + (mRowHeight > 0) ? (mInnerBox.height / mRowHeight) : mRowCount; + } + + int32_t lastPageTopRow = std::max(0, mRowCount - mPageLength); + if (mTopRowIndex > lastPageTopRow) + ScrollToRowInternal(parts, lastPageTopRow); + + XULTreeElement* treeContent = GetBaseElement(); + if (treeContent && treeContent->AttrValueIs( + kNameSpaceID_None, nsGkAtoms::keepcurrentinview, + nsGkAtoms::_true, eCaseMatters)) { + // make sure that the current selected item is still + // visible after the tree changes size. + if (nsCOMPtr sel = GetSelection()) { + int32_t currentIndex; + sel->GetCurrentIndex(¤tIndex); + if (currentIndex != -1) { + EnsureRowIsVisibleInternal(parts, currentIndex); + } + } + } + + if (!FullScrollbarsUpdate(false)) { + return false; + } + } + + mReflowCallbackPosted = false; + return false; +} + +void nsTreeBodyFrame::ReflowCallbackCanceled() { + mReflowCallbackPosted = false; +} + +nsresult nsTreeBodyFrame::GetView(nsITreeView** aView) { + *aView = nullptr; + AutoWeakFrame weakFrame(this); + EnsureView(); + NS_ENSURE_STATE(weakFrame.IsAlive()); + NS_IF_ADDREF(*aView = mView); + return NS_OK; +} + +nsresult nsTreeBodyFrame::SetView(nsITreeView* aView) { + if (aView == mView) { + return NS_OK; + } + + // First clear out the old view. + nsCOMPtr oldView = std::move(mView); + if (oldView) { + AutoWeakFrame weakFrame(this); + + nsCOMPtr sel; + oldView->GetSelection(getter_AddRefs(sel)); + if (sel) { + sel->SetTree(nullptr); + } + oldView->SetTree(nullptr); + + NS_ENSURE_STATE(weakFrame.IsAlive()); + + // Only reset the top row index and delete the columns if we had an old + // non-null view. + mTopRowIndex = 0; + } + + // Tree, meet the view. + mView = aView; + + // Changing the view causes us to refetch our data. This will + // necessarily entail a full invalidation of the tree. + Invalidate(); + + RefPtr treeContent = GetBaseElement(); + if (treeContent) { +#ifdef ACCESSIBILITY + if (nsAccessibilityService* accService = GetAccService()) { + accService->TreeViewChanged(PresContext()->GetPresShell(), treeContent, + mView); + } +#endif // #ifdef ACCESSIBILITY + FireDOMEvent(u"TreeViewChanged"_ns, treeContent); + } + + if (aView) { + // Give the view a new empty selection object to play with, but only if it + // doesn't have one already. + nsCOMPtr sel; + aView->GetSelection(getter_AddRefs(sel)); + if (sel) { + sel->SetTree(treeContent); + } else { + NS_NewTreeSelection(treeContent, getter_AddRefs(sel)); + aView->SetSelection(sel); + } + + // View, meet the tree. + AutoWeakFrame weakFrame(this); + aView->SetTree(treeContent); + NS_ENSURE_STATE(weakFrame.IsAlive()); + aView->GetRowCount(&mRowCount); + + if (!PresShell()->IsReflowLocked()) { + // The scrollbar will need to be updated. + FullScrollbarsUpdate(false); + } else if (!mReflowCallbackPosted) { + mReflowCallbackPosted = true; + PresShell()->PostReflowCallback(this); + } + } + + return NS_OK; +} + +already_AddRefed nsTreeBodyFrame::GetSelection() const { + nsCOMPtr sel; + if (nsCOMPtr view = GetExistingView()) { + view->GetSelection(getter_AddRefs(sel)); + } + return sel.forget(); +} + +nsresult nsTreeBodyFrame::SetFocused(bool aFocused) { + if (mFocused != aFocused) { + mFocused = aFocused; + if (nsCOMPtr sel = GetSelection()) { + sel->InvalidateSelection(); + } + } + return NS_OK; +} + +nsresult nsTreeBodyFrame::GetTreeBody(Element** aElement) { + // NS_ASSERTION(mContent, "no content, see bug #104878"); + if (!mContent) return NS_ERROR_NULL_POINTER; + + RefPtr element = mContent->AsElement(); + element.forget(aElement); + return NS_OK; +} + +int32_t nsTreeBodyFrame::RowHeight() const { + return nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); +} + +int32_t nsTreeBodyFrame::RowWidth() { + return nsPresContext::AppUnitsToIntCSSPixels(CalcHorzWidth(GetScrollParts())); +} + +int32_t nsTreeBodyFrame::GetHorizontalPosition() const { + return nsPresContext::AppUnitsToIntCSSPixels(mHorzPosition); +} + +Maybe nsTreeBodyFrame::GetSelectionRegion() { + if (!mView) { + return Nothing(); + } + + AutoWeakFrame wf(this); + nsCOMPtr selection = GetSelection(); + if (!selection || !wf.IsAlive()) { + return Nothing(); + } + + RefPtr presContext = PresContext(); + nsIntRect rect = mRect.ToOutsidePixels(AppUnitsPerCSSPixel()); + + nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame(); + nsPoint origin = GetOffsetTo(rootFrame); + + CSSIntRegion region; + + // iterate through the visible rows and add the selected ones to the + // drag region + int32_t x = nsPresContext::AppUnitsToIntCSSPixels(origin.x); + int32_t y = nsPresContext::AppUnitsToIntCSSPixels(origin.y); + int32_t top = y; + int32_t end = LastVisibleRow(); + int32_t rowHeight = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); + for (int32_t i = mTopRowIndex; i <= end; i++) { + bool isSelected; + selection->IsSelected(i, &isSelected); + if (isSelected) { + region.OrWith(CSSIntRect(x, y, rect.width, rowHeight)); + } + y += rowHeight; + } + + // clip to the tree boundary in case one row extends past it + region.AndWith(CSSIntRect(x, top, rect.width, rect.height)); + + return Some(region); +} + +nsresult nsTreeBodyFrame::Invalidate() { + if (mUpdateBatchNest) return NS_OK; + + InvalidateFrame(); + + return NS_OK; +} + +nsresult nsTreeBodyFrame::InvalidateColumn(nsTreeColumn* aCol) { + if (mUpdateBatchNest) return NS_OK; + + if (!aCol) return NS_ERROR_INVALID_ARG; + +#ifdef ACCESSIBILITY + if (GetAccService()) { + FireInvalidateEvent(-1, -1, aCol, aCol); + } +#endif // #ifdef ACCESSIBILITY + + nsRect columnRect; + nsresult rv = aCol->GetRect(this, mInnerBox.y, mInnerBox.height, &columnRect); + NS_ENSURE_SUCCESS(rv, rv); + + // When false then column is out of view + if (OffsetForHorzScroll(columnRect, true)) + InvalidateFrameWithRect(columnRect); + + return NS_OK; +} + +nsresult nsTreeBodyFrame::InvalidateRow(int32_t aIndex) { + if (mUpdateBatchNest) return NS_OK; + +#ifdef ACCESSIBILITY + if (GetAccService()) { + FireInvalidateEvent(aIndex, aIndex, nullptr, nullptr); + } +#endif // #ifdef ACCESSIBILITY + + aIndex -= mTopRowIndex; + if (aIndex < 0 || aIndex > mPageLength) return NS_OK; + + nsRect rowRect(mInnerBox.x, mInnerBox.y + mRowHeight * aIndex, + mInnerBox.width, mRowHeight); + InvalidateFrameWithRect(rowRect); + + return NS_OK; +} + +nsresult nsTreeBodyFrame::InvalidateCell(int32_t aIndex, nsTreeColumn* aCol) { + if (mUpdateBatchNest) return NS_OK; + +#ifdef ACCESSIBILITY + if (GetAccService()) { + FireInvalidateEvent(aIndex, aIndex, aCol, aCol); + } +#endif // #ifdef ACCESSIBILITY + + aIndex -= mTopRowIndex; + if (aIndex < 0 || aIndex > mPageLength) return NS_OK; + + if (!aCol) return NS_ERROR_INVALID_ARG; + + nsRect cellRect; + nsresult rv = aCol->GetRect(this, mInnerBox.y + mRowHeight * aIndex, + mRowHeight, &cellRect); + NS_ENSURE_SUCCESS(rv, rv); + + if (OffsetForHorzScroll(cellRect, true)) InvalidateFrameWithRect(cellRect); + + return NS_OK; +} + +nsresult nsTreeBodyFrame::InvalidateRange(int32_t aStart, int32_t aEnd) { + if (mUpdateBatchNest) return NS_OK; + + if (aStart == aEnd) return InvalidateRow(aStart); + + int32_t last = LastVisibleRow(); + if (aStart > aEnd || aEnd < mTopRowIndex || aStart > last) return NS_OK; + + if (aStart < mTopRowIndex) aStart = mTopRowIndex; + + if (aEnd > last) aEnd = last; + +#ifdef ACCESSIBILITY + if (GetAccService()) { + int32_t end = + mRowCount > 0 ? ((mRowCount <= aEnd) ? mRowCount - 1 : aEnd) : 0; + FireInvalidateEvent(aStart, end, nullptr, nullptr); + } +#endif // #ifdef ACCESSIBILITY + + nsRect rangeRect(mInnerBox.x, + mInnerBox.y + mRowHeight * (aStart - mTopRowIndex), + mInnerBox.width, mRowHeight * (aEnd - aStart + 1)); + InvalidateFrameWithRect(rangeRect); + + return NS_OK; +} + +static void FindScrollParts(nsIFrame* aCurrFrame, + nsTreeBodyFrame::ScrollParts* aResult) { + if (!aResult->mColumnsScrollFrame) { + nsIScrollableFrame* f = do_QueryFrame(aCurrFrame); + if (f) { + aResult->mColumnsFrame = aCurrFrame; + aResult->mColumnsScrollFrame = f; + } + } + + if (nsScrollbarFrame* sf = do_QueryFrame(aCurrFrame)) { + if (!sf->IsHorizontal()) { + if (!aResult->mVScrollbar) { + aResult->mVScrollbar = sf; + } + } else { + if (!aResult->mHScrollbar) { + aResult->mHScrollbar = sf; + } + } + // don't bother searching inside a scrollbar + return; + } + + nsIFrame* child = aCurrFrame->PrincipalChildList().FirstChild(); + while (child && !child->GetContent()->IsRootOfNativeAnonymousSubtree() && + (!aResult->mVScrollbar || !aResult->mHScrollbar || + !aResult->mColumnsScrollFrame)) { + FindScrollParts(child, aResult); + child = child->GetNextSibling(); + } +} + +nsTreeBodyFrame::ScrollParts nsTreeBodyFrame::GetScrollParts() { + ScrollParts result = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; + XULTreeElement* tree = GetBaseElement(); + if (nsIFrame* treeFrame = tree ? tree->GetPrimaryFrame() : nullptr) { + // The way we do this, searching through the entire frame subtree, is pretty + // dumb! We should know where these frames are. + FindScrollParts(treeFrame, &result); + if (result.mHScrollbar) { + result.mHScrollbar->SetScrollbarMediatorContent(GetContent()); + result.mHScrollbarContent = result.mHScrollbar->GetContent()->AsElement(); + } + if (result.mVScrollbar) { + result.mVScrollbar->SetScrollbarMediatorContent(GetContent()); + result.mVScrollbarContent = result.mVScrollbar->GetContent()->AsElement(); + } + } + return result; +} + +void nsTreeBodyFrame::UpdateScrollbars(const ScrollParts& aParts) { + nscoord rowHeightAsPixels = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); + + AutoWeakFrame weakFrame(this); + + if (aParts.mVScrollbar) { + nsAutoString curPos; + curPos.AppendInt(mTopRowIndex * rowHeightAsPixels); + aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos, + curPos, true); + // 'this' might be deleted here + } + + if (weakFrame.IsAlive() && aParts.mHScrollbar) { + nsAutoString curPos; + curPos.AppendInt(mHorzPosition); + aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos, + curPos, true); + // 'this' might be deleted here + } + + if (weakFrame.IsAlive() && mScrollbarActivity) { + mScrollbarActivity->ActivityOccurred(); + } +} + +void nsTreeBodyFrame::CheckOverflow(const ScrollParts& aParts) { + bool verticalOverflowChanged = false; + bool horizontalOverflowChanged = false; + + if (!mVerticalOverflow && mRowCount > mPageLength) { + mVerticalOverflow = true; + verticalOverflowChanged = true; + } else if (mVerticalOverflow && mRowCount <= mPageLength) { + mVerticalOverflow = false; + verticalOverflowChanged = true; + } + + if (aParts.mColumnsFrame) { + nsRect bounds = aParts.mColumnsFrame->GetRect(); + if (bounds.width != 0) { + /* Ignore overflows that are less than half a pixel. Yes these happen + all over the place when flex boxes are compressed real small. + Probably a result of a rounding errors somewhere in the layout code. */ + bounds.width += nsPresContext::CSSPixelsToAppUnits(0.5f); + if (!mHorizontalOverflow && bounds.width < mHorzWidth) { + mHorizontalOverflow = true; + horizontalOverflowChanged = true; + } else if (mHorizontalOverflow && bounds.width >= mHorzWidth) { + mHorizontalOverflow = false; + horizontalOverflowChanged = true; + } + } + } + + if (!horizontalOverflowChanged && !verticalOverflowChanged) { + return; + } + + AutoWeakFrame weakFrame(this); + + RefPtr presContext = PresContext(); + RefPtr presShell = presContext->GetPresShell(); + nsCOMPtr content = mContent; + + if (verticalOverflowChanged) { + InternalScrollPortEvent event( + true, mVerticalOverflow ? eScrollPortOverflow : eScrollPortUnderflow, + nullptr); + event.mOrient = InternalScrollPortEvent::eVertical; + EventDispatcher::Dispatch(content, presContext, &event); + } + + if (horizontalOverflowChanged) { + InternalScrollPortEvent event( + true, mHorizontalOverflow ? eScrollPortOverflow : eScrollPortUnderflow, + nullptr); + event.mOrient = InternalScrollPortEvent::eHorizontal; + EventDispatcher::Dispatch(content, presContext, &event); + } + + // The synchronous event dispatch above can trigger reflow notifications. + // Flush those explicitly now, so that we can guard against potential infinite + // recursion. See bug 905909. + if (!weakFrame.IsAlive()) { + return; + } + NS_ASSERTION(!mCheckingOverflow, + "mCheckingOverflow should not already be set"); + // Don't use AutoRestore since we want to not touch mCheckingOverflow if we + // fail the weakFrame.IsAlive() check below + mCheckingOverflow = true; + presShell->FlushPendingNotifications(FlushType::Layout); + if (!weakFrame.IsAlive()) { + return; + } + mCheckingOverflow = false; +} + +void nsTreeBodyFrame::InvalidateScrollbars(const ScrollParts& aParts, + AutoWeakFrame& aWeakColumnsFrame) { + if (mUpdateBatchNest || !mView) return; + AutoWeakFrame weakFrame(this); + + if (aParts.mVScrollbar) { + // Do Vertical Scrollbar + nsAutoString maxposStr; + + nscoord rowHeightAsPixels = + nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); + + int32_t size = rowHeightAsPixels * + (mRowCount > mPageLength ? mRowCount - mPageLength : 0); + maxposStr.AppendInt(size); + aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::maxpos, + maxposStr, true); + NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); + + // Also set our page increment and decrement. + nscoord pageincrement = mPageLength * rowHeightAsPixels; + nsAutoString pageStr; + pageStr.AppendInt(pageincrement); + aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None, + nsGkAtoms::pageincrement, pageStr, true); + NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); + } + + if (aParts.mHScrollbar && aParts.mColumnsFrame && + aWeakColumnsFrame.IsAlive()) { + // And now Horizontal scrollbar + nsRect bounds = aParts.mColumnsFrame->GetRect(); + nsAutoString maxposStr; + + maxposStr.AppendInt(mHorzWidth > bounds.width ? mHorzWidth - bounds.width + : 0); + aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::maxpos, + maxposStr, true); + NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); + + nsAutoString pageStr; + pageStr.AppendInt(bounds.width); + aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, + nsGkAtoms::pageincrement, pageStr, true); + NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); + + pageStr.Truncate(); + pageStr.AppendInt(nsPresContext::CSSPixelsToAppUnits(16)); + aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::increment, + pageStr, true); + } + + if (weakFrame.IsAlive() && mScrollbarActivity) { + mScrollbarActivity->ActivityOccurred(); + } +} + +// Takes client x/y in pixels, converts them to appunits, and converts into +// values relative to this nsTreeBodyFrame frame. +nsPoint nsTreeBodyFrame::AdjustClientCoordsToBoxCoordSpace(int32_t aX, + int32_t aY) { + nsPoint point(nsPresContext::CSSPixelsToAppUnits(aX), + nsPresContext::CSSPixelsToAppUnits(aY)); + + nsPresContext* presContext = PresContext(); + point -= GetOffsetTo(presContext->GetPresShell()->GetRootFrame()); + + // Adjust by the inner box coords, so that we're in the inner box's + // coordinate space. + point -= mInnerBox.TopLeft(); + return point; +} // AdjustClientCoordsToBoxCoordSpace + +int32_t nsTreeBodyFrame::GetRowAt(int32_t aX, int32_t aY) { + if (!mView) { + return 0; + } + + nsPoint point = AdjustClientCoordsToBoxCoordSpace(aX, aY); + + // Check if the coordinates are above our visible space. + if (point.y < 0) { + return -1; + } + + return GetRowAtInternal(point.x, point.y); +} + +nsresult nsTreeBodyFrame::GetCellAt(int32_t aX, int32_t aY, int32_t* aRow, + nsTreeColumn** aCol, + nsACString& aChildElt) { + if (!mView) return NS_OK; + + nsPoint point = AdjustClientCoordsToBoxCoordSpace(aX, aY); + + // Check if the coordinates are above our visible space. + if (point.y < 0) { + *aRow = -1; + return NS_OK; + } + + nsTreeColumn* col; + nsCSSAnonBoxPseudoStaticAtom* child; + GetCellAt(point.x, point.y, aRow, &col, &child); + + if (col) { + NS_ADDREF(*aCol = col); + if (child == nsCSSAnonBoxes::mozTreeCell()) + aChildElt.AssignLiteral("cell"); + else if (child == nsCSSAnonBoxes::mozTreeTwisty()) + aChildElt.AssignLiteral("twisty"); + else if (child == nsCSSAnonBoxes::mozTreeImage()) + aChildElt.AssignLiteral("image"); + else if (child == nsCSSAnonBoxes::mozTreeCellText()) + aChildElt.AssignLiteral("text"); + } + + return NS_OK; +} + +// +// GetCoordsForCellItem +// +// Find the x/y location and width/height (all in PIXELS) of the given object +// in the given column. +// +// XXX IMPORTANT XXX: +// Hyatt says in the bug for this, that the following needs to be done: +// (1) You need to deal with overflow when computing cell rects. See other +// column iteration examples... if you don't deal with this, you'll mistakenly +// extend the cell into the scrollbar's rect. +// +// (2) You are adjusting the cell rect by the *row" border padding. That's +// wrong. You need to first adjust a row rect by its border/padding, and then +// the cell rect fits inside the adjusted row rect. It also can have +// border/padding as well as margins. The vertical direction isn't that +// important, but you need to get the horizontal direction right. +// +// (3) GetImageSize() does not include margins (but it does include +// border/padding). You need to make sure to add in the image's margins as well. +// +nsresult nsTreeBodyFrame::GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol, + const nsACString& aElement, + int32_t* aX, int32_t* aY, + int32_t* aWidth, + int32_t* aHeight) { + *aX = 0; + *aY = 0; + *aWidth = 0; + *aHeight = 0; + + bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; + nscoord currX = mInnerBox.x - mHorzPosition; + + // The Rect for the requested item. + nsRect theRect; + + nsPresContext* presContext = PresContext(); + + nsCOMPtr view = GetExistingView(); + + for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol; + currCol = currCol->GetNext()) { + // The Rect for the current cell. + nscoord colWidth; +#ifdef DEBUG + nsresult rv = +#endif + currCol->GetWidthInTwips(this, &colWidth); + NS_ASSERTION(NS_SUCCEEDED(rv), "invalid column"); + + nsRect cellRect(currX, mInnerBox.y + mRowHeight * (aRow - mTopRowIndex), + colWidth, mRowHeight); + + // Check the ID of the current column to see if it matches. If it doesn't + // increment the current X value and continue to the next column. + if (currCol != aCol) { + currX += cellRect.width; + continue; + } + // Now obtain the properties for our cell. + PrefillPropertyArray(aRow, currCol); + + nsAutoString properties; + view->GetCellProperties(aRow, currCol, properties); + nsTreeUtils::TokenizeProperties(properties, mScratchArray); + + ComputedStyle* rowContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow()); + + // We don't want to consider any of the decorations that may be present + // on the current row, so we have to deflate the rect by the border and + // padding and offset its left and top coordinates appropriately. + AdjustForBorderPadding(rowContext, cellRect); + + ComputedStyle* cellContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); + + constexpr auto cell = "cell"_ns; + if (currCol->IsCycler() || cell.Equals(aElement)) { + // If the current Column is a Cycler, then the Rect is just the cell - the + // margins. Similarly, if we're just being asked for the cell rect, + // provide it. + + theRect = cellRect; + nsMargin cellMargin; + cellContext->StyleMargin()->GetMargin(cellMargin); + theRect.Deflate(cellMargin); + break; + } + + // Since we're not looking for the cell, and since the cell isn't a cycler, + // we're looking for some subcomponent, and now we need to subtract the + // borders and padding of the cell from cellRect so this does not + // interfere with our computations. + AdjustForBorderPadding(cellContext, cellRect); + + UniquePtr rc = + presContext->PresShell()->CreateReferenceRenderingContext(); + + // Now we'll start making our way across the cell, starting at the edge of + // the cell and proceeding until we hit the right edge. |cellX| is the + // working X value that we will increment as we crawl from left to right. + nscoord cellX = cellRect.x; + nscoord remainWidth = cellRect.width; + + if (currCol->IsPrimary()) { + // If the current Column is a Primary, then we need to take into account + // the indentation and possibly a twisty. + + // The amount of indentation is the indentation width (|mIndentation|) by + // the level. + int32_t level; + view->GetLevel(aRow, &level); + if (!isRTL) cellX += mIndentation * level; + remainWidth -= mIndentation * level; + + // Find the twisty rect by computing its size. + nsRect imageRect; + nsRect twistyRect(cellRect); + ComputedStyle* twistyContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); + GetTwistyRect(aRow, currCol, imageRect, twistyRect, presContext, + twistyContext); + + if ("twisty"_ns.Equals(aElement)) { + // If we're looking for the twisty Rect, just return the size + theRect = twistyRect; + break; + } + + // Now we need to add in the margins of the twisty element, so that we + // can find the offset of the next element in the cell. + nsMargin twistyMargin; + twistyContext->StyleMargin()->GetMargin(twistyMargin); + twistyRect.Inflate(twistyMargin); + + // Adjust our working X value with the twisty width (image size, margins, + // borders, padding. + if (!isRTL) cellX += twistyRect.width; + } + + // Cell Image + ComputedStyle* imageContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage()); + + nsRect imageSize = GetImageSize(aRow, currCol, false, imageContext); + if ("image"_ns.Equals(aElement)) { + theRect = imageSize; + theRect.x = cellX; + theRect.y = cellRect.y; + break; + } + + // Add in the margins of the cell image. + nsMargin imageMargin; + imageContext->StyleMargin()->GetMargin(imageMargin); + imageSize.Inflate(imageMargin); + + // Increment cellX by the image width + if (!isRTL) cellX += imageSize.width; + + // Cell Text + nsAutoString cellText; + view->GetCellText(aRow, currCol, cellText); + // We're going to measure this text so we need to ensure bidi is enabled if + // necessary + CheckTextForBidi(cellText); + + // Create a scratch rect to represent the text rectangle, with the current + // X and Y coords, and a guess at the width and height. The width is the + // remaining width we have left to traverse in the cell, which will be the + // widest possible value for the text rect, and the row height. + nsRect textRect(cellX, cellRect.y, remainWidth, cellRect.height); + + // Measure the width of the text. If the width of the text is greater than + // the remaining width available, then we just assume that the text has + // been cropped and use the remaining rect as the text Rect. Otherwise, + // we add in borders and padding to the text dimension and give that back. + ComputedStyle* textContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText()); + + RefPtr fm = + nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, presContext); + nscoord height = fm->MaxHeight(); + + nsMargin textMargin; + textContext->StyleMargin()->GetMargin(textMargin); + textRect.Deflate(textMargin); + + // Center the text. XXX Obey vertical-align style prop? + if (height < textRect.height) { + textRect.y += (textRect.height - height) / 2; + textRect.height = height; + } + + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(textContext, bp); + textRect.height += bp.top + bp.bottom; + + AdjustForCellText(cellText, aRow, currCol, *rc, *fm, textRect); + + theRect = textRect; + } + + if (isRTL) theRect.x = mInnerBox.width - theRect.x - theRect.width; + + *aX = nsPresContext::AppUnitsToIntCSSPixels(theRect.x); + *aY = nsPresContext::AppUnitsToIntCSSPixels(theRect.y); + *aWidth = nsPresContext::AppUnitsToIntCSSPixels(theRect.width); + *aHeight = nsPresContext::AppUnitsToIntCSSPixels(theRect.height); + + return NS_OK; +} + +int32_t nsTreeBodyFrame::GetRowAtInternal(nscoord aX, nscoord aY) { + if (mRowHeight <= 0) return -1; + + // Now just mod by our total inner box height and add to our top row index. + int32_t row = (aY / mRowHeight) + mTopRowIndex; + + // Check if the coordinates are below our visible space (or within our visible + // space but below any row). + if (row > mTopRowIndex + mPageLength || row >= mRowCount) return -1; + + return row; +} + +void nsTreeBodyFrame::CheckTextForBidi(nsAutoString& aText) { + // We could check to see whether the prescontext already has bidi enabled, + // but usually it won't, so it's probably faster to avoid the call to + // GetPresContext() when it's not needed. + if (HasRTLChars(aText)) { + PresContext()->SetBidiEnabled(); + } +} + +void nsTreeBodyFrame::AdjustForCellText(nsAutoString& aText, int32_t aRowIndex, + nsTreeColumn* aColumn, + gfxContext& aRenderingContext, + nsFontMetrics& aFontMetrics, + nsRect& aTextRect) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + + nscoord maxWidth = aTextRect.width; + bool widthIsGreater = nsLayoutUtils::StringWidthIsGreaterThan( + aText, aFontMetrics, drawTarget, maxWidth); + + nsCOMPtr view = GetExistingView(); + if (aColumn->Overflow()) { + DebugOnly rv; + nsTreeColumn* nextColumn = aColumn->GetNext(); + while (nextColumn && widthIsGreater) { + while (nextColumn) { + nscoord width; + rv = nextColumn->GetWidthInTwips(this, &width); + NS_ASSERTION(NS_SUCCEEDED(rv), "nextColumn is invalid"); + + if (width != 0) { + break; + } + + nextColumn = nextColumn->GetNext(); + } + + if (nextColumn) { + nsAutoString nextText; + view->GetCellText(aRowIndex, nextColumn, nextText); + // We don't measure or draw this text so no need to check it for + // bidi-ness + + if (nextText.Length() == 0) { + nscoord width; + rv = nextColumn->GetWidthInTwips(this, &width); + NS_ASSERTION(NS_SUCCEEDED(rv), "nextColumn is invalid"); + + maxWidth += width; + widthIsGreater = nsLayoutUtils::StringWidthIsGreaterThan( + aText, aFontMetrics, drawTarget, maxWidth); + + nextColumn = nextColumn->GetNext(); + } else { + nextColumn = nullptr; + } + } + } + } + + CroppingStyle cropType = CroppingStyle::CropRight; + if (aColumn->GetCropStyle() == 1) { + cropType = CroppingStyle::CropCenter; + } else if (aColumn->GetCropStyle() == 2) { + cropType = CroppingStyle::CropLeft; + } + CropStringForWidth(aText, aRenderingContext, aFontMetrics, maxWidth, + cropType); + + nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi( + aText, this, aFontMetrics, aRenderingContext); + + switch (aColumn->GetTextAlignment()) { + case mozilla::StyleTextAlign::Right: + aTextRect.x += aTextRect.width - width; + break; + case mozilla::StyleTextAlign::Center: + aTextRect.x += (aTextRect.width - width) / 2; + break; + default: + break; + } + + aTextRect.width = width; +} + +nsCSSAnonBoxPseudoStaticAtom* nsTreeBodyFrame::GetItemWithinCellAt( + nscoord aX, const nsRect& aCellRect, int32_t aRowIndex, + nsTreeColumn* aColumn) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + // Obtain the properties for our cell. + PrefillPropertyArray(aRowIndex, aColumn); + nsAutoString properties; + nsCOMPtr view = GetExistingView(); + view->GetCellProperties(aRowIndex, aColumn, properties); + nsTreeUtils::TokenizeProperties(properties, mScratchArray); + + // Resolve style for the cell. + ComputedStyle* cellContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); + + // Obtain the margins for the cell and then deflate our rect by that + // amount. The cell is assumed to be contained within the deflated rect. + nsRect cellRect(aCellRect); + nsMargin cellMargin; + cellContext->StyleMargin()->GetMargin(cellMargin); + cellRect.Deflate(cellMargin); + + // Adjust the rect for its border and padding. + AdjustForBorderPadding(cellContext, cellRect); + + if (aX < cellRect.x || aX >= cellRect.x + cellRect.width) { + // The user clicked within the cell's margins/borders/padding. This + // constitutes a click on the cell. + return nsCSSAnonBoxes::mozTreeCell(); + } + + nscoord currX = cellRect.x; + nscoord remainingWidth = cellRect.width; + + // Handle right alignment hit testing. + bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; + + nsPresContext* presContext = PresContext(); + UniquePtr rc = + presContext->PresShell()->CreateReferenceRenderingContext(); + + if (aColumn->IsPrimary()) { + // If we're the primary column, we have indentation and a twisty. + int32_t level; + view->GetLevel(aRowIndex, &level); + + if (!isRTL) currX += mIndentation * level; + remainingWidth -= mIndentation * level; + + if ((isRTL && aX > currX + remainingWidth) || (!isRTL && aX < currX)) { + // The user clicked within the indentation. + return nsCSSAnonBoxes::mozTreeCell(); + } + + // Always leave space for the twisty. + nsRect twistyRect(currX, cellRect.y, remainingWidth, cellRect.height); + bool hasTwisty = false; + bool isContainer = false; + view->IsContainer(aRowIndex, &isContainer); + if (isContainer) { + bool isContainerEmpty = false; + view->IsContainerEmpty(aRowIndex, &isContainerEmpty); + if (!isContainerEmpty) hasTwisty = true; + } + + // Resolve style for the twisty. + ComputedStyle* twistyContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); + + nsRect imageSize; + GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect, presContext, + twistyContext); + + // We will treat a click as hitting the twisty if it happens on the margins, + // borders, padding, or content of the twisty object. By allowing a "slop" + // into the margin, we make it a little bit easier for a user to hit the + // twisty. (We don't want to be too picky here.) + nsMargin twistyMargin; + twistyContext->StyleMargin()->GetMargin(twistyMargin); + twistyRect.Inflate(twistyMargin); + if (isRTL) twistyRect.x = currX + remainingWidth - twistyRect.width; + + // Now we test to see if aX is actually within the twistyRect. If it is, + // and if the item should have a twisty, then we return "twisty". If it is + // within the rect but we shouldn't have a twisty, then we return "cell". + if (aX >= twistyRect.x && aX < twistyRect.x + twistyRect.width) { + if (hasTwisty) + return nsCSSAnonBoxes::mozTreeTwisty(); + else + return nsCSSAnonBoxes::mozTreeCell(); + } + + if (!isRTL) currX += twistyRect.width; + remainingWidth -= twistyRect.width; + } + + // Now test to see if the user hit the icon for the cell. + nsRect iconRect(currX, cellRect.y, remainingWidth, cellRect.height); + + // Resolve style for the image. + ComputedStyle* imageContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage()); + + nsRect iconSize = GetImageSize(aRowIndex, aColumn, false, imageContext); + nsMargin imageMargin; + imageContext->StyleMargin()->GetMargin(imageMargin); + iconSize.Inflate(imageMargin); + iconRect.width = iconSize.width; + if (isRTL) iconRect.x = currX + remainingWidth - iconRect.width; + + if (aX >= iconRect.x && aX < iconRect.x + iconRect.width) { + // The user clicked on the image. + return nsCSSAnonBoxes::mozTreeImage(); + } + + if (!isRTL) currX += iconRect.width; + remainingWidth -= iconRect.width; + + nsAutoString cellText; + view->GetCellText(aRowIndex, aColumn, cellText); + // We're going to measure this text so we need to ensure bidi is enabled if + // necessary + CheckTextForBidi(cellText); + + nsRect textRect(currX, cellRect.y, remainingWidth, cellRect.height); + + ComputedStyle* textContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText()); + + nsMargin textMargin; + textContext->StyleMargin()->GetMargin(textMargin); + textRect.Deflate(textMargin); + + AdjustForBorderPadding(textContext, textRect); + + RefPtr fm = + nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, presContext); + AdjustForCellText(cellText, aRowIndex, aColumn, *rc, *fm, textRect); + + if (aX >= textRect.x && aX < textRect.x + textRect.width) + return nsCSSAnonBoxes::mozTreeCellText(); + else + return nsCSSAnonBoxes::mozTreeCell(); +} + +void nsTreeBodyFrame::GetCellAt(nscoord aX, nscoord aY, int32_t* aRow, + nsTreeColumn** aCol, + nsCSSAnonBoxPseudoStaticAtom** aChildElt) { + *aCol = nullptr; + *aChildElt = nullptr; + + *aRow = GetRowAtInternal(aX, aY); + if (*aRow < 0) return; + + // Determine the column hit. + for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol; + currCol = currCol->GetNext()) { + nsRect cellRect; + nsresult rv = currCol->GetRect( + this, mInnerBox.y + mRowHeight * (*aRow - mTopRowIndex), mRowHeight, + &cellRect); + if (NS_FAILED(rv)) { + MOZ_ASSERT_UNREACHABLE("column has no frame"); + continue; + } + + if (!OffsetForHorzScroll(cellRect, false)) continue; + + if (aX >= cellRect.x && aX < cellRect.x + cellRect.width) { + // We know the column hit now. + *aCol = currCol; + + if (currCol->IsCycler()) + // Cyclers contain only images. Fill this in immediately and return. + *aChildElt = nsCSSAnonBoxes::mozTreeImage(); + else + *aChildElt = GetItemWithinCellAt(aX, cellRect, *aRow, currCol); + break; + } + } +} + +nsresult nsTreeBodyFrame::GetCellWidth(int32_t aRow, nsTreeColumn* aCol, + gfxContext* aRenderingContext, + nscoord& aDesiredSize, + nscoord& aCurrentSize) { + MOZ_ASSERT(aCol, "aCol must not be null"); + MOZ_ASSERT(aRenderingContext, "aRenderingContext must not be null"); + + // The rect for the current cell. + nscoord colWidth; + nsresult rv = aCol->GetWidthInTwips(this, &colWidth); + NS_ENSURE_SUCCESS(rv, rv); + + nsRect cellRect(0, 0, colWidth, mRowHeight); + + int32_t overflow = + cellRect.x + cellRect.width - (mInnerBox.x + mInnerBox.width); + if (overflow > 0) cellRect.width -= overflow; + + // Adjust borders and padding for the cell. + ComputedStyle* cellContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(cellContext, bp); + + aCurrentSize = cellRect.width; + aDesiredSize = bp.left + bp.right; + nsCOMPtr view = GetExistingView(); + + if (aCol->IsPrimary()) { + // If the current Column is a Primary, then we need to take into account + // the indentation and possibly a twisty. + + // The amount of indentation is the indentation width (|mIndentation|) by + // the level. + int32_t level; + view->GetLevel(aRow, &level); + aDesiredSize += mIndentation * level; + + // Find the twisty rect by computing its size. + ComputedStyle* twistyContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); + + nsRect imageSize; + nsRect twistyRect(cellRect); + GetTwistyRect(aRow, aCol, imageSize, twistyRect, PresContext(), + twistyContext); + + // Add in the margins of the twisty element. + nsMargin twistyMargin; + twistyContext->StyleMargin()->GetMargin(twistyMargin); + twistyRect.Inflate(twistyMargin); + + aDesiredSize += twistyRect.width; + } + + ComputedStyle* imageContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage()); + + // Account for the width of the cell image. + nsRect imageSize = GetImageSize(aRow, aCol, false, imageContext); + // Add in the margins of the cell image. + nsMargin imageMargin; + imageContext->StyleMargin()->GetMargin(imageMargin); + imageSize.Inflate(imageMargin); + + aDesiredSize += imageSize.width; + + // Get the cell text. + nsAutoString cellText; + view->GetCellText(aRow, aCol, cellText); + // We're going to measure this text so we need to ensure bidi is enabled if + // necessary + CheckTextForBidi(cellText); + + ComputedStyle* textContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText()); + + // Get the borders and padding for the text. + GetBorderPadding(textContext, bp); + + RefPtr fm = + nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, PresContext()); + // Get the width of the text itself + nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi(cellText, this, *fm, + *aRenderingContext); + nscoord totalTextWidth = width + bp.left + bp.right; + aDesiredSize += totalTextWidth; + return NS_OK; +} + +nsresult nsTreeBodyFrame::IsCellCropped(int32_t aRow, nsTreeColumn* aCol, + bool* _retval) { + nscoord currentSize, desiredSize; + nsresult rv; + + if (!aCol) return NS_ERROR_INVALID_ARG; + + UniquePtr rc = PresShell()->CreateReferenceRenderingContext(); + + rv = GetCellWidth(aRow, aCol, rc.get(), desiredSize, currentSize); + NS_ENSURE_SUCCESS(rv, rv); + + *_retval = desiredSize > currentSize; + + return NS_OK; +} + +nsresult nsTreeBodyFrame::CreateTimer(const LookAndFeel::IntID aID, + nsTimerCallbackFunc aFunc, int32_t aType, + nsITimer** aTimer, const char* aName) { + // Get the delay from the look and feel service. + int32_t delay = LookAndFeel::GetInt(aID, 0); + + nsCOMPtr timer; + + // Create a new timer only if the delay is greater than zero. + // Zero value means that this feature is completely disabled. + if (delay > 0) { + MOZ_TRY_VAR(timer, + NS_NewTimerWithFuncCallback( + aFunc, this, delay, aType, aName, + mContent->OwnerDoc()->EventTargetFor(TaskCategory::Other))); + } + + timer.forget(aTimer); + return NS_OK; +} + +nsresult nsTreeBodyFrame::RowCountChanged(int32_t aIndex, int32_t aCount) { + if (aCount == 0 || !mView) { + return NS_OK; // Nothing to do. + } + +#ifdef ACCESSIBILITY + if (GetAccService()) { + FireRowCountChangedEvent(aIndex, aCount); + } +#endif // #ifdef ACCESSIBILITY + + AutoWeakFrame weakFrame(this); + + // Adjust our selection. + if (nsCOMPtr sel = GetSelection()) { + sel->AdjustSelection(aIndex, aCount); + } + + NS_ENSURE_STATE(weakFrame.IsAlive()); + + if (mUpdateBatchNest) return NS_OK; + + mRowCount += aCount; +#ifdef DEBUG + int32_t rowCount = mRowCount; + mView->GetRowCount(&rowCount); + NS_ASSERTION( + rowCount == mRowCount, + "row count did not change by the amount suggested, check caller"); +#endif + + int32_t count = Abs(aCount); + int32_t last = LastVisibleRow(); + if (aIndex >= mTopRowIndex && aIndex <= last) InvalidateRange(aIndex, last); + + ScrollParts parts = GetScrollParts(); + + if (mTopRowIndex == 0) { + // Just update the scrollbar and return. + FullScrollbarsUpdate(false); + return NS_OK; + } + + bool needsInvalidation = false; + // Adjust our top row index. + if (aCount > 0) { + if (mTopRowIndex > aIndex) { + // Rows came in above us. Augment the top row index. + mTopRowIndex += aCount; + } + } else if (aCount < 0) { + if (mTopRowIndex > aIndex + count - 1) { + // No need to invalidate. The remove happened + // completely above us (offscreen). + mTopRowIndex -= count; + } else if (mTopRowIndex >= aIndex) { + // This is a full-blown invalidate. + if (mTopRowIndex + mPageLength > mRowCount - 1) { + mTopRowIndex = std::max(0, mRowCount - 1 - mPageLength); + } + needsInvalidation = true; + } + } + + FullScrollbarsUpdate(needsInvalidation); + return NS_OK; +} + +nsresult nsTreeBodyFrame::BeginUpdateBatch() { + ++mUpdateBatchNest; + + return NS_OK; +} + +nsresult nsTreeBodyFrame::EndUpdateBatch() { + NS_ASSERTION(mUpdateBatchNest > 0, "badly nested update batch"); + + if (--mUpdateBatchNest != 0) { + return NS_OK; + } + + nsCOMPtr view = GetExistingView(); + if (!view) { + return NS_OK; + } + + Invalidate(); + int32_t countBeforeUpdate = mRowCount; + view->GetRowCount(&mRowCount); + if (countBeforeUpdate != mRowCount) { + if (mTopRowIndex + mPageLength > mRowCount - 1) { + mTopRowIndex = std::max(0, mRowCount - 1 - mPageLength); + } + FullScrollbarsUpdate(false); + } + + return NS_OK; +} + +void nsTreeBodyFrame::PrefillPropertyArray(int32_t aRowIndex, + nsTreeColumn* aCol) { + MOZ_ASSERT(!aCol || aCol->GetFrame(), "invalid column passed"); + mScratchArray.Clear(); + + // focus + if (mFocused) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::focus); + else + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::blur); + + // sort + bool sorted = false; + mView->IsSorted(&sorted); + if (sorted) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::sorted); + + // drag session + if (mSlots && mSlots->mIsDragging) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dragSession); + + if (aRowIndex != -1) { + if (aRowIndex == mMouseOverRow) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::hover); + + nsCOMPtr selection = GetSelection(); + if (selection) { + // selected + bool isSelected; + selection->IsSelected(aRowIndex, &isSelected); + if (isSelected) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::selected); + + // current + int32_t currentIndex; + selection->GetCurrentIndex(¤tIndex); + if (aRowIndex == currentIndex) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::current); + } + + // container or leaf + bool isContainer = false; + mView->IsContainer(aRowIndex, &isContainer); + if (isContainer) { + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::container); + + // open or closed + bool isOpen = false; + mView->IsContainerOpen(aRowIndex, &isOpen); + if (isOpen) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::open); + else + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::closed); + } else { + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::leaf); + } + + // drop orientation + if (mSlots && mSlots->mDropAllowed && mSlots->mDropRow == aRowIndex) { + if (mSlots->mDropOrient == nsITreeView::DROP_BEFORE) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropBefore); + else if (mSlots->mDropOrient == nsITreeView::DROP_ON) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropOn); + else if (mSlots->mDropOrient == nsITreeView::DROP_AFTER) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropAfter); + } + + // odd or even + if (aRowIndex % 2) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::odd); + else + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::even); + + XULTreeElement* tree = GetBaseElement(); + if (tree && tree->HasAttr(kNameSpaceID_None, nsGkAtoms::editing)) { + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::editing); + } + + // multiple columns + if (mColumns->GetColumnAt(1)) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::multicol); + } + + if (aCol) { + mScratchArray.AppendElement(aCol->GetAtom()); + + if (aCol->IsPrimary()) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::primary); + + if (aCol->GetType() == TreeColumn_Binding::TYPE_CHECKBOX) { + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::checkbox); + + if (aRowIndex != -1) { + nsAutoString value; + mView->GetCellValue(aRowIndex, aCol, value); + if (value.EqualsLiteral("true")) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::checked); + } + } + + // Read special properties from attributes on the column content node + if (aCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::insertbefore, + nsGkAtoms::_true, eCaseMatters)) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::insertbefore); + if (aCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::insertafter, + nsGkAtoms::_true, eCaseMatters)) + mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::insertafter); + } +} + +nsITheme* nsTreeBodyFrame::GetTwistyRect(int32_t aRowIndex, + nsTreeColumn* aColumn, + nsRect& aImageRect, + nsRect& aTwistyRect, + nsPresContext* aPresContext, + ComputedStyle* aTwistyContext) { + // The twisty rect extends all the way to the end of the cell. This is + // incorrect. We need to determine the twisty rect's true width. This is + // done by examining the ComputedStyle for a width first. If it has one, we + // use that. If it doesn't, we use the image's natural width. If the image + // hasn't loaded and if no width is specified, then we just bail. If there is + // a -moz-appearance involved, adjust the rect by the minimum widget size + // provided by the theme implementation. + aImageRect = GetImageSize(aRowIndex, aColumn, true, aTwistyContext); + if (aImageRect.height > aTwistyRect.height) + aImageRect.height = aTwistyRect.height; + if (aImageRect.width > aTwistyRect.width) + aImageRect.width = aTwistyRect.width; + else + aTwistyRect.width = aImageRect.width; + + bool useTheme = false; + nsITheme* theme = nullptr; + StyleAppearance appearance = + aTwistyContext->StyleDisplay()->EffectiveAppearance(); + if (appearance != StyleAppearance::None) { + theme = aPresContext->Theme(); + if (theme->ThemeSupportsWidget(aPresContext, nullptr, appearance)) + useTheme = true; + } + + if (useTheme) { + LayoutDeviceIntSize minTwistySizePx = + theme->GetMinimumWidgetSize(aPresContext, this, appearance); + + // GMWS() returns size in pixels, we need to convert it back to app units + nsSize minTwistySize; + minTwistySize.width = + aPresContext->DevPixelsToAppUnits(minTwistySizePx.width); + minTwistySize.height = + aPresContext->DevPixelsToAppUnits(minTwistySizePx.height); + + if (aTwistyRect.width < minTwistySize.width) { + aTwistyRect.width = minTwistySize.width; + } + } + + return useTheme ? theme : nullptr; +} + +nsresult nsTreeBodyFrame::GetImage(int32_t aRowIndex, nsTreeColumn* aCol, + bool aUseContext, + ComputedStyle* aComputedStyle, + imgIContainer** aResult) { + *aResult = nullptr; + + nsAutoString imageSrc; + mView->GetImageSrc(aRowIndex, aCol, imageSrc); + RefPtr styleRequest; + if (aUseContext || imageSrc.IsEmpty()) { + // Obtain the URL from the ComputedStyle. + styleRequest = + aComputedStyle->StyleList()->mListStyleImage.GetImageRequest(); + if (!styleRequest) return NS_OK; + nsCOMPtr uri; + styleRequest->GetURI(getter_AddRefs(uri)); + nsAutoCString spec; + nsresult rv = uri->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + CopyUTF8toUTF16(spec, imageSrc); + } + + // Look the image up in our cache. + nsTreeImageCacheEntry entry; + if (mImageCache.Get(imageSrc, &entry)) { + // Find out if the image has loaded. + uint32_t status; + imgIRequest* imgReq = entry.request; + imgReq->GetImageStatus(&status); + imgReq->GetImage(aResult); // We hand back the image here. The GetImage + // call addrefs *aResult. + bool animated = true; // Assuming animated is the safe option + + // We can only call GetAnimated if we're decoded + if (*aResult && (status & imgIRequest::STATUS_DECODE_COMPLETE)) + (*aResult)->GetAnimated(&animated); + + if ((!(status & imgIRequest::STATUS_LOAD_COMPLETE)) || animated) { + // We either aren't done loading, or we're animating. Add our row as a + // listener for invalidations. + nsCOMPtr obs; + imgReq->GetNotificationObserver(getter_AddRefs(obs)); + + if (obs) { + static_cast(obs.get())->AddCell(aRowIndex, aCol); + } + + return NS_OK; + } + } + + if (!*aResult) { + // Create a new nsTreeImageListener object and pass it our row and column + // information. + nsTreeImageListener* listener = new nsTreeImageListener(this); + if (!listener) return NS_ERROR_OUT_OF_MEMORY; + + mCreatedListeners.Insert(listener); + + listener->AddCell(aRowIndex, aCol); + nsCOMPtr imgNotificationObserver = listener; + + Document* doc = mContent->GetComposedDoc(); + if (!doc) + // The page is currently being torn down. Why bother. + return NS_ERROR_FAILURE; + + RefPtr imageRequest; + if (styleRequest) { + styleRequest->SyncClone(imgNotificationObserver, doc, + getter_AddRefs(imageRequest)); + } else { + nsCOMPtr srcURI; + nsContentUtils::NewURIWithDocumentCharset( + getter_AddRefs(srcURI), imageSrc, doc, mContent->GetBaseURI()); + if (!srcURI) return NS_ERROR_FAILURE; + + auto referrerInfo = MakeRefPtr(*doc); + + // XXXbz what's the origin principal for this stuff that comes from our + // view? I guess we should assume that it's the node's principal... + nsresult rv = nsContentUtils::LoadImage( + srcURI, mContent, doc, mContent->NodePrincipal(), 0, referrerInfo, + imgNotificationObserver, nsIRequest::LOAD_NORMAL, u""_ns, + getter_AddRefs(imageRequest)); + NS_ENSURE_SUCCESS(rv, rv); + + // NOTE(heycam): If it's an SVG image, and we need to want the image to + // able to respond to media query changes, it needs to be added to the + // document's ImageTracker. For now, assume we don't need this. + } + listener->UnsuppressInvalidation(); + + if (!imageRequest) return NS_ERROR_FAILURE; + + // We don't want discarding/decode-on-draw for xul images + imageRequest->StartDecoding(imgIContainer::FLAG_ASYNC_NOTIFY); + imageRequest->LockImage(); + + // In a case it was already cached. + imageRequest->GetImage(aResult); + nsTreeImageCacheEntry cacheEntry(imageRequest, imgNotificationObserver); + mImageCache.InsertOrUpdate(imageSrc, cacheEntry); + } + return NS_OK; +} + +nsRect nsTreeBodyFrame::GetImageSize(int32_t aRowIndex, nsTreeColumn* aCol, + bool aUseContext, + ComputedStyle* aComputedStyle) { + // XXX We should respond to visibility rules for collapsed vs. hidden. + + // This method returns the width of the twisty INCLUDING borders and padding. + // It first checks the ComputedStyle for a width. If none is found, it tries + // to use the default image width for the twisty. If no image is found, it + // defaults to border+padding. + nsRect r(0, 0, 0, 0); + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(aComputedStyle, bp); + r.Inflate(bp); + + // Now r contains our border+padding info. We now need to get our width and + // height. + bool needWidth = false; + bool needHeight = false; + + // We have to load image even though we already have a size. + // Don't change this, otherwise things start to go awry. + nsCOMPtr image; + GetImage(aRowIndex, aCol, aUseContext, aComputedStyle, getter_AddRefs(image)); + + const nsStylePosition* myPosition = aComputedStyle->StylePosition(); + if (myPosition->mWidth.ConvertsToLength()) { + int32_t val = myPosition->mWidth.ToLength(); + r.width += val; + } else { + needWidth = true; + } + + if (myPosition->mHeight.ConvertsToLength()) { + int32_t val = myPosition->mHeight.ToLength(); + r.height += val; + } else { + needHeight = true; + } + + if (image) { + if (needWidth || needHeight) { + // Get the natural image size. + + if (needWidth) { + // Get the size from the image. + nscoord width; + image->GetWidth(&width); + r.width += nsPresContext::CSSPixelsToAppUnits(width); + } + + if (needHeight) { + nscoord height; + image->GetHeight(&height); + r.height += nsPresContext::CSSPixelsToAppUnits(height); + } + } + } + + return r; +} + +// GetImageDestSize returns the destination size of the image. +// The width and height do not include borders and padding. +// The width and height have not been adjusted to fit in the row height +// or cell width. +// The width and height reflect the destination size specified in CSS, +// or the image region specified in CSS, or the natural size of the +// image. +// If only the destination width has been specified in CSS, the height is +// calculated to maintain the aspect ratio of the image. +// If only the destination height has been specified in CSS, the width is +// calculated to maintain the aspect ratio of the image. +nsSize nsTreeBodyFrame::GetImageDestSize(ComputedStyle* aComputedStyle, + imgIContainer* image) { + nsSize size(0, 0); + + // We need to get the width and height. + bool needWidth = false; + bool needHeight = false; + + // Get the style position to see if the CSS has specified the + // destination width/height. + const nsStylePosition* myPosition = aComputedStyle->StylePosition(); + + if (myPosition->mWidth.ConvertsToLength()) { + // CSS has specified the destination width. + size.width = myPosition->mWidth.ToLength(); + } else { + // We'll need to get the width of the image/region. + needWidth = true; + } + + if (myPosition->mHeight.ConvertsToLength()) { + // CSS has specified the destination height. + size.height = myPosition->mHeight.ToLength(); + } else { + // We'll need to get the height of the image/region. + needHeight = true; + } + + if (needWidth || needHeight) { + // We need to get the size of the image/region. + nsSize imageSize(0, 0); + if (image) { + nscoord width; + image->GetWidth(&width); + imageSize.width = nsPresContext::CSSPixelsToAppUnits(width); + nscoord height; + image->GetHeight(&height); + imageSize.height = nsPresContext::CSSPixelsToAppUnits(height); + } + + if (needWidth) { + if (!needHeight && imageSize.height != 0) { + // The CSS specified the destination height, but not the destination + // width. We need to calculate the width so that we maintain the + // image's aspect ratio. + size.width = imageSize.width * size.height / imageSize.height; + } else { + size.width = imageSize.width; + } + } + + if (needHeight) { + if (!needWidth && imageSize.width != 0) { + // The CSS specified the destination width, but not the destination + // height. We need to calculate the height so that we maintain the + // image's aspect ratio. + size.height = imageSize.height * size.width / imageSize.width; + } else { + size.height = imageSize.height; + } + } + } + + return size; +} + +// GetImageSourceRect returns the source rectangle of the image to be +// displayed. +// The width and height reflect the image region specified in CSS, or +// the natural size of the image. +// The width and height do not include borders and padding. +// The width and height do not reflect the destination size specified +// in CSS. +nsRect nsTreeBodyFrame::GetImageSourceRect(ComputedStyle* aComputedStyle, + imgIContainer* image) { + if (!image) { + return nsRect(); + } + + nsRect r; + // Use the actual image size. + nscoord coord; + if (NS_SUCCEEDED(image->GetWidth(&coord))) { + r.width = nsPresContext::CSSPixelsToAppUnits(coord); + } + if (NS_SUCCEEDED(image->GetHeight(&coord))) { + r.height = nsPresContext::CSSPixelsToAppUnits(coord); + } + return r; +} + +int32_t nsTreeBodyFrame::GetRowHeight() { + // Look up the correct height. It is equal to the specified height + // + the specified margins. + mScratchArray.Clear(); + ComputedStyle* rowContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow()); + if (rowContext) { + const nsStylePosition* myPosition = rowContext->StylePosition(); + + nscoord minHeight = 0; + if (myPosition->mMinHeight.ConvertsToLength()) { + minHeight = myPosition->mMinHeight.ToLength(); + } + + nscoord height = 0; + if (myPosition->mHeight.ConvertsToLength()) { + height = myPosition->mHeight.ToLength(); + } + + if (height < minHeight) height = minHeight; + + if (height > 0) { + height = nsPresContext::AppUnitsToIntCSSPixels(height); + height += height % 2; + height = nsPresContext::CSSPixelsToAppUnits(height); + + // XXX Check box-sizing to determine if border/padding should augment the + // height Inflate the height by our margins. + nsRect rowRect(0, 0, 0, height); + nsMargin rowMargin; + rowContext->StyleMargin()->GetMargin(rowMargin); + rowRect.Inflate(rowMargin); + height = rowRect.height; + return height; + } + } + + return nsPresContext::CSSPixelsToAppUnits(18); // As good a default as any. +} + +int32_t nsTreeBodyFrame::GetIndentation() { + // Look up the correct indentation. It is equal to the specified indentation + // width. + mScratchArray.Clear(); + ComputedStyle* indentContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeIndentation()); + if (indentContext) { + const nsStylePosition* myPosition = indentContext->StylePosition(); + if (myPosition->mWidth.ConvertsToLength()) { + return myPosition->mWidth.ToLength(); + } + } + + return nsPresContext::CSSPixelsToAppUnits(16); // As good a default as any. +} + +void nsTreeBodyFrame::CalcInnerBox() { + mInnerBox.SetRect(0, 0, mRect.width, mRect.height); + AdjustForBorderPadding(mComputedStyle, mInnerBox); +} + +nscoord nsTreeBodyFrame::CalcHorzWidth(const ScrollParts& aParts) { + // Compute the adjustment to the last column. This varies depending on the + // visibility of the columnpicker and the scrollbar. + if (aParts.mColumnsFrame) + mAdjustWidth = mRect.width - aParts.mColumnsFrame->GetRect().width; + else + mAdjustWidth = 0; + + nscoord width = 0; + + // We calculate this from the scrollable frame, so that it + // properly covers all contingencies of what could be + // scrollable (columns, body, etc...) + + if (aParts.mColumnsScrollFrame) { + width = aParts.mColumnsScrollFrame->GetScrollRange().width + + aParts.mColumnsScrollFrame->GetScrollPortRect().width; + } + + // If no horz scrolling periphery is present, then just return our width + if (width == 0) width = mRect.width; + + return width; +} + +Maybe nsTreeBodyFrame::GetCursor(const nsPoint& aPoint) { + // Check the GetScriptHandlingObject so we don't end up running code when + // the document is a zombie. + bool dummy; + if (mView && GetContent()->GetComposedDoc()->GetScriptHandlingObject(dummy)) { + int32_t row; + nsTreeColumn* col; + nsCSSAnonBoxPseudoStaticAtom* child; + GetCellAt(aPoint.x, aPoint.y, &row, &col, &child); + + if (child) { + // Our scratch array is already prefilled. + RefPtr childContext = GetPseudoComputedStyle(child); + StyleCursorKind kind = childContext->StyleUI()->Cursor().keyword; + if (kind == StyleCursorKind::Auto) { + kind = StyleCursorKind::Default; + } + return Some( + Cursor{kind, AllowCustomCursorImage::Yes, std::move(childContext)}); + } + } + return SimpleXULLeafFrame::GetCursor(aPoint); +} + +static uint32_t GetDropEffect(WidgetGUIEvent* aEvent) { + NS_ASSERTION(aEvent->mClass == eDragEventClass, "wrong event type"); + WidgetDragEvent* dragEvent = aEvent->AsDragEvent(); + nsContentUtils::SetDataTransferInEvent(dragEvent); + + uint32_t action = 0; + if (dragEvent->mDataTransfer) { + action = dragEvent->mDataTransfer->DropEffectInt(); + } + return action; +} + +nsresult nsTreeBodyFrame::HandleEvent(nsPresContext* aPresContext, + WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) { + if (aEvent->mMessage == eMouseOver || aEvent->mMessage == eMouseMove) { + nsPoint pt = + nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, RelativeTo{this}); + int32_t xTwips = pt.x - mInnerBox.x; + int32_t yTwips = pt.y - mInnerBox.y; + int32_t newrow = GetRowAtInternal(xTwips, yTwips); + if (mMouseOverRow != newrow) { + // redraw the old and the new row + if (mMouseOverRow != -1) InvalidateRow(mMouseOverRow); + mMouseOverRow = newrow; + if (mMouseOverRow != -1) InvalidateRow(mMouseOverRow); + } + } else if (aEvent->mMessage == eMouseOut) { + if (mMouseOverRow != -1) { + InvalidateRow(mMouseOverRow); + mMouseOverRow = -1; + } + } else if (aEvent->mMessage == eDragEnter) { + if (!mSlots) { + mSlots = MakeUnique(); + } + + // Cache several things we'll need throughout the course of our work. These + // will all get released on a drag exit. + + if (mSlots->mTimer) { + mSlots->mTimer->Cancel(); + mSlots->mTimer = nullptr; + } + + // Cache the drag session. + mSlots->mIsDragging = true; + mSlots->mDropRow = -1; + mSlots->mDropOrient = -1; + mSlots->mDragAction = GetDropEffect(aEvent); + } else if (aEvent->mMessage == eDragOver) { + // The mouse is hovering over this tree. If we determine things are + // different from the last time, invalidate the drop feedback at the old + // position, query the view to see if the current location is droppable, + // and then invalidate the drop feedback at the new location if it is. + // The mouse may or may not have changed position from the last time + // we were called, so optimize out a lot of the extra notifications by + // checking if anything changed first. For drop feedback we use drop, + // dropBefore and dropAfter property. + if (!mView || !mSlots) { + return NS_OK; + } + + // Save last values, we will need them. + int32_t lastDropRow = mSlots->mDropRow; + int16_t lastDropOrient = mSlots->mDropOrient; +#ifndef XP_MACOSX + int16_t lastScrollLines = mSlots->mScrollLines; +#endif + + // Find out the current drag action + uint32_t lastDragAction = mSlots->mDragAction; + mSlots->mDragAction = GetDropEffect(aEvent); + + // Compute the row mouse is over and the above/below/on state. + // Below we'll use this to see if anything changed. + // Also check if we want to auto-scroll. + ComputeDropPosition(aEvent, &mSlots->mDropRow, &mSlots->mDropOrient, + &mSlots->mScrollLines); + + // While we're here, handle tracking of scrolling during a drag. + if (mSlots->mScrollLines) { + if (mSlots->mDropAllowed) { + // Invalidate primary cell at old location. + mSlots->mDropAllowed = false; + InvalidateDropFeedback(lastDropRow, lastDropOrient); + } +#ifdef XP_MACOSX + ScrollByLines(mSlots->mScrollLines); +#else + if (!lastScrollLines) { + // Cancel any previously initialized timer. + if (mSlots->mTimer) { + mSlots->mTimer->Cancel(); + mSlots->mTimer = nullptr; + } + + // Set a timer to trigger the tree scrolling. + CreateTimer(LookAndFeel::IntID::TreeLazyScrollDelay, LazyScrollCallback, + nsITimer::TYPE_ONE_SHOT, getter_AddRefs(mSlots->mTimer), + "nsTreeBodyFrame::LazyScrollCallback"); + } +#endif + // Bail out to prevent spring loaded timer and feedback line settings. + return NS_OK; + } + + // If changed from last time, invalidate primary cell at the old location + // and if allowed, invalidate primary cell at the new location. If nothing + // changed, just bail. + if (mSlots->mDropRow != lastDropRow || + mSlots->mDropOrient != lastDropOrient || + mSlots->mDragAction != lastDragAction) { + // Invalidate row at the old location. + if (mSlots->mDropAllowed) { + mSlots->mDropAllowed = false; + InvalidateDropFeedback(lastDropRow, lastDropOrient); + } + + if (mSlots->mTimer) { + // Timer is active but for a different row than the current one, kill + // it. + mSlots->mTimer->Cancel(); + mSlots->mTimer = nullptr; + } + + if (mSlots->mDropRow >= 0) { + if (!mSlots->mTimer && mSlots->mDropOrient == nsITreeView::DROP_ON) { + // Either there wasn't a timer running or it was just killed above. + // If over a folder, start up a timer to open the folder. + bool isContainer = false; + mView->IsContainer(mSlots->mDropRow, &isContainer); + if (isContainer) { + bool isOpen = false; + mView->IsContainerOpen(mSlots->mDropRow, &isOpen); + if (!isOpen) { + // This node isn't expanded, set a timer to expand it. + CreateTimer(LookAndFeel::IntID::TreeOpenDelay, OpenCallback, + nsITimer::TYPE_ONE_SHOT, + getter_AddRefs(mSlots->mTimer), + "nsTreeBodyFrame::OpenCallback"); + } + } + } + + // The dataTransfer was initialized by the call to GetDropEffect above. + bool canDropAtNewLocation = false; + mView->CanDrop(mSlots->mDropRow, mSlots->mDropOrient, + aEvent->AsDragEvent()->mDataTransfer, + &canDropAtNewLocation); + + if (canDropAtNewLocation) { + // Invalidate row at the new location. + mSlots->mDropAllowed = canDropAtNewLocation; + InvalidateDropFeedback(mSlots->mDropRow, mSlots->mDropOrient); + } + } + } + + // Indicate that the drop is allowed by preventing the default behaviour. + if (mSlots->mDropAllowed) *aEventStatus = nsEventStatus_eConsumeNoDefault; + } else if (aEvent->mMessage == eDrop) { + // this event was meant for another frame, so ignore it + if (!mSlots) return NS_OK; + + // Tell the view where the drop happened. + + // Remove the drop folder and all its parents from the array. + int32_t parentIndex; + nsresult rv = mView->GetParentIndex(mSlots->mDropRow, &parentIndex); + while (NS_SUCCEEDED(rv) && parentIndex >= 0) { + mSlots->mArray.RemoveElement(parentIndex); + rv = mView->GetParentIndex(parentIndex, &parentIndex); + } + + NS_ASSERTION(aEvent->mClass == eDragEventClass, "wrong event type"); + WidgetDragEvent* dragEvent = aEvent->AsDragEvent(); + nsContentUtils::SetDataTransferInEvent(dragEvent); + + mView->Drop(mSlots->mDropRow, mSlots->mDropOrient, + dragEvent->mDataTransfer); + mSlots->mDropRow = -1; + mSlots->mDropOrient = -1; + mSlots->mIsDragging = false; + *aEventStatus = + nsEventStatus_eConsumeNoDefault; // already handled the drop + } else if (aEvent->mMessage == eDragExit) { + // this event was meant for another frame, so ignore it + if (!mSlots) return NS_OK; + + // Clear out all our tracking vars. + + if (mSlots->mDropAllowed) { + mSlots->mDropAllowed = false; + InvalidateDropFeedback(mSlots->mDropRow, mSlots->mDropOrient); + } else + mSlots->mDropAllowed = false; + mSlots->mIsDragging = false; + mSlots->mScrollLines = 0; + // If a drop is occuring, the exit event will fire just before the drop + // event, so don't reset mDropRow or mDropOrient as these fields are used + // by the drop event. + if (mSlots->mTimer) { + mSlots->mTimer->Cancel(); + mSlots->mTimer = nullptr; + } + + if (!mSlots->mArray.IsEmpty()) { + // Close all spring loaded folders except the drop folder. + CreateTimer(LookAndFeel::IntID::TreeCloseDelay, CloseCallback, + nsITimer::TYPE_ONE_SHOT, getter_AddRefs(mSlots->mTimer), + "nsTreeBodyFrame::CloseCallback"); + } + } + + return NS_OK; +} + +namespace mozilla { + +class nsDisplayTreeBody final : public nsPaintedDisplayItem { + public: + nsDisplayTreeBody(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(nsDisplayTreeBody); + } + MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayTreeBody) + + nsDisplayItemGeometry* AllocateGeometry( + nsDisplayListBuilder* aBuilder) override { + return new nsDisplayTreeBodyGeometry(this, aBuilder, IsWindowActive()); + } + + void Destroy(nsDisplayListBuilder* aBuilder) override { + aBuilder->UnregisterThemeGeometry(this); + nsPaintedDisplayItem::Destroy(aBuilder); + } + + bool IsWindowActive() const { + DocumentState docState = + mFrame->PresContext()->Document()->GetDocumentState(); + return !docState.HasState(DocumentState::WINDOW_INACTIVE); + } + + void ComputeInvalidationRegion(nsDisplayListBuilder* aBuilder, + const nsDisplayItemGeometry* aGeometry, + nsRegion* aInvalidRegion) const override { + auto geometry = static_cast(aGeometry); + + if (IsWindowActive() != geometry->mWindowIsActive) { + bool snap; + aInvalidRegion->Or(*aInvalidRegion, GetBounds(aBuilder, &snap)); + } + + nsPaintedDisplayItem::ComputeInvalidationRegion(aBuilder, aGeometry, + aInvalidRegion); + } + + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override { + MOZ_ASSERT(aBuilder); + Unused << static_cast(mFrame)->PaintTreeBody( + *aCtx, GetPaintRect(aBuilder, aCtx), ToReferenceFrame(), aBuilder); + } + + NS_DISPLAY_DECL_NAME("XULTreeBody", TYPE_XUL_TREE_BODY) + + nsRect GetComponentAlphaBounds( + nsDisplayListBuilder* aBuilder) const override { + bool snap; + return GetBounds(aBuilder, &snap); + } +}; + +} // namespace mozilla + +#ifdef XP_MACOSX +static bool IsInSourceList(nsIFrame* aFrame) { + for (nsIFrame* frame = aFrame; frame; + frame = nsLayoutUtils::GetCrossDocParentFrameInProcess(frame)) { + if (frame->StyleDisplay()->EffectiveAppearance() == + StyleAppearance::MozMacSourceList) { + return true; + } + } + return false; +} +#endif + +// Painting routines +void nsTreeBodyFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + // REVIEW: why did we paint if we were collapsed? that makes no sense! + if (!IsVisibleForPainting()) return; // We're invisible. Don't paint. + + // Handles painting our background, border, and outline. + SimpleXULLeafFrame::BuildDisplayList(aBuilder, aLists); + + // Bail out now if there's no view or we can't run script because the + // document is a zombie + if (!mView || !GetContent()->GetComposedDoc()->GetWindow()) return; + + nsDisplayItem* item = MakeDisplayItem(aBuilder, this); + aLists.Content()->AppendToTop(item); + +#ifdef XP_MACOSX + XULTreeElement* tree = GetBaseElement(); + nsIFrame* treeFrame = tree ? tree->GetPrimaryFrame() : nullptr; + nsCOMPtr view = GetExistingView(); + nsCOMPtr selection = GetSelection(); + nsITheme* theme = PresContext()->Theme(); + // On Mac, we support native theming of selected rows. On 10.10 and higher, + // this means applying vibrancy which require us to register the theme + // geometrics for the row. In order to make the vibrancy effect to work + // properly, we also need an ancestor frame to be themed as a source list. + if (selection && theme && IsInSourceList(treeFrame)) { + // Loop through our onscreen rows. If the row is selected and a + // -moz-appearance is provided, RegisterThemeGeometry might be necessary. + const auto end = std::min(mRowCount, LastVisibleRow() + 1); + for (auto i = FirstVisibleRow(); i < end; i++) { + bool isSelected; + selection->IsSelected(i, &isSelected); + if (isSelected) { + PrefillPropertyArray(i, nullptr); + nsAutoString properties; + view->GetRowProperties(i, properties); + nsTreeUtils::TokenizeProperties(properties, mScratchArray); + ComputedStyle* rowContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow()); + auto appearance = rowContext->StyleDisplay()->EffectiveAppearance(); + if (appearance != StyleAppearance::None) { + if (theme->ThemeSupportsWidget(PresContext(), this, appearance)) { + nsITheme::ThemeGeometryType type = + theme->ThemeGeometryTypeForWidget(this, appearance); + if (type != nsITheme::eThemeGeometryTypeUnknown) { + nsRect rowRect(mInnerBox.x, + mInnerBox.y + mRowHeight * (i - FirstVisibleRow()), + mInnerBox.width, mRowHeight); + aBuilder->RegisterThemeGeometry( + type, item, + LayoutDeviceIntRect::FromUnknownRect( + (rowRect + aBuilder->ToReferenceFrame(this)) + .ToNearestPixels( + PresContext()->AppUnitsPerDevPixel()))); + } + } + } + } + } + } +#endif +} + +ImgDrawResult nsTreeBodyFrame::PaintTreeBody(gfxContext& aRenderingContext, + const nsRect& aDirtyRect, + nsPoint aPt, + nsDisplayListBuilder* aBuilder) { + // Update our available height and our page count. + CalcInnerBox(); + + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + + aRenderingContext.Save(); + aRenderingContext.Clip(NSRectToSnappedRect( + mInnerBox + aPt, PresContext()->AppUnitsPerDevPixel(), *drawTarget)); + int32_t oldPageCount = mPageLength; + if (!mHasFixedRowCount) { + mPageLength = + (mRowHeight > 0) ? (mInnerBox.height / mRowHeight) : mRowCount; + } + + if (oldPageCount != mPageLength || + mHorzWidth != CalcHorzWidth(GetScrollParts())) { + // Schedule a ResizeReflow that will update our info properly. + PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors, + NS_FRAME_IS_DIRTY); + } +#ifdef DEBUG + int32_t rowCount = mRowCount; + mView->GetRowCount(&rowCount); + NS_WARNING_ASSERTION(mRowCount == rowCount, "row count changed unexpectedly"); +#endif + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + // Loop through our columns and paint them (e.g., for sorting). This is only + // relevant when painting backgrounds, since columns contain no content. + // Content is contained in the rows. + for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol; + currCol = currCol->GetNext()) { + nsRect colRect; + nsresult rv = + currCol->GetRect(this, mInnerBox.y, mInnerBox.height, &colRect); + // Don't paint hidden columns. + if (NS_FAILED(rv) || colRect.width == 0) continue; + + if (OffsetForHorzScroll(colRect, false)) { + nsRect dirtyRect; + colRect += aPt; + if (dirtyRect.IntersectRect(aDirtyRect, colRect)) { + result &= PaintColumn(currCol, colRect, PresContext(), + aRenderingContext, aDirtyRect); + } + } + } + // Loop through our on-screen rows. + for (int32_t i = mTopRowIndex; + i < mRowCount && i <= mTopRowIndex + mPageLength; i++) { + nsRect rowRect(mInnerBox.x, mInnerBox.y + mRowHeight * (i - mTopRowIndex), + mInnerBox.width, mRowHeight); + nsRect dirtyRect; + if (dirtyRect.IntersectRect(aDirtyRect, rowRect + aPt) && + rowRect.y < (mInnerBox.y + mInnerBox.height)) { + result &= PaintRow(i, rowRect + aPt, PresContext(), aRenderingContext, + aDirtyRect, aPt, aBuilder); + } + } + + if (mSlots && mSlots->mDropAllowed && + (mSlots->mDropOrient == nsITreeView::DROP_BEFORE || + mSlots->mDropOrient == nsITreeView::DROP_AFTER)) { + nscoord yPos = mInnerBox.y + + mRowHeight * (mSlots->mDropRow - mTopRowIndex) - + mRowHeight / 2; + nsRect feedbackRect(mInnerBox.x, yPos, mInnerBox.width, mRowHeight); + if (mSlots->mDropOrient == nsITreeView::DROP_AFTER) + feedbackRect.y += mRowHeight; + + nsRect dirtyRect; + feedbackRect += aPt; + if (dirtyRect.IntersectRect(aDirtyRect, feedbackRect)) { + result &= PaintDropFeedback(feedbackRect, PresContext(), + aRenderingContext, aDirtyRect, aPt); + } + } + aRenderingContext.Restore(); + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintColumn(nsTreeColumn* aColumn, + const nsRect& aColumnRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + // Now obtain the properties for our cell. + PrefillPropertyArray(-1, aColumn); + nsAutoString properties; + + nsCOMPtr view = GetExistingView(); + view->GetColumnProperties(aColumn, properties); + nsTreeUtils::TokenizeProperties(properties, mScratchArray); + + // Resolve style for the column. It contains all the info we need to lay + // ourselves out and to paint. + ComputedStyle* colContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeColumn()); + + // Obtain the margins for the cell and then deflate our rect by that + // amount. The cell is assumed to be contained within the deflated rect. + nsRect colRect(aColumnRect); + nsMargin colMargin; + colContext->StyleMargin()->GetMargin(colMargin); + colRect.Deflate(colMargin); + + return PaintBackgroundLayer(colContext, aPresContext, aRenderingContext, + colRect, aDirtyRect); +} + +ImgDrawResult nsTreeBodyFrame::PaintRow(int32_t aRowIndex, + const nsRect& aRowRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nsPoint aPt, + nsDisplayListBuilder* aBuilder) { + // We have been given a rect for our row. We treat this row like a full-blown + // frame, meaning that it can have borders, margins, padding, and a + // background. + + // Without a view, we have no data. Check for this up front. + nsCOMPtr view = GetExistingView(); + if (!view) { + return ImgDrawResult::SUCCESS; + } + + nsresult rv; + + // Now obtain the properties for our row. + // XXX Automatically fill in the following props: open, closed, container, + // leaf, selected, focused + PrefillPropertyArray(aRowIndex, nullptr); + + nsAutoString properties; + view->GetRowProperties(aRowIndex, properties); + nsTreeUtils::TokenizeProperties(properties, mScratchArray); + + // Resolve style for the row. It contains all the info we need to lay + // ourselves out and to paint. + ComputedStyle* rowContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow()); + + // Obtain the margins for the row and then deflate our rect by that + // amount. The row is assumed to be contained within the deflated rect. + nsRect rowRect(aRowRect); + nsMargin rowMargin; + rowContext->StyleMargin()->GetMargin(rowMargin); + rowRect.Deflate(rowMargin); + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + // Paint our borders and background for our row rect. + nsITheme* theme = nullptr; + auto appearance = rowContext->StyleDisplay()->EffectiveAppearance(); + if (appearance != StyleAppearance::None) { + theme = aPresContext->Theme(); + } + + if (theme && theme->ThemeSupportsWidget(aPresContext, nullptr, appearance)) { + nsRect dirty; + dirty.IntersectRect(rowRect, aDirtyRect); + theme->DrawWidgetBackground(&aRenderingContext, this, appearance, rowRect, + dirty); + } else { + result &= PaintBackgroundLayer(rowContext, aPresContext, aRenderingContext, + rowRect, aDirtyRect); + } + + // Adjust the rect for its border and padding. + nsRect originalRowRect = rowRect; + AdjustForBorderPadding(rowContext, rowRect); + + bool isSeparator = false; + view->IsSeparator(aRowIndex, &isSeparator); + if (isSeparator) { + // The row is a separator. + + nscoord primaryX = rowRect.x; + nsTreeColumn* primaryCol = mColumns->GetPrimaryColumn(); + if (primaryCol) { + // Paint the primary cell. + nsRect cellRect; + rv = primaryCol->GetRect(this, rowRect.y, rowRect.height, &cellRect); + if (NS_FAILED(rv)) { + MOZ_ASSERT_UNREACHABLE("primary column is invalid"); + return result; + } + + if (OffsetForHorzScroll(cellRect, false)) { + cellRect.x += aPt.x; + nsRect dirtyRect; + nsRect checkRect(cellRect.x, originalRowRect.y, cellRect.width, + originalRowRect.height); + if (dirtyRect.IntersectRect(aDirtyRect, checkRect)) { + result &= + PaintCell(aRowIndex, primaryCol, cellRect, aPresContext, + aRenderingContext, aDirtyRect, primaryX, aPt, aBuilder); + } + } + + // Paint the left side of the separator. + nscoord currX; + nsTreeColumn* previousCol = primaryCol->GetPrevious(); + if (previousCol) { + nsRect prevColRect; + rv = previousCol->GetRect(this, 0, 0, &prevColRect); + if (NS_SUCCEEDED(rv)) { + currX = (prevColRect.x - mHorzPosition) + prevColRect.width + aPt.x; + } else { + MOZ_ASSERT_UNREACHABLE( + "The column before the primary column is " + "invalid"); + currX = rowRect.x; + } + } else { + currX = rowRect.x; + } + + int32_t level; + view->GetLevel(aRowIndex, &level); + if (level == 0) currX += mIndentation; + + if (currX > rowRect.x) { + nsRect separatorRect(rowRect); + separatorRect.width -= rowRect.x + rowRect.width - currX; + result &= PaintSeparator(aRowIndex, separatorRect, aPresContext, + aRenderingContext, aDirtyRect); + } + } + + // Paint the right side (whole) separator. + nsRect separatorRect(rowRect); + if (primaryX > rowRect.x) { + separatorRect.width -= primaryX - rowRect.x; + separatorRect.x += primaryX - rowRect.x; + } + result &= PaintSeparator(aRowIndex, separatorRect, aPresContext, + aRenderingContext, aDirtyRect); + } else { + // Now loop over our cells. Only paint a cell if it intersects with our + // dirty rect. + for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol; + currCol = currCol->GetNext()) { + nsRect cellRect; + rv = currCol->GetRect(this, rowRect.y, rowRect.height, &cellRect); + // Don't paint cells in hidden columns. + if (NS_FAILED(rv) || cellRect.width == 0) continue; + + if (OffsetForHorzScroll(cellRect, false)) { + cellRect.x += aPt.x; + + // for primary columns, use the row's vertical size so that the + // lines get drawn properly + nsRect checkRect = cellRect; + if (currCol->IsPrimary()) + checkRect = nsRect(cellRect.x, originalRowRect.y, cellRect.width, + originalRowRect.height); + + nsRect dirtyRect; + nscoord dummy; + if (dirtyRect.IntersectRect(aDirtyRect, checkRect)) + result &= + PaintCell(aRowIndex, currCol, cellRect, aPresContext, + aRenderingContext, aDirtyRect, dummy, aPt, aBuilder); + } + } + } + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintSeparator(int32_t aRowIndex, + const nsRect& aSeparatorRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect) { + // Resolve style for the separator. + ComputedStyle* separatorContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeSeparator()); + bool useTheme = false; + nsITheme* theme = nullptr; + StyleAppearance appearance = + separatorContext->StyleDisplay()->EffectiveAppearance(); + if (appearance != StyleAppearance::None) { + theme = aPresContext->Theme(); + if (theme->ThemeSupportsWidget(aPresContext, nullptr, appearance)) + useTheme = true; + } + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + // use -moz-appearance if provided. + if (useTheme) { + nsRect dirty; + dirty.IntersectRect(aSeparatorRect, aDirtyRect); + theme->DrawWidgetBackground(&aRenderingContext, this, appearance, + aSeparatorRect, dirty); + } else { + const nsStylePosition* stylePosition = separatorContext->StylePosition(); + + // Obtain the height for the separator or use the default value. + nscoord height; + if (stylePosition->mHeight.ConvertsToLength()) { + height = stylePosition->mHeight.ToLength(); + } else { + // Use default height 2px. + height = nsPresContext::CSSPixelsToAppUnits(2); + } + + // Obtain the margins for the separator and then deflate our rect by that + // amount. The separator is assumed to be contained within the deflated + // rect. + nsRect separatorRect(aSeparatorRect.x, aSeparatorRect.y, + aSeparatorRect.width, height); + nsMargin separatorMargin; + separatorContext->StyleMargin()->GetMargin(separatorMargin); + separatorRect.Deflate(separatorMargin); + + // Center the separator. + separatorRect.y += (aSeparatorRect.height - height) / 2; + + result &= + PaintBackgroundLayer(separatorContext, aPresContext, aRenderingContext, + separatorRect, aDirtyRect); + } + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintCell( + int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aCellRect, + nsPresContext* aPresContext, gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aCurrX, nsPoint aPt, + nsDisplayListBuilder* aBuilder) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + // Now obtain the properties for our cell. + // XXX Automatically fill in the following props: open, closed, container, + // leaf, selected, focused, and the col ID. + PrefillPropertyArray(aRowIndex, aColumn); + nsAutoString properties; + nsCOMPtr view = GetExistingView(); + view->GetCellProperties(aRowIndex, aColumn, properties); + nsTreeUtils::TokenizeProperties(properties, mScratchArray); + + // Resolve style for the cell. It contains all the info we need to lay + // ourselves out and to paint. + ComputedStyle* cellContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); + + bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; + + // Obtain the margins for the cell and then deflate our rect by that + // amount. The cell is assumed to be contained within the deflated rect. + nsRect cellRect(aCellRect); + nsMargin cellMargin; + cellContext->StyleMargin()->GetMargin(cellMargin); + cellRect.Deflate(cellMargin); + + // Paint our borders and background for our row rect. + ImgDrawResult result = PaintBackgroundLayer( + cellContext, aPresContext, aRenderingContext, cellRect, aDirtyRect); + + // Adjust the rect for its border and padding. + AdjustForBorderPadding(cellContext, cellRect); + + nscoord currX = cellRect.x; + nscoord remainingWidth = cellRect.width; + + // Now we paint the contents of the cells. + // Directionality of the tree determines the order in which we paint. + // StyleDirection::Ltr means paint from left to right. + // StyleDirection::Rtl means paint from right to left. + + if (aColumn->IsPrimary()) { + // If we're the primary column, we need to indent and paint the twisty and + // any connecting lines between siblings. + + int32_t level; + view->GetLevel(aRowIndex, &level); + + if (!isRTL) currX += mIndentation * level; + remainingWidth -= mIndentation * level; + + // Resolve the style to use for the connecting lines. + ComputedStyle* lineContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeLine()); + + if (mIndentation && level && + lineContext->StyleVisibility()->IsVisibleOrCollapsed()) { + // Paint the thread lines. + + // Get the size of the twisty. We don't want to paint the twisty + // before painting of connecting lines since it would paint lines over + // the twisty. But we need to leave a place for it. + ComputedStyle* twistyContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); + + nsRect imageSize; + nsRect twistyRect(aCellRect); + GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect, aPresContext, + twistyContext); + + nsMargin twistyMargin; + twistyContext->StyleMargin()->GetMargin(twistyMargin); + twistyRect.Inflate(twistyMargin); + + const nsStyleBorder* borderStyle = lineContext->StyleBorder(); + // Resolve currentcolor values against the treeline context + nscolor color = borderStyle->mBorderLeftColor.CalcColor(*lineContext); + ColorPattern colorPatt(ToDeviceColor(color)); + + StyleBorderStyle style = borderStyle->GetBorderStyle(eSideLeft); + StrokeOptions strokeOptions; + nsLayoutUtils::InitDashPattern(strokeOptions, style); + + nscoord srcX = currX + twistyRect.width - mIndentation / 2; + nscoord lineY = (aRowIndex - mTopRowIndex) * mRowHeight + aPt.y; + + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + nsPresContext* pc = PresContext(); + + // Don't paint off our cell. + if (srcX <= cellRect.x + cellRect.width) { + nscoord destX = currX + twistyRect.width; + if (destX > cellRect.x + cellRect.width) + destX = cellRect.x + cellRect.width; + if (isRTL) { + srcX = currX + remainingWidth - (srcX - cellRect.x); + destX = currX + remainingWidth - (destX - cellRect.x); + } + Point p1(pc->AppUnitsToGfxUnits(srcX), + pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2)); + Point p2(pc->AppUnitsToGfxUnits(destX), + pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2)); + SnapLineToDevicePixelsForStroking(p1, p2, *drawTarget, + strokeOptions.mLineWidth); + drawTarget->StrokeLine(p1, p2, colorPatt, strokeOptions); + } + + int32_t currentParent = aRowIndex; + for (int32_t i = level; i > 0; i--) { + if (srcX <= cellRect.x + cellRect.width) { + // Paint full vertical line only if we have next sibling. + bool hasNextSibling; + view->HasNextSibling(currentParent, aRowIndex, &hasNextSibling); + if (hasNextSibling || i == level) { + Point p1(pc->AppUnitsToGfxUnits(srcX), + pc->AppUnitsToGfxUnits(lineY)); + Point p2; + p2.x = pc->AppUnitsToGfxUnits(srcX); + + if (hasNextSibling) + p2.y = pc->AppUnitsToGfxUnits(lineY + mRowHeight); + else if (i == level) + p2.y = pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2); + + SnapLineToDevicePixelsForStroking(p1, p2, *drawTarget, + strokeOptions.mLineWidth); + drawTarget->StrokeLine(p1, p2, colorPatt, strokeOptions); + } + } + + int32_t parent; + if (NS_FAILED(view->GetParentIndex(currentParent, &parent)) || + parent < 0) + break; + currentParent = parent; + srcX -= mIndentation; + } + } + + // Always leave space for the twisty. + nsRect twistyRect(currX, cellRect.y, remainingWidth, cellRect.height); + result &= PaintTwisty(aRowIndex, aColumn, twistyRect, aPresContext, + aRenderingContext, aDirtyRect, remainingWidth, currX); + } + + // Now paint the icon for our cell. + nsRect iconRect(currX, cellRect.y, remainingWidth, cellRect.height); + nsRect dirtyRect; + if (dirtyRect.IntersectRect(aDirtyRect, iconRect)) { + result &= PaintImage(aRowIndex, aColumn, iconRect, aPresContext, + aRenderingContext, aDirtyRect, remainingWidth, currX, + aBuilder); + } + + // Now paint our element, but only if we aren't a cycler column. + // XXX until we have the ability to load images, allow the view to + // insert text into cycler columns... + if (!aColumn->IsCycler()) { + nsRect elementRect(currX, cellRect.y, remainingWidth, cellRect.height); + nsRect dirtyRect; + if (dirtyRect.IntersectRect(aDirtyRect, elementRect)) { + switch (aColumn->GetType()) { + case TreeColumn_Binding::TYPE_TEXT: + result &= PaintText(aRowIndex, aColumn, elementRect, aPresContext, + aRenderingContext, aDirtyRect, currX); + break; + case TreeColumn_Binding::TYPE_CHECKBOX: + result &= PaintCheckbox(aRowIndex, aColumn, elementRect, aPresContext, + aRenderingContext, aDirtyRect); + break; + } + } + } + + aCurrX = currX; + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintTwisty( + int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aTwistyRect, + nsPresContext* aPresContext, gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aRemainingWidth, nscoord& aCurrX) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; + nscoord rightEdge = aCurrX + aRemainingWidth; + // Paint the twisty, but only if we are a non-empty container. + bool shouldPaint = false; + bool isContainer = false; + nsCOMPtr view = GetExistingView(); + view->IsContainer(aRowIndex, &isContainer); + if (isContainer) { + bool isContainerEmpty = false; + view->IsContainerEmpty(aRowIndex, &isContainerEmpty); + if (!isContainerEmpty) shouldPaint = true; + } + + // Resolve style for the twisty. + ComputedStyle* twistyContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); + + // Obtain the margins for the twisty and then deflate our rect by that + // amount. The twisty is assumed to be contained within the deflated rect. + nsRect twistyRect(aTwistyRect); + nsMargin twistyMargin; + twistyContext->StyleMargin()->GetMargin(twistyMargin); + twistyRect.Deflate(twistyMargin); + + nsRect imageSize; + nsITheme* theme = GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect, + aPresContext, twistyContext); + + // Subtract out the remaining width. This is done even when we don't actually + // paint a twisty in this cell, so that cells in different rows still line up. + nsRect copyRect(twistyRect); + copyRect.Inflate(twistyMargin); + aRemainingWidth -= copyRect.width; + if (!isRTL) aCurrX += copyRect.width; + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + if (shouldPaint) { + // Paint our borders and background for our image rect. + result &= PaintBackgroundLayer(twistyContext, aPresContext, + aRenderingContext, twistyRect, aDirtyRect); + + if (theme) { + if (isRTL) twistyRect.x = rightEdge - twistyRect.width; + // yeah, I know it says we're drawing a background, but a twisty is really + // a fg object since it doesn't have anything that gecko would want to + // draw over it. Besides, we have to prevent imagelib from drawing it. + nsRect dirty; + dirty.IntersectRect(twistyRect, aDirtyRect); + theme->DrawWidgetBackground( + &aRenderingContext, this, + twistyContext->StyleDisplay()->EffectiveAppearance(), twistyRect, + dirty); + } else { + // Time to paint the twisty. + // Adjust the rect for its border and padding. + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(twistyContext, bp); + twistyRect.Deflate(bp); + if (isRTL) twistyRect.x = rightEdge - twistyRect.width; + imageSize.Deflate(bp); + + // Get the image for drawing. + nsCOMPtr image; + GetImage(aRowIndex, aColumn, true, twistyContext, getter_AddRefs(image)); + if (image) { + nsPoint anchorPoint = twistyRect.TopLeft(); + + // Center the image. XXX Obey vertical-align style prop? + if (imageSize.height < twistyRect.height) { + anchorPoint.y += (twistyRect.height - imageSize.height) / 2; + } + + // Apply context paint if applicable + SVGImageContext svgContext; + SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext, + *twistyContext, image); + + // Paint the image. + result &= nsLayoutUtils::DrawSingleUnscaledImage( + aRenderingContext, aPresContext, image, SamplingFilter::POINT, + anchorPoint, &aDirtyRect, svgContext, imgIContainer::FLAG_NONE, + &imageSize); + } + } + } + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintImage( + int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aImageRect, + nsPresContext* aPresContext, gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aRemainingWidth, nscoord& aCurrX, + nsDisplayListBuilder* aBuilder) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; + nscoord rightEdge = aCurrX + aRemainingWidth; + // Resolve style for the image. + ComputedStyle* imageContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage()); + + // Obtain the margins for the image and then deflate our rect by that + // amount. The image is assumed to be contained within the deflated rect. + nsRect imageRect(aImageRect); + nsMargin imageMargin; + imageContext->StyleMargin()->GetMargin(imageMargin); + imageRect.Deflate(imageMargin); + + // Get the image. + nsCOMPtr image; + GetImage(aRowIndex, aColumn, false, imageContext, getter_AddRefs(image)); + + // Get the image destination size. + nsSize imageDestSize = GetImageDestSize(imageContext, image); + if (!imageDestSize.width || !imageDestSize.height) { + return ImgDrawResult::SUCCESS; + } + + // Get the borders and padding. + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(imageContext, bp); + + // destRect will be passed as the aDestRect argument in the DrawImage method. + // Start with the imageDestSize width and height. + nsRect destRect(0, 0, imageDestSize.width, imageDestSize.height); + // Inflate destRect for borders and padding so that we can compare/adjust + // with respect to imageRect. + destRect.Inflate(bp); + + // The destRect width and height have not been adjusted to fit within the + // cell width and height. + // We must adjust the width even if image is null, because the width is used + // to update the aRemainingWidth and aCurrX values. + // Since the height isn't used unless the image is not null, we will adjust + // the height inside the if (image) block below. + + if (destRect.width > imageRect.width) { + // The destRect is too wide to fit within the cell width. + // Adjust destRect width to fit within the cell width. + destRect.width = imageRect.width; + } else { + // The cell is wider than the destRect. + // In a cycler column, the image is centered horizontally. + if (!aColumn->IsCycler()) { + // If this column is not a cycler, we won't center the image horizontally. + // We adjust the imageRect width so that the image is placed at the start + // of the cell. + imageRect.width = destRect.width; + } + } + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + if (image) { + if (isRTL) imageRect.x = rightEdge - imageRect.width; + // Paint our borders and background for our image rect + result &= PaintBackgroundLayer(imageContext, aPresContext, + aRenderingContext, imageRect, aDirtyRect); + + // The destRect x and y have not been set yet. Let's do that now. + // Initially, we use the imageRect x and y. + destRect.x = imageRect.x; + destRect.y = imageRect.y; + + if (destRect.width < imageRect.width) { + // The destRect width is smaller than the cell width. + // Center the image horizontally in the cell. + // Adjust the destRect x accordingly. + destRect.x += (imageRect.width - destRect.width) / 2; + } + + // Now it's time to adjust the destRect height to fit within the cell + // height. + if (destRect.height > imageRect.height) { + // The destRect height is larger than the cell height. + // Adjust destRect height to fit within the cell height. + destRect.height = imageRect.height; + } else if (destRect.height < imageRect.height) { + // The destRect height is smaller than the cell height. + // Center the image vertically in the cell. + // Adjust the destRect y accordingly. + destRect.y += (imageRect.height - destRect.height) / 2; + } + + // It's almost time to paint the image. + // Deflate destRect for the border and padding. + destRect.Deflate(bp); + + // Compute the area where our whole image would be mapped, to get the + // desired subregion onto our actual destRect: + nsRect wholeImageDest; + CSSIntSize rawImageCSSIntSize; + if (NS_SUCCEEDED(image->GetWidth(&rawImageCSSIntSize.width)) && + NS_SUCCEEDED(image->GetHeight(&rawImageCSSIntSize.height))) { + // Get the image source rectangle - the rectangle containing the part of + // the image that we are going to display. sourceRect will be passed as + // the aSrcRect argument in the DrawImage method. + nsRect sourceRect = GetImageSourceRect(imageContext, image); + + // Let's say that the image is 100 pixels tall and that the CSS has + // specified that the destination height should be 50 pixels tall. Let's + // say that the cell height is only 20 pixels. So, in those 20 visible + // pixels, we want to see the top 20/50ths of the image. So, the + // sourceRect.height should be 100 * 20 / 50, which is 40 pixels. + // Essentially, we are scaling the image as dictated by the CSS + // destination height and width, and we are then clipping the scaled + // image by the cell width and height. + nsSize rawImageSize(CSSPixel::ToAppUnits(rawImageCSSIntSize)); + wholeImageDest = nsLayoutUtils::GetWholeImageDestination( + rawImageSize, sourceRect, nsRect(destRect.TopLeft(), imageDestSize)); + } else { + // GetWidth/GetHeight failed, so we can't easily map a subregion of the + // source image onto the destination area. + // * If this happens with a RasterImage, it probably means the image is + // in an error state, and we shouldn't draw anything. Hence, we leave + // wholeImageDest as an empty rect (its initial state). + // * If this happens with a VectorImage, it probably means the image has + // no explicit width or height attribute -- but we can still proceed and + // just treat the destination area as our whole SVG image area. Hence, we + // set wholeImageDest to the full destRect. + if (image->GetType() == imgIContainer::TYPE_VECTOR) { + wholeImageDest = destRect; + } + } + + const auto* styleEffects = imageContext->StyleEffects(); + gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&aRenderingContext); + if (!styleEffects->IsOpaque()) { + autoGroupForBlend.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, + styleEffects->mOpacity); + } + + uint32_t drawFlags = aBuilder && aBuilder->UseHighQualityScaling() + ? imgIContainer::FLAG_HIGH_QUALITY_SCALING + : imgIContainer::FLAG_NONE; + result &= nsLayoutUtils::DrawImage( + aRenderingContext, imageContext, aPresContext, image, + nsLayoutUtils::GetSamplingFilterForFrame(this), wholeImageDest, + destRect, destRect.TopLeft(), aDirtyRect, drawFlags); + } + + // Update the aRemainingWidth and aCurrX values. + imageRect.Inflate(imageMargin); + aRemainingWidth -= imageRect.width; + if (!isRTL) { + aCurrX += imageRect.width; + } + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintText( + int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aTextRect, + nsPresContext* aPresContext, gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aCurrX) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; + + // Now obtain the text for our cell. + nsAutoString text; + nsCOMPtr view = GetExistingView(); + view->GetCellText(aRowIndex, aColumn, text); + + // We're going to paint this text so we need to ensure bidi is enabled if + // necessary + CheckTextForBidi(text); + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + if (text.Length() == 0) { + // Don't paint an empty string. XXX What about background/borders? Still + // paint? + return result; + } + + int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel(); + DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); + + // Resolve style for the text. It contains all the info we need to lay + // ourselves out and to paint. + ComputedStyle* textContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText()); + + // Obtain the margins for the text and then deflate our rect by that + // amount. The text is assumed to be contained within the deflated rect. + nsRect textRect(aTextRect); + nsMargin textMargin; + textContext->StyleMargin()->GetMargin(textMargin); + textRect.Deflate(textMargin); + + // Adjust the rect for its border and padding. + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(textContext, bp); + textRect.Deflate(bp); + + // Compute our text size. + RefPtr fontMet = + nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, PresContext()); + + nscoord height = fontMet->MaxHeight(); + nscoord baseline = fontMet->MaxAscent(); + + // Center the text. XXX Obey vertical-align style prop? + if (height < textRect.height) { + textRect.y += (textRect.height - height) / 2; + textRect.height = height; + } + + // Set our font. + AdjustForCellText(text, aRowIndex, aColumn, aRenderingContext, *fontMet, + textRect); + textRect.Inflate(bp); + + // Subtract out the remaining width. + if (!isRTL) aCurrX += textRect.width + textMargin.LeftRight(); + + result &= PaintBackgroundLayer(textContext, aPresContext, aRenderingContext, + textRect, aDirtyRect); + + // Time to paint our text. + textRect.Deflate(bp); + + // Set our color. + ColorPattern color(ToDeviceColor(textContext->StyleText()->mColor)); + + // Draw decorations. + StyleTextDecorationLine decorations = + textContext->StyleTextReset()->mTextDecorationLine; + + nscoord offset; + nscoord size; + if (decorations & (StyleTextDecorationLine::OVERLINE | + StyleTextDecorationLine::UNDERLINE)) { + fontMet->GetUnderline(offset, size); + if (decorations & StyleTextDecorationLine::OVERLINE) { + nsRect r(textRect.x, textRect.y, textRect.width, size); + Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget); + drawTarget->FillRect(devPxRect, color); + } + if (decorations & StyleTextDecorationLine::UNDERLINE) { + nsRect r(textRect.x, textRect.y + baseline - offset, textRect.width, + size); + Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget); + drawTarget->FillRect(devPxRect, color); + } + } + if (decorations & StyleTextDecorationLine::LINE_THROUGH) { + fontMet->GetStrikeout(offset, size); + nsRect r(textRect.x, textRect.y + baseline - offset, textRect.width, size); + Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget); + drawTarget->FillRect(devPxRect, color); + } + ComputedStyle* cellContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); + + const auto* styleEffects = textContext->StyleEffects(); + gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&aRenderingContext); + if (!styleEffects->IsOpaque()) { + autoGroupForBlend.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, + styleEffects->mOpacity); + } + + aRenderingContext.SetColor( + sRGBColor::FromABGR(textContext->StyleText()->mColor.ToColor())); + nsLayoutUtils::DrawString( + this, *fontMet, &aRenderingContext, text.get(), text.Length(), + textRect.TopLeft() + nsPoint(0, baseline), cellContext); + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintCheckbox(int32_t aRowIndex, + nsTreeColumn* aColumn, + const nsRect& aCheckboxRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect) { + MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); + + // Resolve style for the checkbox. + ComputedStyle* checkboxContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCheckbox()); + + nscoord rightEdge = aCheckboxRect.XMost(); + + // Obtain the margins for the checkbox and then deflate our rect by that + // amount. The checkbox is assumed to be contained within the deflated rect. + nsRect checkboxRect(aCheckboxRect); + nsMargin checkboxMargin; + checkboxContext->StyleMargin()->GetMargin(checkboxMargin); + checkboxRect.Deflate(checkboxMargin); + + nsRect imageSize = GetImageSize(aRowIndex, aColumn, true, checkboxContext); + + if (imageSize.height > checkboxRect.height) { + imageSize.height = checkboxRect.height; + } + if (imageSize.width > checkboxRect.width) { + imageSize.width = checkboxRect.width; + } + + if (StyleVisibility()->mDirection == StyleDirection::Rtl) { + checkboxRect.x = rightEdge - checkboxRect.width; + } + + // Paint our borders and background for our image rect. + ImgDrawResult result = + PaintBackgroundLayer(checkboxContext, aPresContext, aRenderingContext, + checkboxRect, aDirtyRect); + + // Time to paint the checkbox. + // Adjust the rect for its border and padding. + nsMargin bp(0, 0, 0, 0); + GetBorderPadding(checkboxContext, bp); + checkboxRect.Deflate(bp); + + // Get the image for drawing. + nsCOMPtr image; + GetImage(aRowIndex, aColumn, true, checkboxContext, getter_AddRefs(image)); + if (image) { + nsPoint pt = checkboxRect.TopLeft(); + + if (imageSize.height < checkboxRect.height) { + pt.y += (checkboxRect.height - imageSize.height) / 2; + } + + if (imageSize.width < checkboxRect.width) { + pt.x += (checkboxRect.width - imageSize.width) / 2; + } + + // Apply context paint if applicable + SVGImageContext svgContext; + SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext, + *checkboxContext, image); + // Paint the image. + result &= nsLayoutUtils::DrawSingleUnscaledImage( + aRenderingContext, aPresContext, image, SamplingFilter::POINT, pt, + &aDirtyRect, svgContext, imgIContainer::FLAG_NONE, &imageSize); + } + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintDropFeedback( + const nsRect& aDropFeedbackRect, nsPresContext* aPresContext, + gfxContext& aRenderingContext, const nsRect& aDirtyRect, nsPoint aPt) { + // Paint the drop feedback in between rows. + + nscoord currX; + + // Adjust for the primary cell. + nsTreeColumn* primaryCol = mColumns->GetPrimaryColumn(); + + if (primaryCol) { +#ifdef DEBUG + nsresult rv = +#endif + primaryCol->GetXInTwips(this, &currX); + NS_ASSERTION(NS_SUCCEEDED(rv), "primary column is invalid?"); + + currX += aPt.x - mHorzPosition; + } else { + currX = aDropFeedbackRect.x; + } + + PrefillPropertyArray(mSlots->mDropRow, primaryCol); + + // Resolve the style to use for the drop feedback. + ComputedStyle* feedbackContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeDropFeedback()); + + ImgDrawResult result = ImgDrawResult::SUCCESS; + + // Paint only if it is visible. + nsCOMPtr view = GetExistingView(); + if (feedbackContext->StyleVisibility()->IsVisibleOrCollapsed()) { + int32_t level; + view->GetLevel(mSlots->mDropRow, &level); + + // If our previous or next row has greater level use that for + // correct visual indentation. + if (mSlots->mDropOrient == nsITreeView::DROP_BEFORE) { + if (mSlots->mDropRow > 0) { + int32_t previousLevel; + view->GetLevel(mSlots->mDropRow - 1, &previousLevel); + if (previousLevel > level) level = previousLevel; + } + } else { + if (mSlots->mDropRow < mRowCount - 1) { + int32_t nextLevel; + view->GetLevel(mSlots->mDropRow + 1, &nextLevel); + if (nextLevel > level) level = nextLevel; + } + } + + currX += mIndentation * level; + + if (primaryCol) { + ComputedStyle* twistyContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); + nsRect imageSize; + nsRect twistyRect; + GetTwistyRect(mSlots->mDropRow, primaryCol, imageSize, twistyRect, + aPresContext, twistyContext); + nsMargin twistyMargin; + twistyContext->StyleMargin()->GetMargin(twistyMargin); + twistyRect.Inflate(twistyMargin); + currX += twistyRect.width; + } + + const nsStylePosition* stylePosition = feedbackContext->StylePosition(); + + // Obtain the width for the drop feedback or use default value. + nscoord width; + if (stylePosition->mWidth.ConvertsToLength()) { + width = stylePosition->mWidth.ToLength(); + } else { + // Use default width 50px. + width = nsPresContext::CSSPixelsToAppUnits(50); + } + + // Obtain the height for the drop feedback or use default value. + nscoord height; + if (stylePosition->mHeight.ConvertsToLength()) { + height = stylePosition->mHeight.ToLength(); + } else { + // Use default height 2px. + height = nsPresContext::CSSPixelsToAppUnits(2); + } + + // Obtain the margins for the drop feedback and then deflate our rect + // by that amount. + nsRect feedbackRect(currX, aDropFeedbackRect.y, width, height); + nsMargin margin; + feedbackContext->StyleMargin()->GetMargin(margin); + feedbackRect.Deflate(margin); + + feedbackRect.y += (aDropFeedbackRect.height - height) / 2; + + // Finally paint the drop feedback. + result &= PaintBackgroundLayer(feedbackContext, aPresContext, + aRenderingContext, feedbackRect, aDirtyRect); + } + + return result; +} + +ImgDrawResult nsTreeBodyFrame::PaintBackgroundLayer( + ComputedStyle* aComputedStyle, nsPresContext* aPresContext, + gfxContext& aRenderingContext, const nsRect& aRect, + const nsRect& aDirtyRect) { + const nsStyleBorder* myBorder = aComputedStyle->StyleBorder(); + nsCSSRendering::PaintBGParams params = + nsCSSRendering::PaintBGParams::ForAllLayers( + *aPresContext, aDirtyRect, aRect, this, + nsCSSRendering::PAINTBG_SYNC_DECODE_IMAGES); + ImgDrawResult result = nsCSSRendering::PaintStyleImageLayerWithSC( + params, aRenderingContext, aComputedStyle, *myBorder); + + result &= nsCSSRendering::PaintBorderWithStyleBorder( + aPresContext, aRenderingContext, this, aDirtyRect, aRect, *myBorder, + mComputedStyle, PaintBorderFlags::SyncDecodeImages); + + nsCSSRendering::PaintNonThemedOutline(aPresContext, aRenderingContext, this, + aDirtyRect, aRect, aComputedStyle); + + return result; +} + +// Scrolling +nsresult nsTreeBodyFrame::EnsureRowIsVisible(int32_t aRow) { + ScrollParts parts = GetScrollParts(); + nsresult rv = EnsureRowIsVisibleInternal(parts, aRow); + NS_ENSURE_SUCCESS(rv, rv); + UpdateScrollbars(parts); + return rv; +} + +nsresult nsTreeBodyFrame::EnsureRowIsVisibleInternal(const ScrollParts& aParts, + int32_t aRow) { + if (!mView || !mPageLength) { + return NS_OK; + } + + if (mTopRowIndex <= aRow && mTopRowIndex + mPageLength > aRow) return NS_OK; + + if (aRow < mTopRowIndex) + ScrollToRowInternal(aParts, aRow); + else { + // Bring it just on-screen. + int32_t distance = aRow - (mTopRowIndex + mPageLength) + 1; + ScrollToRowInternal(aParts, mTopRowIndex + distance); + } + + return NS_OK; +} + +nsresult nsTreeBodyFrame::EnsureCellIsVisible(int32_t aRow, + nsTreeColumn* aCol) { + if (!aCol) return NS_ERROR_INVALID_ARG; + + ScrollParts parts = GetScrollParts(); + + nscoord result = -1; + nsresult rv; + + nscoord columnPos; + rv = aCol->GetXInTwips(this, &columnPos); + if (NS_FAILED(rv)) return rv; + + nscoord columnWidth; + rv = aCol->GetWidthInTwips(this, &columnWidth); + if (NS_FAILED(rv)) return rv; + + // If the start of the column is before the + // start of the horizontal view, then scroll + if (columnPos < mHorzPosition) result = columnPos; + // If the end of the column is past the end of + // the horizontal view, then scroll + else if ((columnPos + columnWidth) > (mHorzPosition + mInnerBox.width)) + result = ((columnPos + columnWidth) - (mHorzPosition + mInnerBox.width)) + + mHorzPosition; + + if (result != -1) { + rv = ScrollHorzInternal(parts, result); + if (NS_FAILED(rv)) return rv; + } + + rv = EnsureRowIsVisibleInternal(parts, aRow); + NS_ENSURE_SUCCESS(rv, rv); + UpdateScrollbars(parts); + return rv; +} + +void nsTreeBodyFrame::ScrollToRow(int32_t aRow) { + ScrollParts parts = GetScrollParts(); + ScrollToRowInternal(parts, aRow); + UpdateScrollbars(parts); +} + +nsresult nsTreeBodyFrame::ScrollToRowInternal(const ScrollParts& aParts, + int32_t aRow) { + ScrollInternal(aParts, aRow); + + return NS_OK; +} + +void nsTreeBodyFrame::ScrollByLines(int32_t aNumLines) { + if (!mView) { + return; + } + int32_t newIndex = mTopRowIndex + aNumLines; + ScrollToRow(newIndex); +} + +void nsTreeBodyFrame::ScrollByPages(int32_t aNumPages) { + if (!mView) { + return; + } + int32_t newIndex = mTopRowIndex + aNumPages * mPageLength; + ScrollToRow(newIndex); +} + +nsresult nsTreeBodyFrame::ScrollInternal(const ScrollParts& aParts, + int32_t aRow) { + if (!mView) { + return NS_OK; + } + + // Note that we may be "over scrolled" at this point; that is the + // current mTopRowIndex may be larger than mRowCount - mPageLength. + // This can happen when items are removed for example. (bug 1085050) + + int32_t maxTopRowIndex = std::max(0, mRowCount - mPageLength); + aRow = mozilla::clamped(aRow, 0, maxTopRowIndex); + if (aRow == mTopRowIndex) { + return NS_OK; + } + mTopRowIndex = aRow; + Invalidate(); + PostScrollEvent(); + return NS_OK; +} + +nsresult nsTreeBodyFrame::ScrollHorzInternal(const ScrollParts& aParts, + int32_t aPosition) { + if (!mView || !aParts.mColumnsScrollFrame || !aParts.mHScrollbar) + return NS_OK; + + if (aPosition == mHorzPosition) return NS_OK; + + if (aPosition < 0 || aPosition > mHorzWidth) return NS_OK; + + nsRect bounds = aParts.mColumnsFrame->GetRect(); + if (aPosition > (mHorzWidth - bounds.width)) + aPosition = mHorzWidth - bounds.width; + + mHorzPosition = aPosition; + + Invalidate(); + + // Update the column scroll view + AutoWeakFrame weakFrame(this); + aParts.mColumnsScrollFrame->ScrollTo(nsPoint(mHorzPosition, 0), + ScrollMode::Instant); + if (!weakFrame.IsAlive()) { + return NS_ERROR_FAILURE; + } + // And fire off an event about it all + PostScrollEvent(); + return NS_OK; +} + +void nsTreeBodyFrame::ScrollByPage(nsScrollbarFrame* aScrollbar, + int32_t aDirection, + ScrollSnapFlags aSnapFlags) { + // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored + MOZ_ASSERT(aScrollbar != nullptr); + ScrollByPages(aDirection); +} + +void nsTreeBodyFrame::ScrollByWhole(nsScrollbarFrame* aScrollbar, + int32_t aDirection, + ScrollSnapFlags aSnapFlags) { + // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored + MOZ_ASSERT(aScrollbar != nullptr); + int32_t newIndex = aDirection < 0 ? 0 : mTopRowIndex; + ScrollToRow(newIndex); +} + +void nsTreeBodyFrame::ScrollByLine(nsScrollbarFrame* aScrollbar, + int32_t aDirection, + ScrollSnapFlags aSnapFlags) { + // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored + MOZ_ASSERT(aScrollbar != nullptr); + ScrollByLines(aDirection); +} + +void nsTreeBodyFrame::ScrollByUnit( + nsScrollbarFrame* aScrollbar, ScrollMode aMode, int32_t aDirection, + ScrollUnit aUnit, ScrollSnapFlags aSnapFlags /* = Disabled */) { + MOZ_ASSERT_UNREACHABLE("Can't get here, we pass false to MoveToNewPosition"); +} + +void nsTreeBodyFrame::RepeatButtonScroll(nsScrollbarFrame* aScrollbar) { + ScrollParts parts = GetScrollParts(); + int32_t increment = aScrollbar->GetIncrement(); + int32_t direction = 0; + if (increment < 0) { + direction = -1; + } else if (increment > 0) { + direction = 1; + } + bool isHorizontal = aScrollbar->IsHorizontal(); + + AutoWeakFrame weakFrame(this); + if (isHorizontal) { + int32_t curpos = aScrollbar->MoveToNewPosition( + nsScrollbarFrame::ImplementsScrollByUnit::No); + if (weakFrame.IsAlive()) { + ScrollHorzInternal(parts, curpos); + } + } else { + ScrollToRowInternal(parts, mTopRowIndex + direction); + } + + if (weakFrame.IsAlive() && mScrollbarActivity) { + mScrollbarActivity->ActivityOccurred(); + } + if (weakFrame.IsAlive()) { + UpdateScrollbars(parts); + } +} + +void nsTreeBodyFrame::ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos, + nscoord aNewPos) { + ScrollParts parts = GetScrollParts(); + + if (aOldPos == aNewPos) return; + + AutoWeakFrame weakFrame(this); + + // Vertical Scrollbar + if (parts.mVScrollbar == aScrollbar) { + nscoord rh = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); + nscoord newIndex = nsPresContext::AppUnitsToIntCSSPixels(aNewPos); + nscoord newrow = (rh > 0) ? (newIndex / rh) : 0; + ScrollInternal(parts, newrow); + // Horizontal Scrollbar + } else if (parts.mHScrollbar == aScrollbar) { + int32_t newIndex = nsPresContext::AppUnitsToIntCSSPixels(aNewPos); + ScrollHorzInternal(parts, newIndex); + } + if (weakFrame.IsAlive()) { + UpdateScrollbars(parts); + } +} + +// The style cache. +ComputedStyle* nsTreeBodyFrame::GetPseudoComputedStyle( + nsCSSAnonBoxPseudoStaticAtom* aPseudoElement) { + return mStyleCache.GetComputedStyle(PresContext(), mContent, mComputedStyle, + aPseudoElement, mScratchArray); +} + +XULTreeElement* nsTreeBodyFrame::GetBaseElement() { + if (!mTree) { + nsIFrame* parent = GetParent(); + while (parent) { + nsIContent* content = parent->GetContent(); + if (content && content->IsXULElement(nsGkAtoms::tree)) { + mTree = XULTreeElement::FromNodeOrNull(content->AsElement()); + break; + } + + parent = parent->GetInFlowParent(); + } + } + + return mTree; +} + +nsresult nsTreeBodyFrame::ClearStyleAndImageCaches() { + mStyleCache.Clear(); + CancelImageRequests(); + mImageCache.Clear(); + return NS_OK; +} + +void nsTreeBodyFrame::RemoveImageCacheEntry(int32_t aRowIndex, + nsTreeColumn* aCol) { + nsAutoString imageSrc; + nsCOMPtr view = GetExistingView(); + if (NS_FAILED(view->GetImageSrc(aRowIndex, aCol, imageSrc))) { + return; + } + nsTreeImageCacheEntry entry; + if (!mImageCache.Get(imageSrc, &entry)) { + return; + } + nsLayoutUtils::DeregisterImageRequest(PresContext(), entry.request, nullptr); + entry.request->UnlockImage(); + entry.request->CancelAndForgetObserver(NS_BINDING_ABORTED); + mImageCache.Remove(imageSrc); +} + +/* virtual */ +void nsTreeBodyFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) { + SimpleXULLeafFrame::DidSetComputedStyle(aOldComputedStyle); + + // Clear the style cache; the pointers are no longer even valid + mStyleCache.Clear(); + // XXX The following is hacky, but it's not incorrect, + // and appears to fix a few bugs with style changes, like text zoom and + // dpi changes + mIndentation = GetIndentation(); + mRowHeight = GetRowHeight(); +} + +bool nsTreeBodyFrame::OffsetForHorzScroll(nsRect& rect, bool clip) { + rect.x -= mHorzPosition; + + // Scrolled out before + if (rect.XMost() <= mInnerBox.x) return false; + + // Scrolled out after + if (rect.x > mInnerBox.XMost()) return false; + + if (clip) { + nscoord leftEdge = std::max(rect.x, mInnerBox.x); + nscoord rightEdge = std::min(rect.XMost(), mInnerBox.XMost()); + rect.x = leftEdge; + rect.width = rightEdge - leftEdge; + + // Should have returned false above + NS_ASSERTION(rect.width >= 0, "horz scroll code out of sync"); + } + + return true; +} + +bool nsTreeBodyFrame::CanAutoScroll(int32_t aRowIndex) { + // Check first for partially visible last row. + if (aRowIndex == mRowCount - 1) { + nscoord y = mInnerBox.y + (aRowIndex - mTopRowIndex) * mRowHeight; + if (y < mInnerBox.height && y + mRowHeight > mInnerBox.height) return true; + } + + if (aRowIndex > 0 && aRowIndex < mRowCount - 1) return true; + + return false; +} + +// Given a dom event, figure out which row in the tree the mouse is over, +// if we should drop before/after/on that row or we should auto-scroll. +// Doesn't query the content about if the drag is allowable, that's done +// elsewhere. +// +// For containers, we break up the vertical space of the row as follows: if in +// the topmost 25%, the drop is _before_ the row the mouse is over; if in the +// last 25%, _after_; in the middle 50%, we consider it a drop _on_ the +// container. +// +// For non-containers, if the mouse is in the top 50% of the row, the drop is +// _before_ and the bottom 50% _after_ +void nsTreeBodyFrame::ComputeDropPosition(WidgetGUIEvent* aEvent, int32_t* aRow, + int16_t* aOrient, + int16_t* aScrollLines) { + *aOrient = -1; + *aScrollLines = 0; + + // Convert the event's point to our coordinates. We want it in + // the coordinates of our inner box's coordinates. + nsPoint pt = + nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, RelativeTo{this}); + int32_t xTwips = pt.x - mInnerBox.x; + int32_t yTwips = pt.y - mInnerBox.y; + + nsCOMPtr view = GetExistingView(); + *aRow = GetRowAtInternal(xTwips, yTwips); + if (*aRow >= 0) { + // Compute the top/bottom of the row in question. + int32_t yOffset = yTwips - mRowHeight * (*aRow - mTopRowIndex); + + bool isContainer = false; + view->IsContainer(*aRow, &isContainer); + if (isContainer) { + // for a container, use a 25%/50%/25% breakdown + if (yOffset < mRowHeight / 4) + *aOrient = nsITreeView::DROP_BEFORE; + else if (yOffset > mRowHeight - (mRowHeight / 4)) + *aOrient = nsITreeView::DROP_AFTER; + else + *aOrient = nsITreeView::DROP_ON; + } else { + // for a non-container use a 50%/50% breakdown + if (yOffset < mRowHeight / 2) + *aOrient = nsITreeView::DROP_BEFORE; + else + *aOrient = nsITreeView::DROP_AFTER; + } + } + + if (CanAutoScroll(*aRow)) { + // Get the max value from the look and feel service. + int32_t scrollLinesMax = + LookAndFeel::GetInt(LookAndFeel::IntID::TreeScrollLinesMax, 0); + scrollLinesMax--; + if (scrollLinesMax < 0) scrollLinesMax = 0; + + // Determine if we're w/in a margin of the top/bottom of the tree during a + // drag. This will ultimately cause us to scroll, but that's done elsewhere. + nscoord height = (3 * mRowHeight) / 4; + if (yTwips < height) { + // scroll up + *aScrollLines = + NSToIntRound(-scrollLinesMax * (1 - (float)yTwips / height) - 1); + } else if (yTwips > mRect.height - height) { + // scroll down + *aScrollLines = NSToIntRound( + scrollLinesMax * (1 - (float)(mRect.height - yTwips) / height) + 1); + } + } +} // ComputeDropPosition + +void nsTreeBodyFrame::OpenCallback(nsITimer* aTimer, void* aClosure) { + auto* self = static_cast(aClosure); + if (!self) { + return; + } + + aTimer->Cancel(); + self->mSlots->mTimer = nullptr; + + nsCOMPtr view = self->GetExistingView(); + if (self->mSlots->mDropRow >= 0) { + self->mSlots->mArray.AppendElement(self->mSlots->mDropRow); + view->ToggleOpenState(self->mSlots->mDropRow); + } +} + +void nsTreeBodyFrame::CloseCallback(nsITimer* aTimer, void* aClosure) { + auto* self = static_cast(aClosure); + if (!self) { + return; + } + + aTimer->Cancel(); + self->mSlots->mTimer = nullptr; + + nsCOMPtr view = self->GetExistingView(); + auto array = std::move(self->mSlots->mArray); + if (!view) { + return; + } + for (auto elem : Reversed(array)) { + view->ToggleOpenState(elem); + } +} + +void nsTreeBodyFrame::LazyScrollCallback(nsITimer* aTimer, void* aClosure) { + nsTreeBodyFrame* self = static_cast(aClosure); + if (self) { + aTimer->Cancel(); + self->mSlots->mTimer = nullptr; + + if (self->mView) { + // Set a new timer to scroll the tree repeatedly. + self->CreateTimer(LookAndFeel::IntID::TreeScrollDelay, ScrollCallback, + nsITimer::TYPE_REPEATING_SLACK, + getter_AddRefs(self->mSlots->mTimer), + "nsTreeBodyFrame::ScrollCallback"); + self->ScrollByLines(self->mSlots->mScrollLines); + // ScrollByLines may have deleted |self|. + } + } +} + +void nsTreeBodyFrame::ScrollCallback(nsITimer* aTimer, void* aClosure) { + nsTreeBodyFrame* self = static_cast(aClosure); + if (self) { + // Don't scroll if we are already at the top or bottom of the view. + if (self->mView && self->CanAutoScroll(self->mSlots->mDropRow)) { + self->ScrollByLines(self->mSlots->mScrollLines); + } else { + aTimer->Cancel(); + self->mSlots->mTimer = nullptr; + } + } +} + +// TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsTreeBodyFrame::ScrollEvent::Run() { + if (mInner) { + mInner->FireScrollEvent(); + } + return NS_OK; +} + +void nsTreeBodyFrame::FireScrollEvent() { + mScrollEvent.Forget(); + WidgetGUIEvent event(true, eScroll, nullptr); + // scroll events fired at elements don't bubble + event.mFlags.mBubbles = false; + RefPtr content = GetContent(); + RefPtr presContext = PresContext(); + EventDispatcher::Dispatch(content, presContext, &event); +} + +void nsTreeBodyFrame::PostScrollEvent() { + if (mScrollEvent.IsPending()) return; + + RefPtr event = new ScrollEvent(this); + nsresult rv = + mContent->OwnerDoc()->Dispatch(TaskCategory::Other, do_AddRef(event)); + if (NS_FAILED(rv)) { + NS_WARNING("failed to dispatch ScrollEvent"); + } else { + mScrollEvent = std::move(event); + } +} + +void nsTreeBodyFrame::ScrollbarActivityStarted() const { + if (mScrollbarActivity) { + mScrollbarActivity->ActivityStarted(); + } +} + +void nsTreeBodyFrame::ScrollbarActivityStopped() const { + if (mScrollbarActivity) { + mScrollbarActivity->ActivityStopped(); + } +} + +void nsTreeBodyFrame::DetachImageListeners() { mCreatedListeners.Clear(); } + +void nsTreeBodyFrame::RemoveTreeImageListener(nsTreeImageListener* aListener) { + if (aListener) { + mCreatedListeners.Remove(aListener); + } +} + +#ifdef ACCESSIBILITY +static void InitCustomEvent(CustomEvent* aEvent, const nsAString& aType, + nsIWritablePropertyBag2* aDetail) { + AutoJSAPI jsapi; + if (!jsapi.Init(aEvent->GetParentObject())) { + return; + } + + JSContext* cx = jsapi.cx(); + JS::Rooted detail(cx); + if (!ToJSValue(cx, aDetail, &detail)) { + jsapi.ClearException(); + return; + } + + aEvent->InitCustomEvent(cx, aType, /* aCanBubble = */ true, + /* aCancelable = */ false, detail); +} + +void nsTreeBodyFrame::FireRowCountChangedEvent(int32_t aIndex, int32_t aCount) { + RefPtr tree(GetBaseElement()); + if (!tree) return; + + RefPtr doc = tree->OwnerDoc(); + MOZ_ASSERT(doc); + + RefPtr event = + doc->CreateEvent(u"customevent"_ns, CallerType::System, IgnoreErrors()); + + CustomEvent* treeEvent = event->AsCustomEvent(); + if (!treeEvent) { + return; + } + + nsCOMPtr propBag( + do_CreateInstance("@mozilla.org/hash-property-bag;1")); + if (!propBag) { + return; + } + + // Set 'index' data - the row index rows are changed from. + propBag->SetPropertyAsInt32(u"index"_ns, aIndex); + + // Set 'count' data - the number of changed rows. + propBag->SetPropertyAsInt32(u"count"_ns, aCount); + + InitCustomEvent(treeEvent, u"TreeRowCountChanged"_ns, propBag); + + event->SetTrusted(true); + + RefPtr asyncDispatcher = + new AsyncEventDispatcher(tree, event); + asyncDispatcher->PostDOMEvent(); +} + +void nsTreeBodyFrame::FireInvalidateEvent(int32_t aStartRowIdx, + int32_t aEndRowIdx, + nsTreeColumn* aStartCol, + nsTreeColumn* aEndCol) { + RefPtr tree(GetBaseElement()); + if (!tree) return; + + RefPtr doc = tree->OwnerDoc(); + + RefPtr event = + doc->CreateEvent(u"customevent"_ns, CallerType::System, IgnoreErrors()); + + CustomEvent* treeEvent = event->AsCustomEvent(); + if (!treeEvent) { + return; + } + + nsCOMPtr propBag( + do_CreateInstance("@mozilla.org/hash-property-bag;1")); + if (!propBag) { + return; + } + + if (aStartRowIdx != -1 && aEndRowIdx != -1) { + // Set 'startrow' data - the start index of invalidated rows. + propBag->SetPropertyAsInt32(u"startrow"_ns, aStartRowIdx); + + // Set 'endrow' data - the end index of invalidated rows. + propBag->SetPropertyAsInt32(u"endrow"_ns, aEndRowIdx); + } + + if (aStartCol && aEndCol) { + // Set 'startcolumn' data - the start index of invalidated rows. + int32_t startColIdx = aStartCol->GetIndex(); + + propBag->SetPropertyAsInt32(u"startcolumn"_ns, startColIdx); + + // Set 'endcolumn' data - the start index of invalidated rows. + int32_t endColIdx = aEndCol->GetIndex(); + propBag->SetPropertyAsInt32(u"endcolumn"_ns, endColIdx); + } + + InitCustomEvent(treeEvent, u"TreeInvalidated"_ns, propBag); + + event->SetTrusted(true); + + RefPtr asyncDispatcher = + new AsyncEventDispatcher(tree, event); + asyncDispatcher->PostDOMEvent(); +} +#endif + +class nsOverflowChecker : public Runnable { + public: + explicit nsOverflowChecker(nsTreeBodyFrame* aFrame) + : mozilla::Runnable("nsOverflowChecker"), mFrame(aFrame) {} + NS_IMETHOD Run() override { + if (mFrame.IsAlive()) { + nsTreeBodyFrame* tree = static_cast(mFrame.GetFrame()); + nsTreeBodyFrame::ScrollParts parts = tree->GetScrollParts(); + tree->CheckOverflow(parts); + } + return NS_OK; + } + + private: + WeakFrame mFrame; +}; + +bool nsTreeBodyFrame::FullScrollbarsUpdate(bool aNeedsFullInvalidation) { + ScrollParts parts = GetScrollParts(); + AutoWeakFrame weakFrame(this); + AutoWeakFrame weakColumnsFrame(parts.mColumnsFrame); + UpdateScrollbars(parts); + NS_ENSURE_TRUE(weakFrame.IsAlive(), false); + if (aNeedsFullInvalidation) { + Invalidate(); + } + InvalidateScrollbars(parts, weakColumnsFrame); + NS_ENSURE_TRUE(weakFrame.IsAlive(), false); + + // Overflow checking dispatches synchronous events, which can cause infinite + // recursion during reflow. Do the first overflow check synchronously, but + // force any nested checks to round-trip through the event loop. See bug + // 905909. + RefPtr checker = new nsOverflowChecker(this); + if (!mCheckingOverflow) { + nsContentUtils::AddScriptRunner(checker); + } else { + mContent->OwnerDoc()->Dispatch(TaskCategory::Other, checker.forget()); + } + return weakFrame.IsAlive(); +} + +void nsTreeBodyFrame::OnImageIsAnimated(imgIRequest* aRequest) { + nsLayoutUtils::RegisterImageRequest(PresContext(), aRequest, nullptr); +} diff --git a/layout/xul/tree/nsTreeBodyFrame.h b/layout/xul/tree/nsTreeBodyFrame.h new file mode 100644 index 0000000000..35fa7eb1f0 --- /dev/null +++ b/layout/xul/tree/nsTreeBodyFrame.h @@ -0,0 +1,607 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeBodyFrame_h +#define nsTreeBodyFrame_h + +#include "mozilla/AtomArray.h" +#include "mozilla/Attributes.h" + +#include "nsITreeView.h" +#include "nsIScrollbarMediator.h" +#include "nsITimer.h" +#include "nsIReflowCallback.h" +#include "nsTArray.h" +#include "nsTreeStyleCache.h" +#include "nsTreeColumns.h" +#include "nsTHashMap.h" +#include "nsTHashSet.h" +#include "imgIRequest.h" +#include "imgINotificationObserver.h" +#include "nsScrollbarFrame.h" +#include "nsThreadUtils.h" +#include "SimpleXULLeafFrame.h" +#include "mozilla/LookAndFeel.h" + +class nsFontMetrics; +class nsOverflowChecker; +class nsTreeImageListener; + +namespace mozilla { +class PresShell; +namespace layout { +class ScrollbarActivity; +} // namespace layout +} // namespace mozilla + +// An entry in the tree's image cache +struct nsTreeImageCacheEntry { + nsTreeImageCacheEntry() = default; + nsTreeImageCacheEntry(imgIRequest* aRequest, + imgINotificationObserver* aListener) + : request(aRequest), listener(aListener) {} + + nsCOMPtr request; + nsCOMPtr listener; +}; + +// The actual frame that paints the cells and rows. +class nsTreeBodyFrame final : public mozilla::SimpleXULLeafFrame, + public nsIScrollbarMediator, + public nsIReflowCallback { + typedef mozilla::layout::ScrollbarActivity ScrollbarActivity; + typedef mozilla::image::ImgDrawResult ImgDrawResult; + + public: + explicit nsTreeBodyFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + ~nsTreeBodyFrame(); + + nscoord GetIntrinsicBSize() override; + + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(nsTreeBodyFrame) + + // Callback handler methods for refresh driver based animations. + // Calls to these functions are forwarded from nsTreeImageListener. These + // mirror how nsImageFrame works. + void OnImageIsAnimated(imgIRequest* aRequest); + + // non-virtual signatures like nsITreeBodyFrame + already_AddRefed Columns() const { + RefPtr cols = mColumns; + return cols.forget(); + } + already_AddRefed GetExistingView() const { + nsCOMPtr view = mView; + return view.forget(); + } + already_AddRefed GetSelection() const; + nsresult GetView(nsITreeView** aView); + nsresult SetView(nsITreeView* aView); + bool GetFocused() const { return mFocused; } + nsresult SetFocused(bool aFocused); + nsresult GetTreeBody(mozilla::dom::Element** aElement); + int32_t RowHeight() const; + int32_t RowWidth(); + int32_t GetHorizontalPosition() const; + mozilla::Maybe GetSelectionRegion(); + int32_t FirstVisibleRow() const { return mTopRowIndex; } + int32_t LastVisibleRow() const { return mTopRowIndex + mPageLength; } + int32_t PageLength() const { return mPageLength; } + nsresult EnsureRowIsVisible(int32_t aRow); + nsresult EnsureCellIsVisible(int32_t aRow, nsTreeColumn* aCol); + void ScrollToRow(int32_t aRow); + void ScrollByLines(int32_t aNumLines); + void ScrollByPages(int32_t aNumPages); + nsresult Invalidate(); + nsresult InvalidateColumn(nsTreeColumn* aCol); + nsresult InvalidateRow(int32_t aRow); + nsresult InvalidateCell(int32_t aRow, nsTreeColumn* aCol); + nsresult InvalidateRange(int32_t aStart, int32_t aEnd); + int32_t GetRowAt(int32_t aX, int32_t aY); + nsresult GetCellAt(int32_t aX, int32_t aY, int32_t* aRow, nsTreeColumn** aCol, + nsACString& aChildElt); + nsresult GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol, + const nsACString& aElt, int32_t* aX, + int32_t* aY, int32_t* aWidth, int32_t* aHeight); + nsresult IsCellCropped(int32_t aRow, nsTreeColumn* aCol, bool* aResult); + nsresult RowCountChanged(int32_t aIndex, int32_t aCount); + nsresult BeginUpdateBatch(); + nsresult EndUpdateBatch(); + nsresult ClearStyleAndImageCaches(); + void RemoveImageCacheEntry(int32_t aRowIndex, nsTreeColumn* aCol); + + void CancelImageRequests(); + + void ManageReflowCallback(); + + void DidReflow(nsPresContext*, const ReflowInput*) override; + + // nsIReflowCallback + bool ReflowFinished() override; + void ReflowCallbackCanceled() override; + + // nsIScrollbarMediator + void ScrollByPage(nsScrollbarFrame* aScrollbar, int32_t aDirection, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) override; + void ScrollByWhole(nsScrollbarFrame* aScrollbar, int32_t aDirection, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) override; + void ScrollByLine(nsScrollbarFrame* aScrollbar, int32_t aDirection, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) override; + void ScrollByUnit(nsScrollbarFrame* aScrollbar, mozilla::ScrollMode aMode, + int32_t aDirection, mozilla::ScrollUnit aUnit, + mozilla::ScrollSnapFlags aSnapFlags = + mozilla::ScrollSnapFlags::Disabled) override; + void RepeatButtonScroll(nsScrollbarFrame* aScrollbar) override; + void ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos, + nscoord aNewPos) override; + void ScrollbarReleased(nsScrollbarFrame* aScrollbar) override {} + void VisibilityChanged(bool aVisible) override { Invalidate(); } + nsScrollbarFrame* GetScrollbarBox(bool aVertical) override { + ScrollParts parts = GetScrollParts(); + return aVertical ? parts.mVScrollbar : parts.mHScrollbar; + } + void ScrollbarActivityStarted() const override; + void ScrollbarActivityStopped() const override; + bool IsScrollbarOnRight() const override { + return StyleVisibility()->mDirection == mozilla::StyleDirection::Ltr; + } + bool ShouldSuppressScrollbarRepaints() const override { return false; } + + // Overridden from nsIFrame to cache our pres context. + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + void Destroy(DestroyContext&) override; + + mozilla::Maybe GetCursor(const nsPoint&) override; + + nsresult HandleEvent(nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent, + nsEventStatus* aEventStatus) override; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + void DidSetComputedStyle(ComputedStyle* aOldComputedStyle) override; + + friend nsIFrame* NS_NewTreeBodyFrame(mozilla::PresShell* aPresShell); + friend class nsTreeColumn; + + struct ScrollParts { + nsScrollbarFrame* mVScrollbar; + RefPtr mVScrollbarContent; + nsScrollbarFrame* mHScrollbar; + RefPtr mHScrollbarContent; + nsIFrame* mColumnsFrame; + nsIScrollableFrame* mColumnsScrollFrame; + }; + + ImgDrawResult PaintTreeBody(gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nsPoint aPt, + nsDisplayListBuilder* aBuilder); + + // Get the base element, + mozilla::dom::XULTreeElement* GetBaseElement(); + + bool GetVerticalOverflow() const { return mVerticalOverflow; } + bool GetHorizontalOverflow() const { return mHorizontalOverflow; } + + // This returns the property array where atoms are stored for style during + // draw, whether the row currently being drawn is selected, hovered, etc. + const mozilla::AtomArray& GetPropertyArrayForCurrentDrawingItem() { + return mScratchArray; + } + + protected: + friend class nsOverflowChecker; + + // This method paints a specific column background of the tree. + ImgDrawResult PaintColumn(nsTreeColumn* aColumn, const nsRect& aColumnRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect); + + // This method paints a single row in the tree. + ImgDrawResult PaintRow(int32_t aRowIndex, const nsRect& aRowRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nsPoint aPt, + nsDisplayListBuilder* aBuilder); + + // This method paints a single separator in the tree. + ImgDrawResult PaintSeparator(int32_t aRowIndex, const nsRect& aSeparatorRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect); + + // This method paints a specific cell in a given row of the tree. + ImgDrawResult PaintCell(int32_t aRowIndex, nsTreeColumn* aColumn, + const nsRect& aCellRect, nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aCurrX, + nsPoint aPt, nsDisplayListBuilder* aBuilder); + + // This method paints the twisty inside a cell in the primary column of an + // tree. + ImgDrawResult PaintTwisty(int32_t aRowIndex, nsTreeColumn* aColumn, + const nsRect& aTwistyRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aRemainingWidth, + nscoord& aCurrX); + + // This method paints the image inside the cell of an tree. + ImgDrawResult PaintImage(int32_t aRowIndex, nsTreeColumn* aColumn, + const nsRect& aImageRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aRemainingWidth, + nscoord& aCurrX, nsDisplayListBuilder* aBuilder); + + // This method paints the text string inside a particular cell of the tree. + ImgDrawResult PaintText(int32_t aRowIndex, nsTreeColumn* aColumn, + const nsRect& aTextRect, nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nscoord& aCurrX); + + // This method paints the checkbox inside a particular cell of the tree. + ImgDrawResult PaintCheckbox(int32_t aRowIndex, nsTreeColumn* aColumn, + const nsRect& aCheckboxRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect); + + // This method paints a drop feedback of the tree. + ImgDrawResult PaintDropFeedback(const nsRect& aDropFeedbackRect, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aDirtyRect, nsPoint aPt); + + // This method is called with a specific ComputedStyle and rect to + // paint the background rect as if it were a full-blown frame. + ImgDrawResult PaintBackgroundLayer(ComputedStyle* aComputedStyle, + nsPresContext* aPresContext, + gfxContext& aRenderingContext, + const nsRect& aRect, + const nsRect& aDirtyRect); + + // An internal hit test. aX and aY are expected to be in twips in the + // coordinate system of this frame. + int32_t GetRowAtInternal(nscoord aX, nscoord aY); + + // Check for bidi characters in the text, and if there are any, ensure + // that the prescontext is in bidi mode. + void CheckTextForBidi(nsAutoString& aText); + + void AdjustForCellText(nsAutoString& aText, int32_t aRowIndex, + nsTreeColumn* aColumn, gfxContext& aRenderingContext, + nsFontMetrics& aFontMetrics, nsRect& aTextRect); + + // A helper used when hit testing. + nsCSSAnonBoxPseudoStaticAtom* GetItemWithinCellAt(nscoord aX, + const nsRect& aCellRect, + int32_t aRowIndex, + nsTreeColumn* aColumn); + + // An internal hit test. aX and aY are expected to be in twips in the + // coordinate system of this frame. + void GetCellAt(nscoord aX, nscoord aY, int32_t* aRow, nsTreeColumn** aCol, + nsCSSAnonBoxPseudoStaticAtom** aChildElt); + + // Retrieve the area for the twisty for a cell. + nsITheme* GetTwistyRect(int32_t aRowIndex, nsTreeColumn* aColumn, + nsRect& aImageRect, nsRect& aTwistyRect, + nsPresContext* aPresContext, + ComputedStyle* aTwistyContext); + + // Fetch an image from the image cache. + nsresult GetImage(int32_t aRowIndex, nsTreeColumn* aCol, bool aUseContext, + ComputedStyle* aComputedStyle, imgIContainer** aResult); + + // Returns the size of a given image. This size *includes* border and + // padding. It does not include margins. + nsRect GetImageSize(int32_t aRowIndex, nsTreeColumn* aCol, bool aUseContext, + ComputedStyle* aComputedStyle); + + // Returns the destination size of the image, not including borders and + // padding. + nsSize GetImageDestSize(ComputedStyle*, imgIContainer*); + + // Returns the source rectangle of the image to be displayed. + nsRect GetImageSourceRect(ComputedStyle*, imgIContainer*); + + // Returns the height of rows in the tree. + int32_t GetRowHeight(); + + // Returns our indentation width. + int32_t GetIndentation(); + + // Calculates our width/height once border and padding have been removed. + void CalcInnerBox(); + + // Calculate the total width of our scrollable portion + nscoord CalcHorzWidth(const ScrollParts& aParts); + + // Looks up a ComputedStyle in the style cache. On a cache miss we resolve + // the pseudo-styles passed in and place them into the cache. + ComputedStyle* GetPseudoComputedStyle( + nsCSSAnonBoxPseudoStaticAtom* aPseudoElement); + + // Retrieves the scrollbars and scrollview relevant to this treebody. We + // traverse the frame tree under our base element, in frame order, looking + // for the first relevant vertical scrollbar, horizontal scrollbar, and + // scrollable frame (with associated content and scrollable view). These + // are all volatile and should not be retained. + ScrollParts GetScrollParts(); + + // Update the curpos of the scrollbar. + void UpdateScrollbars(const ScrollParts& aParts); + + // Update the maxpos of the scrollbar. + void InvalidateScrollbars(const ScrollParts& aParts, + AutoWeakFrame& aWeakColumnsFrame); + + // Check overflow and generate events. + MOZ_CAN_RUN_SCRIPT_BOUNDARY void CheckOverflow(const ScrollParts& aParts); + + // Calls UpdateScrollbars, Invalidate aNeedsFullInvalidation if true, + // InvalidateScrollbars and finally CheckOverflow. + // returns true if the frame is still alive after the method call. + bool FullScrollbarsUpdate(bool aNeedsFullInvalidation); + + // Use to auto-fill some of the common properties without the view having to + // do it. Examples include container, open, selected, and focus. + void PrefillPropertyArray(int32_t aRowIndex, nsTreeColumn* aCol); + + // Our internal scroll method, used by all the public scroll methods. + nsresult ScrollInternal(const ScrollParts& aParts, int32_t aRow); + nsresult ScrollToRowInternal(const ScrollParts& aParts, int32_t aRow); + nsresult ScrollHorzInternal(const ScrollParts& aParts, int32_t aPosition); + nsresult EnsureRowIsVisibleInternal(const ScrollParts& aParts, int32_t aRow); + + // Convert client pixels into appunits in our coordinate space. + nsPoint AdjustClientCoordsToBoxCoordSpace(int32_t aX, int32_t aY); + + void EnsureView(); + + nsresult GetCellWidth(int32_t aRow, nsTreeColumn* aCol, + gfxContext* aRenderingContext, nscoord& aDesiredSize, + nscoord& aCurrentSize); + + // Translate the given rect horizontally from tree coordinates into the + // coordinate system of our nsTreeBodyFrame. If clip is true, then clip the + // rect to its intersection with mInnerBox in the horizontal direction. + // Return whether the result has a nonempty intersection with mInnerBox + // after projecting both onto the horizontal coordinate axis. + bool OffsetForHorzScroll(nsRect& rect, bool clip); + + bool CanAutoScroll(int32_t aRowIndex); + + // Calc the row and above/below/on status given where the mouse currently is + // hovering. Also calc if we're in the region in which we want to auto-scroll + // the tree. A positive value of |aScrollLines| means scroll down, a negative + // value means scroll up, a zero value means that we aren't in drag scroll + // region. + void ComputeDropPosition(mozilla::WidgetGUIEvent* aEvent, int32_t* aRow, + int16_t* aOrient, int16_t* aScrollLines); + + void InvalidateDropFeedback(int32_t aRow, int16_t aOrientation) { + InvalidateRow(aRow); + if (aOrientation != nsITreeView::DROP_ON) + InvalidateRow(aRow + aOrientation); + } + + public: + /** + * Remove an nsITreeImageListener from being tracked by this frame. Only tree + * image listeners that are created by this frame are tracked. + * + * @param aListener A pointer to an nsTreeImageListener to no longer + * track. + */ + void RemoveTreeImageListener(nsTreeImageListener* aListener); + + protected: + // Create a new timer. This method is used to delay various actions like + // opening/closing folders or tree scrolling. + // aID is type of the action, aFunc is the function to be called when + // the timer fires and aType is type of timer - one shot or repeating. + nsresult CreateTimer(const mozilla::LookAndFeel::IntID aID, + nsTimerCallbackFunc aFunc, int32_t aType, + nsITimer** aTimer, const char* aName); + + static void OpenCallback(nsITimer* aTimer, void* aClosure); + + static void CloseCallback(nsITimer* aTimer, void* aClosure); + + static void LazyScrollCallback(nsITimer* aTimer, void* aClosure); + + static void ScrollCallback(nsITimer* aTimer, void* aClosure); + + class ScrollEvent : public mozilla::Runnable { + public: + NS_DECL_NSIRUNNABLE + explicit ScrollEvent(nsTreeBodyFrame* aInner) + : mozilla::Runnable("nsTreeBodyFrame::ScrollEvent"), mInner(aInner) {} + void Revoke() { mInner = nullptr; } + + private: + nsTreeBodyFrame* mInner; + }; + + void PostScrollEvent(); + MOZ_CAN_RUN_SCRIPT void FireScrollEvent(); + + /** + * Clear the pointer to this frame for all nsTreeImageListeners that were + * created by this frame. + */ + void DetachImageListeners(); + +#ifdef ACCESSIBILITY + /** + * Fires 'treeRowCountChanged' event asynchronously. The event is a + * CustomEvent that is used to expose the following information structures + * via a property bag. + * + * @param aIndex the row index rows are added/removed from + * @param aCount the number of added/removed rows (the sign points to + * an operation, plus - addition, minus - removing) + */ + void FireRowCountChangedEvent(int32_t aIndex, int32_t aCount); + + /** + * Fires 'treeInvalidated' event asynchronously. The event is a CustomEvent + * that is used to expose the information structures described by method + * arguments via a property bag. + * + * @param aStartRow the start index of invalidated rows, -1 means that + * columns have been invalidated only + * @param aEndRow the end index of invalidated rows, -1 means that columns + * have been invalidated only + * @param aStartCol the start invalidated column, nullptr means that only + * rows have been invalidated + * @param aEndCol the end invalidated column, nullptr means that rows have + * been invalidated only + */ + void FireInvalidateEvent(int32_t aStartRow, int32_t aEndRow, + nsTreeColumn* aStartCol, nsTreeColumn* aEndCol); +#endif + + protected: // Data Members + class Slots { + public: + Slots() = default; + + ~Slots() { + if (mTimer) { + mTimer->Cancel(); + } + } + + friend class nsTreeBodyFrame; + + protected: + // If the drop is actually allowed here or not. + bool mDropAllowed = false; + + // True while dragging over the tree. + bool mIsDragging = false; + + // The row the mouse is hovering over during a drop. + int32_t mDropRow = -1; + + // Where we want to draw feedback (above/on this row/below) if allowed. + int16_t mDropOrient = -1; + + // Number of lines to be scrolled. + int16_t mScrollLines = 0; + + // The drag action that was received for this slot + uint32_t mDragAction = 0; + + // Timer for opening/closing spring loaded folders or scrolling the tree. + nsCOMPtr mTimer; + + // An array used to keep track of all spring loaded folders. + nsTArray mArray; + }; + + mozilla::UniquePtr mSlots; + + nsRevocableEventPtr mScrollEvent; + + RefPtr mScrollbarActivity; + + // The element containing this treebody. + RefPtr mTree; + + // Cached column information. + RefPtr mColumns; + + // The current view for this tree widget. We get all of our row and cell data + // from the view. + nsCOMPtr mView; + + // A cache of all the ComputedStyles we have seen for rows and cells of the + // tree. This is a mapping from a list of atoms to a corresponding + // ComputedStyle. This cache stores every combination that occurs in the + // tree, so for n distinct properties, this cache could have 2 to the n + // entries (the power set of all row properties). + nsTreeStyleCache mStyleCache; + + // A hashtable that maps from URLs to image request/listener pairs. The URL + // is provided by the view or by the ComputedStyle. The ComputedStyle + // represents a resolved :-moz-tree-cell-image (or twisty) pseudo-element. + // It maps directly to an imgIRequest. + nsTHashMap mImageCache; + + // A scratch array used when looking up cached ComputedStyles. + mozilla::AtomArray mScratchArray; + + // The index of the first visible row and the # of rows visible onscreen. + // The tree only examines onscreen rows, starting from + // this index and going up to index+pageLength. + int32_t mTopRowIndex; + int32_t mPageLength; + + // The horizontal scroll position + nscoord mHorzPosition; + + // The original desired horizontal width before changing it and posting a + // reflow callback. In some cases, the desired horizontal width can first be + // different from the current desired horizontal width, only to return to + // the same value later during the same reflow. In this case, we can cancel + // the posted reflow callback and prevent an unnecessary reflow. + nscoord mOriginalHorzWidth; + // Our desired horizontal width (the width for which we actually have tree + // columns). + nscoord mHorzWidth; + // The amount by which to adjust the width of the last cell. + // This depends on whether or not the columnpicker and scrollbars are present. + nscoord mAdjustWidth; + + // Our last reflowed rect, used for invalidation, see ManageReflowCallback(). + Maybe mLastReflowRect; + + // Cached heights and indent info. + nsRect mInnerBox; // 4-byte aligned + int32_t mRowHeight; + int32_t mIndentation; + + int32_t mUpdateBatchNest; + + // Cached row count. + int32_t mRowCount; + + // The row the mouse is hovering over. + int32_t mMouseOverRow; + + // Whether or not we're currently focused. + bool mFocused; + + // Do we have a fixed number of onscreen rows? + bool mHasFixedRowCount; + + bool mVerticalOverflow; + bool mHorizontalOverflow; + + bool mReflowCallbackPosted; + + // Set while we flush layout to take account of effects of + // overflow/underflow event handlers + bool mCheckingOverflow; + + // Hash set to keep track of which listeners we created and thus + // have pointers to us. + nsTHashSet mCreatedListeners; + +}; // class nsTreeBodyFrame + +#endif diff --git a/layout/xul/tree/nsTreeColumns.cpp b/layout/xul/tree/nsTreeColumns.cpp new file mode 100644 index 0000000000..62a8299c6f --- /dev/null +++ b/layout/xul/tree/nsTreeColumns.cpp @@ -0,0 +1,462 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsTreeColumns.h" +#include "nsTreeUtils.h" +#include "mozilla/ComputedStyle.h" +#include "nsContentUtils.h" +#include "nsTreeBodyFrame.h" +#include "mozilla/dom/Element.h" +#include "mozilla/CSSOrderAwareFrameIterator.h" +#include "mozilla/dom/TreeColumnBinding.h" +#include "mozilla/dom/TreeColumnsBinding.h" +#include "mozilla/dom/XULTreeElement.h" + +using namespace mozilla; +using namespace mozilla::dom; + +// Column class that caches all the info about our column. +nsTreeColumn::nsTreeColumn(nsTreeColumns* aColumns, dom::Element* aElement) + : mContent(aElement), mColumns(aColumns), mIndex(0), mPrevious(nullptr) { + NS_ASSERTION(aElement && aElement->NodeInfo()->Equals(nsGkAtoms::treecol, + kNameSpaceID_XUL), + "nsTreeColumn's content must be a "); + + Invalidate(IgnoreErrors()); +} + +nsTreeColumn::~nsTreeColumn() { + if (mNext) { + mNext->SetPrevious(nullptr); + } +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsTreeColumn) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsTreeColumn) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_UNLINK(mContent) + if (tmp->mNext) { + tmp->mNext->SetPrevious(nullptr); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNext) + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsTreeColumn) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNext) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeColumn) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeColumn) + +// QueryInterface implementation for nsTreeColumn +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeColumn) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY_CONCRETE(nsTreeColumn) +NS_INTERFACE_MAP_END + +nsIFrame* nsTreeColumn::GetFrame() { return mContent->GetPrimaryFrame(); } + +bool nsTreeColumn::IsLastVisible(nsTreeBodyFrame* aBodyFrame) { + NS_ASSERTION(GetFrame(), "should have checked for this already"); + + // cyclers are fixed width, don't adjust them + if (IsCycler()) return false; + + // we're certainly not the last visible if we're not visible + if (GetFrame()->GetRect().width == 0) return false; + + // try to find a visible successor + for (nsTreeColumn* next = GetNext(); next; next = next->GetNext()) { + nsIFrame* frame = next->GetFrame(); + if (frame && frame->GetRect().width > 0) return false; + } + return true; +} + +nsresult nsTreeColumn::GetRect(nsTreeBodyFrame* aBodyFrame, nscoord aY, + nscoord aHeight, nsRect* aResult) { + nsIFrame* frame = GetFrame(); + if (!frame) { + *aResult = nsRect(); + return NS_ERROR_FAILURE; + } + + const bool isRTL = + aBodyFrame->StyleVisibility()->mDirection == StyleDirection::Rtl; + *aResult = frame->GetRect(); + if (frame->StyleVisibility()->IsCollapse()) { + aResult->SizeTo(nsSize()); + } + aResult->y = aY; + aResult->height = aHeight; + if (isRTL) + aResult->x += aBodyFrame->mAdjustWidth; + else if (IsLastVisible(aBodyFrame)) + aResult->width += aBodyFrame->mAdjustWidth; + return NS_OK; +} + +nsresult nsTreeColumn::GetXInTwips(nsTreeBodyFrame* aBodyFrame, + nscoord* aResult) { + nsIFrame* frame = GetFrame(); + if (!frame) { + *aResult = 0; + return NS_ERROR_FAILURE; + } + *aResult = frame->GetRect().x; + return NS_OK; +} + +nsresult nsTreeColumn::GetWidthInTwips(nsTreeBodyFrame* aBodyFrame, + nscoord* aResult) { + nsIFrame* frame = GetFrame(); + if (!frame) { + *aResult = 0; + return NS_ERROR_FAILURE; + } + *aResult = frame->GetRect().width; + if (IsLastVisible(aBodyFrame)) *aResult += aBodyFrame->mAdjustWidth; + return NS_OK; +} + +void nsTreeColumn::GetId(nsAString& aId) const { aId = GetId(); } + +void nsTreeColumn::Invalidate(ErrorResult& aRv) { + nsIFrame* frame = GetFrame(); + if (NS_WARN_IF(!frame)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // Fetch the Id. + mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::id, mId); + + // If we have an Id, cache the Id as an atom. + if (!mId.IsEmpty()) { + mAtom = NS_Atomize(mId); + } + + // Cache our index. + nsTreeUtils::GetColumnIndex(mContent, &mIndex); + + const nsStyleVisibility* vis = frame->StyleVisibility(); + + // Cache our text alignment policy. + const nsStyleText* textStyle = frame->StyleText(); + + mTextAlignment = textStyle->mTextAlign; + // START or END alignment sometimes means RIGHT + if ((mTextAlignment == StyleTextAlign::Start && + vis->mDirection == StyleDirection::Rtl) || + (mTextAlignment == StyleTextAlign::End && + vis->mDirection == StyleDirection::Ltr)) { + mTextAlignment = StyleTextAlign::Right; + } else if (mTextAlignment == StyleTextAlign::Start || + mTextAlignment == StyleTextAlign::End) { + mTextAlignment = StyleTextAlign::Left; + } + + // Figure out if we're the primary column (that has to have indentation + // and twisties drawn. + mIsPrimary = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::primary, + nsGkAtoms::_true, eCaseMatters); + + // Figure out if we're a cycling column (one that doesn't cause a selection + // to happen). + mIsCycler = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::cycler, + nsGkAtoms::_true, eCaseMatters); + + mIsEditable = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::editable, + nsGkAtoms::_true, eCaseMatters); + + mOverflow = mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::overflow, + nsGkAtoms::_true, eCaseMatters); + + // Figure out our column type. Default type is text. + mType = TreeColumn_Binding::TYPE_TEXT; + static Element::AttrValuesArray typestrings[] = {nsGkAtoms::checkbox, + nullptr}; + switch (mContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::type, + typestrings, eCaseMatters)) { + case 0: + mType = TreeColumn_Binding::TYPE_CHECKBOX; + break; + } + + // Fetch the crop style. + mCropStyle = 0; + static Element::AttrValuesArray cropstrings[] = { + nsGkAtoms::center, nsGkAtoms::left, nsGkAtoms::start, nullptr}; + switch (mContent->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::crop, + cropstrings, eCaseMatters)) { + case 0: + mCropStyle = 1; + break; + case 1: + case 2: + mCropStyle = 2; + break; + } +} + +nsIContent* nsTreeColumn::GetParentObject() const { return mContent; } + +/* virtual */ +JSObject* nsTreeColumn::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return dom::TreeColumn_Binding::Wrap(aCx, this, aGivenProto); +} + +Element* nsTreeColumn::Element() { return mContent; } + +int32_t nsTreeColumn::GetX(mozilla::ErrorResult& aRv) { + nsIFrame* frame = GetFrame(); + if (NS_WARN_IF(!frame)) { + aRv.Throw(NS_ERROR_FAILURE); + return 0; + } + + return nsPresContext::AppUnitsToIntCSSPixels(frame->GetRect().x); +} + +int32_t nsTreeColumn::GetWidth(mozilla::ErrorResult& aRv) { + nsIFrame* frame = GetFrame(); + if (NS_WARN_IF(!frame)) { + aRv.Throw(NS_ERROR_FAILURE); + return 0; + } + + return nsPresContext::AppUnitsToIntCSSPixels(frame->GetRect().width); +} + +already_AddRefed nsTreeColumn::GetPreviousColumn() { + return do_AddRef(mPrevious); +} + +nsTreeColumns::nsTreeColumns(nsTreeBodyFrame* aTree) : mTree(aTree) {} + +nsTreeColumns::~nsTreeColumns() { nsTreeColumns::InvalidateColumns(); } + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(nsTreeColumns) + +// QueryInterface implementation for nsTreeColumns +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeColumns) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeColumns) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeColumns) + +nsIContent* nsTreeColumns::GetParentObject() const { + return mTree ? mTree->GetBaseElement() : nullptr; +} + +/* virtual */ +JSObject* nsTreeColumns::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return dom::TreeColumns_Binding::Wrap(aCx, this, aGivenProto); +} + +XULTreeElement* nsTreeColumns::GetTree() const { + if (!mTree) { + return nullptr; + } + + return XULTreeElement::FromNodeOrNull(mTree->GetBaseElement()); +} + +uint32_t nsTreeColumns::Count() { + EnsureColumns(); + uint32_t count = 0; + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + ++count; + } + return count; +} + +nsTreeColumn* nsTreeColumns::GetLastColumn() { + EnsureColumns(); + nsTreeColumn* currCol = mFirstColumn; + while (currCol) { + nsTreeColumn* next = currCol->GetNext(); + if (!next) { + return currCol; + } + currCol = next; + } + return nullptr; +} + +nsTreeColumn* nsTreeColumns::GetSortedColumn() { + EnsureColumns(); + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + if (nsContentUtils::HasNonEmptyAttr(currCol->mContent, kNameSpaceID_None, + nsGkAtoms::sortDirection)) { + return currCol; + } + } + return nullptr; +} + +nsTreeColumn* nsTreeColumns::GetKeyColumn() { + EnsureColumns(); + + nsTreeColumn* first = nullptr; + nsTreeColumn* primary = nullptr; + nsTreeColumn* sorted = nullptr; + + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + // Skip hidden columns. + if (currCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters)) + continue; + + // Skip non-text column + if (currCol->GetType() != TreeColumn_Binding::TYPE_TEXT) continue; + + if (!first) first = currCol; + + if (nsContentUtils::HasNonEmptyAttr(currCol->mContent, kNameSpaceID_None, + nsGkAtoms::sortDirection)) { + // Use sorted column as the key. + sorted = currCol; + break; + } + + if (currCol->IsPrimary()) + if (!primary) primary = currCol; + } + + if (sorted) return sorted; + if (primary) return primary; + return first; +} + +nsTreeColumn* nsTreeColumns::GetColumnFor(dom::Element* aElement) { + EnsureColumns(); + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + if (currCol->mContent == aElement) { + return currCol; + } + } + return nullptr; +} + +nsTreeColumn* nsTreeColumns::NamedGetter(const nsAString& aId, bool& aFound) { + EnsureColumns(); + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + if (currCol->GetId().Equals(aId)) { + aFound = true; + return currCol; + } + } + aFound = false; + return nullptr; +} + +nsTreeColumn* nsTreeColumns::GetNamedColumn(const nsAString& aId) { + bool dummy; + return NamedGetter(aId, dummy); +} + +void nsTreeColumns::GetSupportedNames(nsTArray& aNames) { + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + aNames.AppendElement(currCol->GetId()); + } +} + +nsTreeColumn* nsTreeColumns::IndexedGetter(uint32_t aIndex, bool& aFound) { + EnsureColumns(); + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + if (currCol->GetIndex() == static_cast(aIndex)) { + aFound = true; + return currCol; + } + } + aFound = false; + return nullptr; +} + +nsTreeColumn* nsTreeColumns::GetColumnAt(uint32_t aIndex) { + bool dummy; + return IndexedGetter(aIndex, dummy); +} + +void nsTreeColumns::InvalidateColumns() { + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + currCol->SetColumns(nullptr); + } + mFirstColumn = nullptr; +} + +nsTreeColumn* nsTreeColumns::GetPrimaryColumn() { + EnsureColumns(); + for (nsTreeColumn* currCol = mFirstColumn; currCol; + currCol = currCol->GetNext()) { + if (currCol->IsPrimary()) { + return currCol; + } + } + return nullptr; +} + +void nsTreeColumns::EnsureColumns() { + if (mTree && !mFirstColumn) { + nsIContent* treeContent = mTree->GetBaseElement(); + if (!treeContent) return; + + nsIContent* colsContent = + nsTreeUtils::GetDescendantChild(treeContent, nsGkAtoms::treecols); + if (!colsContent) return; + + nsIContent* colContent = + nsTreeUtils::GetDescendantChild(colsContent, nsGkAtoms::treecol); + if (!colContent) return; + + nsIFrame* colFrame = colContent->GetPrimaryFrame(); + if (!colFrame) return; + + colFrame = colFrame->GetParent(); + if (!colFrame || !colFrame->GetContent()) return; + + nsTreeColumn* currCol = nullptr; + + // Enumerate the columns in visible order + CSSOrderAwareFrameIterator iter( + colFrame, FrameChildListID::Principal, + CSSOrderAwareFrameIterator::ChildFilter::IncludeAll); + for (; !iter.AtEnd(); iter.Next()) { + nsIFrame* colFrame = iter.get(); + nsIContent* colContent = colFrame->GetContent(); + if (!colContent->IsXULElement(nsGkAtoms::treecol)) { + continue; + } + // Create a new column structure. + nsTreeColumn* col = new nsTreeColumn(this, colContent->AsElement()); + + if (currCol) { + currCol->SetNext(col); + col->SetPrevious(currCol); + } else { + mFirstColumn = col; + } + currCol = col; + } + } +} diff --git a/layout/xul/tree/nsTreeColumns.h b/layout/xul/tree/nsTreeColumns.h new file mode 100644 index 0000000000..677206a89d --- /dev/null +++ b/layout/xul/tree/nsTreeColumns.h @@ -0,0 +1,214 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeColumns_h__ +#define nsTreeColumns_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/RefPtr.h" +#include "nsCoord.h" +#include "nsCycleCollectionParticipant.h" +#include "nsQueryObject.h" +#include "nsWrapperCache.h" +#include "nsString.h" + +class nsAtom; +class nsTreeBodyFrame; +class nsTreeColumns; +class nsIFrame; +class nsIContent; +struct nsRect; + +namespace mozilla { +enum class StyleTextAlignKeyword : uint8_t; +using StyleTextAlign = StyleTextAlignKeyword; +class ErrorResult; +namespace dom { +class Element; +class XULTreeElement; +} // namespace dom +} // namespace mozilla + +#define NS_TREECOLUMN_IMPL_CID \ + { /* 02cd1963-4b5d-4a6c-9223-814d3ade93a3 */ \ + 0x02cd1963, 0x4b5d, 0x4a6c, { \ + 0x92, 0x23, 0x81, 0x4d, 0x3a, 0xde, 0x93, 0xa3 \ + } \ + } + +// This class is our column info. We use it to iterate our columns and to +// obtain information about each column. +class nsTreeColumn final : public nsISupports, public nsWrapperCache { + public: + nsTreeColumn(nsTreeColumns* aColumns, mozilla::dom::Element* aElement); + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_TREECOLUMN_IMPL_CID) + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsTreeColumn) + + // WebIDL + nsIContent* GetParentObject() const; + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + mozilla::dom::Element* Element(); + + nsTreeColumns* GetColumns() const { return mColumns; } + + int32_t GetX(mozilla::ErrorResult& aRv); + int32_t GetWidth(mozilla::ErrorResult& aRv); + + void GetId(nsAString& aId) const; + int32_t Index() const { return mIndex; } + + bool Primary() const { return mIsPrimary; } + bool Cycler() const { return mIsCycler; } + bool Editable() const { return mIsEditable; } + int16_t Type() const { return mType; } + + nsTreeColumn* GetNext() const { return mNext; } + nsTreeColumn* GetPrevious() const { return mPrevious; } + + already_AddRefed GetPreviousColumn(); + + void Invalidate(mozilla::ErrorResult& aRv); + + friend class nsTreeBodyFrame; + friend class nsTreeColumns; + + protected: + ~nsTreeColumn(); + nsIFrame* GetFrame(); + nsIFrame* GetFrame(nsTreeBodyFrame* aBodyFrame); + // Don't call this if GetWidthInTwips or GetRect fails + bool IsLastVisible(nsTreeBodyFrame* aBodyFrame); + + /** + * Returns a rect with x and width taken from the frame's rect and specified + * y and height. May fail in case there's no frame for the column. + */ + nsresult GetRect(nsTreeBodyFrame* aBodyFrame, nscoord aY, nscoord aHeight, + nsRect* aResult); + + nsresult GetXInTwips(nsTreeBodyFrame* aBodyFrame, nscoord* aResult); + nsresult GetWidthInTwips(nsTreeBodyFrame* aBodyFrame, nscoord* aResult); + + void SetColumns(nsTreeColumns* aColumns) { mColumns = aColumns; } + + public: + const nsAString& GetId() const { return mId; } + nsAtom* GetAtom() { return mAtom; } + int32_t GetIndex() { return mIndex; } + + protected: + bool IsPrimary() { return mIsPrimary; } + bool IsCycler() { return mIsCycler; } + bool IsEditable() { return mIsEditable; } + bool Overflow() { return mOverflow; } + + int16_t GetType() { return mType; } + + int8_t GetCropStyle() { return mCropStyle; } + mozilla::StyleTextAlign GetTextAlignment() { return mTextAlignment; } + + void SetNext(nsTreeColumn* aNext) { + NS_ASSERTION(!mNext, "already have a next sibling"); + mNext = aNext; + } + void SetPrevious(nsTreeColumn* aPrevious) { mPrevious = aPrevious; } + + private: + /** + * Non-null nsIContent for the associated element. + */ + RefPtr mContent; + + nsTreeColumns* mColumns; + + nsString mId; + RefPtr mAtom; + + int32_t mIndex; + + bool mIsPrimary; + bool mIsCycler; + bool mIsEditable; + bool mOverflow; + + int16_t mType; + + int8_t mCropStyle; + mozilla::StyleTextAlign mTextAlignment; + + RefPtr mNext; + nsTreeColumn* mPrevious; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsTreeColumn, NS_TREECOLUMN_IMPL_CID) + +class nsTreeColumns final : public nsISupports, public nsWrapperCache { + private: + ~nsTreeColumns(); + + public: + explicit nsTreeColumns(nsTreeBodyFrame* aTree); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsTreeColumns) + + nsIContent* GetParentObject() const; + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + // WebIDL + mozilla::dom::XULTreeElement* GetTree() const; + uint32_t Count(); + uint32_t Length() { return Count(); } + + nsTreeColumn* GetFirstColumn() { + EnsureColumns(); + return mFirstColumn; + } + nsTreeColumn* GetLastColumn(); + + nsTreeColumn* GetPrimaryColumn(); + nsTreeColumn* GetSortedColumn(); + nsTreeColumn* GetKeyColumn(); + + nsTreeColumn* GetColumnFor(mozilla::dom::Element* aElement); + + nsTreeColumn* IndexedGetter(uint32_t aIndex, bool& aFound); + nsTreeColumn* GetColumnAt(uint32_t aIndex); + nsTreeColumn* NamedGetter(const nsAString& aId, bool& aFound); + nsTreeColumn* GetNamedColumn(const nsAString& aId); + void GetSupportedNames(nsTArray& aNames); + + void InvalidateColumns(); + + friend class nsTreeBodyFrame; + + protected: + void SetTree(nsTreeBodyFrame* aTree) { mTree = aTree; } + + // Builds our cache of column info. + void EnsureColumns(); + + private: + nsTreeBodyFrame* mTree; + + /** + * The first column in the list of columns. All of the columns are supposed + * to be "alive", i.e. have a frame. This is achieved by clearing the columns + * list each time a treecol changes size. + * + * XXX this means that new nsTreeColumn objects are unnecessarily created + * for untouched columns. + */ + RefPtr mFirstColumn; +}; + +#endif // nsTreeColumns_h__ diff --git a/layout/xul/tree/nsTreeContentView.cpp b/layout/xul/tree/nsTreeContentView.cpp new file mode 100644 index 0000000000..edb5368e2a --- /dev/null +++ b/layout/xul/tree/nsTreeContentView.cpp @@ -0,0 +1,1269 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsTreeUtils.h" +#include "nsTreeContentView.h" +#include "ChildIterator.h" +#include "nsError.h" +#include "nsXULSortService.h" +#include "nsTreeBodyFrame.h" +#include "nsTreeColumns.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/TreeContentViewBinding.h" +#include "mozilla/dom/XULTreeElement.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/dom/Document.h" + +using namespace mozilla; +using namespace mozilla::dom; + +// A content model view implementation for the tree. + +#define ROW_FLAG_CONTAINER 0x01 +#define ROW_FLAG_OPEN 0x02 +#define ROW_FLAG_EMPTY 0x04 +#define ROW_FLAG_SEPARATOR 0x08 + +class Row { + public: + Row(Element* aContent, int32_t aParentIndex) + : mContent(aContent), + mParentIndex(aParentIndex), + mSubtreeSize(0), + mFlags(0) {} + + ~Row() = default; + + void SetContainer(bool aContainer) { + aContainer ? mFlags |= ROW_FLAG_CONTAINER : mFlags &= ~ROW_FLAG_CONTAINER; + } + bool IsContainer() { return mFlags & ROW_FLAG_CONTAINER; } + + void SetOpen(bool aOpen) { + aOpen ? mFlags |= ROW_FLAG_OPEN : mFlags &= ~ROW_FLAG_OPEN; + } + bool IsOpen() { return !!(mFlags & ROW_FLAG_OPEN); } + + void SetEmpty(bool aEmpty) { + aEmpty ? mFlags |= ROW_FLAG_EMPTY : mFlags &= ~ROW_FLAG_EMPTY; + } + bool IsEmpty() { return !!(mFlags & ROW_FLAG_EMPTY); } + + void SetSeparator(bool aSeparator) { + aSeparator ? mFlags |= ROW_FLAG_SEPARATOR : mFlags &= ~ROW_FLAG_SEPARATOR; + } + bool IsSeparator() { return !!(mFlags & ROW_FLAG_SEPARATOR); } + + // Weak reference to a content item. + Element* mContent; + + // The parent index of the item, set to -1 for the top level items. + int32_t mParentIndex; + + // Subtree size for this item. + int32_t mSubtreeSize; + + private: + // State flags + int8_t mFlags; +}; + +// We don't reference count the reference to the document +// If the document goes away first, we'll be informed and we +// can drop our reference. +// If we go away first, we'll get rid of ourselves from the +// document's observer list. + +nsTreeContentView::nsTreeContentView(void) + : mTree(nullptr), mSelection(nullptr), mDocument(nullptr) {} + +nsTreeContentView::~nsTreeContentView(void) { + // Remove ourselves from mDocument's observers. + if (mDocument) mDocument->RemoveObserver(this); +} + +nsresult NS_NewTreeContentView(nsITreeView** aResult) { + *aResult = new nsTreeContentView; + if (!*aResult) return NS_ERROR_OUT_OF_MEMORY; + NS_ADDREF(*aResult); + return NS_OK; +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(nsTreeContentView, mTree, mSelection, + mBody) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeContentView) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeContentView) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeContentView) + NS_INTERFACE_MAP_ENTRY(nsITreeView) + NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITreeView) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END + +JSObject* nsTreeContentView::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return TreeContentView_Binding::Wrap(aCx, this, aGivenProto); +} + +nsISupports* nsTreeContentView::GetParentObject() { return mTree; } + +NS_IMETHODIMP +nsTreeContentView::GetRowCount(int32_t* aRowCount) { + *aRowCount = mRows.Length(); + + return NS_OK; +} + +NS_IMETHODIMP +nsTreeContentView::GetSelection(nsITreeSelection** aSelection) { + NS_IF_ADDREF(*aSelection = GetSelection()); + + return NS_OK; +} + +bool nsTreeContentView::CanTrustTreeSelection(nsISupports* aValue) { + // Untrusted content is only allowed to specify known-good views + if (nsContentUtils::LegacyIsCallerChromeOrNativeCode()) return true; + nsCOMPtr nativeTreeSel = do_QueryInterface(aValue); + return nativeTreeSel && NS_SUCCEEDED(nativeTreeSel->EnsureNative()); +} + +NS_IMETHODIMP +nsTreeContentView::SetSelection(nsITreeSelection* aSelection) { + ErrorResult rv; + SetSelection(aSelection, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::SetSelection(nsITreeSelection* aSelection, + ErrorResult& aError) { + if (aSelection && !CanTrustTreeSelection(aSelection)) { + aError.ThrowSecurityError("Not allowed to set tree selection"); + return; + } + + mSelection = aSelection; +} + +void nsTreeContentView::GetRowProperties(int32_t aRow, nsAString& aProperties, + ErrorResult& aError) { + aProperties.Truncate(); + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + nsIContent* realRow; + if (row->IsSeparator()) + realRow = row->mContent; + else + realRow = nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + + if (realRow && realRow->IsElement()) { + realRow->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::properties, + aProperties); + } +} + +NS_IMETHODIMP +nsTreeContentView::GetRowProperties(int32_t aIndex, nsAString& aProps) { + ErrorResult rv; + GetRowProperties(aIndex, aProps, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::GetCellProperties(int32_t aRow, nsTreeColumn& aColumn, + nsAString& aProperties, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell) { + cell->GetAttr(kNameSpaceID_None, nsGkAtoms::properties, aProperties); + } + } +} + +NS_IMETHODIMP +nsTreeContentView::GetCellProperties(int32_t aRow, nsTreeColumn* aCol, + nsAString& aProps) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + GetCellProperties(aRow, *aCol, aProps, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::GetColumnProperties(nsTreeColumn& aColumn, + nsAString& aProperties) { + RefPtr element = aColumn.Element(); + + if (element) { + element->GetAttr(nsGkAtoms::properties, aProperties); + } +} + +NS_IMETHODIMP +nsTreeContentView::GetColumnProperties(nsTreeColumn* aCol, nsAString& aProps) { + NS_ENSURE_ARG(aCol); + + GetColumnProperties(*aCol, aProps); + return NS_OK; +} + +bool nsTreeContentView::IsContainer(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + return mRows[aRow]->IsContainer(); +} + +NS_IMETHODIMP +nsTreeContentView::IsContainer(int32_t aIndex, bool* _retval) { + ErrorResult rv; + *_retval = IsContainer(aIndex, rv); + return rv.StealNSResult(); +} + +bool nsTreeContentView::IsContainerOpen(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + return mRows[aRow]->IsOpen(); +} + +NS_IMETHODIMP +nsTreeContentView::IsContainerOpen(int32_t aIndex, bool* _retval) { + ErrorResult rv; + *_retval = IsContainerOpen(aIndex, rv); + return rv.StealNSResult(); +} + +bool nsTreeContentView::IsContainerEmpty(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + return mRows[aRow]->IsEmpty(); +} + +NS_IMETHODIMP +nsTreeContentView::IsContainerEmpty(int32_t aIndex, bool* _retval) { + ErrorResult rv; + *_retval = IsContainerEmpty(aIndex, rv); + return rv.StealNSResult(); +} + +bool nsTreeContentView::IsSeparator(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + return mRows[aRow]->IsSeparator(); +} + +NS_IMETHODIMP +nsTreeContentView::IsSeparator(int32_t aIndex, bool* _retval) { + ErrorResult rv; + *_retval = IsSeparator(aIndex, rv); + return rv.StealNSResult(); +} + +NS_IMETHODIMP +nsTreeContentView::IsSorted(bool* _retval) { + *_retval = IsSorted(); + + return NS_OK; +} + +bool nsTreeContentView::CanDrop(int32_t aRow, int32_t aOrientation, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + } + return false; +} + +bool nsTreeContentView::CanDrop(int32_t aRow, int32_t aOrientation, + DataTransfer* aDataTransfer, + ErrorResult& aError) { + return CanDrop(aRow, aOrientation, aError); +} + +NS_IMETHODIMP +nsTreeContentView::CanDrop(int32_t aIndex, int32_t aOrientation, + DataTransfer* aDataTransfer, bool* _retval) { + ErrorResult rv; + *_retval = CanDrop(aIndex, aOrientation, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + } +} + +void nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation, + DataTransfer* aDataTransfer, ErrorResult& aError) { + Drop(aRow, aOrientation, aError); +} + +NS_IMETHODIMP +nsTreeContentView::Drop(int32_t aRow, int32_t aOrientation, + DataTransfer* aDataTransfer) { + ErrorResult rv; + Drop(aRow, aOrientation, rv); + return rv.StealNSResult(); +} + +int32_t nsTreeContentView::GetParentIndex(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return 0; + } + + return mRows[aRow]->mParentIndex; +} + +NS_IMETHODIMP +nsTreeContentView::GetParentIndex(int32_t aRowIndex, int32_t* _retval) { + ErrorResult rv; + *_retval = GetParentIndex(aRowIndex, rv); + return rv.StealNSResult(); +} + +bool nsTreeContentView::HasNextSibling(int32_t aRow, int32_t aAfterIndex, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + // We have a next sibling if the row is not the last in the subtree. + int32_t parentIndex = mRows[aRow]->mParentIndex; + if (parentIndex < 0) { + return uint32_t(aRow) < mRows.Length() - 1; + } + + // Compute the last index in this subtree. + int32_t lastIndex = parentIndex + (mRows[parentIndex])->mSubtreeSize; + Row* row = mRows[lastIndex].get(); + while (row->mParentIndex != parentIndex) { + lastIndex = row->mParentIndex; + row = mRows[lastIndex].get(); + } + + return aRow < lastIndex; +} + +NS_IMETHODIMP +nsTreeContentView::HasNextSibling(int32_t aRowIndex, int32_t aAfterIndex, + bool* _retval) { + ErrorResult rv; + *_retval = HasNextSibling(aRowIndex, aAfterIndex, rv); + return rv.StealNSResult(); +} + +int32_t nsTreeContentView::GetLevel(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return 0; + } + + int32_t level = 0; + Row* row = mRows[aRow].get(); + while (row->mParentIndex >= 0) { + level++; + row = mRows[row->mParentIndex].get(); + } + return level; +} + +NS_IMETHODIMP +nsTreeContentView::GetLevel(int32_t aIndex, int32_t* _retval) { + ErrorResult rv; + *_retval = GetLevel(aIndex, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::GetImageSrc(int32_t aRow, nsTreeColumn& aColumn, + nsAString& aSrc, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell) cell->GetAttr(kNameSpaceID_None, nsGkAtoms::src, aSrc); + } +} + +NS_IMETHODIMP +nsTreeContentView::GetImageSrc(int32_t aRow, nsTreeColumn* aCol, + nsAString& _retval) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + GetImageSrc(aRow, *aCol, _retval, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::GetCellValue(int32_t aRow, nsTreeColumn& aColumn, + nsAString& aValue, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell) cell->GetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue); + } +} + +NS_IMETHODIMP +nsTreeContentView::GetCellValue(int32_t aRow, nsTreeColumn* aCol, + nsAString& _retval) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + GetCellValue(aRow, *aCol, _retval, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::GetCellText(int32_t aRow, nsTreeColumn& aColumn, + nsAString& aText, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + + // Check for a "label" attribute - this is valid on an + // with a single implied column. + if (row->mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aText) && + !aText.IsEmpty()) { + return; + } + + if (row->mContent->IsXULElement(nsGkAtoms::treeitem)) { + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell) cell->GetAttr(kNameSpaceID_None, nsGkAtoms::label, aText); + } + } +} + +NS_IMETHODIMP +nsTreeContentView::GetCellText(int32_t aRow, nsTreeColumn* aCol, + nsAString& _retval) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + GetCellText(aRow, *aCol, _retval, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::SetTree(XULTreeElement* aTree, ErrorResult& aError) { + aError = SetTree(aTree); +} + +NS_IMETHODIMP +nsTreeContentView::SetTree(XULTreeElement* aTree) { + ClearRows(); + + mTree = aTree; + + if (aTree) { + // Add ourselves to document's observers. + Document* document = mTree->GetComposedDoc(); + if (document) { + document->AddObserver(this); + mDocument = document; + } + + RefPtr bodyElement = mTree->GetTreeBody(); + if (bodyElement) { + mBody = std::move(bodyElement); + int32_t index = 0; + Serialize(mBody, -1, &index, mRows); + } + } + + return NS_OK; +} + +void nsTreeContentView::ToggleOpenState(int32_t aRow, ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + // We don't serialize content right here, since content might be generated + // lazily. + Row* row = mRows[aRow].get(); + + if (row->IsOpen()) + row->mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"false"_ns, + true); + else + row->mContent->SetAttr(kNameSpaceID_None, nsGkAtoms::open, u"true"_ns, + true); +} + +NS_IMETHODIMP +nsTreeContentView::ToggleOpenState(int32_t aIndex) { + ErrorResult rv; + ToggleOpenState(aIndex, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::CycleHeader(nsTreeColumn& aColumn, + ErrorResult& aError) { + if (!mTree) return; + + RefPtr column = aColumn.Element(); + nsAutoString sort; + column->GetAttr(kNameSpaceID_None, nsGkAtoms::sort, sort); + if (!sort.IsEmpty()) { + nsAutoString sortdirection; + static Element::AttrValuesArray strings[] = { + nsGkAtoms::ascending, nsGkAtoms::descending, nullptr}; + switch (column->FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::sortDirection, + strings, eCaseMatters)) { + case 0: + sortdirection.AssignLiteral("descending"); + break; + case 1: + sortdirection.AssignLiteral("natural"); + break; + default: + sortdirection.AssignLiteral("ascending"); + break; + } + + nsAutoString hints; + column->GetAttr(kNameSpaceID_None, nsGkAtoms::sorthints, hints); + sortdirection.Append(' '); + sortdirection += hints; + + XULWidgetSort(mTree, sort, sortdirection); + } +} + +NS_IMETHODIMP +nsTreeContentView::CycleHeader(nsTreeColumn* aCol) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + CycleHeader(*aCol, rv); + return rv.StealNSResult(); +} + +NS_IMETHODIMP +nsTreeContentView::SelectionChangedXPCOM() { return NS_OK; } + +NS_IMETHODIMP +nsTreeContentView::CycleCell(int32_t aRow, nsTreeColumn* aCol) { return NS_OK; } + +bool nsTreeContentView::IsEditable(int32_t aRow, nsTreeColumn& aColumn, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return false; + } + + Row* row = mRows[aRow].get(); + + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell && cell->AttrValueIs(kNameSpaceID_None, nsGkAtoms::editable, + nsGkAtoms::_false, eCaseMatters)) { + return false; + } + } + + return true; +} + +NS_IMETHODIMP +nsTreeContentView::IsEditable(int32_t aRow, nsTreeColumn* aCol, bool* _retval) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + *_retval = IsEditable(aRow, *aCol, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::SetCellValue(int32_t aRow, nsTreeColumn& aColumn, + const nsAString& aValue, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell) cell->SetAttr(kNameSpaceID_None, nsGkAtoms::value, aValue, true); + } +} + +NS_IMETHODIMP +nsTreeContentView::SetCellValue(int32_t aRow, nsTreeColumn* aCol, + const nsAString& aValue) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + SetCellValue(aRow, *aCol, aValue, rv); + return rv.StealNSResult(); +} + +void nsTreeContentView::SetCellText(int32_t aRow, nsTreeColumn& aColumn, + const nsAString& aValue, + ErrorResult& aError) { + if (!IsValidRowIndex(aRow)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Row* row = mRows[aRow].get(); + + nsIContent* realRow = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treerow); + if (realRow) { + Element* cell = GetCell(realRow, aColumn); + if (cell) cell->SetAttr(kNameSpaceID_None, nsGkAtoms::label, aValue, true); + } +} + +NS_IMETHODIMP +nsTreeContentView::SetCellText(int32_t aRow, nsTreeColumn* aCol, + const nsAString& aValue) { + NS_ENSURE_ARG(aCol); + + ErrorResult rv; + SetCellText(aRow, *aCol, aValue, rv); + return rv.StealNSResult(); +} + +Element* nsTreeContentView::GetItemAtIndex(int32_t aIndex, + ErrorResult& aError) { + if (!IsValidRowIndex(aIndex)) { + aError.Throw(NS_ERROR_INVALID_ARG); + return nullptr; + } + + return mRows[aIndex]->mContent; +} + +int32_t nsTreeContentView::GetIndexOfItem(Element* aItem) { + return FindContent(aItem); +} + +void nsTreeContentView::AttributeChanged(dom::Element* aElement, + int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) { + // Lots of codepaths under here that do all sorts of stuff, so be safe. + nsCOMPtr kungFuDeathGrip(this); + + // Make sure this notification concerns us. + // First check the tag to see if it's one that we care about. + if (aElement == mTree || aElement == mBody) { + mTree->ClearStyleAndImageCaches(); + mTree->Invalidate(); + } + + // We don't consider non-XUL nodes. + nsIContent* parent = nullptr; + if (!aElement->IsXULElement() || + ((parent = aElement->GetParent()) && !parent->IsXULElement())) { + return; + } + if (!aElement->IsAnyOfXULElements(nsGkAtoms::treecol, nsGkAtoms::treeitem, + nsGkAtoms::treeseparator, + nsGkAtoms::treerow, nsGkAtoms::treecell)) { + return; + } + + // If we have a legal tag, go up to the tree/select and make sure + // that it's ours. + + for (nsIContent* element = aElement; element != mBody; + element = element->GetParent()) { + if (!element) return; // this is not for us + if (element->IsXULElement(nsGkAtoms::tree)) return; // this is not for us + } + + // Handle changes of the hidden attribute. + if (aAttribute == nsGkAtoms::hidden && + aElement->IsAnyOfXULElements(nsGkAtoms::treeitem, + nsGkAtoms::treeseparator)) { + bool hidden = aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters); + + int32_t index = FindContent(aElement); + if (hidden && index >= 0) { + // Hide this row along with its children. + int32_t count = RemoveRow(index); + if (mTree) mTree->RowCountChanged(index, -count); + } else if (!hidden && index < 0) { + // Show this row along with its children. + nsCOMPtr parent = aElement->GetParent(); + if (parent) { + InsertRowFor(parent, aElement); + } + } + + return; + } + + if (aElement->IsXULElement(nsGkAtoms::treecol)) { + if (aAttribute == nsGkAtoms::properties) { + if (mTree) { + RefPtr cols = mTree->GetColumns(); + if (cols) { + RefPtr col = cols->GetColumnFor(aElement); + mTree->InvalidateColumn(col); + } + } + } + } else if (aElement->IsXULElement(nsGkAtoms::treeitem)) { + int32_t index = FindContent(aElement); + if (index >= 0) { + Row* row = mRows[index].get(); + if (aAttribute == nsGkAtoms::container) { + bool isContainer = + aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::container, + nsGkAtoms::_true, eCaseMatters); + row->SetContainer(isContainer); + if (mTree) mTree->InvalidateRow(index); + } else if (aAttribute == nsGkAtoms::open) { + bool isOpen = aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::open, + nsGkAtoms::_true, eCaseMatters); + bool wasOpen = row->IsOpen(); + if (!isOpen && wasOpen) + CloseContainer(index); + else if (isOpen && !wasOpen) + OpenContainer(index); + } else if (aAttribute == nsGkAtoms::empty) { + bool isEmpty = + aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::empty, + nsGkAtoms::_true, eCaseMatters); + row->SetEmpty(isEmpty); + if (mTree) mTree->InvalidateRow(index); + } + } + } else if (aElement->IsXULElement(nsGkAtoms::treeseparator)) { + int32_t index = FindContent(aElement); + if (index >= 0) { + if (aAttribute == nsGkAtoms::properties && mTree) { + mTree->InvalidateRow(index); + } + } + } else if (aElement->IsXULElement(nsGkAtoms::treerow)) { + if (aAttribute == nsGkAtoms::properties) { + nsCOMPtr parent = aElement->GetParent(); + if (parent) { + int32_t index = FindContent(parent); + if (index >= 0 && mTree) { + mTree->InvalidateRow(index); + } + } + } + } else if (aElement->IsXULElement(nsGkAtoms::treecell)) { + if (aAttribute == nsGkAtoms::properties || aAttribute == nsGkAtoms::mode || + aAttribute == nsGkAtoms::src || aAttribute == nsGkAtoms::value || + aAttribute == nsGkAtoms::label) { + nsIContent* parent = aElement->GetParent(); + if (parent) { + nsCOMPtr grandParent = parent->GetParent(); + if (grandParent && grandParent->IsXULElement()) { + int32_t index = FindContent(grandParent); + if (index >= 0 && mTree) { + // XXX Should we make an effort to invalidate only cell ? + mTree->InvalidateRow(index); + } + } + } + } + } +} + +void nsTreeContentView::ContentAppended(nsIContent* aFirstNewContent) { + for (nsIContent* cur = aFirstNewContent; cur; cur = cur->GetNextSibling()) { + // Our contentinserted doesn't use the index + ContentInserted(cur); + } +} + +void nsTreeContentView::ContentInserted(nsIContent* aChild) { + NS_ASSERTION(aChild, "null ptr"); + nsIContent* container = aChild->GetParent(); + + // Make sure this notification concerns us. + // First check the tag to see if it's one that we care about. + + // Don't allow non-XUL nodes. + if (!aChild->IsXULElement() || !container->IsXULElement()) return; + + if (!aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, nsGkAtoms::treeseparator, + nsGkAtoms::treechildren, nsGkAtoms::treerow, + nsGkAtoms::treecell)) { + return; + } + + // If we have a legal tag, go up to the tree/select and make sure + // that it's ours. + + for (nsIContent* element = container; element != mBody; + element = element->GetParent()) { + if (!element) return; // this is not for us + if (element->IsXULElement(nsGkAtoms::tree)) return; // this is not for us + } + + // Lots of codepaths under here that do all sorts of stuff, so be safe. + nsCOMPtr kungFuDeathGrip(this); + + if (aChild->IsXULElement(nsGkAtoms::treechildren)) { + int32_t index = FindContent(container); + if (index >= 0) { + Row* row = mRows[index].get(); + row->SetEmpty(false); + if (mTree) mTree->InvalidateRow(index); + if (row->IsContainer() && row->IsOpen()) { + int32_t count = EnsureSubtree(index); + if (mTree) mTree->RowCountChanged(index + 1, count); + } + } + } else if (aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, + nsGkAtoms::treeseparator)) { + InsertRowFor(container, aChild); + } else if (aChild->IsXULElement(nsGkAtoms::treerow)) { + int32_t index = FindContent(container); + if (index >= 0 && mTree) mTree->InvalidateRow(index); + } else if (aChild->IsXULElement(nsGkAtoms::treecell)) { + nsCOMPtr parent = container->GetParent(); + if (parent) { + int32_t index = FindContent(parent); + if (index >= 0 && mTree) mTree->InvalidateRow(index); + } + } +} + +void nsTreeContentView::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + NS_ASSERTION(aChild, "null ptr"); + + nsIContent* container = aChild->GetParent(); + // Make sure this notification concerns us. + // First check the tag to see if it's one that we care about. + + // We don't consider non-XUL nodes. + if (!aChild->IsXULElement() || !container->IsXULElement()) return; + + if (!aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, nsGkAtoms::treeseparator, + nsGkAtoms::treechildren, nsGkAtoms::treerow, + nsGkAtoms::treecell)) { + return; + } + + // If we have a legal tag, go up to the tree/select and make sure + // that it's ours. + + for (nsIContent* element = container; element != mBody; + element = element->GetParent()) { + if (!element) return; // this is not for us + if (element->IsXULElement(nsGkAtoms::tree)) return; // this is not for us + } + + // Lots of codepaths under here that do all sorts of stuff, so be safe. + nsCOMPtr kungFuDeathGrip(this); + + if (aChild->IsXULElement(nsGkAtoms::treechildren)) { + int32_t index = FindContent(container); + if (index >= 0) { + Row* row = mRows[index].get(); + row->SetEmpty(true); + int32_t count = RemoveSubtree(index); + // Invalidate also the row to update twisty. + if (mTree) { + mTree->InvalidateRow(index); + mTree->RowCountChanged(index + 1, -count); + } + } + } else if (aChild->IsAnyOfXULElements(nsGkAtoms::treeitem, + nsGkAtoms::treeseparator)) { + int32_t index = FindContent(aChild); + if (index >= 0) { + int32_t count = RemoveRow(index); + if (mTree) mTree->RowCountChanged(index, -count); + } + } else if (aChild->IsXULElement(nsGkAtoms::treerow)) { + int32_t index = FindContent(container); + if (index >= 0 && mTree) mTree->InvalidateRow(index); + } else if (aChild->IsXULElement(nsGkAtoms::treecell)) { + nsCOMPtr parent = container->GetParent(); + if (parent) { + int32_t index = FindContent(parent); + if (index >= 0 && mTree) mTree->InvalidateRow(index); + } + } +} + +void nsTreeContentView::NodeWillBeDestroyed(nsINode* aNode) { + // XXXbz do we need this strong ref? Do we drop refs to self in ClearRows? + nsCOMPtr kungFuDeathGrip(this); + ClearRows(); +} + +// Recursively serialize content, starting with aContent. +void nsTreeContentView::Serialize(nsIContent* aContent, int32_t aParentIndex, + int32_t* aIndex, + nsTArray>& aRows) { + // Don't allow non-XUL nodes. + if (!aContent->IsXULElement()) return; + + dom::FlattenedChildIterator iter(aContent); + for (nsIContent* content = iter.GetNextChild(); content; + content = iter.GetNextChild()) { + int32_t count = aRows.Length(); + + if (content->IsXULElement(nsGkAtoms::treeitem)) { + SerializeItem(content->AsElement(), aParentIndex, aIndex, aRows); + } else if (content->IsXULElement(nsGkAtoms::treeseparator)) { + SerializeSeparator(content->AsElement(), aParentIndex, aIndex, aRows); + } + + *aIndex += aRows.Length() - count; + } +} + +void nsTreeContentView::SerializeItem(Element* aContent, int32_t aParentIndex, + int32_t* aIndex, + nsTArray>& aRows) { + if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters)) + return; + + aRows.AppendElement(MakeUnique(aContent, aParentIndex)); + Row* row = aRows.LastElement().get(); + + if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::container, + nsGkAtoms::_true, eCaseMatters)) { + row->SetContainer(true); + if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::open, + nsGkAtoms::_true, eCaseMatters)) { + row->SetOpen(true); + nsIContent* child = + nsTreeUtils::GetImmediateChild(aContent, nsGkAtoms::treechildren); + if (child && child->IsXULElement()) { + // Now, recursively serialize our child. + int32_t count = aRows.Length(); + int32_t index = 0; + Serialize(child, aParentIndex + *aIndex + 1, &index, aRows); + row->mSubtreeSize += aRows.Length() - count; + } else + row->SetEmpty(true); + } else if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::empty, + nsGkAtoms::_true, eCaseMatters)) { + row->SetEmpty(true); + } + } +} + +void nsTreeContentView::SerializeSeparator(Element* aContent, + int32_t aParentIndex, + int32_t* aIndex, + nsTArray>& aRows) { + if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters)) + return; + + auto row = MakeUnique(aContent, aParentIndex); + row->SetSeparator(true); + aRows.AppendElement(std::move(row)); +} + +void nsTreeContentView::GetIndexInSubtree(nsIContent* aContainer, + nsIContent* aContent, + int32_t* aIndex) { + if (!aContainer->IsXULElement()) return; + + for (nsIContent* content = aContainer->GetFirstChild(); content; + content = content->GetNextSibling()) { + if (content == aContent) break; + + if (content->IsXULElement(nsGkAtoms::treeitem)) { + if (!content->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters)) { + (*aIndex)++; + if (content->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::container, + nsGkAtoms::_true, eCaseMatters) && + content->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::open, nsGkAtoms::_true, + eCaseMatters)) { + nsIContent* child = + nsTreeUtils::GetImmediateChild(content, nsGkAtoms::treechildren); + if (child && child->IsXULElement()) + GetIndexInSubtree(child, aContent, aIndex); + } + } + } else if (content->IsXULElement(nsGkAtoms::treeseparator)) { + if (!content->AsElement()->AttrValueIs(kNameSpaceID_None, + nsGkAtoms::hidden, + nsGkAtoms::_true, eCaseMatters)) + (*aIndex)++; + } + } +} + +int32_t nsTreeContentView::EnsureSubtree(int32_t aIndex) { + Row* row = mRows[aIndex].get(); + + nsIContent* child; + child = + nsTreeUtils::GetImmediateChild(row->mContent, nsGkAtoms::treechildren); + if (!child || !child->IsXULElement()) { + return 0; + } + + AutoTArray, 8> rows; + int32_t index = 0; + Serialize(child, aIndex, &index, rows); + // Insert |rows| into |mRows| at position |aIndex|, by first creating empty + // UniquePtr entries and then Move'ing |rows|'s entries into them. (Note + // that we can't simply use InsertElementsAt with an array argument, since + // the destination can't steal ownership from its const source argument.) + UniquePtr* newRows = mRows.InsertElementsAt(aIndex + 1, rows.Length()); + for (nsTArray::index_type i = 0; i < rows.Length(); i++) { + newRows[i] = std::move(rows[i]); + } + int32_t count = rows.Length(); + + row->mSubtreeSize += count; + UpdateSubtreeSizes(row->mParentIndex, count); + + // Update parent indexes, but skip newly added rows. + // They already have correct values. + UpdateParentIndexes(aIndex, count + 1, count); + + return count; +} + +int32_t nsTreeContentView::RemoveSubtree(int32_t aIndex) { + Row* row = mRows[aIndex].get(); + int32_t count = row->mSubtreeSize; + + mRows.RemoveElementsAt(aIndex + 1, count); + + row->mSubtreeSize -= count; + UpdateSubtreeSizes(row->mParentIndex, -count); + + UpdateParentIndexes(aIndex, 0, -count); + + return count; +} + +void nsTreeContentView::InsertRowFor(nsIContent* aParent, nsIContent* aChild) { + int32_t grandParentIndex = -1; + bool insertRow = false; + + nsCOMPtr grandParent = aParent->GetParent(); + + if (grandParent->IsXULElement(nsGkAtoms::tree)) { + // Allow insertion to the outermost container. + insertRow = true; + } else { + // Test insertion to an inner container. + + // First try to find this parent in our array of rows, if we find one + // we can be sure that all other parents are open too. + grandParentIndex = FindContent(grandParent); + if (grandParentIndex >= 0) { + // Got it, now test if it is open. + if (mRows[grandParentIndex]->IsOpen()) insertRow = true; + } + } + + if (insertRow) { + int32_t index = 0; + GetIndexInSubtree(aParent, aChild, &index); + + int32_t count = InsertRow(grandParentIndex, index, aChild); + if (mTree) mTree->RowCountChanged(grandParentIndex + index + 1, count); + } +} + +int32_t nsTreeContentView::InsertRow(int32_t aParentIndex, int32_t aIndex, + nsIContent* aContent) { + AutoTArray, 8> rows; + if (aContent->IsXULElement(nsGkAtoms::treeitem)) { + SerializeItem(aContent->AsElement(), aParentIndex, &aIndex, rows); + } else if (aContent->IsXULElement(nsGkAtoms::treeseparator)) { + SerializeSeparator(aContent->AsElement(), aParentIndex, &aIndex, rows); + } + + // We can't use InsertElementsAt since the destination can't steal + // ownership from its const source argument. + int32_t count = rows.Length(); + for (nsTArray::index_type i = 0; i < size_t(count); i++) { + mRows.InsertElementAt(aParentIndex + aIndex + i + 1, std::move(rows[i])); + } + + UpdateSubtreeSizes(aParentIndex, count); + + // Update parent indexes, but skip added rows. + // They already have correct values. + UpdateParentIndexes(aParentIndex + aIndex, count + 1, count); + + return count; +} + +int32_t nsTreeContentView::RemoveRow(int32_t aIndex) { + Row* row = mRows[aIndex].get(); + int32_t count = row->mSubtreeSize + 1; + int32_t parentIndex = row->mParentIndex; + + mRows.RemoveElementsAt(aIndex, count); + + UpdateSubtreeSizes(parentIndex, -count); + + UpdateParentIndexes(aIndex, 0, -count); + + return count; +} + +void nsTreeContentView::ClearRows() { + mRows.Clear(); + mBody = nullptr; + // Remove ourselves from mDocument's observers. + if (mDocument) { + mDocument->RemoveObserver(this); + mDocument = nullptr; + } +} + +void nsTreeContentView::OpenContainer(int32_t aIndex) { + Row* row = mRows[aIndex].get(); + row->SetOpen(true); + + int32_t count = EnsureSubtree(aIndex); + if (mTree) { + mTree->InvalidateRow(aIndex); + mTree->RowCountChanged(aIndex + 1, count); + } +} + +void nsTreeContentView::CloseContainer(int32_t aIndex) { + Row* row = mRows[aIndex].get(); + row->SetOpen(false); + + int32_t count = RemoveSubtree(aIndex); + if (mTree) { + mTree->InvalidateRow(aIndex); + mTree->RowCountChanged(aIndex + 1, -count); + } +} + +int32_t nsTreeContentView::FindContent(nsIContent* aContent) { + for (uint32_t i = 0; i < mRows.Length(); i++) { + if (mRows[i]->mContent == aContent) { + return i; + } + } + + return -1; +} + +void nsTreeContentView::UpdateSubtreeSizes(int32_t aParentIndex, + int32_t count) { + while (aParentIndex >= 0) { + Row* row = mRows[aParentIndex].get(); + row->mSubtreeSize += count; + aParentIndex = row->mParentIndex; + } +} + +void nsTreeContentView::UpdateParentIndexes(int32_t aIndex, int32_t aSkip, + int32_t aCount) { + int32_t count = mRows.Length(); + for (int32_t i = aIndex + aSkip; i < count; i++) { + Row* row = mRows[i].get(); + if (row->mParentIndex > aIndex) { + row->mParentIndex += aCount; + } + } +} + +Element* nsTreeContentView::GetCell(nsIContent* aContainer, + nsTreeColumn& aCol) { + int32_t colIndex(aCol.GetIndex()); + + // Traverse through cells, try to find the cell by index in a row. + Element* result = nullptr; + int32_t j = 0; + dom::FlattenedChildIterator iter(aContainer); + for (nsIContent* cell = iter.GetNextChild(); cell; + cell = iter.GetNextChild()) { + if (cell->IsXULElement(nsGkAtoms::treecell)) { + if (j == colIndex) { + result = cell->AsElement(); + break; + } + j++; + } + } + + return result; +} + +bool nsTreeContentView::IsValidRowIndex(int32_t aRowIndex) { + return aRowIndex >= 0 && aRowIndex < int32_t(mRows.Length()); +} diff --git a/layout/xul/tree/nsTreeContentView.h b/layout/xul/tree/nsTreeContentView.h new file mode 100644 index 0000000000..8138ab44fc --- /dev/null +++ b/layout/xul/tree/nsTreeContentView.h @@ -0,0 +1,164 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeContentView_h__ +#define nsTreeContentView_h__ + +#include "nsCycleCollectionParticipant.h" +#include "nsTArray.h" +#include "nsStubDocumentObserver.h" +#include "nsITreeView.h" +#include "nsITreeSelection.h" +#include "nsWrapperCache.h" +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" + +class nsSelection; +class nsTreeColumn; +class Row; + +namespace mozilla { +class ErrorResult; + +namespace dom { +class DataTransfer; +class Document; +class Element; +class XULTreeElement; +} // namespace dom +} // namespace mozilla + +nsresult NS_NewTreeContentView(nsITreeView** aResult); + +class nsTreeContentView final : public nsITreeView, + public nsStubDocumentObserver, + public nsWrapperCache { + typedef mozilla::dom::Element Element; + + public: + nsTreeContentView(void); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(nsTreeContentView, + nsITreeView) + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + nsISupports* GetParentObject(); + + int32_t RowCount() { return mRows.Length(); } + nsITreeSelection* GetSelection() { return mSelection; } + void SetSelection(nsITreeSelection* aSelection, mozilla::ErrorResult& aError); + void GetRowProperties(int32_t aRow, nsAString& aProperties, + mozilla::ErrorResult& aError); + void GetCellProperties(int32_t aRow, nsTreeColumn& aColumn, + nsAString& aProperies, mozilla::ErrorResult& aError); + void GetColumnProperties(nsTreeColumn& aColumn, nsAString& aProperies); + bool IsContainer(int32_t aRow, mozilla::ErrorResult& aError); + bool IsContainerOpen(int32_t aRow, mozilla::ErrorResult& aError); + bool IsContainerEmpty(int32_t aRow, mozilla::ErrorResult& aError); + bool IsSeparator(int32_t aRow, mozilla::ErrorResult& aError); + bool IsSorted() { return false; } + bool CanDrop(int32_t aRow, int32_t aOrientation, + mozilla::dom::DataTransfer* aDataTransfer, + mozilla::ErrorResult& aError); + void Drop(int32_t aRow, int32_t aOrientation, + mozilla::dom::DataTransfer* aDataTransfer, + mozilla::ErrorResult& aError); + int32_t GetParentIndex(int32_t aRow, mozilla::ErrorResult& aError); + bool HasNextSibling(int32_t aRow, int32_t aAfterIndex, + mozilla::ErrorResult& aError); + int32_t GetLevel(int32_t aRow, mozilla::ErrorResult& aError); + void GetImageSrc(int32_t aRow, nsTreeColumn& aColumn, nsAString& aSrc, + mozilla::ErrorResult& aError); + void GetCellValue(int32_t aRow, nsTreeColumn& aColumn, nsAString& aValue, + mozilla::ErrorResult& aError); + void GetCellText(int32_t aRow, nsTreeColumn& aColumn, nsAString& aText, + mozilla::ErrorResult& aError); + void SetTree(mozilla::dom::XULTreeElement* aTree, + mozilla::ErrorResult& aError); + void ToggleOpenState(int32_t aRow, mozilla::ErrorResult& aError); + void CycleHeader(nsTreeColumn& aColumn, mozilla::ErrorResult& aError); + void SelectionChanged() {} + void CycleCell(int32_t aRow, nsTreeColumn& aColumn) {} + bool IsEditable(int32_t aRow, nsTreeColumn& aColumn, + mozilla::ErrorResult& aError); + void SetCellValue(int32_t aRow, nsTreeColumn& aColumn, + const nsAString& aValue, mozilla::ErrorResult& aError); + void SetCellText(int32_t aRow, nsTreeColumn& aColumn, const nsAString& aText, + mozilla::ErrorResult& aError); + Element* GetItemAtIndex(int32_t aRow, mozilla::ErrorResult& aError); + int32_t GetIndexOfItem(Element* aItem); + + NS_DECL_NSITREEVIEW + + // nsIDocumentObserver + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED + + static bool CanTrustTreeSelection(nsISupports* aValue); + + protected: + ~nsTreeContentView(void); + + // Recursive methods which deal with serializing of nested content. + void Serialize(nsIContent* aContent, int32_t aParentIndex, int32_t* aIndex, + nsTArray>& aRows); + + void SerializeItem(Element* aContent, int32_t aParentIndex, int32_t* aIndex, + nsTArray>& aRows); + + void SerializeSeparator(Element* aContent, int32_t aParentIndex, + int32_t* aIndex, + nsTArray>& aRows); + + void GetIndexInSubtree(nsIContent* aContainer, nsIContent* aContent, + int32_t* aResult); + + // Helper methods which we use to manage our plain array of rows. + int32_t EnsureSubtree(int32_t aIndex); + + int32_t RemoveSubtree(int32_t aIndex); + + int32_t InsertRow(int32_t aParentIndex, int32_t aIndex, nsIContent* aContent); + + void InsertRowFor(nsIContent* aParent, nsIContent* aChild); + + int32_t RemoveRow(int32_t aIndex); + + void ClearRows(); + + void OpenContainer(int32_t aIndex); + + void CloseContainer(int32_t aIndex); + + int32_t FindContent(nsIContent* aContent); + + void UpdateSubtreeSizes(int32_t aIndex, int32_t aCount); + + void UpdateParentIndexes(int32_t aIndex, int32_t aSkip, int32_t aCount); + + bool CanDrop(int32_t aRow, int32_t aOrientation, + mozilla::ErrorResult& aError); + void Drop(int32_t aRow, int32_t aOrientation, mozilla::ErrorResult& aError); + + // Content helpers. + Element* GetCell(nsIContent* aContainer, nsTreeColumn& aCol); + + private: + bool IsValidRowIndex(int32_t aRowIndex); + + RefPtr mTree; + nsCOMPtr mSelection; + nsCOMPtr mBody; + mozilla::dom::Document* mDocument; // WEAK + nsTArray> mRows; +}; + +#endif // nsTreeContentView_h__ diff --git a/layout/xul/tree/nsTreeImageListener.cpp b/layout/xul/tree/nsTreeImageListener.cpp new file mode 100644 index 0000000000..a560ada948 --- /dev/null +++ b/layout/xul/tree/nsTreeImageListener.cpp @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsTreeImageListener.h" +#include "XULTreeElement.h" +#include "imgIRequest.h" +#include "imgIContainer.h" +#include "nsIContent.h" +#include "nsTreeColumns.h" + +using mozilla::dom::XULTreeElement; + +NS_IMPL_ISUPPORTS(nsTreeImageListener, imgINotificationObserver) + +nsTreeImageListener::nsTreeImageListener(nsTreeBodyFrame* aTreeFrame) + : mTreeFrame(aTreeFrame), + mInvalidationSuppressed(true), + mInvalidationArea(nullptr) {} + +nsTreeImageListener::~nsTreeImageListener() { delete mInvalidationArea; } + +void nsTreeImageListener::Notify(imgIRequest* aRequest, int32_t aType, + const nsIntRect* aData) { + if (aType == imgINotificationObserver::IS_ANIMATED) { + if (mTreeFrame) { + mTreeFrame->OnImageIsAnimated(aRequest); + } + return; + } + + if (aType == imgINotificationObserver::SIZE_AVAILABLE) { + // Ensure the animation (if any) is started. Note: There is no + // corresponding call to Decrement for this. This Increment will be + // 'cleaned up' by the Request when it is destroyed, but only then. + aRequest->IncrementAnimationConsumers(); + + if (mTreeFrame) { + nsCOMPtr image; + aRequest->GetImage(getter_AddRefs(image)); + if (image) { + nsPresContext* presContext = mTreeFrame->PresContext(); + image->SetAnimationMode(presContext->ImageAnimationMode()); + } + } + } + + if (aType == imgINotificationObserver::FRAME_UPDATE) { + Invalidate(); + } +} + +void nsTreeImageListener::AddCell(int32_t aIndex, nsTreeColumn* aCol) { + if (!mInvalidationArea) { + mInvalidationArea = new InvalidationArea(aCol); + mInvalidationArea->AddRow(aIndex); + } else { + InvalidationArea* currArea; + for (currArea = mInvalidationArea; currArea; + currArea = currArea->GetNext()) { + if (currArea->GetCol() == aCol) { + currArea->AddRow(aIndex); + break; + } + } + if (!currArea) { + currArea = new InvalidationArea(aCol); + currArea->SetNext(mInvalidationArea); + mInvalidationArea = currArea; + mInvalidationArea->AddRow(aIndex); + } + } +} + +void nsTreeImageListener::Invalidate() { + if (!mInvalidationSuppressed) { + for (InvalidationArea* currArea = mInvalidationArea; currArea; + currArea = currArea->GetNext()) { + // Loop from min to max, invalidating each cell that was listening for + // this image. + for (int32_t i = currArea->GetMin(); i <= currArea->GetMax(); ++i) { + if (mTreeFrame) { + RefPtr tree = + XULTreeElement::FromNodeOrNull(mTreeFrame->GetBaseElement()); + if (tree) { + tree->InvalidateCell(i, currArea->GetCol()); + } + } + } + } + } +} + +nsTreeImageListener::InvalidationArea::InvalidationArea(nsTreeColumn* aCol) + : mCol(aCol), + mMin(-1), // min should start out "undefined" + mMax(0), + mNext(nullptr) {} + +void nsTreeImageListener::InvalidationArea::AddRow(int32_t aIndex) { + if (mMin == -1) + mMin = mMax = aIndex; + else if (aIndex < mMin) + mMin = aIndex; + else if (aIndex > mMax) + mMax = aIndex; +} + +NS_IMETHODIMP +nsTreeImageListener::ClearFrame() { + mTreeFrame = nullptr; + return NS_OK; +} diff --git a/layout/xul/tree/nsTreeImageListener.h b/layout/xul/tree/nsTreeImageListener.h new file mode 100644 index 0000000000..f5e6e70512 --- /dev/null +++ b/layout/xul/tree/nsTreeImageListener.h @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeImageListener_h__ +#define nsTreeImageListener_h__ + +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsTreeBodyFrame.h" +#include "mozilla/Attributes.h" + +class nsTreeColumn; + +// This class handles image load observation. +class nsTreeImageListener final : public imgINotificationObserver { + public: + explicit nsTreeImageListener(nsTreeBodyFrame* aTreeFrame); + + NS_DECL_ISUPPORTS + NS_DECL_IMGINOTIFICATIONOBSERVER + + NS_IMETHOD ClearFrame(); + + friend class nsTreeBodyFrame; + + protected: + ~nsTreeImageListener(); + + void UnsuppressInvalidation() { mInvalidationSuppressed = false; } + void Invalidate(); + void AddCell(int32_t aIndex, nsTreeColumn* aCol); + + private: + nsTreeBodyFrame* mTreeFrame; + + // A guard that prevents us from recursive painting. + bool mInvalidationSuppressed; + + class InvalidationArea { + public: + explicit InvalidationArea(nsTreeColumn* aCol); + ~InvalidationArea() { delete mNext; } + + friend class nsTreeImageListener; + + protected: + void AddRow(int32_t aIndex); + nsTreeColumn* GetCol() { return mCol.get(); } + int32_t GetMin() { return mMin; } + int32_t GetMax() { return mMax; } + InvalidationArea* GetNext() { return mNext; } + void SetNext(InvalidationArea* aNext) { mNext = aNext; } + + private: + RefPtr mCol; + int32_t mMin; + int32_t mMax; + InvalidationArea* mNext; + }; + + InvalidationArea* mInvalidationArea; +}; + +#endif // nsTreeImageListener_h__ diff --git a/layout/xul/tree/nsTreeSelection.cpp b/layout/xul/tree/nsTreeSelection.cpp new file mode 100644 index 0000000000..952a9bf376 --- /dev/null +++ b/layout/xul/tree/nsTreeSelection.cpp @@ -0,0 +1,724 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/Element.h" +#include "nsCOMPtr.h" +#include "nsTreeSelection.h" +#include "XULTreeElement.h" +#include "nsITreeView.h" +#include "nsString.h" +#include "nsIContent.h" +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsComponentManagerUtils.h" +#include "nsTreeColumns.h" + +using namespace mozilla; +using dom::XULTreeElement; + +// A helper class for managing our ranges of selection. +struct nsTreeRange { + nsTreeSelection* mSelection; + + nsTreeRange* mPrev; + nsTreeRange* mNext; + + int32_t mMin; + int32_t mMax; + + nsTreeRange(nsTreeSelection* aSel, int32_t aSingleVal) + : mSelection(aSel), + mPrev(nullptr), + mNext(nullptr), + mMin(aSingleVal), + mMax(aSingleVal) {} + nsTreeRange(nsTreeSelection* aSel, int32_t aMin, int32_t aMax) + : mSelection(aSel), + mPrev(nullptr), + mNext(nullptr), + mMin(aMin), + mMax(aMax) {} + + ~nsTreeRange() { delete mNext; } + + void Connect(nsTreeRange* aPrev = nullptr, nsTreeRange* aNext = nullptr) { + if (aPrev) + aPrev->mNext = this; + else + mSelection->mFirstRange = this; + + if (aNext) aNext->mPrev = this; + + mPrev = aPrev; + mNext = aNext; + } + + nsresult RemoveRange(int32_t aStart, int32_t aEnd) { + // This should so be a loop... sigh... + // We start past the range to remove, so no more to remove + if (aEnd < mMin) return NS_OK; + // We are the last range to be affected + if (aEnd < mMax) { + if (aStart <= mMin) { + // Just chop the start of the range off + mMin = aEnd + 1; + } else { + // We need to split the range + nsTreeRange* range = new nsTreeRange(mSelection, aEnd + 1, mMax); + if (!range) return NS_ERROR_OUT_OF_MEMORY; + + mMax = aStart - 1; + range->Connect(this, mNext); + } + return NS_OK; + } + nsTreeRange* next = mNext; + if (aStart <= mMin) { + // The remove includes us, remove ourselves from the list + if (mPrev) + mPrev->mNext = next; + else + mSelection->mFirstRange = next; + + if (next) next->mPrev = mPrev; + mPrev = mNext = nullptr; + delete this; + } else if (aStart <= mMax) { + // Just chop the end of the range off + mMax = aStart - 1; + } + return next ? next->RemoveRange(aStart, aEnd) : NS_OK; + } + + nsresult Remove(int32_t aIndex) { + if (aIndex >= mMin && aIndex <= mMax) { + // We have found the range that contains us. + if (mMin == mMax) { + // Delete the whole range. + if (mPrev) mPrev->mNext = mNext; + if (mNext) mNext->mPrev = mPrev; + nsTreeRange* first = mSelection->mFirstRange; + if (first == this) mSelection->mFirstRange = mNext; + mNext = mPrev = nullptr; + delete this; + } else if (aIndex == mMin) + mMin++; + else if (aIndex == mMax) + mMax--; + else { + // We have to break this range. + nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex + 1, mMax); + if (!newRange) return NS_ERROR_OUT_OF_MEMORY; + + newRange->Connect(this, mNext); + mMax = aIndex - 1; + } + } else if (mNext) + return mNext->Remove(aIndex); + + return NS_OK; + } + + nsresult Add(int32_t aIndex) { + if (aIndex < mMin) { + // We have found a spot to insert. + if (aIndex + 1 == mMin) + mMin = aIndex; + else if (mPrev && mPrev->mMax + 1 == aIndex) + mPrev->mMax = aIndex; + else { + // We have to create a new range. + nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex); + if (!newRange) return NS_ERROR_OUT_OF_MEMORY; + + newRange->Connect(mPrev, this); + } + } else if (mNext) + mNext->Add(aIndex); + else { + // Insert on to the end. + if (mMax + 1 == aIndex) + mMax = aIndex; + else { + // We have to create a new range. + nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex); + if (!newRange) return NS_ERROR_OUT_OF_MEMORY; + + newRange->Connect(this, nullptr); + } + } + return NS_OK; + } + + bool Contains(int32_t aIndex) { + if (aIndex >= mMin && aIndex <= mMax) return true; + + if (mNext) return mNext->Contains(aIndex); + + return false; + } + + int32_t Count() { + int32_t total = mMax - mMin + 1; + if (mNext) total += mNext->Count(); + return total; + } + + static void CollectRanges(nsTreeRange* aRange, nsTArray& aRanges) { + nsTreeRange* cur = aRange; + while (cur) { + aRanges.AppendElement(cur->mMin); + aRanges.AppendElement(cur->mMax); + cur = cur->mNext; + } + } + + static void InvalidateRanges(XULTreeElement* aTree, + nsTArray& aRanges) { + if (aTree) { + RefPtr tree = aTree; + for (uint32_t i = 0; i < aRanges.Length(); i += 2) { + aTree->InvalidateRange(aRanges[i], aRanges[i + 1]); + } + } + } + + void Invalidate() { + nsTArray ranges; + CollectRanges(this, ranges); + InvalidateRanges(mSelection->mTree, ranges); + } + + void RemoveAllBut(int32_t aIndex) { + if (aIndex >= mMin && aIndex <= mMax) { + // Invalidate everything in this list. + nsTArray ranges; + CollectRanges(mSelection->mFirstRange, ranges); + + mMin = aIndex; + mMax = aIndex; + + nsTreeRange* first = mSelection->mFirstRange; + if (mPrev) mPrev->mNext = mNext; + if (mNext) mNext->mPrev = mPrev; + mNext = mPrev = nullptr; + + if (first != this) { + delete mSelection->mFirstRange; + mSelection->mFirstRange = this; + } + InvalidateRanges(mSelection->mTree, ranges); + } else if (mNext) + mNext->RemoveAllBut(aIndex); + } + + void Insert(nsTreeRange* aRange) { + if (mMin >= aRange->mMax) + aRange->Connect(mPrev, this); + else if (mNext) + mNext->Insert(aRange); + else + aRange->Connect(this, nullptr); + } +}; + +nsTreeSelection::nsTreeSelection(XULTreeElement* aTree) + : mTree(aTree), + mSuppressed(false), + mCurrentIndex(-1), + mShiftSelectPivot(-1), + mFirstRange(nullptr) {} + +nsTreeSelection::~nsTreeSelection() { + delete mFirstRange; + if (mSelectTimer) mSelectTimer->Cancel(); +} + +NS_IMPL_CYCLE_COLLECTION(nsTreeSelection, mTree) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeSelection) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeSelection) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeSelection) + NS_INTERFACE_MAP_ENTRY(nsITreeSelection) + NS_INTERFACE_MAP_ENTRY(nsINativeTreeSelection) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMETHODIMP nsTreeSelection::GetTree(XULTreeElement** aTree) { + NS_IF_ADDREF(*aTree = mTree); + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::SetTree(XULTreeElement* aTree) { + if (mSelectTimer) { + mSelectTimer->Cancel(); + mSelectTimer = nullptr; + } + + mTree = aTree; + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::GetSingle(bool* aSingle) { + if (!mTree) { + return NS_ERROR_NULL_POINTER; + } + + *aSingle = mTree->AttrValueIs(kNameSpaceID_None, nsGkAtoms::seltype, + u"single"_ns, eCaseMatters); + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::IsSelected(int32_t aIndex, bool* aResult) { + if (mFirstRange) + *aResult = mFirstRange->Contains(aIndex); + else + *aResult = false; + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::TimedSelect(int32_t aIndex, int32_t aMsec) { + bool suppressSelect = mSuppressed; + + if (aMsec != -1) mSuppressed = true; + + nsresult rv = Select(aIndex); + if (NS_FAILED(rv)) return rv; + + if (aMsec != -1) { + mSuppressed = suppressSelect; + if (!mSuppressed) { + if (mSelectTimer) mSelectTimer->Cancel(); + + if (!mTree) { + return NS_ERROR_UNEXPECTED; + } + nsIEventTarget* target = + mTree->OwnerDoc()->EventTargetFor(TaskCategory::Other); + NS_NewTimerWithFuncCallback(getter_AddRefs(mSelectTimer), SelectCallback, + this, aMsec, nsITimer::TYPE_ONE_SHOT, + "nsTreeSelection::SelectCallback", target); + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::Select(int32_t aIndex) { + mShiftSelectPivot = -1; + + nsresult rv = SetCurrentIndex(aIndex); + if (NS_FAILED(rv)) return rv; + + if (mFirstRange) { + bool alreadySelected = mFirstRange->Contains(aIndex); + + if (alreadySelected) { + int32_t count = mFirstRange->Count(); + if (count > 1) { + // We need to deselect everything but our item. + mFirstRange->RemoveAllBut(aIndex); + FireOnSelectHandler(); + } + return NS_OK; + } else { + // Clear out our selection. + mFirstRange->Invalidate(); + delete mFirstRange; + } + } + + // Create our new selection. + mFirstRange = new nsTreeRange(this, aIndex); + if (!mFirstRange) return NS_ERROR_OUT_OF_MEMORY; + + mFirstRange->Invalidate(); + + // Fire the select event + FireOnSelectHandler(); + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::ToggleSelect(int32_t aIndex) { + // There are six cases that can occur on a ToggleSelect with our + // range code. + // (1) A new range should be made for a selection. + // (2) A single range is removed from the selection. + // (3) The item is added to an existing range. + // (4) The item is removed from an existing range. + // (5) The addition of the item causes two ranges to be merged. + // (6) The removal of the item causes two ranges to be split. + mShiftSelectPivot = -1; + nsresult rv = SetCurrentIndex(aIndex); + if (NS_FAILED(rv)) return rv; + + if (!mFirstRange) + Select(aIndex); + else { + if (!mFirstRange->Contains(aIndex)) { + bool single; + rv = GetSingle(&single); + if (NS_SUCCEEDED(rv) && !single) rv = mFirstRange->Add(aIndex); + } else + rv = mFirstRange->Remove(aIndex); + if (NS_SUCCEEDED(rv)) { + if (mTree) mTree->InvalidateRow(aIndex); + + FireOnSelectHandler(); + } + } + + return rv; +} + +NS_IMETHODIMP nsTreeSelection::RangedSelect(int32_t aStartIndex, + int32_t aEndIndex, bool aAugment) { + bool single; + nsresult rv = GetSingle(&single); + if (NS_FAILED(rv)) return rv; + + if ((mFirstRange || (aStartIndex != aEndIndex)) && single) return NS_OK; + + if (!aAugment) { + // Clear our selection. + if (mFirstRange) { + mFirstRange->Invalidate(); + delete mFirstRange; + mFirstRange = nullptr; + } + } + + if (aStartIndex == -1) { + if (mShiftSelectPivot != -1) + aStartIndex = mShiftSelectPivot; + else if (mCurrentIndex != -1) + aStartIndex = mCurrentIndex; + else + aStartIndex = aEndIndex; + } + + mShiftSelectPivot = aStartIndex; + rv = SetCurrentIndex(aEndIndex); + if (NS_FAILED(rv)) return rv; + + int32_t start = aStartIndex < aEndIndex ? aStartIndex : aEndIndex; + int32_t end = aStartIndex < aEndIndex ? aEndIndex : aStartIndex; + + if (aAugment && mFirstRange) { + // We need to remove all the items within our selected range from the + // selection, and then we insert our new range into the list. + nsresult rv = mFirstRange->RemoveRange(start, end); + if (NS_FAILED(rv)) return rv; + } + + nsTreeRange* range = new nsTreeRange(this, start, end); + if (!range) return NS_ERROR_OUT_OF_MEMORY; + + range->Invalidate(); + + if (aAugment && mFirstRange) + mFirstRange->Insert(range); + else + mFirstRange = range; + + FireOnSelectHandler(); + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::ClearRange(int32_t aStartIndex, + int32_t aEndIndex) { + nsresult rv = SetCurrentIndex(aEndIndex); + if (NS_FAILED(rv)) return rv; + + if (mFirstRange) { + int32_t start = aStartIndex < aEndIndex ? aStartIndex : aEndIndex; + int32_t end = aStartIndex < aEndIndex ? aEndIndex : aStartIndex; + + mFirstRange->RemoveRange(start, end); + + if (mTree) mTree->InvalidateRange(start, end); + } + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::ClearSelection() { + if (mFirstRange) { + mFirstRange->Invalidate(); + delete mFirstRange; + mFirstRange = nullptr; + } + mShiftSelectPivot = -1; + + FireOnSelectHandler(); + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::SelectAll() { + if (!mTree) return NS_OK; + + nsCOMPtr view = mTree->GetView(); + if (!view) return NS_OK; + + int32_t rowCount; + view->GetRowCount(&rowCount); + bool single; + nsresult rv = GetSingle(&single); + if (NS_FAILED(rv)) return rv; + + if (rowCount == 0 || (rowCount > 1 && single)) return NS_OK; + + mShiftSelectPivot = -1; + + // Invalidate not necessary when clearing selection, since + // we're going to invalidate the world on the SelectAll. + delete mFirstRange; + + mFirstRange = new nsTreeRange(this, 0, rowCount - 1); + mFirstRange->Invalidate(); + + FireOnSelectHandler(); + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::GetRangeCount(int32_t* aResult) { + int32_t count = 0; + nsTreeRange* curr = mFirstRange; + while (curr) { + count++; + curr = curr->mNext; + } + + *aResult = count; + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::GetRangeAt(int32_t aIndex, int32_t* aMin, + int32_t* aMax) { + *aMin = *aMax = -1; + int32_t i = -1; + nsTreeRange* curr = mFirstRange; + while (curr) { + i++; + if (i == aIndex) { + *aMin = curr->mMin; + *aMax = curr->mMax; + break; + } + curr = curr->mNext; + } + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::GetCount(int32_t* count) { + if (mFirstRange) + *count = mFirstRange->Count(); + else // No range available, so there's no selected row. + *count = 0; + + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::GetSelectEventsSuppressed( + bool* aSelectEventsSuppressed) { + *aSelectEventsSuppressed = mSuppressed; + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::SetSelectEventsSuppressed( + bool aSelectEventsSuppressed) { + mSuppressed = aSelectEventsSuppressed; + if (!mSuppressed) FireOnSelectHandler(); + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::GetCurrentIndex(int32_t* aCurrentIndex) { + *aCurrentIndex = mCurrentIndex; + return NS_OK; +} + +NS_IMETHODIMP nsTreeSelection::SetCurrentIndex(int32_t aIndex) { + if (!mTree) { + return NS_ERROR_UNEXPECTED; + } + if (mCurrentIndex == aIndex) { + return NS_OK; + } + if (mCurrentIndex != -1 && mTree) mTree->InvalidateRow(mCurrentIndex); + + mCurrentIndex = aIndex; + if (!mTree) return NS_OK; + + if (aIndex != -1) mTree->InvalidateRow(aIndex); + + // Fire DOMMenuItemActive or DOMMenuItemInactive event for tree. + NS_ENSURE_STATE(mTree); + + constexpr auto DOMMenuItemActive = u"DOMMenuItemActive"_ns; + constexpr auto DOMMenuItemInactive = u"DOMMenuItemInactive"_ns; + + RefPtr asyncDispatcher = new AsyncEventDispatcher( + mTree, (aIndex != -1 ? DOMMenuItemActive : DOMMenuItemInactive), + CanBubble::eYes, ChromeOnlyDispatch::eNo); + return asyncDispatcher->PostDOMEvent(); +} + +#define ADD_NEW_RANGE(macro_range, macro_selection, macro_start, macro_end) \ + { \ + int32_t start = macro_start; \ + int32_t end = macro_end; \ + if (start > end) { \ + end = start; \ + } \ + nsTreeRange* macro_new_range = \ + new nsTreeRange(macro_selection, start, end); \ + if (macro_range) \ + macro_range->Insert(macro_new_range); \ + else \ + macro_range = macro_new_range; \ + } + +NS_IMETHODIMP +nsTreeSelection::AdjustSelection(int32_t aIndex, int32_t aCount) { + NS_ASSERTION(aCount != 0, "adjusting by zero"); + if (!aCount) return NS_OK; + + // adjust mShiftSelectPivot, if necessary + if ((mShiftSelectPivot != 1) && (aIndex <= mShiftSelectPivot)) { + // if we are deleting and the delete includes the shift select pivot, reset + // it + if (aCount < 0 && (mShiftSelectPivot <= (aIndex - aCount - 1))) { + mShiftSelectPivot = -1; + } else { + mShiftSelectPivot += aCount; + } + } + + // adjust mCurrentIndex, if necessary + if ((mCurrentIndex != -1) && (aIndex <= mCurrentIndex)) { + // if we are deleting and the delete includes the current index, reset it + if (aCount < 0 && (mCurrentIndex <= (aIndex - aCount - 1))) { + mCurrentIndex = -1; + } else { + mCurrentIndex += aCount; + } + } + + // no selection, so nothing to do. + if (!mFirstRange) return NS_OK; + + bool selChanged = false; + nsTreeRange* oldFirstRange = mFirstRange; + nsTreeRange* curr = mFirstRange; + mFirstRange = nullptr; + while (curr) { + if (aCount > 0) { + // inserting + if (aIndex > curr->mMax) { + // adjustment happens after the range, so no change + ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax); + } else if (aIndex <= curr->mMin) { + // adjustment happens before the start of the range, so shift down + ADD_NEW_RANGE(mFirstRange, this, curr->mMin + aCount, + curr->mMax + aCount); + selChanged = true; + } else { + // adjustment happen inside the range. + // break apart the range and create two ranges + ADD_NEW_RANGE(mFirstRange, this, curr->mMin, aIndex - 1); + ADD_NEW_RANGE(mFirstRange, this, aIndex + aCount, curr->mMax + aCount); + selChanged = true; + } + } else { + // deleting + if (aIndex > curr->mMax) { + // adjustment happens after the range, so no change + ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax); + } else { + // remember, aCount is negative + selChanged = true; + int32_t lastIndexOfAdjustment = aIndex - aCount - 1; + if (aIndex <= curr->mMin) { + if (lastIndexOfAdjustment < curr->mMin) { + // adjustment happens before the start of the range, so shift up + ADD_NEW_RANGE(mFirstRange, this, curr->mMin + aCount, + curr->mMax + aCount); + } else if (lastIndexOfAdjustment >= curr->mMax) { + // adjustment contains the range. remove the range by not adding it + // to the newRange + } else { + // adjustment starts before the range, and ends in the middle of it, + // so trim the range + ADD_NEW_RANGE(mFirstRange, this, aIndex, curr->mMax + aCount) + } + } else if (lastIndexOfAdjustment >= curr->mMax) { + // adjustment starts in the middle of the current range, and contains + // the end of the range, so trim the range + ADD_NEW_RANGE(mFirstRange, this, curr->mMin, aIndex - 1) + } else { + // range contains the adjustment, so shorten the range + ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax + aCount) + } + } + } + curr = curr->mNext; + } + + delete oldFirstRange; + + // Fire the select event + if (selChanged) FireOnSelectHandler(); + + return NS_OK; +} + +NS_IMETHODIMP +nsTreeSelection::InvalidateSelection() { + if (mFirstRange) mFirstRange->Invalidate(); + return NS_OK; +} + +NS_IMETHODIMP +nsTreeSelection::GetShiftSelectPivot(int32_t* aIndex) { + *aIndex = mShiftSelectPivot; + return NS_OK; +} + +nsresult nsTreeSelection::FireOnSelectHandler() { + if (mSuppressed || !mTree) { + return NS_OK; + } + + AsyncEventDispatcher::RunDOMEventWhenSafe( + *mTree, u"select"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo); + return NS_OK; +} + +void nsTreeSelection::SelectCallback(nsITimer* aTimer, void* aClosure) { + RefPtr self = static_cast(aClosure); + if (self) { + self->FireOnSelectHandler(); + aTimer->Cancel(); + self->mSelectTimer = nullptr; + } +} + +/////////////////////////////////////////////////////////////////////////////////// + +nsresult NS_NewTreeSelection(XULTreeElement* aTree, + nsITreeSelection** aResult) { + *aResult = new nsTreeSelection(aTree); + if (!*aResult) return NS_ERROR_OUT_OF_MEMORY; + NS_ADDREF(*aResult); + return NS_OK; +} diff --git a/layout/xul/tree/nsTreeSelection.h b/layout/xul/tree/nsTreeSelection.h new file mode 100644 index 0000000000..3b0eb6b21e --- /dev/null +++ b/layout/xul/tree/nsTreeSelection.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeSelection_h__ +#define nsTreeSelection_h__ + +#include "nsITreeSelection.h" +#include "nsITimer.h" +#include "nsCycleCollectionParticipant.h" +#include "mozilla/Attributes.h" +#include "XULTreeElement.h" + +class nsTreeColumn; +struct nsTreeRange; + +class nsTreeSelection final : public nsINativeTreeSelection { + public: + explicit nsTreeSelection(mozilla::dom::XULTreeElement* aTree); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(nsTreeSelection) + NS_DECL_NSITREESELECTION + + // nsINativeTreeSelection: Untrusted code can use us + NS_IMETHOD EnsureNative() override { return NS_OK; } + + friend struct nsTreeRange; + + protected: + ~nsTreeSelection(); + + nsresult FireOnSelectHandler(); + static void SelectCallback(nsITimer* aTimer, void* aClosure); + + protected: + // The tree will hold on to us through the view and let go when it dies. + RefPtr mTree; + + bool mSuppressed; // Whether or not we should be firing onselect events. + int32_t mCurrentIndex; // The item to draw the rect around. The last one + // clicked, etc. + int32_t mShiftSelectPivot; // Used when multiple SHIFT+selects are performed + // to pivot on. + + nsTreeRange* mFirstRange; // Our list of ranges. + + nsCOMPtr mSelectTimer; +}; + +nsresult NS_NewTreeSelection(mozilla::dom::XULTreeElement* aTree, + nsITreeSelection** aResult); + +#endif diff --git a/layout/xul/tree/nsTreeStyleCache.cpp b/layout/xul/tree/nsTreeStyleCache.cpp new file mode 100644 index 0000000000..d5823ac38e --- /dev/null +++ b/layout/xul/tree/nsTreeStyleCache.cpp @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsTreeStyleCache.h" +#include "mozilla/ComputedStyleInlines.h" +#include "mozilla/dom/Element.h" +#include "mozilla/ServoStyleSet.h" +#include "nsPresContextInlines.h" + +using namespace mozilla; + +nsTreeStyleCache::Transition::Transition(DFAState aState, nsAtom* aSymbol) + : mState(aState), mInputSymbol(aSymbol) {} + +bool nsTreeStyleCache::Transition::operator==(const Transition& aOther) const { + return aOther.mState == mState && aOther.mInputSymbol == mInputSymbol; +} + +uint32_t nsTreeStyleCache::Transition::Hash() const { + // Make a 32-bit integer that combines the low-order 16 bits of the state and + // the input symbol. + uint32_t hb = mState << 16; + uint32_t lb = (NS_PTR_TO_UINT32(mInputSymbol.get()) << 16) >> 16; + return hb + lb; +} + +// The ComputedStyle cache impl +ComputedStyle* nsTreeStyleCache::GetComputedStyle( + nsPresContext* aPresContext, nsIContent* aContent, ComputedStyle* aStyle, + nsCSSAnonBoxPseudoStaticAtom* aPseudoElement, const AtomArray& aInputWord) { + MOZ_ASSERT(nsCSSAnonBoxes::IsTreePseudoElement(aPseudoElement)); + + uint32_t count = aInputWord.Length(); + + // Go ahead and init the transition table. + if (!mTransitionTable) { + // Automatic miss. Build the table + mTransitionTable = MakeUnique(); + } + + // The first transition is always made off the supplied pseudo-element. + Transition transition(0, aPseudoElement); + DFAState currState = mTransitionTable->Get(transition); + + if (!currState) { + // We had a miss. Make a new state and add it to our hash. + currState = mNextState; + mNextState++; + mTransitionTable->InsertOrUpdate(transition, currState); + } + + for (uint32_t i = 0; i < count; i++) { + Transition transition(currState, aInputWord[i]); + currState = mTransitionTable->Get(transition); + + if (!currState) { + // We had a miss. Make a new state and add it to our hash. + currState = mNextState; + mNextState++; + mTransitionTable->InsertOrUpdate(transition, currState); + } + } + + // We're in a final state. + // Look up our ComputedStyle for this state. + ComputedStyle* result = nullptr; + if (mCache) { + result = mCache->GetWeak(currState); + } + if (!result) { + // We missed the cache. Resolve this pseudo-style. + RefPtr newResult = + aPresContext->StyleSet()->ResolveXULTreePseudoStyle( + aContent->AsElement(), aPseudoElement, aStyle, aInputWord); + + // Normally we rely on nsIFrame::Init / RestyleManager to call this, but + // these are weird and don't use a frame, yet ::-moz-tree-twisty definitely + // pokes at list-style-image. + newResult->StartImageLoads(*aPresContext->Document()); + + // Even though xul-tree pseudos are defined in nsCSSAnonBoxList, nothing has + // ever treated them as an anon box, and they don't ever get boxes anyway. + // + // This is really weird, and probably nothing really relies on the result of + // these assert, but it's here just to avoid changing them accidentally. + MOZ_ASSERT(newResult->GetPseudoType() == PseudoStyleType::XULTree); + MOZ_ASSERT(!newResult->IsAnonBox()); + MOZ_ASSERT(!newResult->IsPseudoElement()); + + // Put the ComputedStyle in our table, transferring the owning reference to + // the table. + if (!mCache) { + mCache = MakeUnique(); + } + result = newResult.get(); + mCache->InsertOrUpdate(currState, std::move(newResult)); + } + + return result; +} diff --git a/layout/xul/tree/nsTreeStyleCache.h b/layout/xul/tree/nsTreeStyleCache.h new file mode 100644 index 0000000000..b0c0e4d9dc --- /dev/null +++ b/layout/xul/tree/nsTreeStyleCache.h @@ -0,0 +1,82 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeStyleCache_h__ +#define nsTreeStyleCache_h__ + +#include "mozilla/AtomArray.h" +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" +#include "nsCOMArray.h" +#include "nsTHashMap.h" +#include "nsRefPtrHashtable.h" +#include "mozilla/ComputedStyle.h" + +class nsIContent; + +class nsTreeStyleCache { + public: + nsTreeStyleCache() : mNextState(0) {} + + ~nsTreeStyleCache() { Clear(); } + + void Clear() { + mTransitionTable = nullptr; + mCache = nullptr; + mNextState = 0; + } + + mozilla::ComputedStyle* GetComputedStyle( + nsPresContext* aPresContext, nsIContent* aContent, + mozilla::ComputedStyle* aStyle, + nsCSSAnonBoxPseudoStaticAtom* aPseudoElement, + const mozilla::AtomArray& aInputWord); + + protected: + typedef uint32_t DFAState; + + class Transition final { + public: + Transition(DFAState aState, nsAtom* aSymbol); + bool operator==(const Transition& aOther) const; + uint32_t Hash() const; + + private: + DFAState mState; + RefPtr mInputSymbol; + }; + + typedef nsTHashMap, DFAState> TransitionTable; + + // A transition table for a deterministic finite automaton. The DFA + // takes as its input a single pseudoelement and an ordered set of properties. + // It transitions on an input word that is the concatenation of the + // pseudoelement supplied with the properties in the array. + // + // It transitions from state to state by looking up entries in the transition + // table (which is a mapping from (S,i)->S', where S is the current state, i + // is the next property in the input word, and S' is the state to transition + // to. + // + // If S' is not found, it is constructed and entered into the hashtable + // under the key (S,i). + // + // Once the entire word has been consumed, the final state is used + // to reference the cache table to locate the ComputedStyle. + mozilla::UniquePtr mTransitionTable; + + // The cache of all active ComputedStyles. This is a hash from + // a final state in the DFA, Sf, to the resultant ComputedStyle. + typedef nsRefPtrHashtable + ComputedStyleCache; + mozilla::UniquePtr mCache; + + // An integer counter that is used when we need to make new states in the + // DFA. + DFAState mNextState; +}; + +#endif // nsTreeStyleCache_h__ diff --git a/layout/xul/tree/nsTreeUtils.cpp b/layout/xul/tree/nsTreeUtils.cpp new file mode 100644 index 0000000000..10767e22d2 --- /dev/null +++ b/layout/xul/tree/nsTreeUtils.cpp @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsReadableUtils.h" +#include "nsTreeUtils.h" +#include "ChildIterator.h" +#include "nsCRT.h" +#include "nsAtom.h" +#include "nsNameSpaceManager.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" + +using namespace mozilla; + +nsresult nsTreeUtils::TokenizeProperties(const nsAString& aProperties, + AtomArray& aPropertiesArray) { + nsAString::const_iterator end; + aProperties.EndReading(end); + + nsAString::const_iterator iter; + aProperties.BeginReading(iter); + + do { + // Skip whitespace + while (iter != end && nsCRT::IsAsciiSpace(*iter)) ++iter; + + // If only whitespace, we're done + if (iter == end) break; + + // Note the first non-whitespace character + nsAString::const_iterator first = iter; + + // Advance to the next whitespace character + while (iter != end && !nsCRT::IsAsciiSpace(*iter)) ++iter; + + // XXX this would be nonsensical + NS_ASSERTION(iter != first, "eh? something's wrong here"); + if (iter == first) break; + + RefPtr atom = NS_Atomize(Substring(first, iter)); + aPropertiesArray.AppendElement(atom); + } while (iter != end); + + return NS_OK; +} + +nsIContent* nsTreeUtils::GetImmediateChild(nsIContent* aContainer, + nsAtom* aTag) { + dom::FlattenedChildIterator iter(aContainer); + for (nsIContent* child = iter.GetNextChild(); child; + child = iter.GetNextChild()) { + if (child->IsXULElement(aTag)) { + return child; + } + // is in the flattened tree, but code is used to work with + // which is not, so recurse in here. + if (child->IsHTMLElement(nsGkAtoms::slot)) { + if (nsIContent* c = GetImmediateChild(child, aTag)) { + return c; + } + } + } + + return nullptr; +} + +nsIContent* nsTreeUtils::GetDescendantChild(nsIContent* aContainer, + nsAtom* aTag) { + dom::FlattenedChildIterator iter(aContainer); + for (nsIContent* child = iter.GetNextChild(); child; + child = iter.GetNextChild()) { + if (child->IsXULElement(aTag)) { + return child; + } + + child = GetDescendantChild(child, aTag); + if (child) { + return child; + } + } + + return nullptr; +} + +nsresult nsTreeUtils::UpdateSortIndicators(dom::Element* aColumn, + const nsAString& aDirection) { + aColumn->SetAttr(kNameSpaceID_None, nsGkAtoms::sortDirection, aDirection, + true); + aColumn->SetAttr(kNameSpaceID_None, nsGkAtoms::sortActive, u"true"_ns, true); + + // Unset sort attribute(s) on the other columns + nsCOMPtr parentContent = aColumn->GetParent(); + if (parentContent && parentContent->NodeInfo()->Equals(nsGkAtoms::treecols, + kNameSpaceID_XUL)) { + for (nsINode* childContent = parentContent->GetFirstChild(); childContent; + childContent = childContent->GetNextSibling()) { + if (childContent != aColumn && + childContent->NodeInfo()->Equals(nsGkAtoms::treecol, + kNameSpaceID_XUL)) { + childContent->AsElement()->UnsetAttr(kNameSpaceID_None, + nsGkAtoms::sortDirection, true); + childContent->AsElement()->UnsetAttr(kNameSpaceID_None, + nsGkAtoms::sortActive, true); + } + } + } + + return NS_OK; +} + +nsresult nsTreeUtils::GetColumnIndex(dom::Element* aColumn, int32_t* aResult) { + nsIContent* parentContent = aColumn->GetParent(); + if (parentContent && parentContent->NodeInfo()->Equals(nsGkAtoms::treecols, + kNameSpaceID_XUL)) { + int32_t colIndex = 0; + + for (nsINode* childContent = parentContent->GetFirstChild(); childContent; + childContent = childContent->GetNextSibling()) { + if (childContent->NodeInfo()->Equals(nsGkAtoms::treecol, + kNameSpaceID_XUL)) { + if (childContent == aColumn) { + *aResult = colIndex; + return NS_OK; + } + ++colIndex; + } + } + } + + *aResult = -1; + return NS_OK; +} diff --git a/layout/xul/tree/nsTreeUtils.h b/layout/xul/tree/nsTreeUtils.h new file mode 100644 index 0000000000..d0588f1273 --- /dev/null +++ b/layout/xul/tree/nsTreeUtils.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsTreeUtils_h__ +#define nsTreeUtils_h__ + +#include "mozilla/AtomArray.h" +#include "nsError.h" +#include "nsString.h" +#include "nsTreeStyleCache.h" + +class nsAtom; +class nsIContent; +namespace mozilla { +namespace dom { +class Element; +} +} // namespace mozilla + +class nsTreeUtils { + public: + /** + * Parse a whitespace separated list of properties into an array + * of atoms. + */ + static nsresult TokenizeProperties(const nsAString& aProperties, + mozilla::AtomArray& aPropertiesArray); + + static nsIContent* GetImmediateChild(nsIContent* aContainer, nsAtom* aTag); + + static nsIContent* GetDescendantChild(nsIContent* aContainer, nsAtom* aTag); + + static nsresult UpdateSortIndicators(mozilla::dom::Element* aColumn, + const nsAString& aDirection); + + static nsresult GetColumnIndex(mozilla::dom::Element* aColumn, + int32_t* aResult); +}; + +#endif // nsTreeUtils_h__ -- cgit v1.2.3