tdf#160543 Quickfind sidebar enhancements

* Makes the search independent of find and replace options.
* Adds a button to launch a dialog for match case, whole words only,
and similarity search options.
* Adds a button to launch the find and replace dialog.
* Makes the find entry control have visual feedback when there are no
matches.
* Adds a label at the bottom of panel that shows the number of matches.

Change-Id: I2cffcf86978773471bb86c5e5cf8b967c24efa7b
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/168834
Reviewed-by: Jim Raykowski <raykowj@gmail.com>
Tested-by: Jenkins
diff --git a/sw/UIConfig_swriter.mk b/sw/UIConfig_swriter.mk
index 27b7248..2621239 100644
--- a/sw/UIConfig_swriter.mk
+++ b/sw/UIConfig_swriter.mk
@@ -292,6 +292,7 @@ $(eval $(call gb_UIConfig_add_uifiles,modules/swriter,\
	sw/uiconfig/swriter/ui/sidebartableedit \
	sw/uiconfig/swriter/ui/sidebartheme \
	sw/uiconfig/swriter/ui/sidebarquickfind \
        sw/uiconfig/swriter/ui/sidebarquickfindoptionsdialog \
	sw/uiconfig/swriter/ui/sortdialog \
	sw/uiconfig/swriter/ui/spellmenu \
	sw/uiconfig/swriter/ui/splittable \
diff --git a/sw/inc/strings.hrc b/sw/inc/strings.hrc
index 298b861..b9e46f1 100644
--- a/sw/inc/strings.hrc
+++ b/sw/inc/strings.hrc
@@ -1396,7 +1396,7 @@
#define STR_NUM_OUTLINE                         NC_("STR_NUM_OUTLINE", "Outline ")
#define STR_EDIT_FOOTNOTE                       NC_("STR_EDIT_FOOTNOTE", "Edit Footnote/Endnote")
#define STR_NB_REPLACED                         NC_("STR_NB_REPLACED", "Search key replaced XX times.")
#define STR_SEARCH_KEY_FOUND_TIMES              NC_("STR_SEARCH_KEY_FOUND_TIMES", "Search key found %1 times.")
#define STR_SEARCH_KEY_FOUND_TIMES              NNC_("STR_SEARCH_KEY_FOUND_TIMES", "One match found.", "%1 matches found.")
#define STR_SRCVIEW_ROW                         NC_("STR_SRCVIEW_ROW", "Row ")
#define STR_SRCVIEW_COL                         NC_("STR_SRCVIEW_COL", "Column ")
#define STR_SAVEAS_SRC                          NC_("STR_SAVEAS_SRC", "~Export source...")
diff --git a/sw/source/uibase/sidebar/QuickFindPanel.cxx b/sw/source/uibase/sidebar/QuickFindPanel.cxx
index da6e016..0f620fd 100644
--- a/sw/source/uibase/sidebar/QuickFindPanel.cxx
+++ b/sw/source/uibase/sidebar/QuickFindPanel.cxx
@@ -12,8 +12,6 @@
#include <com/sun/star/lang/IllegalArgumentException.hpp>
#include <svl/srchitem.hxx>
#include <view.hxx>
#include <comphelper/dispatchcommand.hxx>
#include <comphelper/propertysequence.hxx>
#include <swmodule.hxx>
#include <pam.hxx>
#include <node.hxx>
@@ -25,9 +23,8 @@
#include <vcl/event.hxx>
#include <vcl/svapp.hxx>
#include <vcl/sysdata.hxx>
#include <swwait.hxx>

const int MinimumContainerWidth = 250;
const int Rounding = 6;
const int CharactersBeforeAndAfter = 40;

namespace
@@ -52,25 +49,85 @@ void getAnchorPos(SwPosition& rPos)

namespace sw::sidebar
{
std::unique_ptr<PanelLayout> QuickFindPanel::Create(weld::Widget* pParent)
QuickFindPanel::SearchOptionsDialog::SearchOptionsDialog(weld::Window* pParent)
    : GenericDialogController(pParent, u"modules/swriter/ui/sidebarquickfindoptionsdialog.ui"_ustr,
                              u"SearchOptionsDialog"_ustr)
    , m_xMatchCaseCheckButton(m_xBuilder->weld_check_button(u"matchcase"_ustr))
    , m_xWholeWordsOnlyCheckButton(m_xBuilder->weld_check_button(u"wholewordsonly"_ustr))
    , m_xSimilarityCheckButton(m_xBuilder->weld_check_button(u"similarity"_ustr))
    , m_xSimilaritySettingsDialogButton(m_xBuilder->weld_button(u"similaritysettingsdialog"_ustr))
{
    if (pParent == nullptr)
        throw css::lang::IllegalArgumentException(
            u"no parent Window given to QuickFindPanel::Create"_ustr, nullptr, 0);
    return std::make_unique<QuickFindPanel>(pParent);
    m_xSimilarityCheckButton->connect_toggled(
        LINK(this, SearchOptionsDialog, SimilarityCheckButtonToggledHandler));
    m_xSimilaritySettingsDialogButton->connect_clicked(
        LINK(this, SearchOptionsDialog, SimilaritySettingsDialogButtonClickedHandler));
}

QuickFindPanel::QuickFindPanel(weld::Widget* pParent)
short QuickFindPanel::SearchOptionsDialog::executeSubDialog(VclAbstractDialog* dialog)
{
    assert(!m_executingSubDialog);
    comphelper::ScopeGuard g([this] { m_executingSubDialog = false; });
    m_executingSubDialog = true;
    return dialog->Execute();
}

IMPL_LINK_NOARG(QuickFindPanel::SearchOptionsDialog, SimilarityCheckButtonToggledHandler,
                weld::Toggleable&, void)
{
    m_xSimilaritySettingsDialogButton->set_sensitive(m_xSimilarityCheckButton->get_active());
}

IMPL_LINK_NOARG(QuickFindPanel::SearchOptionsDialog, SimilaritySettingsDialogButtonClickedHandler,
                weld::Button&, void)
{
    SvxAbstractDialogFactory* pFact = SvxAbstractDialogFactory::Create();
    ScopedVclPtr<AbstractSvxSearchSimilarityDialog> pDlg(pFact->CreateSvxSearchSimilarityDialog(
        m_xDialog.get(), m_bIsLEVRelaxed, m_nLEVOther, m_nLEVShorter, m_nLEVLonger));

    if (executeSubDialog(pDlg.get()) == RET_OK)
    {
        m_bIsLEVRelaxed = pDlg->IsRelaxed();
        m_nLEVOther = pDlg->GetOther();
        m_nLEVShorter = pDlg->GetShorter();
        m_nLEVLonger = pDlg->GetLonger();
    }
}

std::unique_ptr<PanelLayout>
QuickFindPanel::Create(weld::Widget* pParent,
                       const css::uno::Reference<css::frame::XFrame>& rxFrame)
{
    if (pParent == nullptr)
        throw lang::IllegalArgumentException("no parent Window given to QuickFindPanel::Create",
                                             nullptr, 0);
    if (!rxFrame.is())
        throw lang::IllegalArgumentException("no XFrame given to QuickFindPanel::Create", nullptr,
                                             0);
    return std::make_unique<QuickFindPanel>(pParent, rxFrame);
}

QuickFindPanel::QuickFindPanel(weld::Widget* pParent, const uno::Reference<frame::XFrame>& rxFrame)
    : PanelLayout(pParent, u"QuickFindPanel"_ustr, u"modules/swriter/ui/sidebarquickfind.ui"_ustr)
    , m_xSearchFindEntry(m_xBuilder->weld_entry(u"Find"_ustr))
    , m_xSearchOptionsToolbar(m_xBuilder->weld_toolbar(u"searchoptionstoolbar"_ustr))
    , m_xFindAndReplaceToolbar(m_xBuilder->weld_toolbar(u"findandreplacetoolbar"_ustr))
    , m_xFindAndReplaceToolbarDispatch(
          new ToolbarUnoDispatcher(*m_xFindAndReplaceToolbar, *m_xBuilder, rxFrame))
    , m_xSearchFindsList(m_xBuilder->weld_tree_view(u"searchfinds"_ustr))
    , m_xSearchFindFoundTimesLabel(m_xBuilder->weld_label("numberofsearchfinds"))
    , m_pWrtShell(::GetActiveWrtShell())
{
    m_xContainer->set_size_request(MinimumContainerWidth, 1);
    m_nMinimumPanelWidth
        = m_xBuilder->weld_widget(u"box"_ustr)->get_preferred_size().getWidth() + (6 * 2) + 6;
    m_xContainer->set_size_request(m_nMinimumPanelWidth, 1);

    m_xSearchFindEntry->connect_activate(
        LINK(this, QuickFindPanel, SearchFindEntryActivateHandler));
    m_xSearchFindEntry->connect_changed(LINK(this, QuickFindPanel, SearchFindEntryChangedHandler));

    m_xSearchOptionsToolbar->connect_clicked(
        LINK(this, QuickFindPanel, SearchOptionsToolbarClickedHandler));

    m_xSearchFindsList->connect_custom_get_size(
        LINK(this, QuickFindPanel, SearchFindsListCustomGetSizeHandler));
    m_xSearchFindsList->connect_custom_render(LINK(this, QuickFindPanel, SearchFindsListRender));
@@ -79,7 +136,40 @@ QuickFindPanel::QuickFindPanel(weld::Widget* pParent)
        LINK(this, QuickFindPanel, SearchFindsListSelectionChangedHandler));
    m_xSearchFindsList->connect_row_activated(
        LINK(this, QuickFindPanel, SearchFindsListRowActivatedHandler));
    m_xSearchFindsList->connect_mouse_press(LINK(this, QuickFindPanel, MousePressHandler));
    m_xSearchFindsList->connect_mouse_press(
        LINK(this, QuickFindPanel, SearchFindsListMousePressHandler));
}

IMPL_LINK_NOARG(QuickFindPanel, SearchOptionsToolbarClickedHandler, const OUString&, void)
{
    SearchOptionsDialog aDlg(GetFrameWeld());

    aDlg.m_xMatchCaseCheckButton->set_active(m_bMatchCase);
    aDlg.m_xWholeWordsOnlyCheckButton->set_active(m_bWholeWordsOnly);
    aDlg.m_xSimilarityCheckButton->set_active(m_bSimilarity);
    aDlg.m_xSimilaritySettingsDialogButton->set_sensitive(m_bSimilarity);
    if (m_bSimilarity)
    {
        aDlg.m_bIsLEVRelaxed = m_bIsLEVRelaxed;
        aDlg.m_nLEVOther = m_nLEVOther;
        aDlg.m_nLEVShorter = m_nLEVShorter;
        aDlg.m_nLEVLonger = m_nLEVLonger;
    }

    if (aDlg.run() == RET_OK)
    {
        m_bMatchCase = aDlg.m_xMatchCaseCheckButton->get_active();
        m_bWholeWordsOnly = aDlg.m_xWholeWordsOnlyCheckButton->get_active();
        m_bSimilarity = aDlg.m_xSimilarityCheckButton->get_active();
        if (m_bSimilarity)
        {
            m_bIsLEVRelaxed = aDlg.m_bIsLEVRelaxed;
            m_nLEVOther = aDlg.m_nLEVOther;
            m_nLEVShorter = aDlg.m_nLEVShorter;
            m_nLEVLonger = aDlg.m_nLEVLonger;
        }
        FillSearchFindsList();
    }
}

QuickFindPanel::~QuickFindPanel()
@@ -88,7 +178,20 @@ QuickFindPanel::~QuickFindPanel()
    m_xSearchFindsList.reset();
}

IMPL_LINK(QuickFindPanel, MousePressHandler, const MouseEvent&, rMEvt, bool)
IMPL_LINK_NOARG(QuickFindPanel, SearchFindEntryChangedHandler, weld::Entry&, void)
{
    m_xSearchFindEntry->set_message_type(weld::EntryMessageType::Normal);
    m_xSearchFindsList->clear();
    m_xSearchFindFoundTimesLabel->set_label(OUString());
}

IMPL_LINK_NOARG(QuickFindPanel, SearchFindEntryActivateHandler, weld::Entry&, bool)
{
    FillSearchFindsList();
    return true;
}

IMPL_LINK(QuickFindPanel, SearchFindsListMousePressHandler, const MouseEvent&, rMEvt, bool)
{
    if (std::unique_ptr<weld::TreeIter> xEntry(m_xSearchFindsList->make_iterator());
        m_xSearchFindsList->get_dest_row_at_pos(rMEvt.GetPosPixel(), xEntry.get(), false, false))
@@ -98,12 +201,6 @@ IMPL_LINK(QuickFindPanel, MousePressHandler, const MouseEvent&, rMEvt, bool)
    return false;
}

IMPL_LINK_NOARG(QuickFindPanel, SearchFindEntryActivateHandler, weld::Entry&, bool)
{
    FillSearchFindsList();
    return true;
}

IMPL_LINK(QuickFindPanel, SearchFindsListCustomGetSizeHandler, weld::TreeView::get_size_args,
          aPayload, Size)
{
@@ -129,7 +226,7 @@ IMPL_LINK(QuickFindPanel, SearchFindsListCustomGetSizeHandler, weld::TreeView::g
    tools::Long nScrollBarThickness
        = Application::GetSettings().GetStyleSettings().GetScrollBarSize();

    tools::Rectangle aInRect(Point(), Size(MinimumContainerWidth - (x * 2) - leftTextMargin
    tools::Rectangle aInRect(Point(), Size(m_nMinimumPanelWidth - (x * 2) - leftTextMargin
                                               - nScrollBarThickness - rightTextMargin,
                                           1));

@@ -198,7 +295,7 @@ IMPL_LINK(QuickFindPanel, SearchFindsListRender, weld::TreeView::render_args, aP
    if (!bPageEntry)
    {
        aRect.AdjustRight(-3);
        rRenderContext.DrawRect(aRect, Rounding, Rounding);
        rRenderContext.DrawRect(aRect, 6, 6);

        aRect.AdjustLeft(+6);
        rRenderContext.DrawText(aRect, aEntry,
@@ -282,161 +379,201 @@ IMPL_LINK_NOARG(QuickFindPanel, SearchFindsListRowActivatedHandler, weld::TreeVi
    return true;
}

IMPL_LINK_NOARG(QuickFindPanel, SearchFindEntryChangedHandler, weld::Entry&, void)
{
    m_xSearchFindsList->clear();
}

void QuickFindPanel::FillSearchFindsList()
{
    m_vPaMs.clear();
    m_xSearchFindsList->clear();
    const OUString& sText = m_xSearchFindEntry->get_text();
    css::uno::Sequence<css::beans::PropertyValue> aPropertyValues(comphelper::InitPropertySequence({
        { "SearchItem.SearchString", css::uno::Any(sText) },
        { "SearchItem.Backward", css::uno::Any(false) },
        { "SearchItem.Command", css::uno::Any(sal_uInt16(SvxSearchCmd::FIND_ALL)) },
    }));
    m_xSearchFindFoundTimesLabel->set_label(OUString());

    comphelper::dispatchCommand(u".uno:ExecuteSearch"_ustr, aPropertyValues);

    if (!m_pWrtShell->HasMark())
    const OUString& rsFindEntry = m_xSearchFindEntry->get_text();
    if (rsFindEntry.isEmpty())
        return;

    for (SwPaM& rPaM : m_pWrtShell->GetCursor()->GetRingContainer())
    SwWait aWait(*m_pWrtShell->GetDoc()->GetDocShell(), true);

    m_pWrtShell->AssureStdMode();

    i18nutil::SearchOptions2 aSearchOptions;
    aSearchOptions.Locale = GetAppLanguageTag().getLocale();
    aSearchOptions.searchString = rsFindEntry;
    aSearchOptions.replaceString.clear();
    if (m_bWholeWordsOnly)
        aSearchOptions.searchFlag |= css::util::SearchFlags::NORM_WORD_ONLY;
    if (m_bSimilarity)
    {
        SwPosition* pMarkPosition = rPaM.GetMark();
        SwPosition* pPointPosition = rPaM.GetPoint();
        std::unique_ptr<SwPaM> xPaM(std::make_unique<SwPaM>(*pMarkPosition, *pPointPosition));
        m_vPaMs.push_back(std::move(xPaM));
        aSearchOptions.AlgorithmType2 = css::util::SearchAlgorithms2::APPROXIMATE;
        if (m_bIsLEVRelaxed)
            aSearchOptions.searchFlag |= css::util::SearchFlags::LEV_RELAXED;
        aSearchOptions.changedChars = m_nLEVOther;
        aSearchOptions.insertedChars = m_nLEVShorter;
        aSearchOptions.deletedChars = m_nLEVLonger;
    }
    else
        aSearchOptions.AlgorithmType2 = css::util::SearchAlgorithms2::ABSOLUTE;
    TransliterationFlags nTransliterationFlags = TransliterationFlags::IGNORE_WIDTH;
    if (!m_bMatchCase)
        nTransliterationFlags |= TransliterationFlags::IGNORE_CASE;
    aSearchOptions.transliterateFlags = nTransliterationFlags;

    m_pWrtShell->SttSelect();
    /*sal_Int32 nFound =*/m_pWrtShell->SearchPattern(
        aSearchOptions, false, SwDocPositions::Start, SwDocPositions::End,
        FindRanges::InBody | FindRanges::InSelAll, false);
    m_pWrtShell->EndSelect();

    if (m_pWrtShell->HasMark())
    {
        for (SwPaM& rPaM : m_pWrtShell->GetCursor()->GetRingContainer())
        {
            SwPosition* pMarkPosition = rPaM.GetMark();
            SwPosition* pPointPosition = rPaM.GetPoint();
            std::unique_ptr<SwPaM> xPaM(std::make_unique<SwPaM>(*pMarkPosition, *pPointPosition));
            m_vPaMs.push_back(std::move(xPaM));
        }

        // tdf#160538 sort finds in frames and footnotes in the order they occur in the document
        const SwNodeOffset nEndOfInsertsIndex
            = m_pWrtShell->GetNodes().GetEndOfInserts().GetIndex();
        const SwNodeOffset nEndOfExtrasIndex = m_pWrtShell->GetNodes().GetEndOfExtras().GetIndex();
        std::stable_sort(m_vPaMs.begin(), m_vPaMs.end(),
                         [&nEndOfInsertsIndex, &nEndOfExtrasIndex,
                          this](const std::unique_ptr<SwPaM>& a, const std::unique_ptr<SwPaM>& b) {
                             SwPosition aPos(*a->Start());
                             SwPosition bPos(*b->Start());
                             // use page number for footnotes and endnotes
                             if (aPos.GetNodeIndex() >= nEndOfInsertsIndex
                                 && bPos.GetNodeIndex() < nEndOfInsertsIndex)
                                 return b->GetPageNum() >= a->GetPageNum();
                             // use anchor position for finds that are located in flys
                             if (nEndOfExtrasIndex >= aPos.GetNodeIndex())
                                 getAnchorPos(aPos);
                             if (nEndOfExtrasIndex >= bPos.GetNodeIndex())
                                 getAnchorPos(bPos);
                             if (aPos == bPos)
                             {
                                 // probably in same or nested fly frame
                                 // sort using layout position
                                 SwRect aCharRect, bCharRect;
                                 if (SwContentFrame* pFrame
                                     = a->GetMarkContentNode()->GetTextNode()->getLayoutFrame(
                                         m_pWrtShell->GetLayout()))
                                 {
                                     pFrame->GetCharRect(aCharRect, *a->GetMark());
                                 }
                                 if (SwContentFrame* pFrame
                                     = b->GetMarkContentNode()->GetTextNode()->getLayoutFrame(
                                         m_pWrtShell->GetLayout()))
                                 {
                                     pFrame->GetCharRect(bCharRect, *b->GetMark());
                                 }
                                 return aCharRect.Top() < bCharRect.Top();
                             }
                             return aPos < bPos;
                         });

        // fill list
        for (sal_uInt16 nPage = 0, i = 0; std::unique_ptr<SwPaM> & xPaM : m_vPaMs)
        {
            SwPosition* pMarkPosition = xPaM->GetMark();
            SwPosition* pPointPosition = xPaM->GetPoint();

            const SwContentNode* pContentNode = pMarkPosition->GetContentNode();
            const SwTextNode* pTextNode = pContentNode->GetTextNode();
            const OUString& sNodeText = pTextNode->GetText();

            auto nMarkIndex = pMarkPosition->GetContentIndex();
            auto nPointIndex = pPointPosition->GetContentIndex();

            // determine the text node text subview start index for the list entry text
            auto nStartIndex = nMarkIndex - CharactersBeforeAndAfter;
            if (nStartIndex < 0)
            {
                nStartIndex = 0;
            }
            else
            {
                // tdf#160539 format search finds results also to word boundaries
                sal_Unicode ch;
                do
                {
                    ch = sNodeText[nStartIndex];
                } while (++nStartIndex < nMarkIndex && ch != ' ' && ch != '\t');
                if (nStartIndex < nMarkIndex)
                {
                    // move past neighboring space and tab characters
                    ch = sNodeText[nStartIndex];
                    while (nStartIndex < nMarkIndex && (ch == ' ' || ch == '\t'))
                        ch = sNodeText[++nStartIndex];
                }
                if (nStartIndex == nMarkIndex) // no white space found
                    nStartIndex = nMarkIndex - CharactersBeforeAndAfter;
            }

            // determine the text node text subview end index for the list entry text
            auto nEndIndex = nPointIndex + CharactersBeforeAndAfter;
            if (nEndIndex >= sNodeText.getLength())
            {
                nEndIndex = sNodeText.getLength() - 1;
            }
            else
            {
                // tdf#160539 format search finds results also to word boundaries
                sal_Unicode ch;
                do
                {
                    ch = sNodeText[nEndIndex];
                } while (--nEndIndex > nPointIndex && ch != ' ' && ch != '\t');
                if (nEndIndex > nPointIndex)
                {
                    // move past neighboring space and tab characters
                    ch = sNodeText[nEndIndex];
                    while (nEndIndex > nPointIndex && (ch == ' ' || ch == '\t'))
                        ch = sNodeText[--nEndIndex];
                }
                if (nEndIndex == nPointIndex) // no white space found
                {
                    nEndIndex = nPointIndex + CharactersBeforeAndAfter;
                    if (nEndIndex >= sNodeText.getLength())
                        nEndIndex = sNodeText.getLength() - 1;
                }
            }

            // tdf#161291 indicate page of search finds
            if (xPaM->GetPageNum() != nPage)
            {
                nPage = xPaM->GetPageNum();
                OUString sPageEntry(u"-"_ustr + SwResId(ST_PGE) + u" "_ustr
                                    + OUString::number(nPage));
                m_xSearchFindsList->append(sPageEntry, sPageEntry);
            }

            auto nCount = nMarkIndex - nStartIndex;
            OUString sTextBeforeFind = OUString::Concat(sNodeText.subView(nStartIndex, nCount));
            auto nCount1 = nPointIndex - nMarkIndex;
            OUString sFind = OUString::Concat(sNodeText.subView(nMarkIndex, nCount1));
            auto nCount2 = nEndIndex - nPointIndex + 1;
            OUString sTextAfterFind = OUString::Concat(sNodeText.subView(nPointIndex, nCount2));
            OUString sStr = sTextBeforeFind + "[" + sFind + "]" + sTextAfterFind;

            OUString sId = OUString::number(i++);
            m_xSearchFindsList->append(sId, sStr);
        }
    }

    // tdf#160538 sort finds in frames and footnotes in the order they occur in the document
    const SwNodeOffset nEndOfInsertsIndex = m_pWrtShell->GetNodes().GetEndOfInserts().GetIndex();
    const SwNodeOffset nEndOfExtrasIndex = m_pWrtShell->GetNodes().GetEndOfExtras().GetIndex();
    std::stable_sort(
        m_vPaMs.begin(), m_vPaMs.end(),
        [&nEndOfInsertsIndex, &nEndOfExtrasIndex, this](const std::unique_ptr<SwPaM>& a,
                                                        const std::unique_ptr<SwPaM>& b) {
            SwPosition aPos(*a->Start());
            SwPosition bPos(*b->Start());
            // use page number for footnotes and endnotes
            if (aPos.GetNodeIndex() >= nEndOfInsertsIndex
                && bPos.GetNodeIndex() < nEndOfInsertsIndex)
                return b->GetPageNum() >= a->GetPageNum();
            // use anchor position for finds that are located in flys
            if (nEndOfExtrasIndex >= aPos.GetNodeIndex())
                getAnchorPos(aPos);
            if (nEndOfExtrasIndex >= bPos.GetNodeIndex())
                getAnchorPos(bPos);
            if (aPos == bPos)
            {
                // probably in same or nested fly frame
                // sort using layout position
                SwRect aCharRect, bCharRect;
                if (SwContentFrame* pFrame = a->GetMarkContentNode()->GetTextNode()->getLayoutFrame(
                        m_pWrtShell->GetLayout()))
                {
                    pFrame->GetCharRect(aCharRect, *a->GetMark());
                }
                if (SwContentFrame* pFrame = b->GetMarkContentNode()->GetTextNode()->getLayoutFrame(
                        m_pWrtShell->GetLayout()))
                {
                    pFrame->GetCharRect(bCharRect, *b->GetMark());
                }
                return aCharRect.Top() < bCharRect.Top();
            }
            return aPos < bPos;
        });
    // Any finds?
    auto nSearchFindFoundTimes = m_vPaMs.size();

    // fill list
    for (sal_uInt16 nPage = 0, i = 0; std::unique_ptr<SwPaM> & xPaM : m_vPaMs)
    {
        SwPosition* pMarkPosition = xPaM->GetMark();
        SwPosition* pPointPosition = xPaM->GetPoint();
    // set the search term entry background
    m_xSearchFindEntry->set_message_type(nSearchFindFoundTimes ? weld::EntryMessageType::Normal
                                                               : weld::EntryMessageType::Error);
    // make the search finds list focusable or not
    m_xSearchFindsList->set_sensitive(bool(nSearchFindFoundTimes));

        const SwContentNode* pContentNode = pMarkPosition->GetContentNode();
        const SwTextNode* pTextNode = pContentNode->GetTextNode();
        const OUString& sNodeText = pTextNode->GetText();

        auto nMarkIndex = pMarkPosition->GetContentIndex();
        auto nPointIndex = pPointPosition->GetContentIndex();

        // determine the text node text subview start index for the list entry text
        auto nStartIndex = nMarkIndex - CharactersBeforeAndAfter;
        if (nStartIndex < 0)
        {
            nStartIndex = 0;
        }
        else
        {
            // tdf#160539 format search finds results also to word boundaries
            sal_Unicode ch;
            do
            {
                ch = sNodeText[nStartIndex];
            } while (++nStartIndex < nMarkIndex && ch != ' ' && ch != '\t');
            if (nStartIndex < nMarkIndex)
            {
                // move past neighboring space and tab characters
                ch = sNodeText[nStartIndex];
                while (nStartIndex < nMarkIndex && (ch == ' ' || ch == '\t'))
                    ch = sNodeText[++nStartIndex];
            }
            if (nStartIndex == nMarkIndex) // no white space found
                nStartIndex = nMarkIndex - CharactersBeforeAndAfter;
        }

        // determine the text node text subview end index for the list entry text
        auto nEndIndex = nPointIndex + CharactersBeforeAndAfter;
        if (nEndIndex >= sNodeText.getLength())
        {
            nEndIndex = sNodeText.getLength() - 1;
        }
        else
        {
            // tdf#160539 format search finds results also to word boundaries
            sal_Unicode ch;
            do
            {
                ch = sNodeText[nEndIndex];
            } while (--nEndIndex > nPointIndex && ch != ' ' && ch != '\t');
            if (nEndIndex > nPointIndex)
            {
                // move past neighboring space and tab characters
                ch = sNodeText[nEndIndex];
                while (nEndIndex > nPointIndex && (ch == ' ' || ch == '\t'))
                    ch = sNodeText[--nEndIndex];
            }
            if (nEndIndex == nPointIndex) // no white space found
            {
                nEndIndex = nPointIndex + CharactersBeforeAndAfter;
                if (nEndIndex >= sNodeText.getLength())
                    nEndIndex = sNodeText.getLength() - 1;
            }
        }

        // tdf#161291 indicate page of search finds
        if (xPaM->GetPageNum() != nPage)
        {
            nPage = xPaM->GetPageNum();
            OUString sPageEntry(u"-"_ustr + SwResId(ST_PGE) + u" "_ustr + OUString::number(nPage));
            m_xSearchFindsList->append(sPageEntry, sPageEntry);
        }

        auto nCount = nMarkIndex - nStartIndex;
        OUString sTextBeforeFind = OUString::Concat(sNodeText.subView(nStartIndex, nCount));
        auto nCount1 = nPointIndex - nMarkIndex;
        OUString sFind = OUString::Concat(sNodeText.subView(nMarkIndex, nCount1));
        auto nCount2 = nEndIndex - nPointIndex + 1;
        OUString sTextAfterFind = OUString::Concat(sNodeText.subView(nPointIndex, nCount2));
        OUString sStr = sTextBeforeFind + "[" + sFind + "]" + sTextAfterFind;

        OUString sId = OUString::number(i++);
        m_xSearchFindsList->append(sId, sStr);
    }
    // set the search term found label number of times found
    OUString sText(SwResId(STR_SEARCH_KEY_FOUND_TIMES, nSearchFindFoundTimes));
    sText = sText.replaceFirst("%1", OUString::number(nSearchFindFoundTimes));
    m_xSearchFindFoundTimesLabel->set_label(sText);
}
}

// end of namespace ::sw::sidebar
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sw/source/uibase/sidebar/QuickFindPanel.hxx b/sw/source/uibase/sidebar/QuickFindPanel.hxx
index 2f9cf4b..10d36648 100644
--- a/sw/source/uibase/sidebar/QuickFindPanel.hxx
+++ b/sw/source/uibase/sidebar/QuickFindPanel.hxx
@@ -10,32 +10,76 @@

#pragma once
#include <sfx2/sidebar/PanelLayout.hxx>
#include <svx/svxdlg.hxx>
#include <wrtsh.hxx>
#include <sfx2/weldutils.hxx>

namespace sw::sidebar
{
class QuickFindPanel : public PanelLayout
{
public:
    static std::unique_ptr<PanelLayout> Create(weld::Widget* pParent);
    class SearchOptionsDialog final : public weld::GenericDialogController
    {
        friend class QuickFindPanel;

    QuickFindPanel(weld::Widget* pParent);
        std::unique_ptr<weld::CheckButton> m_xMatchCaseCheckButton;
        std::unique_ptr<weld::CheckButton> m_xWholeWordsOnlyCheckButton;
        std::unique_ptr<weld::CheckButton> m_xSimilarityCheckButton;
        std::unique_ptr<weld::Button> m_xSimilaritySettingsDialogButton;

        DECL_LINK(SimilarityCheckButtonToggledHandler, weld::Toggleable&, void);
        DECL_LINK(SimilaritySettingsDialogButtonClickedHandler, weld::Button&, void);

        short executeSubDialog(VclAbstractDialog* pVclAbstractDialog);

        bool m_executingSubDialog = false;

        bool m_bIsLEVRelaxed = true;
        sal_uInt16 m_nLEVOther = 2;
        sal_uInt16 m_nLEVShorter = 2;
        sal_uInt16 m_nLEVLonger = 2;

    public:
        SearchOptionsDialog(weld::Window* pParent);
    };

public:
    static std::unique_ptr<PanelLayout> Create(weld::Widget* pParent,
                                               const uno::Reference<frame::XFrame>& rxFrame);

    QuickFindPanel(weld::Widget* pParent, const uno::Reference<frame::XFrame>& rxFrame);
    virtual ~QuickFindPanel() override;

private:
    std::unique_ptr<weld::Entry> m_xSearchFindEntry;
    std::unique_ptr<weld::TreeView> m_xSearchFindsList;
    std::vector<std::unique_ptr<SwPaM>> m_vPaMs;

    std::unique_ptr<weld::Entry> m_xSearchFindEntry;
    std::unique_ptr<weld::Toolbar> m_xSearchOptionsToolbar;
    std::unique_ptr<weld::Toolbar> m_xFindAndReplaceToolbar;
    std::unique_ptr<ToolbarUnoDispatcher> m_xFindAndReplaceToolbarDispatch;
    std::unique_ptr<weld::TreeView> m_xSearchFindsList;
    std::unique_ptr<weld::Label> m_xSearchFindFoundTimesLabel;

    SwWrtShell* m_pWrtShell;

    int m_nMinimumPanelWidth;

    bool m_bMatchCase = false;
    bool m_bWholeWordsOnly = false;
    bool m_bSimilarity = false;
    bool m_bIsLEVRelaxed = true;
    sal_uInt16 m_nLEVOther = 2;
    sal_uInt16 m_nLEVShorter = 2;
    sal_uInt16 m_nLEVLonger = 2;

    DECL_LINK(SearchFindEntryActivateHandler, weld::Entry&, bool);
    DECL_LINK(SearchFindEntryChangedHandler, weld::Entry&, void);
    DECL_LINK(SearchFindsListCustomGetSizeHandler, weld::TreeView::get_size_args, Size);
    DECL_LINK(SearchFindsListRender, weld::TreeView::render_args, void);
    DECL_LINK(SearchFindsListSelectionChangedHandler, weld::TreeView&, void);
    DECL_LINK(SearchFindEntryChangedHandler, weld::Entry&, void);
    DECL_LINK(SearchFindsListRowActivatedHandler, weld::TreeView&, bool);
    DECL_LINK(MousePressHandler, const MouseEvent&, bool);
    DECL_LINK(SearchFindsListMousePressHandler, const MouseEvent&, bool);
    DECL_LINK(SearchOptionsToolbarClickedHandler, const OUString&, void);

    void FillSearchFindsList();
};
diff --git a/sw/source/uibase/sidebar/SwPanelFactory.cxx b/sw/source/uibase/sidebar/SwPanelFactory.cxx
index ee378d8..4e5cd83 100644
--- a/sw/source/uibase/sidebar/SwPanelFactory.cxx
+++ b/sw/source/uibase/sidebar/SwPanelFactory.cxx
@@ -206,7 +206,8 @@ Reference<ui::XUIElement> SAL_CALL SwPanelFactory::createUIElement (
    }
    else if (rsResourceURL.endsWith("/QuickFindPanel"))
    {
        std::unique_ptr<PanelLayout> xPanel = sw::sidebar::QuickFindPanel::Create(pParent);
        std::unique_ptr<PanelLayout> xPanel
            = sw::sidebar::QuickFindPanel::Create(pParent, xFrame);
        xElement = sfx2::sidebar::SidebarPanelBase::Create(rsResourceURL, xFrame, std::move(xPanel),
                                                           ui::LayoutSize(-1, -1, -1));
    }
diff --git a/sw/source/uibase/uiview/viewsrch.cxx b/sw/source/uibase/uiview/viewsrch.cxx
index d005899..9f7c9ca 100644
--- a/sw/source/uibase/uiview/viewsrch.cxx
+++ b/sw/source/uibase/uiview/viewsrch.cxx
@@ -278,7 +278,7 @@ void SwView::ExecSearch(SfxRequest& rReq)
                        lcl_emitSearchResultCallbacks(s_pSrchItem, m_pWrtShell.get(), /* bHighlightAll = */ true);
                    if (!bQuiet)
                    {
                        OUString sText(SwResId(STR_SEARCH_KEY_FOUND_TIMES));
                        OUString sText(SwResId(STR_SEARCH_KEY_FOUND_TIMES, nFound));
                        sText = sText.replaceFirst("%1", OUString::number(nFound));
                        SvxSearchDialogWrapper::SetSearchLabel(sText);
                    }
diff --git a/sw/uiconfig/swriter/ui/sidebarquickfind.ui b/sw/uiconfig/swriter/ui/sidebarquickfind.ui
index e2dfdb0..cc9c122 100644
--- a/sw/uiconfig/swriter/ui/sidebarquickfind.ui
+++ b/sw/uiconfig/swriter/ui/sidebarquickfind.ui
@@ -2,11 +2,9 @@
<!-- Generated with glade 3.40.0 -->
<interface domain="sw">
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkTreeStore" id="liststore1">
  <object class="GtkListStore" id="liststore">
    <columns>
      <!-- column-name text1 -->
      <column type="gchararray"/>
      <!-- column-name text2 -->
      <!-- column-name text -->
      <column type="gchararray"/>
      <!-- column-name id -->
      <column type="gchararray"/>
@@ -18,22 +16,89 @@
    <property name="can-focus">False</property>
    <property name="hexpand">True</property>
    <property name="vexpand">True</property>
    <property name="row-spacing">6</property>
    <property name="column-spacing">6</property>
    <property name="row-homogeneous">True</property>
    <property name="column-homogeneous">True</property>
    <child>
      <object class="GtkBox">
      <object class="GtkBox" id="box">
        <property name="visible">True</property>
        <property name="can-focus">False</property>
        <property name="hexpand">True</property>
        <property name="vexpand">True</property>
        <property name="border-width">6</property>
        <property name="orientation">vertical</property>
        <property name="spacing">2</property>
        <property name="spacing">6</property>
        <child>
          <object class="GtkEntry" id="Find">
          <object class="GtkBox">
            <property name="visible">True</property>
            <property name="can-focus">True</property>
            <property name="placeholder-text" translatable="yes" context="sidebarquickfind|find">Find</property>
            <property name="can-focus">False</property>
            <property name="spacing">2</property>
            <child>
              <object class="GtkEntry" id="Find">
                <property name="visible">True</property>
                <property name="can-focus">True</property>
                <property name="truncate-multiline">True</property>
                <property name="placeholder-text" translatable="yes" context="sidebarquickfind|find">Find</property>
              </object>
              <packing>
                <property name="expand">True</property>
                <property name="fill">True</property>
                <property name="position">0</property>
              </packing>
            </child>
            <child>
              <object class="GtkToolbar" id="searchoptionstoolbar">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="toolbar-style">icons</property>
                <property name="show-arrow">False</property>
                <child>
                  <object class="GtkToolButton" id="searchoptions">
                    <property name="visible">True</property>
                    <property name="can-focus">False</property>
                    <property name="tooltip-text" translatable="yes" context="quickfindpanel|moresearchoptions|tooltip_text">Search Options</property>
                    <property name="icon-name">sw/res/sr20006.png</property>
                    <child internal-child="accessible">
                      <object class="AtkObject" id="searchoptions-atkobject">
                        <property name="AtkObject::accessible-name" translatable="yes" context="quickfindpanel|moresearchoptions|accessible_name">More Search Options</property>
                        <property name="AtkObject::accessible-description" translatable="yes" context="quickfindpanel|extended_tip|moresearchoptions">Click here to open a dialog to set more search options.</property>
                      </object>
                    </child>
                  </object>
                  <packing>
                    <property name="expand">False</property>
                    <property name="homogeneous">False</property>
                  </packing>
                </child>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">False</property>
                <property name="position">1</property>
              </packing>
            </child>
            <child>
              <object class="GtkToolbar" id="findandreplacetoolbar">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="toolbar-style">icons</property>
                <property name="show-arrow">False</property>
                <child>
                  <object class="GtkToolButton" id=".uno:SearchDialog">
                    <property name="visible">True</property>
                    <property name="can-focus">False</property>
                  </object>
                  <packing>
                    <property name="expand">False</property>
                    <property name="homogeneous">False</property>
                  </packing>
                </child>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">False</property>
                <property name="position">2</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
@@ -47,19 +112,18 @@
            <property name="can-focus">True</property>
            <property name="hexpand">True</property>
            <property name="vexpand">True</property>
            <property name="vscrollbar-policy">always</property>
            <property name="shadow-type">in</property>
            <child>
              <object class="GtkTreeView" id="searchfinds">
                <property name="width-request">-1</property>
                <property name="visible">True</property>
                <property name="can-focus">True</property>
                <property name="hexpand">True</property>
                <property name="vexpand">True</property>
                <property name="border-width">0</property>
                <property name="model">liststore1</property>
                <property name="model">liststore</property>
                <property name="headers-visible">False</property>
                <property name="enable-search">False</property>
                <property name="search-column">1</property>
                <property name="headers-clickable">False</property>
                <property name="search-column">0</property>
                <property name="show-expanders">False</property>
                <child internal-child="selection">
                  <object class="GtkTreeSelection"/>
@@ -70,22 +134,38 @@
                <child>
                  <object class="GtkTreeViewColumn" id="treeviewcolumn1">
                    <child>
                      <object class="GtkCellRendererText" id="cellrenderertext"/>
                      <object class="GtkCellRendererText" id="cellrenderertext1"/>
                      <attributes>
                        <attribute name="text">0</attribute>
                      </attributes>
                    </child>
                  </object>
                </child>
                <child internal-child="accessible">
                  <object class="AtkObject" id="searchfinds-atkobject">
                    <property name="AtkObject::accessible-description" translatable="yes" context="sidebarquickfind|extended_tip|searchfinds">Lists the positions in the document that the searched term is found.</property>
                  </object>
                </child>
              </object>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="expand">True</property>
            <property name="fill">True</property>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <object class="GtkLabel" id="numberofsearchfinds">
            <property name="visible">True</property>
            <property name="can-focus">False</property>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">3</property>
          </packing>
        </child>
      </object>
      <packing>
        <property name="left-attach">0</property>
diff --git a/sw/uiconfig/swriter/ui/sidebarquickfindoptionsdialog.ui b/sw/uiconfig/swriter/ui/sidebarquickfindoptionsdialog.ui
new file mode 100644
index 0000000..d80bafa
--- /dev/null
+++ b/sw/uiconfig/swriter/ui/sidebarquickfindoptionsdialog.ui
@@ -0,0 +1,175 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.40.0 -->
<interface domain="sw">
  <requires lib="gtk+" version="3.20"/>
  <object class="GtkDialog" id="SearchOptionsDialog">
    <property name="can-focus">False</property>
    <property name="border-width">6</property>
    <property name="title" translatable="yes" context="sidebarquickfindoptionsdialog|SearchOptionsDialog">Search Options</property>
    <property name="resizable">False</property>
    <property name="modal">True</property>
    <property name="default-width">0</property>
    <property name="default-height">0</property>
    <property name="type-hint">dialog</property>
    <child internal-child="vbox">
      <object class="GtkBox" id="dialog-vbox1">
        <property name="can-focus">False</property>
        <property name="orientation">vertical</property>
        <property name="spacing">2</property>
        <child internal-child="action_area">
          <object class="GtkButtonBox" id="dialog-action_area1">
            <property name="can-focus">False</property>
            <property name="layout-style">end</property>
            <child>
              <object class="GtkButton" id="ok">
                <property name="label" translatable="yes" context="stock">_OK</property>
                <property name="visible">True</property>
                <property name="can-focus">True</property>
                <property name="can-default">True</property>
                <property name="has-default">True</property>
                <property name="receives-default">True</property>
                <property name="use-underline">True</property>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">0</property>
              </packing>
            </child>
            <child>
              <object class="GtkButton" id="cancel">
                <property name="label" translatable="yes" context="stock">_Cancel</property>
                <property name="visible">True</property>
                <property name="can-focus">True</property>
                <property name="receives-default">True</property>
                <property name="use-underline">True</property>
              </object>
              <packing>
                <property name="expand">False</property>
                <property name="fill">True</property>
                <property name="position">1</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">False</property>
            <property name="position">1</property>
          </packing>
        </child>
        <child>
          <!-- n-columns=1 n-rows=3 -->
          <object class="GtkGrid" id="grid4">
            <property name="visible">True</property>
            <property name="can-focus">False</property>
            <property name="hexpand">True</property>
            <property name="row-spacing">6</property>
            <child>
              <object class="GtkBox" id="box3">
                <property name="visible">True</property>
                <property name="can-focus">False</property>
                <property name="spacing">12</property>
                <child>
                  <object class="GtkCheckButton" id="similarity">
                    <property name="label" translatable="yes" context="sidebarquickfindoptionsdialog|similarity">Similarity</property>
                    <property name="visible">True</property>
                    <property name="can-focus">True</property>
                    <property name="receives-default">False</property>
                    <property name="no-show-all">True</property>
                    <property name="use-underline">True</property>
                    <property name="draw-indicator">True</property>
                    <child internal-child="accessible">
                      <object class="AtkObject" id="similarity-atkobject">
                        <property name="AtkObject::accessible-description" translatable="yes" context="sidebarquickfindoptionsdialog|extended_tip|similarity">Find terms that are similar to the Find text. Select this checkbox, and then click the Similarities button to define the similarity options.</property>
                      </object>
                    </child>
                  </object>
                  <packing>
                    <property name="expand">False</property>
                    <property name="fill">True</property>
                    <property name="position">0</property>
                  </packing>
                </child>
                <child>
                  <object class="GtkButton" id="similaritysettingsdialog">
                    <property name="label" translatable="yes" context="sidebarquickfindoptionsdialog|similaritybtn">Similarities...</property>
                    <property name="visible">True</property>
                    <property name="can-focus">True</property>
                    <property name="receives-default">True</property>
                    <property name="no-show-all">True</property>
                    <property name="halign">end</property>
                    <property name="hexpand">True</property>
                    <property name="use-underline">True</property>
                    <child internal-child="accessible">
                      <object class="AtkObject" id="similaritysettingsdialog-atkobject">
                        <property name="AtkObject::accessible-description" translatable="yes" context="sidebarquickfindoptionsdialog|extended_tip|similaritybtn">Set the options for the similarity search.</property>
                      </object>
                    </child>
                  </object>
                  <packing>
                    <property name="expand">False</property>
                    <property name="fill">True</property>
                    <property name="position">1</property>
                  </packing>
                </child>
              </object>
              <packing>
                <property name="left-attach">0</property>
                <property name="top-attach">2</property>
              </packing>
            </child>
            <child>
              <object class="GtkCheckButton" id="matchcase">
                <property name="label" translatable="yes" context="sidebarquickfindoptionsdialog|matchcase">Match case</property>
                <property name="visible">True</property>
                <property name="can-focus">True</property>
                <property name="receives-default">False</property>
                <property name="no-show-all">True</property>
                <property name="use-underline">True</property>
                <property name="draw-indicator">True</property>
                <child internal-child="accessible">
                  <object class="AtkObject" id="matchcase-atkobject">
                    <property name="AtkObject::accessible-description" translatable="yes" context="sidebarquickfindoptionsdialog|extended_tip|matchcase">Finds only exact case matches.</property>
                  </object>
                </child>
              </object>
              <packing>
                <property name="left-attach">0</property>
                <property name="top-attach">0</property>
              </packing>
            </child>
            <child>
              <object class="GtkCheckButton" id="wholewordsonly">
                <property name="label" translatable="yes" context="sidebarquickfindoptionsdialog|wholewordsonly">Whole words only</property>
                <property name="visible">True</property>
                <property name="can-focus">True</property>
                <property name="receives-default">False</property>
                <property name="no-show-all">True</property>
                <property name="use-underline">True</property>
                <property name="draw-indicator">True</property>
                <child internal-child="accessible">
                  <object class="AtkObject" id="wholewordsonly-atkobject">
                    <property name="AtkObject::accessible-description" translatable="yes" context="sidebarquickfindoptionsdialog|extended_tip|wholewordsonly">Finds only whole words.</property>
                  </object>
                </child>
              </object>
              <packing>
                <property name="left-attach">0</property>
                <property name="top-attach">1</property>
              </packing>
            </child>
          </object>
          <packing>
            <property name="expand">False</property>
            <property name="fill">True</property>
            <property name="position">0</property>
          </packing>
        </child>
      </object>
    </child>
    <action-widgets>
      <action-widget response="-5">ok</action-widget>
      <action-widget response="-6">cancel</action-widget>
    </action-widgets>
  </object>
</interface>