tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

nsComboboxControlFrame.cpp (17544B)


      1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
      2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
      3 /* This Source Code Form is subject to the terms of the Mozilla Public
      4 * License, v. 2.0. If a copy of the MPL was not distributed with this
      5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      6 
      7 #include "nsComboboxControlFrame.h"
      8 
      9 #include <algorithm>
     10 
     11 #include "HTMLSelectEventListener.h"
     12 #include "gfxContext.h"
     13 #include "gfxUtils.h"
     14 #include "mozilla/AsyncEventDispatcher.h"
     15 #include "mozilla/Likely.h"
     16 #include "mozilla/PresShell.h"
     17 #include "mozilla/PresShellInlines.h"
     18 #include "mozilla/dom/Document.h"
     19 #include "mozilla/dom/HTMLSelectElement.h"
     20 #include "nsCOMPtr.h"
     21 #include "nsContentUtils.h"
     22 #include "nsGkAtoms.h"
     23 #include "nsISelectControlFrame.h"
     24 #include "nsITheme.h"
     25 #include "nsLayoutUtils.h"
     26 #include "nsStyleConsts.h"
     27 #include "nsTextFrameUtils.h"
     28 #include "nsTextNode.h"
     29 #include "nsTextRunTransformations.h"
     30 
     31 using namespace mozilla;
     32 using namespace mozilla::gfx;
     33 
     34 NS_IMETHODIMP
     35 nsComboboxControlFrame::RedisplayTextEvent::Run() {
     36  if (mControlFrame) {
     37    mControlFrame->HandleRedisplayTextEvent();
     38  }
     39  return NS_OK;
     40 }
     41 
     42 // Drop down list event management.
     43 // The combo box uses the following strategy for managing the drop-down list.
     44 // If the combo box or its arrow button is clicked on the drop-down list is
     45 // displayed If mouse exits the combo box with the drop-down list displayed the
     46 // drop-down list is asked to capture events The drop-down list will capture all
     47 // events including mouse down and up and will always return with
     48 // ListWasSelected method call regardless of whether an item in the list was
     49 // actually selected.
     50 // The ListWasSelected code will turn off mouse-capture for the drop-down list.
     51 // The drop-down list does not explicitly set capture when it is in the
     52 // drop-down mode.
     53 
     54 nsComboboxControlFrame* NS_NewComboboxControlFrame(PresShell* aPresShell,
     55                                                   ComputedStyle* aStyle) {
     56  return new (aPresShell)
     57      nsComboboxControlFrame(aStyle, aPresShell->GetPresContext());
     58 }
     59 
     60 NS_IMPL_FRAMEARENA_HELPERS(nsComboboxControlFrame)
     61 
     62 nsComboboxControlFrame::nsComboboxControlFrame(ComputedStyle* aStyle,
     63                                               nsPresContext* aPresContext)
     64    : ButtonControlFrame(aStyle, aPresContext, kClassID) {}
     65 
     66 nsComboboxControlFrame::~nsComboboxControlFrame() = default;
     67 
     68 NS_QUERYFRAME_HEAD(nsComboboxControlFrame)
     69  NS_QUERYFRAME_ENTRY(nsComboboxControlFrame)
     70  NS_QUERYFRAME_ENTRY(nsISelectControlFrame)
     71 NS_QUERYFRAME_TAIL_INHERITING(ButtonControlFrame)
     72 
     73 #ifdef ACCESSIBILITY
     74 a11y::AccType nsComboboxControlFrame::AccessibleType() {
     75  return a11y::eHTMLComboboxType;
     76 }
     77 #endif
     78 
     79 bool nsComboboxControlFrame::HasDropDownButton() const {
     80  const nsStyleDisplay* disp = StyleDisplay();
     81  switch (disp->EffectiveAppearance()) {
     82    case StyleAppearance::MenulistButton:
     83      return true;
     84    case StyleAppearance::Menulist:
     85      return !IsThemed(disp) ||
     86             PresContext()->Theme()->ThemeNeedsComboboxDropmarker();
     87    default:
     88      return false;
     89  }
     90 }
     91 
     92 nscoord nsComboboxControlFrame::DropDownButtonISize() {
     93  if (!HasDropDownButton()) {
     94    return 0;
     95  }
     96 
     97  nsPresContext* pc = PresContext();
     98  LayoutDeviceIntSize dropdownButtonSize = pc->Theme()->GetMinimumWidgetSize(
     99      pc, this, StyleAppearance::MozMenulistArrowButton);
    100  return pc->DevPixelsToAppUnits(dropdownButtonSize.width);
    101 }
    102 
    103 int32_t nsComboboxControlFrame::CharCountOfLargestOptionForInflation() const {
    104  uint32_t maxLength = 0;
    105  nsAutoString label;
    106  for (auto i : IntegerRange(Select().Options()->Length())) {
    107    GetOptionText(i, label);
    108    maxLength = std::max(
    109        maxLength,
    110        nsTextFrameUtils::ComputeApproximateLengthWithWhitespaceCompression(
    111            label, StyleText()));
    112  }
    113  if (MOZ_UNLIKELY(maxLength > uint32_t(INT32_MAX))) {
    114    return INT32_MAX;
    115  }
    116  return int32_t(maxLength);
    117 }
    118 
    119 nscoord nsComboboxControlFrame::GetOptionISize(gfxContext* aRenderingContext,
    120                                               Type aType) const {
    121  // Compute the width of each option's (potentially text-transformed) text,
    122  // and use the widest one as part of our intrinsic size.
    123  nscoord maxOptionSize = 0;
    124  nsAutoString label;
    125  nsAutoString transformedLabel;
    126  RefPtr<nsFontMetrics> fm =
    127      nsLayoutUtils::GetInflatedFontMetricsForFrame(this);
    128  const nsStyleText* textStyle = StyleText();
    129  auto textTransform = textStyle->mTextTransform.IsNone()
    130                           ? Nothing()
    131                           : Some(textStyle->mTextTransform);
    132  nsAtom* language = StyleFont()->mLanguage;
    133  AutoTArray<bool, 50> charsToMergeArray;
    134  AutoTArray<bool, 50> deletedCharsArray;
    135  auto GetOptionSize = [&](uint32_t aIndex) -> nscoord {
    136    GetOptionText(aIndex, label);
    137    const nsAutoString* stringToUse = &label;
    138    if (textTransform ||
    139        textStyle->mWebkitTextSecurity != StyleTextSecurity::None) {
    140      transformedLabel.Truncate();
    141      charsToMergeArray.SetLengthAndRetainStorage(0);
    142      deletedCharsArray.SetLengthAndRetainStorage(0);
    143      nsCaseTransformTextRunFactory::TransformString(
    144          label, transformedLabel, textTransform,
    145          textStyle->TextSecurityMaskChar(),
    146          /* aCaseTransformsOnly = */ false, language, charsToMergeArray,
    147          deletedCharsArray);
    148      stringToUse = &transformedLabel;
    149    }
    150    return nsLayoutUtils::AppUnitWidthOfStringBidi(*stringToUse, this, *fm,
    151                                                   *aRenderingContext);
    152  };
    153  if (aType == Type::Longest) {
    154    for (auto i : IntegerRange(Select().Options()->Length())) {
    155      maxOptionSize = std::max(maxOptionSize, GetOptionSize(i));
    156    }
    157  } else {
    158    maxOptionSize = GetOptionSize(mDisplayedIndex);
    159  }
    160  if (maxOptionSize) {
    161    // HACK: Add one app unit to workaround silly Netgear router styling, see
    162    // bug 1769580. In practice since this comes from font metrics is unlikely
    163    // to be perceivable.
    164    maxOptionSize += 1;
    165  }
    166  return maxOptionSize;
    167 }
    168 
    169 nscoord nsComboboxControlFrame::IntrinsicISize(const IntrinsicSizeInput& aInput,
    170                                               IntrinsicISizeType aType) {
    171  Maybe<nscoord> containISize = ContainIntrinsicISize(NS_UNCONSTRAINEDSIZE);
    172  if (containISize && *containISize != NS_UNCONSTRAINEDSIZE) {
    173    return *containISize;
    174  }
    175 
    176  nscoord displayISize = 0;
    177  if (!containISize) {
    178    auto optionType = StyleUIReset()->mFieldSizing == StyleFieldSizing::Content
    179                          ? Type::Current
    180                          : Type::Longest;
    181    displayISize += GetOptionISize(aInput.mContext, optionType);
    182  }
    183 
    184  // Add room for the dropmarker button (if there is one).
    185  displayISize += DropDownButtonISize();
    186  return displayISize;
    187 }
    188 
    189 dom::HTMLSelectElement& nsComboboxControlFrame::Select() const {
    190  return *static_cast<dom::HTMLSelectElement*>(GetContent());
    191 }
    192 
    193 void nsComboboxControlFrame::GetOptionText(uint32_t aIndex,
    194                                           nsAString& aText) const {
    195  aText.Truncate();
    196  if (Element* el = Select().Options()->GetElementAt(aIndex)) {
    197    static_cast<dom::HTMLOptionElement*>(el)->GetRenderedLabel(aText);
    198  }
    199 }
    200 
    201 void nsComboboxControlFrame::Reflow(nsPresContext* aPresContext,
    202                                    ReflowOutput& aDesiredSize,
    203                                    const ReflowInput& aReflowInput,
    204                                    nsReflowStatus& aStatus) {
    205  // We don't call MarkInReflow() here; that happens in our superclass's
    206  // implementation of Reflow (which we invoke further down).
    207  MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
    208  // Constraints we try to satisfy:
    209 
    210  // 1) Default inline size of button is the vertical scrollbar size
    211  // 2) If the inline size of button is bigger than our inline size, set
    212  //    inline size of button to 0.
    213  // 3) Default block size of button is block size of display area
    214  // 4) Inline size of display area is whatever is left over from our
    215  //    inline size after allocating inline size for the button.
    216  WritingMode wm = aReflowInput.GetWritingMode();
    217 
    218  // Check if the theme specifies a minimum size for the dropdown button
    219  // first.
    220  const nscoord buttonISize = DropDownButtonISize();
    221  const auto padding = aReflowInput.ComputedLogicalPadding(wm);
    222 
    223  // We ignore inline-end-padding (by adding it to our label box size) if we
    224  // have a dropdown button, so that the button aligns with the end of the
    225  // padding box.
    226  mDisplayISize = aReflowInput.ComputedISize() - buttonISize;
    227  if (buttonISize) {
    228    mDisplayISize += padding.IEnd(wm);
    229  }
    230 
    231  ButtonControlFrame::Reflow(aPresContext, aDesiredSize, aReflowInput, aStatus);
    232 }
    233 
    234 void nsComboboxControlFrame::Init(nsIContent* aContent,
    235                                  nsContainerFrame* aParent,
    236                                  nsIFrame* aPrevInFlow) {
    237  ButtonControlFrame::Init(aContent, aParent, aPrevInFlow);
    238  mEventListener = new HTMLSelectEventListener(
    239      Select(), HTMLSelectEventListener::SelectType::Combobox);
    240  mDisplayedIndex = Select().SelectedIndex();
    241 }
    242 
    243 nsresult nsComboboxControlFrame::RedisplaySelectedText() {
    244  nsAutoScriptBlocker scriptBlocker;
    245  mDisplayedIndex = Select().SelectedIndex();
    246  return RedisplayText();
    247 }
    248 
    249 nsresult nsComboboxControlFrame::RedisplayText() {
    250  nsAutoString currentLabel;
    251  mDisplayLabel->GetFirstChild()->AsText()->GetData(currentLabel);
    252 
    253  nsAutoString newLabel;
    254  GetLabelText(newLabel);
    255 
    256  // Revoke outstanding events to avoid out-of-order events which could mean
    257  // displaying the wrong text.
    258  mRedisplayTextEvent.Revoke();
    259 
    260  if (currentLabel == newLabel) {
    261    return NS_OK;
    262  }
    263 
    264  NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(),
    265               "If we happen to run our redisplay event now, we might kill "
    266               "ourselves!");
    267  mRedisplayTextEvent = new RedisplayTextEvent(this);
    268  nsContentUtils::AddScriptRunner(mRedisplayTextEvent.get());
    269  return NS_OK;
    270 }
    271 
    272 void nsComboboxControlFrame::UpdateLabelText() {
    273  RefPtr<dom::Text> displayContent = mDisplayLabel->GetFirstChild()->AsText();
    274  nsAutoString newLabel;
    275  GetLabelText(newLabel);
    276  displayContent->SetText(newLabel, true);
    277 }
    278 
    279 void nsComboboxControlFrame::HandleRedisplayTextEvent() {
    280  // First, make sure that the content model is up to date and we've constructed
    281  // the frames for all our content in the right places. Otherwise they'll end
    282  // up under the wrong insertion frame when we UpdateLabel, since that
    283  // flushes out the content sink by calling SetText on a DOM node with aNotify
    284  // set to true.  See bug 289730.
    285  AutoWeakFrame weakThis(this);
    286  PresContext()->Document()->FlushPendingNotifications(
    287      FlushType::ContentAndNotify);
    288  if (!weakThis.IsAlive()) {
    289    return;
    290  }
    291  mRedisplayTextEvent.Forget();
    292  UpdateLabelText();
    293  // Note: `this` might be dead here.
    294 }
    295 
    296 void nsComboboxControlFrame::GetLabelText(nsAString& aLabel) {
    297  Select().GetPreviewValue(aLabel);
    298  // Get the text to display
    299  if (!aLabel.IsEmpty()) {
    300    return;
    301  }
    302  if (mDisplayedIndex != -1) {
    303    GetOptionText(mDisplayedIndex, aLabel);
    304  }
    305  EnsureNonEmptyLabel(aLabel);
    306 }
    307 
    308 bool nsComboboxControlFrame::IsDroppedDown() const {
    309  return Select().OpenInParentProcess();
    310 }
    311 
    312 //----------------------------------------------------------------------
    313 // nsISelectControlFrame
    314 //----------------------------------------------------------------------
    315 NS_IMETHODIMP
    316 nsComboboxControlFrame::DoneAddingChildren(bool aIsDone) { return NS_OK; }
    317 
    318 NS_IMETHODIMP
    319 nsComboboxControlFrame::AddOption(int32_t aIndex) {
    320  if (aIndex <= mDisplayedIndex) {
    321    ++mDisplayedIndex;
    322  }
    323 
    324  return NS_OK;
    325 }
    326 
    327 NS_IMETHODIMP
    328 nsComboboxControlFrame::RemoveOption(int32_t aIndex) {
    329  if (Select().Options()->Length()) {
    330    if (aIndex < mDisplayedIndex) {
    331      --mDisplayedIndex;
    332    } else if (aIndex == mDisplayedIndex) {
    333      mDisplayedIndex = 0;  // IE6 compat
    334      RedisplayText();
    335    }
    336  } else {
    337    // If we removed the last option, we need to blank things out
    338    mDisplayedIndex = -1;
    339    RedisplayText();
    340  }
    341  return NS_OK;
    342 }
    343 
    344 NS_IMETHODIMP_(void)
    345 nsComboboxControlFrame::OnSetSelectedIndex(int32_t aOldIndex,
    346                                           int32_t aNewIndex) {
    347  nsAutoScriptBlocker scriptBlocker;
    348  mDisplayedIndex = aNewIndex;
    349  RedisplayText();
    350 }
    351 
    352 // End nsISelectControlFrame
    353 //----------------------------------------------------------------------
    354 
    355 nsresult nsComboboxControlFrame::HandleEvent(nsPresContext* aPresContext,
    356                                             WidgetGUIEvent* aEvent,
    357                                             nsEventStatus* aEventStatus) {
    358  NS_ENSURE_ARG_POINTER(aEventStatus);
    359 
    360  if (nsEventStatus_eConsumeNoDefault == *aEventStatus) {
    361    return NS_OK;
    362  }
    363 
    364  return ButtonControlFrame::HandleEvent(aPresContext, aEvent, aEventStatus);
    365 }
    366 
    367 nsresult nsComboboxControlFrame::CreateAnonymousContent(
    368    nsTArray<ContentInfo>& aElements) {
    369  dom::Document* doc = mContent->OwnerDoc();
    370  mDisplayLabel = doc->CreateHTMLElement(nsGkAtoms::label);
    371  {
    372    RefPtr<nsTextNode> text = doc->CreateEmptyTextNode();
    373    mDisplayLabel->AppendChildTo(text, false, IgnoreErrors());
    374    // set the value of the text node
    375    UpdateLabelText();
    376  }
    377  aElements.AppendElement(mDisplayLabel);
    378 
    379  if (HasDropDownButton()) {
    380    mButtonContent = mContent->OwnerDoc()->CreateHTMLElement(nsGkAtoms::button);
    381    {
    382      // This gives the button a reasonable height. This could be done via CSS
    383      // instead, but relative font units like 1lh don't play very well with our
    384      // font inflation implementation, so we do it this way instead.
    385      RefPtr<nsTextNode> text = doc->CreateTextNode(u"\ufeff"_ns);
    386      mButtonContent->AppendChildTo(text, false, IgnoreErrors());
    387    }
    388    // Make someone to listen to the button.
    389    mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::type, u"button"_ns,
    390                            false);
    391    // Set tabindex="-1" so that the button is not tabbable
    392    mButtonContent->SetAttr(kNameSpaceID_None, nsGkAtoms::tabindex, u"-1"_ns,
    393                            false);
    394    aElements.AppendElement(mButtonContent);
    395  }
    396 
    397  return NS_OK;
    398 }
    399 
    400 void nsComboboxControlFrame::AppendAnonymousContentTo(
    401    nsTArray<nsIContent*>& aElements, uint32_t aFilter) {
    402  if (mDisplayLabel) {
    403    aElements.AppendElement(mDisplayLabel);
    404  }
    405 
    406  if (mButtonContent) {
    407    aElements.AppendElement(mButtonContent);
    408  }
    409 }
    410 
    411 namespace mozilla {
    412 
    413 class ComboboxLabelFrame final : public nsBlockFrame {
    414 public:
    415  NS_DECL_QUERYFRAME
    416  NS_DECL_FRAMEARENA_HELPERS(ComboboxLabelFrame)
    417 
    418 #ifdef DEBUG_FRAME_DUMP
    419  nsresult GetFrameName(nsAString& aResult) const final {
    420    return MakeFrameName(u"ComboboxLabel"_ns, aResult);
    421  }
    422 #endif
    423 
    424  void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize,
    425              const ReflowInput& aReflowInput, nsReflowStatus& aStatus) final;
    426 
    427 public:
    428  ComboboxLabelFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
    429      : nsBlockFrame(aStyle, aPresContext, kClassID) {}
    430 };
    431 
    432 NS_QUERYFRAME_HEAD(ComboboxLabelFrame)
    433  NS_QUERYFRAME_ENTRY(ComboboxLabelFrame)
    434 NS_QUERYFRAME_TAIL_INHERITING(nsBlockFrame)
    435 NS_IMPL_FRAMEARENA_HELPERS(ComboboxLabelFrame)
    436 
    437 void ComboboxLabelFrame::Reflow(nsPresContext* aPresContext,
    438                                ReflowOutput& aDesiredSize,
    439                                const ReflowInput& aReflowInput,
    440                                nsReflowStatus& aStatus) {
    441  MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
    442 
    443  const nsComboboxControlFrame* combobox = do_QueryFrame(GetParent());
    444  MOZ_ASSERT(combobox, "Combobox's frame tree is wrong!");
    445  MOZ_ASSERT(aReflowInput.ComputedPhysicalBorderPadding() == nsMargin(),
    446             "We shouldn't have border and padding in UA!");
    447 
    448  ReflowInput state(aReflowInput);
    449  state.SetComputedISize(combobox->mDisplayISize);
    450  nsBlockFrame::Reflow(aPresContext, aDesiredSize, state, aStatus);
    451  aStatus.Reset();  // this type of frame can't be split
    452 }
    453 
    454 }  // namespace mozilla
    455 
    456 nsIFrame* NS_NewComboboxLabelFrame(PresShell* aPresShell,
    457                                   ComputedStyle* aStyle) {
    458  return new (aPresShell)
    459      ComboboxLabelFrame(aStyle, aPresShell->GetPresContext());
    460 }
    461 
    462 void nsComboboxControlFrame::Destroy(DestroyContext& aContext) {
    463  // Revoke any pending RedisplayTextEvent
    464  mRedisplayTextEvent.Revoke();
    465  mEventListener->Detach();
    466 
    467  aContext.AddAnonymousContent(mDisplayLabel.forget());
    468  aContext.AddAnonymousContent(mButtonContent.forget());
    469  ButtonControlFrame::Destroy(aContext);
    470 }
    471 
    472 //---------------------------------------------------------
    473 // gets the content (an option) by index and then set it as
    474 // being selected or not selected
    475 //---------------------------------------------------------
    476 NS_IMETHODIMP
    477 nsComboboxControlFrame::OnOptionSelected(int32_t aIndex, bool aSelected) {
    478  if (aSelected) {
    479    nsAutoScriptBlocker blocker;
    480    mDisplayedIndex = aIndex;
    481    RedisplayText();
    482  } else {
    483    AutoWeakFrame weakFrame(this);
    484    RedisplaySelectedText();
    485    if (weakFrame.IsAlive()) {
    486      FireValueChangeEvent();  // Fire after old option is unselected
    487    }
    488  }
    489  return NS_OK;
    490 }
    491 
    492 void nsComboboxControlFrame::FireValueChangeEvent() {
    493  // Fire ValueChange event to indicate data value of combo box has changed
    494  // FIXME(emilio): This shouldn't be exposed to content.
    495  nsContentUtils::AddScriptRunner(new AsyncEventDispatcher(
    496      mContent, u"ValueChange"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo));
    497 }