Files
ElyOffice/patches/libreoffice/0018-lok-calc-reads-requests-search.patch
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

388 lines
18 KiB
Diff

diff --git a/sc/source/ui/unoobj/docuno.cxx b/sc/source/ui/unoobj/docuno.cxx
index 0bd1bb34e..50582b5e4 100644
--- a/sc/source/ui/unoobj/docuno.cxx
+++ b/sc/source/ui/unoobj/docuno.cxx
@@ -97,6 +97,17 @@
#include <o3tl/string_view.hxx>
#include <formulacell.hxx>
#include <rangenam.hxx>
+#include <com/sun/star/sheet/TableValidationVisibility.hpp>
+#include <svl/zformat.hxx>
+#include <svl/numformat.hxx>
+#include <typedstrdata.hxx>
+#include <filterentries.hxx>
+#include <queryentry.hxx>
+#include <queryparam.hxx>
+#include <dbdata.hxx>
+#include <unordered_set>
+#include <conditio.hxx>
+#include <validat.hxx>
#include <tokenarray.hxx>
#include <convuno.hxx>
#include <datauno.hxx>
@@ -736,16 +747,209 @@ void lcl_DefinedNames(tools::JsonWriter& rJsonWriter, ScDocument& rDoc)
}
}
+
+namespace
+{
+/// The cell a `?col=&row=` query names, on the shown sheet.
+bool lcl_QueryCell(std::string_view rCommand, SCCOL& rCol, SCROW& rRow)
+{
+ std::map<OUString, OUString> aMap
+ = SfxLokHelper::parseCommandParameters(OUString::fromUtf8(rCommand));
+ if (!aMap.count(u"col"_ustr) || !aMap.count(u"row"_ustr))
+ return false;
+ rCol = aMap[u"col"_ustr].toInt32();
+ rRow = aMap[u"row"_ustr].toInt32();
+ return true;
+}
+
+/// A filter column's entries, as the kernel menu lists them.
+void lcl_FilterValues(tools::JsonWriter& rJson, ScDocument& rDoc, SCCOL nCol, SCROW nRow, SCTAB nTab)
+{
+ ScFilterEntries aEntries;
+ rDoc.GetFilterEntries(nCol, nRow, nTab, aEntries);
+ std::unordered_set<OUString> aStrings;
+ std::unordered_set<double> aNumbers;
+ bool bNonEmpty = false;
+ bool bFiltered = false;
+ if (const ScDBData* pDBData = rDoc.GetDBAtCursor(nCol, nRow, nTab, ScDBDataPortion::AREA))
+ {
+ ScQueryParam aParam;
+ pDBData->GetQueryParam(aParam);
+ const std::vector<ScQueryEntry*> aFound = aParam.FindAllEntriesByField(nCol);
+ bFiltered = !aFound.empty();
+ bNonEmpty = aFound.size() == 1 && aFound[0]->IsQueryByNonEmpty();
+ for (const ScQueryEntry* pEntry : aFound)
+ {
+ if (bNonEmpty || pEntry->eOp != SC_EQUAL)
+ continue;
+ for (const ScQueryEntry::Item& rItem : pEntry->GetQueryItems())
+ {
+ if (rItem.meType == ScQueryEntry::ByValue)
+ aNumbers.insert(rItem.mfVal);
+ else
+ aStrings.insert(rItem.maString.getString());
+ }
+ }
+ }
+ rJson.put("filtered", bFiltered);
+ auto aValues = rJson.startArray("values");
+ for (const ScTypedStrData& rStr : aEntries)
+ {
+ // The rules of ScGridWindow::LaunchAutoFilterMenu.
+ bool bSelected = !rStr.IsHiddenByFilter();
+ if (!aStrings.empty() || !aNumbers.empty())
+ bSelected = aStrings.count(rStr.GetString())
+ || (rStr.GetStringType() == ScTypedStrData::Value
+ && (aNumbers.count(rStr.GetValue())
+ || aNumbers.count(rStr.GetRoundedValue())));
+ else if (bNonEmpty && rStr.GetString().isEmpty())
+ bSelected = false;
+ auto aValue = rJson.startStruct();
+ rJson.put("text", rStr.GetString());
+ rJson.put("hidden", rStr.IsHiddenByFilter());
+ rJson.put("selected", bSelected);
+ }
+}
+
+/// The current cell's validation, as the kit client sets it.
+void lcl_Validation(tools::JsonWriter& rJson, ScDocument& rDoc, const ScAddress& rPos)
+{
+ static const char* const aModes[] = { "any", "whole", "decimal", "date",
+ "time", "textlength", "list", "custom" };
+ static const std::pair<ScConditionMode, const char*> aOperators[]
+ = { { ScConditionMode::Equal, "equal" }, { ScConditionMode::NotEqual, "notequal" },
+ { ScConditionMode::Less, "less" }, { ScConditionMode::EqLess, "lessequal" },
+ { ScConditionMode::Greater, "greater" }, { ScConditionMode::EqGreater, "greaterequal" },
+ { ScConditionMode::Between, "between" }, { ScConditionMode::NotBetween, "notbetween" } };
+ const sal_uInt32 nIndex = rDoc.GetAttr(rPos, ATTR_VALIDDATA).GetValue();
+ const ScValidationData* pData = nIndex ? rDoc.GetValidationEntry(nIndex) : nullptr;
+ rJson.put("type", pData ? aModes[pData->GetDataMode()] : "any");
+ if (!pData)
+ return;
+ const auto pOperator = std::find_if(std::begin(aOperators), std::end(aOperators),
+ [pData](const auto& rPair) { return rPair.first == pData->GetOperation(); });
+ rJson.put("operator", pOperator != std::end(aOperators) ? pOperator->second : "equal");
+ rJson.put("value1", pData->GetExpression(rPos, 0));
+ rJson.put("value2", pData->GetExpression(rPos, 1));
+ rJson.put("dropdown", pData->GetListType() != css::sheet::TableValidationVisibility::INVISIBLE);
+ rJson.put("ignoreBlank", pData->IsIgnoreBlank());
+ OUString aTitle, aMessage;
+ rJson.put("showInput", pData->GetInput(aTitle, aMessage));
+ rJson.put("inputTitle", aTitle);
+ rJson.put("inputMessage", aMessage);
+ ScValidErrorStyle eStyle = SC_VALERR_STOP;
+ rJson.put("showError", pData->GetErrMsg(aTitle, aMessage, eStyle));
+ rJson.put("errorTitle", aTitle);
+ rJson.put("errorMessage", aMessage);
+ rJson.put("errorStyle", eStyle == SC_VALERR_INFO ? "info"
+ : eStyle == SC_VALERR_WARNING ? "warning"
+ : "stop");
+}
+
+/// The current cell's number format code and kind.
+void lcl_NumberFormat(tools::JsonWriter& rJson, ScDocument& rDoc, const ScAddress& rPos,
+ std::string_view rCommand)
+{
+ SvNumberFormatter* pFormatter = rDoc.GetFormatTable();
+ std::map<OUString, OUString> aMap
+ = SfxLokHelper::parseCommandParameters(OUString::fromUtf8(rCommand));
+ if (aMap.count(u"code"_ustr))
+ {
+ bool bThousand, bRed;
+ sal_uInt16 nPrecision, nLeading;
+ rJson.put("valid", pFormatter->GetFormatSpecialInfo(aMap[u"code"_ustr], bThousand, bRed,
+ nPrecision, nLeading) == 0);
+ return;
+ }
+ const sal_uInt32 nIndex = rDoc.GetNumberFormat(rPos.Col(), rPos.Row(), rPos.Tab());
+ const SvNumberformat* pEntry = pFormatter->GetEntry(nIndex);
+ rJson.put("code", pEntry ? pEntry->GetFormatstring() : OUString());
+ static const std::pair<SvNumFormatType, const char*> aTypes[]
+ = { { SvNumFormatType::NUMBER, "number" }, { SvNumFormatType::CURRENCY, "currency" },
+ { SvNumFormatType::PERCENT, "percent" }, { SvNumFormatType::DATE, "date" },
+ { SvNumFormatType::TIME, "time" }, { SvNumFormatType::DATETIME, "datetime" },
+ { SvNumFormatType::SCIENTIFIC, "scientific" }, { SvNumFormatType::FRACTION, "fraction" },
+ { SvNumFormatType::LOGICAL, "boolean" }, { SvNumFormatType::TEXT, "text" } };
+ const SvNumFormatType eType = pEntry ? pEntry->GetMaskedType() : SvNumFormatType::ALL;
+ const auto pType = std::find_if(std::begin(aTypes), std::end(aTypes),
+ [eType](const auto& rPair) { return rPair.first == eType; });
+ // General opens each locale's block and reads as a number.
+ const bool bGeneral = !pEntry || nIndex % SV_COUNTRY_LANGUAGE_OFFSET == 0;
+ rJson.put("type", bGeneral ? "general" : pType != std::end(aTypes) ? pType->second : "general");
+}
+
+/// The shown sheet's conditional formats.
+void lcl_ConditionalFormats(tools::JsonWriter& rJson, ScDocument& rDoc, SCTAB nTab)
+{
+ auto aFormats = rJson.startArray("formats");
+ const ScConditionalFormatList* pList = rDoc.GetCondFormList(nTab);
+ if (!pList)
+ return;
+ const ScAddress::Details aDetails(rDoc.GetAddressConvention(), 0, 0);
+ for (const auto& pFormat : *pList)
+ {
+ auto aFormat = rJson.startStruct();
+ rJson.put("key", static_cast<sal_Int64>(pFormat->GetKey()));
+ OUString aRange;
+ pFormat->GetRange().Format(aRange, ScRefFlags::VALID, rDoc, aDetails.eConv);
+ rJson.put("range", aRange);
+ const ScFormatEntry* pEntry = pFormat->size() ? pFormat->GetEntry(0) : nullptr;
+ const ScFormatEntry::Type eType = pEntry ? pEntry->GetType() : ScFormatEntry::Type::Date;
+ rJson.put("kind", eType == ScFormatEntry::Type::Colorscale ? "scale"
+ : eType == ScFormatEntry::Type::Databar ? "databar"
+ : eType == ScFormatEntry::Type::Iconset ? "iconset"
+ : eType == ScFormatEntry::Type::Condition || eType == ScFormatEntry::Type::ExtCondition
+ ? "cell"
+ : "other");
+ if (eType == ScFormatEntry::Type::Condition || eType == ScFormatEntry::Type::ExtCondition)
+ {
+ const auto* pCondition = static_cast<const ScCondFormatEntry*>(pEntry);
+ rJson.put("style", pCondition->GetStyle());
+ rJson.put("value1", pCondition->GetExpression(pFormat->GetRange().GetTopLeftCorner(), 0));
+ }
+ }
+}
+}
+
bool ScModelObj::supportsCommand(std::u16string_view rCommand)
{
- return rCommand == u"SheetReferences" || rCommand == u"DefinedNames"
- || rCommand == u"SelectedRange";
+ static constexpr std::u16string_view aCommands[]
+ = { u"SheetReferences", u"DefinedNames", u"SelectedRange",
+ u"AutoFilterValues", u"DataSelectValues", u"Validation",
+ u"NumberFormatCode", u"ConditionalFormats" };
+ return std::find(std::begin(aCommands), std::end(aCommands), rCommand) != std::end(aCommands);
}
void ScModelObj::getCommandValues(tools::JsonWriter& rJsonWriter, std::string_view rCommand)
{
if (pDocShell && rCommand == ".uno:DefinedNames")
return lcl_DefinedNames(rJsonWriter, pDocShell->GetDocument());
+ ScViewData* pCurrent = ScDocShell::GetViewData();
+ if (pDocShell && pCurrent)
+ {
+ ScDocument& rDoc = pDocShell->GetDocument();
+ const SCTAB nTab = pCurrent->CurrentTabForData();
+ const ScAddress aCursor(pCurrent->GetCurX(), pCurrent->GetCurY(), nTab);
+ SCCOL nCol = 0;
+ SCROW nRow = 0;
+ if (o3tl::starts_with(rCommand, ".uno:AutoFilterValues?") && lcl_QueryCell(rCommand, nCol, nRow))
+ return lcl_FilterValues(rJsonWriter, rDoc, nCol, nRow, nTab);
+ if (o3tl::starts_with(rCommand, ".uno:DataSelectValues?") && lcl_QueryCell(rCommand, nCol, nRow))
+ {
+ std::vector<ScTypedStrData> aStrings;
+ rDoc.GetDataEntries(nCol, nRow, nTab, aStrings, true);
+ auto aValues = rJsonWriter.startArray("values");
+ for (const ScTypedStrData& rStr : aStrings)
+ rJsonWriter.putSimpleValue(rStr.GetString());
+ return;
+ }
+ if (rCommand == ".uno:Validation")
+ return lcl_Validation(rJsonWriter, rDoc, aCursor);
+ if (o3tl::starts_with(rCommand, ".uno:NumberFormatCode"))
+ return lcl_NumberFormat(rJsonWriter, rDoc, aCursor, rCommand);
+ if (rCommand == ".uno:ConditionalFormats")
+ return lcl_ConditionalFormats(rJsonWriter, rDoc, nTab);
+ }
if (pDocShell && rCommand == ".uno:SelectedRange")
{
// The one selected range, absolute; empty for several.
@@ -760,6 +964,27 @@ void ScModelObj::getCommandValues(tools::JsonWriter& rJsonWriter, std::string_vi
: aRange.Format(rDoc, ScRefFlags::RANGE_ABS_3D, aDetails);
rJsonWriter.put("commandName", ".uno:SelectedRange");
rJsonWriter.put("range", aTarget);
+ if (aTarget.isEmpty())
+ return;
+ // The data inside it: its corners, first row, header guess.
+ SCCOL nCol1 = aRange.aStart.Col(), nCol2 = aRange.aEnd.Col();
+ SCROW nRow1 = aRange.aStart.Row(), nRow2 = aRange.aEnd.Row();
+ const SCTAB nTab = aRange.aStart.Tab();
+ if (!rDoc.ShrinkToDataArea(nTab, nCol1, nRow1, nCol2, nRow2))
+ return;
+ {
+ auto aArea = rJsonWriter.startArray("area");
+ rJsonWriter.putSimpleValue(OUString::number(nCol1));
+ rJsonWriter.putSimpleValue(OUString::number(nRow1));
+ rJsonWriter.putSimpleValue(OUString::number(nCol2));
+ rJsonWriter.putSimpleValue(OUString::number(nRow2));
+ }
+ {
+ auto aTop = rJsonWriter.startArray("top");
+ for (SCCOL nCol = nCol1; nCol <= nCol2; ++nCol)
+ rJsonWriter.putSimpleValue(rDoc.GetString(nCol, nRow1, nTab));
+ }
+ rJsonWriter.put("header", rDoc.HasColHeader(nCol1, nRow1, nCol2, nRow2, nTab));
return;
}
if (pDocShell && o3tl::starts_with(rCommand, ".uno:DefinedNames?"))
diff --git a/sc/source/ui/view/gridwin.cxx b/sc/source/ui/view/gridwin.cxx
index 401a26b18..7aa4b4ec7 100644
--- a/sc/source/ui/view/gridwin.cxx
+++ b/sc/source/ui/view/gridwin.cxx
@@ -956,8 +956,25 @@ void ScGridWindow::SendAutofilterPopupPosition(SCCOL nCol, SCROW nRow) {
}
}
+namespace
+{
+/// Asks the kit client for its menu at a cell.
+void lcl_AskClientMenu(const ScViewData& rViewData, const char* pType, SCCOL nCol, SCROW nRow)
+{
+ tools::JsonWriter aJson;
+ aJson.put("type", pType);
+ aJson.put("col", static_cast<sal_Int64>(nCol));
+ aJson.put("row", static_cast<sal_Int64>(nRow));
+ rViewData.GetViewShell()->libreOfficeKitViewCallback(LOK_CALLBACK_TOOLTIP,
+ aJson.finishAndGetAsOString());
+}
+}
+
void ScGridWindow::LaunchAutoFilterMenu(SCCOL nCol, SCROW nRow)
{
+ // The kit client draws its own filter menu.
+ if (comphelper::LibreOfficeKit::isActive())
+ return lcl_AskClientMenu(mrViewData, "autofilter", nCol, nRow);
SCTAB nTab = mrViewData.CurrentTabForData();
ScDocument& rDoc = mrViewData.GetDocument();
bool bLOKActive = comphelper::LibreOfficeKit::isActive();
@@ -1512,6 +1529,9 @@ void ScGridWindow::DoScenarioMenu( const ScRange& rScenRange )
void ScGridWindow::LaunchDataSelectMenu(const SCCOL nCol, const SCROW nRow)
{
+ // The kit client draws its own list.
+ if (comphelper::LibreOfficeKit::isActive())
+ return lcl_AskClientMenu(mrViewData, "datalist", nCol, nRow);
mpFilterBox.reset();
ScDocument& rDoc = mrViewData.GetDocument();
diff --git a/sc/source/core/data/validat.cxx b/sc/source/core/data/validat.cxx
index 7850c0e52..8c7dde103 100644
--- a/sc/source/core/data/validat.cxx
+++ b/sc/source/core/data/validat.cxx
@@ -24,6 +24,9 @@
#include <sfx2/app.hxx>
#include <sfx2/viewsh.hxx>
+#include <LibreOfficeKit/LibreOfficeKitEnums.h>
+#include <tools/json_writer.hxx>
+#include <comphelper/lok.hxx>
#include <basic/sbmeth.hxx>
#include <basic/sbmod.hxx>
#include <basic/sbstar.hxx>
@@ -396,6 +399,25 @@ void ScValidationData::DoError(weld::Window* pParent, const OUString& rInput, co
if (aMessage.isEmpty())
aMessage = ScResId( STR_VALID_DEFERROR );
+ if (comphelper::LibreOfficeKit::isActive())
+ {
+ // No dialog: the default answer, and the client tells.
+ const bool bForget = eErrorStyle != SC_VALERR_INFO;
+ tools::JsonWriter aJson;
+ aJson.put("type", "validityerror");
+ aJson.put("title", aTitle);
+ aJson.put("message", aMessage);
+ aJson.put("style", eErrorStyle == SC_VALERR_INFO ? "info"
+ : eErrorStyle == SC_VALERR_WARNING ? "warning"
+ : "stop");
+ aJson.put("accepted", !bForget);
+ if (SfxViewShell* pViewShell = SfxViewShell::Current())
+ pViewShell->libreOfficeKitViewCallback(LOK_CALLBACK_TOOLTIP,
+ aJson.finishAndGetAsOString());
+ callback(bForget);
+ return;
+ }
+
VclButtonsType eStyle = VclButtonsType::Ok;
VclMessageType eType = VclMessageType::Error;
switch (eErrorStyle)
diff --git a/sc/source/ui/view/viewfun2.cxx b/sc/source/ui/view/viewfun2.cxx
index 83bb29d05..e98b7c6f9 100644
--- a/sc/source/ui/view/viewfun2.cxx
+++ b/sc/source/ui/view/viewfun2.cxx
@@ -2049,6 +2049,8 @@ bool ScViewFunc::SearchAndReplace( const SvxSearchItem* pSearchItem,
bFirst = false;
bool bFound = false;
+ // Find-all's cells, one rectangle each for the kit client.
+ ScRangeList aFoundAll;
while (true)
{
GetFrameWin()->EnterWait();
@@ -2065,6 +2067,8 @@ bool ScViewFunc::SearchAndReplace( const SvxSearchItem* pSearchItem,
aUndoStr, std::move(pUndoDoc), pSearchItem ) );
}
+ if (nCommand == SvxSearchCmd::FIND_ALL)
+ aFoundAll = aMatchedRanges;
if (nCommand == SvxSearchCmd::FIND_ALL || nCommand == SvxSearchCmd::REPLACE_ALL)
{
SfxViewFrame* pViewFrm = SfxViewFrame::Current();
@@ -2199,7 +2203,21 @@ bool ScViewFunc::SearchAndReplace( const SvxSearchItem* pSearchItem,
if (pGridWindow)
{
std::vector<tools::Rectangle> aLogicRects;
- pGridWindow->GetCellSelection(aLogicRects);
+ if (nCommand == SvxSearchCmd::FIND_ALL)
+ {
+ // The marks join neighbors; the client counts cells.
+ for (size_t i = 0; i < aFoundAll.size(); ++i)
+ {
+ const ScRange& rRange = aFoundAll[i];
+ for (SCROW nY = rRange.aStart.Row(); nY <= rRange.aEnd.Row(); ++nY)
+ for (SCCOL nX = rRange.aStart.Col(); nX <= rRange.aEnd.Col(); ++nX)
+ aLogicRects.emplace_back(
+ GetViewData().GetPrintTwipsPos(nX, nY),
+ Size(rDoc.GetColWidth(nX, nTab), rDoc.GetRowHeight(nY, nTab)));
+ }
+ }
+ else
+ pGridWindow->GetCellSelection(aLogicRects);
boost::property_tree::ptree aTree;
aTree.put("searchString", pSearchItem->GetSearchString().toUtf8().getStr());