cui/source/dialogs/SpellDialog.cxx           |   72 ++++++++++-----------------
 cui/source/factory/dlgfact.cxx               |    1 
 cui/source/inc/SpellDialog.hxx               |    8 +--
 include/sfx2/childwin.hxx                    |    3 +
 include/svx/SpellDialogChildWindow.hxx       |    2 
 include/svx/svxdlg.hxx                       |    1 
 sfx2/source/appl/childwin.cxx                |    2 
 svx/source/dialog/SpellDialogChildWindow.cxx |    5 +
 8 files changed, 45 insertions(+), 49 deletions(-)

New commits:
commit 94442bd94e504b067efd0b992525b53fc660f114
Author:     Michael Weghorn <m.wegh...@posteo.de>
AuthorDate: Tue Aug 5 11:07:25 2025 +0200
Commit:     Michael Weghorn <m.wegh...@posteo.de>
CommitDate: Tue Aug 5 23:47:16 2025 +0200

    tdf#163802 a11y: Fully initialize SpellDialog before showing
    
    SpellDialog can't do all of its initialization in the
    ctor because it needs to call virtual methods on the
    SpellDialogChildWindow* that gets passed as a param,
    but that one calls the SpellDialog from its ctor, i.e.
    it hasn't been fully initialized yet.
    
    For that reason, the part of the initialization done in
    InitHdl was delayed by using a user event (posted via
    Application::PostUserEvent).
    This also includes setting the dialog title based on
    the selected text language.
    
    As a consequence of this, the title, and therefore the
    accessible name of the dialog would change after it was
    shown, which would result in Orca speech output for the
    dialog getting interrupted (see tdf#163802).
    
    Avoid the problem by not depending on the next user
    event getting processed, but calling the method
    that does the rest of the initialization manually
    after the SpellDialogChildWindow constructor has
    finished.
    
    In order to do that, add a virtual SfxChildWindow::Initialize
    (that does nothing) and override it in the
    SpellDialogChildWindow subclass.
    
    Since SpellDialog::Initialize (renamed from SpellDialog::InitHdl)
    is now guaranteed to be called before the dialog shows
    and that one always sets a title (by calling
    UpdateBoxes_Impl), drop the no longer needed
    
        // fdo#68794 set initial title for cases where no text has been 
processed
        // yet to show its language attributes
        OUString sTitle = rParent.HasGrammarChecking() ? 
m_sTitleSpellingGrammar : m_sTitleSpelling;
        
m_xDialog->set_title(m_xDialog->strip_mnemonic(sTitle.replaceFirst("$LANGUAGE 
($LOCATION)", "")));
    
    from the ctor.
    
    Additional fix for GTK 3 to avoid incorrect
    object:property-change:accessible-name events from
    getting sent when the title didn't actually change [1]:
    
        commit 0cc9e603f4635c31d2a8a5f602869474d329e728
        Author: Michael Weghorn <m.wegh...@posteo.de>
        Date:   Mon Aug 4 15:31:06 2025 +0200
    
            a11y: Only notify when window title changed
    
            In gtk_window_set_title_internal, return early
            when called with the same title as is already set.
    
            This also ensures that notifying of a change
            of PROP_TITLE only happens when the property
            actually changed, which also prevents invalid
            object:property-change:accessible-name AT-SPI
            events from getting emitted on the accessibility
            layer when the title hasn't actually changed.
    
            Fixes: #7690
            Part-of: <https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/8814>
    
    Output of the pyatspi listener script from tdf#163802,
    attachment 197494 for gtk3 before this LO commit (note the
    object:property-change:accessible-name event after
    the object:state-changed:showing event):
    
        event type: object:property-change:accessible-name, event source: 
Spelling: , states: ['ENABLED', 'SENSITIVE', 'SHOWING', 'VISIBLE']
        object:property-change:accessible-name(0, 0, Spelling: )
                source: [dialog | Spelling: ]
                host_application: [application | soffice]
                sender: [application | soffice]
    
        event type: object:state-changed:showing, event source: Spelling: , 
states: ['ENABLED', 'SENSITIVE', 'SHOWING', 'VISIBLE']
        object:state-changed:showing(1, 0, 0)
                source: [dialog | Spelling: ]
                host_application: [application | soffice]
                sender: [application | soffice]
    
        event type: object:property-change:accessible-name, event source: 
Spelling: English (UK), states: ['ENABLED', 'SENSITIVE', 'SHOWING', 'VISIBLE']
        object:property-change:accessible-name(0, 0, Spelling: English (UK))
                source: [dialog | Spelling: English (UK)]
                host_application: [application | soffice]
                sender: [application | soffice]
    
    Output with this change in place:
    
        event type: object:property-change:accessible-name, event source: 
Spelling: English (UK), states: ['ENABLED', 'SENSITIVE', 'SHOWING', 'VISIBLE']
        object:property-change:accessible-name(0, 0, Spelling: English (UK))
                source: [dialog | Spelling: English (UK)]
                host_application: [application | soffice]
                sender: [application | soffice]
    
        event type: object:state-changed:showing, event source: Spelling: 
English (UK), states: ['ENABLED', 'SENSITIVE', 'SHOWING', 'VISIBLE']
        object:state-changed:showing(1, 0, 0)
                source: [dialog | Spelling: English (UK)]
                host_application: [application | soffice]
                sender: [application | soffice]
    
    [1] 
https://gitlab.gnome.org/GNOME/gtk/-/commit/0cc9e603f4635c31d2a8a5f602869474d329e728
    
    Change-Id: I6cdfe8443ae435e1e644b78942cf63e9df1d400f
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/188937
    Reviewed-by: Michael Weghorn <m.wegh...@posteo.de>
    Tested-by: Jenkins

diff --git a/cui/source/dialogs/SpellDialog.cxx 
b/cui/source/dialogs/SpellDialog.cxx
index 4a43b37ca9a0..2986c60af2d1 100644
--- a/cui/source/dialogs/SpellDialog.cxx
+++ b/cui/source/dialogs/SpellDialog.cxx
@@ -162,7 +162,6 @@ SpellDialog::SpellDialog(SpellDialogChildWindow* 
pChildWindow,
     : SfxModelessDialogController (_pBindings, pChildWindow,
         pParent, u"cui/ui/spellingdialog.ui"_ustr, u"SpellingDialog"_ustr)
     , aDialogUndoLink(LINK (this, SpellDialog, DialogUndoHdl))
-    , m_pInitHdlEvent(nullptr)
     , bFocusLocked(true)
     , rParent(*pChildWindow)
     , pImpl( new SpellDialog_Impl )
@@ -198,11 +197,6 @@ SpellDialog::SpellDialog(SpellDialogChildWindow* 
pChildWindow,
     m_sTitleSpellingGrammar = m_xDialog->get_title();
     m_sTitleSpelling = m_xAltTitle->get_label();
 
-    // fdo#68794 set initial title for cases where no text has been processed
-    // yet to show its language attributes
-    OUString sTitle = rParent.HasGrammarChecking() ? m_sTitleSpellingGrammar : 
m_sTitleSpelling;
-    
m_xDialog->set_title(m_xDialog->strip_mnemonic(sTitle.replaceFirst("$LANGUAGE 
($LOCATION)", "")));
-
     m_sResumeST = m_xResumeFT->get_label();
     m_sNoSuggestionsST = 
m_xNoSuggestionsFT->strip_mnemonic(m_xNoSuggestionsFT->get_label());
 
@@ -244,10 +238,8 @@ SpellDialog::SpellDialog(SpellDialogChildWindow* 
pChildWindow,
     // disable controls if service is missing
     m_xDialog->set_sensitive(xSpell.is());
 
-    //InitHdl wants to use virtual methods, so it
-    //can't be called during the ctor, so init
-    //it on next event cycle post-ctor
-    m_pInitHdlEvent = Application::PostUserEvent(LINK(this, SpellDialog, 
InitHdl));
+    // further initialization happens in Initialize() to prevent virtual
+    // calls from the constructor
 }
 
 SpellDialog::~SpellDialog()
@@ -258,8 +250,6 @@ SpellDialog::~SpellDialog()
         m_xOptionsDlg.reset();
     }
 
-    if (m_pInitHdlEvent)
-        Application::RemoveUserEvent(m_pInitHdlEvent);
     if (pImpl)
     {
         // save possibly modified user-dictionaries
@@ -376,12 +366,9 @@ void 
SpellDialog::SpellContinue_Impl(std::unique_ptr<UndoChangeGroupGuard>* pGua
         m_xUndoPB->set_sensitive(false);
     }
 }
-/* Initialize, asynchronous to prevent virtual calls
-   from a constructor
- */
-IMPL_LINK_NOARG( SpellDialog, InitHdl, void*, void)
+
+void SpellDialog::Initialize()
 {
-    m_pInitHdlEvent = nullptr;
     m_xDialog->freeze();
     //show or hide AutoCorrect depending on the modules abilities
     m_xAutoCorrPB->set_visible(rParent.HasAutoCorrection());
diff --git a/cui/source/factory/dlgfact.cxx b/cui/source/factory/dlgfact.cxx
index 58ee73e78163..d7e58ebac577 100644
--- a/cui/source/factory/dlgfact.cxx
+++ b/cui/source/factory/dlgfact.cxx
@@ -450,6 +450,7 @@ class AbstractSpellDialog_Impl final
 {
 public:
     using AbstractDialogImpl_BASE::AbstractDialogImpl_BASE;
+    void Initialize() override { m_pDlg->Initialize(); }
     void InvalidateDialog() override { m_pDlg->InvalidateDialog(); }
     std::shared_ptr<SfxDialogController> GetController() override { return 
m_pDlg; }
     SfxBindings& GetBindings() override { return m_pDlg->GetBindings(); }
diff --git a/cui/source/inc/SpellDialog.hxx b/cui/source/inc/SpellDialog.hxx
index 9f43d2e774fc..f868f42a70cb 100644
--- a/cui/source/inc/SpellDialog.hxx
+++ b/cui/source/inc/SpellDialog.hxx
@@ -139,7 +139,6 @@ private:
     OUString        m_sTitleSpellingGrammar;
 
     Link<SpellUndoAction_Impl&,void> aDialogUndoLink;
-    ImplSVEvent *   m_pInitHdlEvent;
     bool            bFocusLocked;
 
     svx::SpellDialogChildWindow& rParent;
@@ -193,8 +192,6 @@ private:
     DECL_LINK(LanguageSelectHdl, weld::ComboBox&, void);
     DECL_LINK(DialogUndoHdl, SpellUndoAction_Impl&, void);
 
-    DECL_LINK(InitHdl, void*, void);
-
     void            AddToDictionaryExecute(const OUString& rItemId);
     void            StartSpellOptDlg_Impl();
     int             InitUserDicts();
@@ -225,6 +222,10 @@ public:
         SfxBindings* pBindings);
     virtual ~SpellDialog() override;
 
+    /* Initialize, to be called after the constructor to prevent virtual calls
+       from a constructor. */
+    void Initialize();
+
     virtual void    Activate() override;
     virtual void    Deactivate() override;
 
diff --git a/include/sfx2/childwin.hxx b/include/sfx2/childwin.hxx
index c52a1258b700..86cfe0aefdab 100644
--- a/include/sfx2/childwin.hxx
+++ b/include/sfx2/childwin.hxx
@@ -113,6 +113,9 @@ protected:
 
 public:
     virtual             ~SfxChildWindow();
+    // subclasses can override this in case any initialization needs to happen 
after
+    // the constructor was called (e.g. to avoid calling virtual methods in 
the constructor)
+    virtual             void Initialize() {}
     void                Destroy();
     vcl::Window*        GetWindow() const
                         { return pWindow; }
diff --git a/include/svx/SpellDialogChildWindow.hxx 
b/include/svx/SpellDialogChildWindow.hxx
index 0e6ed66616b0..9b50e5c8fc71 100644
--- a/include/svx/SpellDialogChildWindow.hxx
+++ b/include/svx/SpellDialogChildWindow.hxx
@@ -49,6 +49,8 @@ public:
     SpellDialogChildWindow(vcl::Window* pParent, sal_uInt16 nId, SfxBindings* 
pBindings);
     virtual ~SpellDialogChildWindow() override;
 
+    virtual void Initialize() override;
+
 protected:
     /** This abstract method has to be defined by a derived class.  It
         returns the next wrong sentence.
diff --git a/include/svx/svxdlg.hxx b/include/svx/svxdlg.hxx
index d3d844977493..5fe7b1eaf70e 100644
--- a/include/svx/svxdlg.hxx
+++ b/include/svx/svxdlg.hxx
@@ -75,6 +75,7 @@ class AbstractSpellDialog : public VclAbstractDialog
 protected:
     virtual ~AbstractSpellDialog() override = default;
 public:
+    virtual void Initialize() = 0;
     virtual void InvalidateDialog() = 0;
     virtual std::shared_ptr<SfxDialogController> GetController() = 0;
     virtual SfxBindings& GetBindings() = 0;
diff --git a/sfx2/source/appl/childwin.cxx b/sfx2/source/appl/childwin.cxx
index 82dc3f3a66fc..fb35a06acf63 100644
--- a/sfx2/source/appl/childwin.cxx
+++ b/sfx2/source/appl/childwin.cxx
@@ -219,6 +219,7 @@ std::unique_ptr<SfxChildWindow> 
SfxChildWindow::CreateChildWindow( sal_uInt16 nI
                 SfxChildWinInfo aInfo = rInfo;
                 Application::SetSystemWindowMode( 
SystemWindowFlags::NOAUTOMODE );
                 pChild = pFact->pCtor( pParent, nId, pBindings, &aInfo );
+                pChild->Initialize();
                 Application::SetSystemWindowMode( nOldMode );
                 if ( pBindings )
                     pBindings->LEAVEREGISTRATIONS();
@@ -240,6 +241,7 @@ std::unique_ptr<SfxChildWindow> 
SfxChildWindow::CreateChildWindow( sal_uInt16 nI
                 SfxChildWinInfo aInfo = rInfo;
                 Application::SetSystemWindowMode( 
SystemWindowFlags::NOAUTOMODE );
                 pChild = pFact->pCtor( pParent, nId, pBindings, &aInfo );
+                pChild->Initialize();
                 rInfo.nFlags |= aInfo.nFlags;
                 Application::SetSystemWindowMode( nOldMode );
                 if ( pBindings )
diff --git a/svx/source/dialog/SpellDialogChildWindow.cxx 
b/svx/source/dialog/SpellDialogChildWindow.cxx
index ae82769dfb9b..76f0803a2b63 100644
--- a/svx/source/dialog/SpellDialogChildWindow.cxx
+++ b/svx/source/dialog/SpellDialogChildWindow.cxx
@@ -44,6 +44,11 @@ SpellDialogChildWindow::~SpellDialogChildWindow()
     m_xAbstractSpellDialog.disposeAndClear();
 }
 
+void SpellDialogChildWindow::Initialize()
+{
+    m_xAbstractSpellDialog->Initialize();
+}
+
 SfxBindings& SpellDialogChildWindow::GetBindings() const
 {
     assert(m_xAbstractSpellDialog);
commit 82422565eb8f0e2cab33c82fd8feba60249a0d32
Author:     Michael Weghorn <m.wegh...@posteo.de>
AuthorDate: Tue Aug 5 10:34:48 2025 +0200
Commit:     Michael Weghorn <m.wegh...@posteo.de>
CommitDate: Tue Aug 5 23:47:09 2025 +0200

    tdf#163802 Merge SpellDialog::Init_Impl into ctor
    
    This is also to make a bit more obvious what part of the
    initialization happens in the ctor and what only
    happens later:
    The (similarly named) SpellDialog::InitHdl
    only gets called later. (And the logic for the latter
    will change and it will be renamed to
    SpellDialog::Initialize in an upcoming commit.)
    
    Change-Id: Ia2647eddb15163f92c10b0ae9396b54895a38192
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/188936
    Tested-by: Jenkins
    Reviewed-by: Michael Weghorn <m.wegh...@posteo.de>

diff --git a/cui/source/dialogs/SpellDialog.cxx 
b/cui/source/dialogs/SpellDialog.cxx
index e03eb313427c..4a43b37ca9a0 100644
--- a/cui/source/dialogs/SpellDialog.cxx
+++ b/cui/source/dialogs/SpellDialog.cxx
@@ -213,7 +213,33 @@ SpellDialog::SpellDialog(SpellDialogChildWindow* 
pChildWindow,
     m_xAddToDictMB->set_help_id(m_xAddToDictPB->get_help_id());
     xSpell = LinguMgr::GetSpellChecker();
 
-    Init_Impl();
+    // initialize handler
+    m_xClosePB->connect_clicked(LINK( this, SpellDialog, CancelHdl ) );
+    m_xChangePB->connect_clicked(LINK( this, SpellDialog, ChangeHdl ) );
+    m_xChangeAllPB->connect_clicked(LINK( this, SpellDialog, ChangeAllHdl ) );
+    m_xIgnorePB->connect_clicked(LINK( this, SpellDialog, IgnoreHdl ) );
+    m_xIgnoreAllPB->connect_clicked(LINK( this, SpellDialog, IgnoreAllHdl ) );
+    m_xIgnoreRulePB->connect_clicked(LINK( this, SpellDialog, IgnoreAllHdl ) );
+    m_xUndoPB->connect_clicked(LINK( this, SpellDialog, UndoHdl ) );
+
+    m_xAutoCorrPB->connect_clicked( LINK( this, SpellDialog, ExtClickHdl ) );
+    m_xCheckGrammarCB->connect_toggled( LINK( this, SpellDialog, 
CheckGrammarHdl ));
+    m_xOptionsPB->connect_clicked( LINK( this, SpellDialog, ExtClickHdl ) );
+
+    m_xSuggestionLB->connect_row_activated( LINK( this, SpellDialog, 
DoubleClickChangeHdl ) );
+
+    m_xSentenceED->SetModifyHdl(LINK ( this, SpellDialog, ModifyHdl) );
+
+    m_xAddToDictMB->connect_selected(LINK ( this, SpellDialog, 
AddToDictSelectHdl ) );
+    m_xAddToDictPB->connect_clicked(LINK ( this, SpellDialog, 
AddToDictClickHdl ) );
+
+    m_xLanguageLB->connect_changed(LINK( this, SpellDialog, LanguageSelectHdl 
) );
+
+    // initialize language ListBox
+    m_xLanguageLB->SetLanguageList(SvxLanguageListFlags::SPELL_USED, false, 
false, true);
+
+    m_xSentenceED->ClearModifyFlag();
+    LinguMgr::GetChangeAllList()->clear();
 
     // disable controls if service is missing
     m_xDialog->set_sensitive(xSpell.is());
@@ -245,37 +271,6 @@ SpellDialog::~SpellDialog()
     }
 }
 
-void SpellDialog::Init_Impl()
-{
-    // initialize handler
-    m_xClosePB->connect_clicked(LINK( this, SpellDialog, CancelHdl ) );
-    m_xChangePB->connect_clicked(LINK( this, SpellDialog, ChangeHdl ) );
-    m_xChangeAllPB->connect_clicked(LINK( this, SpellDialog, ChangeAllHdl ) );
-    m_xIgnorePB->connect_clicked(LINK( this, SpellDialog, IgnoreHdl ) );
-    m_xIgnoreAllPB->connect_clicked(LINK( this, SpellDialog, IgnoreAllHdl ) );
-    m_xIgnoreRulePB->connect_clicked(LINK( this, SpellDialog, IgnoreAllHdl ) );
-    m_xUndoPB->connect_clicked(LINK( this, SpellDialog, UndoHdl ) );
-
-    m_xAutoCorrPB->connect_clicked( LINK( this, SpellDialog, ExtClickHdl ) );
-    m_xCheckGrammarCB->connect_toggled( LINK( this, SpellDialog, 
CheckGrammarHdl ));
-    m_xOptionsPB->connect_clicked( LINK( this, SpellDialog, ExtClickHdl ) );
-
-    m_xSuggestionLB->connect_row_activated( LINK( this, SpellDialog, 
DoubleClickChangeHdl ) );
-
-    m_xSentenceED->SetModifyHdl(LINK ( this, SpellDialog, ModifyHdl) );
-
-    m_xAddToDictMB->connect_selected(LINK ( this, SpellDialog, 
AddToDictSelectHdl ) );
-    m_xAddToDictPB->connect_clicked(LINK ( this, SpellDialog, 
AddToDictClickHdl ) );
-
-    m_xLanguageLB->connect_changed(LINK( this, SpellDialog, LanguageSelectHdl 
) );
-
-    // initialize language ListBox
-    m_xLanguageLB->SetLanguageList(SvxLanguageListFlags::SPELL_USED, false, 
false, true);
-
-    m_xSentenceED->ClearModifyFlag();
-    LinguMgr::GetChangeAllList()->clear();
-}
-
 void SpellDialog::UpdateBoxes_Impl(bool bCallFromSelectHdl)
 {
     m_xSuggestionLB->clear();
diff --git a/cui/source/inc/SpellDialog.hxx b/cui/source/inc/SpellDialog.hxx
index d09e813e4bbc..9f43d2e774fc 100644
--- a/cui/source/inc/SpellDialog.hxx
+++ b/cui/source/inc/SpellDialog.hxx
@@ -199,7 +199,6 @@ private:
     void            StartSpellOptDlg_Impl();
     int             InitUserDicts();
     void            UpdateBoxes_Impl(bool bCallFromSelectHdl = false);
-    void            Init_Impl();
     void            SpellContinue_Impl(std::unique_ptr<UndoChangeGroupGuard>* 
pGuard = nullptr, bool UseSavedSentence = false, bool bIgnoreCurrentError = 
false );
     void            LockFocusChanges( bool bLock ) {bFocusLocked = bLock;}
     void            ToplevelFocusChanged();

Reply via email to