// Minimal repro: destroying a TableListBox while the accessibility focus sits in a cell // component trips ListBox::checkModelPtrIsValid() (juce_ListBox.cpp), because the // TableListBox's private ListBoxModel base is destroyed before its ListBox base deletes the rows. // // Default: automatic. After the window is shown, the focus is given to the slider in row 1, // then the table is destroyed while the user model is still alive. // --manual: click a slider in the table, then click "Destroy table" (that button does not take // the keyboard focus, so the focus stays in the cell). // Needs juce_gui_basics only. Run a Debug build. #include namespace { struct Model final : juce::TableListBoxModel { int getNumRows() override { return 2; } void paintRowBackground (juce::Graphics&, int, int, int, bool) override {} void paintCell (juce::Graphics&, int, int, int, int, bool) override {} juce::Component* refreshComponentForCell (int, int, bool, juce::Component* existing) override { return existing != nullptr ? existing : new juce::Slider(); } }; struct Content final : juce::Component, private juce::Timer { explicit Content (bool manualIn) : manual (manualIn) { table = std::make_unique ("table", &model); table->getHeader().addColumn ("col", 1, 280); table->getHeader().setStretchToFitActive (true); addAndMakeVisible (*table); destroyButton.setWantsKeyboardFocus (false); destroyButton.onClick = [this] { destroyTable(); }; addAndMakeVisible (destroyButton); setSize (300, 200); if (! manual) startTimer (500); } void resized() override { auto r = getLocalBounds(); destroyButton.setBounds (r.removeFromBottom (30).reduced (4)); if (table != nullptr) table->setBounds (r); } void timerCallback() override { stopTimer(); // Accessibility handlers are created lazily. Among other things, a setBounds on a component // that sits on a peer creates one. Resizing the window, as a user would, gives one to the // slider's text-box Label. That Label is the non-focused handler whose destruction walks // up from the focused slider. if (auto* window = getTopLevelComponent()) window->setSize (window->getWidth() + 40, window->getHeight()); auto* cell = table->getCellComponent (1, 1); jassert (cell != nullptr); // What a mouse click on the slider does: keyboard focus, hence accessibility focus. if (auto* handler = cell->getAccessibilityHandler()) handler->grabFocus(); destroyTable(); juce::JUCEApplication::quit(); } void destroyTable() { if (table == nullptr) return; auto* focused = juce::Component::getCurrentlyFocusedComponent(); auto* handler = table->getAccessibilityHandler(); std::fprintf (stderr, "focused component: %s, table has accessibility focus in a child: %s\n", focused != nullptr ? typeid (*focused).name() : "none", handler != nullptr && handler->hasFocus (true) ? "yes" : "no"); std::fprintf (stderr, "destroying table (model still alive)...\n"); table.reset(); std::fprintf (stderr, "table destroyed\n"); } const bool manual; Model model; // Declared before the table: it outlives it. std::unique_ptr table; juce::TextButton destroyButton { "Destroy table" }; }; struct App final : juce::JUCEApplication { const juce::String getApplicationName() override { return "TableListBoxRepro"; } const juce::String getApplicationVersion() override { return "1.0"; } void initialise (const juce::String& commandLine) override { window = std::make_unique ("TableListBox repro", juce::Colours::darkgrey, juce::DocumentWindow::closeButton); window->setUsingNativeTitleBar (true); window->setContentOwned (new Content (commandLine.contains ("--manual")), true); window->centreWithSize (window->getWidth(), window->getHeight()); window->setVisible (true); } void shutdown() override { window = nullptr; } std::unique_ptr window; }; } // namespace START_JUCE_APPLICATION (App)