sw content controls, dropdown: add LOK API

- expose the available list items in a new "items" key of the
  LOK_CALLBACK_CONTENT_CONTROL callback

- add a new lok::Document::sendContentControlEvent() function to be able
  to select a list item from the current drop-down

- add a new listbox to the gtktiledviewer toolbar to select a content
  control list item when the cursor is inside a dropdown

- add tests for the array API of tools::JsonWriter

Change-Id: I47f1333a7815d67952f7c20a9cba1b248886f6dd
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/134256
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
Tested-by: Jenkins
This commit is contained in:
Miklos Vajna 2022-05-13 08:26:32 +02:00
parent 09e0cfd9b3
commit c7d80d229a
20 changed files with 319 additions and 6 deletions

View File

@ -3631,10 +3631,12 @@ void DesktopLOKTest::testABI()
CPPUNIT_ASSERT_EQUAL(documentClassOffset(61), offsetof(struct _LibreOfficeKitDocumentClass, sendFormFieldEvent));
CPPUNIT_ASSERT_EQUAL(documentClassOffset(62), offsetof(struct _LibreOfficeKitDocumentClass, setBlockedCommandList));
CPPUNIT_ASSERT_EQUAL(documentClassOffset(63), offsetof(struct _LibreOfficeKitDocumentClass, renderSearchResult));
CPPUNIT_ASSERT_EQUAL(documentClassOffset(64),
offsetof(struct _LibreOfficeKitDocumentClass, sendContentControlEvent));
// Extending is fine, update this, and add new assert for the offsetof the
// new method
CPPUNIT_ASSERT_EQUAL(documentClassOffset(64), sizeof(struct _LibreOfficeKitDocumentClass));
CPPUNIT_ASSERT_EQUAL(documentClassOffset(65), sizeof(struct _LibreOfficeKitDocumentClass));
}
CPPUNIT_TEST_SUITE_REGISTRATION(DesktopLOKTest);

View File

@ -1143,6 +1143,8 @@ static bool doc_renderSearchResult(LibreOfficeKitDocument* pThis,
const char* pSearchResult, unsigned char** pBitmapBuffer,
int* pWidth, int* pHeight, size_t* pByteSize);
static void doc_sendContentControlEvent(LibreOfficeKitDocument* pThis, const char* pArguments);
} // extern "C"
namespace {
@ -1286,6 +1288,8 @@ LibLODocument_Impl::LibLODocument_Impl(const uno::Reference <css::lang::XCompone
m_pDocumentClass->setBlockedCommandList = doc_setBlockedCommandList;
m_pDocumentClass->sendContentControlEvent = doc_sendContentControlEvent;
gDocumentClass = m_pDocumentClass;
}
pClass = m_pDocumentClass.get();
@ -6070,6 +6074,34 @@ static bool doc_renderSearchResult(LibreOfficeKitDocument* pThis,
return true;
}
static void doc_sendContentControlEvent(LibreOfficeKitDocument* pThis, const char* pArguments)
{
SolarMutexGuard aGuard;
// Supported in Writer only
if (doc_getDocumentType(pThis) != LOK_DOCTYPE_TEXT)
{
return;
}
StringMap aMap(jsdialog::jsonToStringMap(pArguments));
ITiledRenderable* pDoc = getTiledRenderable(pThis);
if (!pDoc)
{
SetLastExceptionMsg("Document doesn't support tiled rendering");
return;
}
// Sanity check
if (aMap.find("type") == aMap.end() || aMap.find("selected") == aMap.end())
{
SetLastExceptionMsg("Wrong arguments for sendContentControlEvent");
return;
}
pDoc->executeContentControlEvent(aMap);
}
static char* lo_getError (LibreOfficeKit *pThis)
{
comphelper::ProfileZone aZone("lo_getError");

View File

@ -469,6 +469,9 @@ struct _LibreOfficeKitDocumentClass
unsigned char** pBitmapBuffer,
int* pWidth, int* pHeight, size_t* pByteSize);
/// @see lok::Document::sendContentControlEvent().
void (*sendContentControlEvent)(LibreOfficeKitDocument* pThis, const char* pArguments);
#endif // defined LOK_USE_UNSTABLE_API || defined LIBO_INTERNAL_ONLY
};

View File

@ -807,6 +807,25 @@ public:
return mpDoc->pClass->renderSearchResult(mpDoc, pSearchResult, pBitmapBuffer, pWidth, pHeight, pByteSize);
}
/**
* Posts an event for the content control at the cursor position.
*
* @param pArguments arguments of the event.
*
* Example argument string:
*
* {
* "type": "drop-down",
* "selected": "2"
* }
*
* selects the 3rd list item of the drop-down.
*/
void sendContentControlEvent(const char* pArguments)
{
mpDoc->pClass->sendContentControlEvent(mpDoc, pArguments);
}
#endif // defined LOK_USE_UNSTABLE_API || defined LIBO_INTERNAL_ONLY
};

View File

@ -367,6 +367,13 @@ gfloat lok_doc_view_pixel_to_twip (LOKDocView*
gfloat lok_doc_view_twip_to_pixel (LOKDocView* pDocView,
float fInput);
/**
* lok_doc_view_send_content_control_event:
* @pDocView: The #LOKDocView instance
* @pArguments: (nullable) (allow-none): see lok::Document::sendContentControlEvent() for the details.
*/
void lok_doc_view_send_content_control_event(LOKDocView* pDocView, const gchar* pArguments);
G_END_DECLS
#endif // INCLUDED_LIBREOFFICEKIT_LIBREOFFICEKITGTK_H

View File

@ -336,6 +336,12 @@ public:
{
return std::vector<basegfx::B2DRange>();
}
/**
* Execute a content control event in the document.
* E.g. select a list item from a drop down content control.
*/
virtual void executeContentControlEvent(const StringMap&) {}
};
} // namespace vcl

View File

@ -331,6 +331,8 @@ static void setupDocView(GtvApplicationWindow* window)
g_signal_connect(window->lokdocview, "search-result-count", G_CALLBACK(LOKDocViewSigHandlers::searchResultCount), nullptr);
g_signal_connect(window->lokdocview, "part-changed", G_CALLBACK(LOKDocViewSigHandlers::partChanged), nullptr);
g_signal_connect(window->lokdocview, "hyperlink-clicked", G_CALLBACK(LOKDocViewSigHandlers::hyperlinkClicked), nullptr);
g_signal_connect(window->lokdocview, "content-control",
G_CALLBACK(LOKDocViewSigHandlers::contentControl), nullptr);
g_signal_connect(window->lokdocview, "cursor-changed", G_CALLBACK(LOKDocViewSigHandlers::cursorChanged), nullptr);
g_signal_connect(window->lokdocview, "address-changed", G_CALLBACK(LOKDocViewSigHandlers::addressChanged), nullptr);
g_signal_connect(window->lokdocview, "formula-changed", G_CALLBACK(LOKDocViewSigHandlers::formulaChanged), nullptr);

View File

@ -202,6 +202,36 @@ void LOKDocViewSigHandlers::formulaChanged(LOKDocView* pDocView, char* pPayload,
gtk_entry_set_text(pFormulabar, pPayload);
}
void LOKDocViewSigHandlers::contentControl(LOKDocView* pDocView, gchar* pJson, gpointer)
{
GtvApplicationWindow* window
= GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView)));
GtvMainToolbar* toolbar = gtv_application_window_get_main_toolbar(window);
gtv_application_window_set_part_broadcast(window, false);
gtk_list_store_clear(
GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(toolbar->m_pContentControlSelector))));
if (!window->lokdocview)
{
return;
}
std::stringstream aStream(pJson);
boost::property_tree::ptree aTree;
boost::property_tree::read_json(aStream, aTree);
boost::optional<boost::property_tree::ptree&> oItems = aTree.get_child_optional("items");
if (oItems)
{
for (const auto& rItem : *oItems)
{
std::string aValue = rItem.second.get_value<std::string>();
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(toolbar->m_pContentControlSelector),
aValue.c_str());
}
}
gtv_application_window_set_part_broadcast(window, true);
}
void LOKDocViewSigHandlers::passwordRequired(LOKDocView* pDocView, char* pUrl, gboolean bModify, gpointer)
{
GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView)));

View File

@ -27,6 +27,7 @@ namespace LOKDocViewSigHandlers {
void passwordRequired(LOKDocView* pDocView, char* pUrl, gboolean bModify, gpointer);
void comment(LOKDocView* pDocView, gchar* pComment, gpointer);
void window(LOKDocView* pDocView, gchar* pPayload, gpointer);
void contentControl(LOKDocView* pDocView, gchar* pComment, gpointer);
gboolean configureEvent(GtkWidget* pWidget, GdkEventConfigure* pEvent, gpointer pData);
}

View File

@ -116,6 +116,8 @@ gtv_main_toolbar_init(GtvMainToolbar* toolbar)
toolbar->m_pAddressbar = GTK_WIDGET(gtk_builder_get_object(builder.get(), "addressbar_entry"));
toolbar->m_pFormulabar = GTK_WIDGET(gtk_builder_get_object(builder.get(), "formulabar_entry"));
toolbar->m_pContentControlSelector
= GTK_WIDGET(gtk_builder_get_object(builder.get(), "combo_contentcontrolselector"));
// TODO: compile with -rdynamic and get rid of it
gtk_builder_add_callback_symbol(builder.get(), "btn_clicked", G_CALLBACK(btn_clicked));
@ -128,6 +130,8 @@ gtv_main_toolbar_init(GtvMainToolbar* toolbar)
gtk_builder_add_callback_symbol(builder.get(), "toggleEditing", G_CALLBACK(toggleEditing));
gtk_builder_add_callback_symbol(builder.get(), "changePartMode", G_CALLBACK(changePartMode));
gtk_builder_add_callback_symbol(builder.get(), "changePart", G_CALLBACK(changePart));
gtk_builder_add_callback_symbol(builder.get(), "changeContentControl",
G_CALLBACK(changeContentControl));
gtk_builder_add_callback_symbol(builder.get(), "changeZoom", G_CALLBACK(changeZoom));
gtk_builder_add_callback_symbol(builder.get(), "toggleFindbar", G_CALLBACK(toggleFindbar));
gtk_builder_add_callback_symbol(builder.get(), "documentRedline", G_CALLBACK(documentRedline));

View File

@ -29,6 +29,7 @@ struct GtvMainToolbar
GtkWidget* m_pAddressbar;
GtkWidget* m_pFormulabar;
GtkWidget* m_pContentControlSelector;
};
struct GtvMainToolbarClass

View File

@ -318,6 +318,22 @@ void changePartMode( GtkWidget* pSelector, gpointer /* pItem */ )
}
}
void changeContentControl(GtkWidget* pSelector, gpointer /*pItem*/)
{
GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pSelector));
if (gtv_application_window_get_part_broadcast(window) && window->lokdocview)
{
int nItem = gtk_combo_box_get_active(GTK_COMBO_BOX(pSelector));
boost::property_tree::ptree aValues;
aValues.put("type", "drop-down");
aValues.put("selected", std::to_string(nItem));
std::stringstream aStream;
boost::property_tree::write_json(aStream, aValues);
std::string aJson = aStream.str();
lok_doc_view_send_content_control_event(LOK_DOC_VIEW(window->lokdocview), aJson.c_str());
}
}
void changeZoom( GtkWidget* pButton, gpointer /* pItem */ )
{
static const float fZooms[] = { 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, 5.0 };

View File

@ -67,6 +67,8 @@ gboolean signalAddressbar(GtkWidget* pWidget, GdkEventKey* pEvent, gpointer /*pD
/// Handles the key-press-event of the formula entry widget.
gboolean signalFormulabar(GtkWidget* /*pWidget*/, GdkEventKey* /*pEvent*/, gpointer /*pData*/);
void changeContentControl(GtkWidget* pSelector, gpointer /*pItem*/);
#endif
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */

View File

@ -427,6 +427,24 @@
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolItem" id="contentcontrolselectortoolitem">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkComboBoxText" id="combo_contentcontrolselector">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text">Content control list items</property>
<signal name="changed" handler="changeContentControl" swapped="no"/>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToggleToolButton" id="btn_editmode">
<property name="visible">True</property>

View File

@ -1410,8 +1410,8 @@ callback (gpointer pData)
{
priv->m_aContentControlRectangles.clear();
}
bool bIsTextSelected = !priv->m_aContentControlRectangles.empty();
g_signal_emit(pDocView, doc_view_signals[CONTENT_CONTROL], 0, bIsTextSelected);
g_signal_emit(pCallback->m_pDocView, doc_view_signals[CONTENT_CONTROL], 0,
pCallback->m_aPayload.c_str());
gtk_widget_queue_draw(GTK_WIDGET(pDocView));
}
break;
@ -3325,7 +3325,7 @@ static void lok_doc_view_class_init (LOKDocViewClass* pClass)
/**
* LOKDocView::content-control:
* @pDocView: the #LOKDocView on which the signal is emitted
* @bIsTextSelected: whether current content control is non-null
* @pPayload: the JSON string containing the information about ruler properties
*/
doc_view_signals[CONTENT_CONTROL] =
g_signal_new("content-control",
@ -3333,9 +3333,9 @@ static void lok_doc_view_class_init (LOKDocViewClass* pClass)
G_SIGNAL_RUN_FIRST,
0,
nullptr, nullptr,
g_cclosure_marshal_VOID__BOOLEAN,
g_cclosure_marshal_generic,
G_TYPE_NONE, 1,
G_TYPE_BOOLEAN);
G_TYPE_STRING);
/**
* LOKDocView::password-required:
@ -3726,6 +3726,23 @@ lok_doc_view_set_part (LOKDocView* pDocView, int nPart)
priv->m_nPartId = nPart;
}
SAL_DLLPUBLIC_EXPORT void lok_doc_view_send_content_control_event(LOKDocView* pDocView,
const gchar* pArguments)
{
LOKDocViewPrivate& priv = getPrivate(pDocView);
if (!priv->m_pDocument)
{
return;
}
std::scoped_lock<std::mutex> aGuard(g_aLOKMutex);
std::stringstream ss;
ss << "lok::Document::sendContentControlEvent('" << pArguments << "')";
g_info("%s", ss.str().c_str());
priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId);
return priv->m_pDocument->pClass->sendContentControlEvent(priv->m_pDocument, pArguments);
}
SAL_DLLPUBLIC_EXPORT gchar*
lok_doc_view_get_part_name (LOKDocView* pDocView, int nPart)
{

View File

@ -449,6 +449,9 @@ public:
// css::tiledrendering::XTiledRenderable
virtual void SAL_CALL paintTile( const ::css::uno::Any& Parent, ::sal_Int32 nOutputWidth, ::sal_Int32 nOutputHeight, ::sal_Int32 nTilePosX, ::sal_Int32 nTilePosY, ::sal_Int32 nTileWidth, ::sal_Int32 nTileHeight ) override;
/// @see vcl::ITiledRenderable::executeContentControlEvent().
void executeContentControlEvent(const StringMap& aArguments) override;
void Invalidate();
void Reactivate(SwDocShell* pNewDocShell);
SwXDocumentPropertyHelper * GetPropertyHelper ();

View File

@ -168,6 +168,7 @@ public:
void testMoveShapeHandle();
void testRedlinePortions();
void testContentControl();
void testDropDownContentControl();
CPPUNIT_TEST_SUITE(SwTiledRenderingTest);
CPPUNIT_TEST(testRegisterCallback);
@ -256,6 +257,7 @@ public:
CPPUNIT_TEST(testMoveShapeHandle);
CPPUNIT_TEST(testRedlinePortions);
CPPUNIT_TEST(testContentControl);
CPPUNIT_TEST(testDropDownContentControl);
CPPUNIT_TEST_SUITE_END();
private:
@ -3637,6 +3639,81 @@ void SwTiledRenderingTest::testContentControl()
CPPUNIT_ASSERT_EQUAL(OString("hide"), sAction);
}
void SwTiledRenderingTest::testDropDownContentControl()
{
// Given a document with a dropdown content control:
SwXTextDocument* pXTextDocument = createDoc();
SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell();
setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell());
uno::Reference<lang::XMultiServiceFactory> xMSF(mxComponent, uno::UNO_QUERY);
uno::Reference<text::XTextDocument> xTextDocument(mxComponent, uno::UNO_QUERY);
uno::Reference<text::XText> xText = xTextDocument->getText();
uno::Reference<text::XTextCursor> xCursor = xText->createTextCursor();
xText->insertString(xCursor, "choose an item", /*bAbsorb=*/false);
xCursor->gotoStart(/*bExpand=*/false);
xCursor->gotoEnd(/*bExpand=*/true);
uno::Reference<text::XTextContent> xContentControl(
xMSF->createInstance("com.sun.star.text.ContentControl"), uno::UNO_QUERY);
uno::Reference<beans::XPropertySet> xContentControlProps(xContentControl, uno::UNO_QUERY);
{
uno::Sequence<beans::PropertyValues> aListItems = {
{
comphelper::makePropertyValue("DisplayText", uno::Any(OUString("red"))),
comphelper::makePropertyValue("Value", uno::Any(OUString("R"))),
},
{
comphelper::makePropertyValue("DisplayText", uno::Any(OUString("green"))),
comphelper::makePropertyValue("Value", uno::Any(OUString("G"))),
},
{
comphelper::makePropertyValue("DisplayText", uno::Any(OUString("blue"))),
comphelper::makePropertyValue("Value", uno::Any(OUString("B"))),
},
};
xContentControlProps->setPropertyValue("ListItems", uno::Any(aListItems));
}
xText->insertTextContent(xCursor, xContentControl, /*bAbsorb=*/true);
pWrtShell->SttEndDoc(/*bStt=*/true);
m_aContentControl.clear();
// When entering that content control:
pWrtShell->Right(CRSR_SKIP_CHARS, /*bSelect=*/false, /*nCount=*/1, /*bBasicCall=*/false);
// Then make sure that the callback is emitted:
CPPUNIT_ASSERT(!m_aContentControl.isEmpty());
{
std::stringstream aStream(m_aContentControl.getStr());
boost::property_tree::ptree aTree;
boost::property_tree::read_json(aStream, aTree);
OString sAction = aTree.get_child("action").get_value<std::string>().c_str();
CPPUNIT_ASSERT_EQUAL(OString("show"), sAction);
OString sRectangles = aTree.get_child("rectangles").get_value<std::string>().c_str();
CPPUNIT_ASSERT(!sRectangles.isEmpty());
boost::optional<boost::property_tree::ptree&> oItems = aTree.get_child_optional("items");
CPPUNIT_ASSERT(oItems);
static const std::vector<std::string> vExpected = { "red", "green", "blue" };
size_t i = 0;
for (const auto& rItem : *oItems)
{
CPPUNIT_ASSERT_EQUAL(vExpected[i++], rItem.second.get_value<std::string>());
}
}
// And when selecting the 2nd item (green):
std::map<OUString, OUString> aArguments;
aArguments.emplace("type", "drop-down");
aArguments.emplace("selected", "1");
pXTextDocument->executeContentControlEvent(aArguments);
// Then make sure that the document is updated accordingly:
SwTextNode* pTextNode = pWrtShell->GetCursor()->GetNode().GetTextNode();
// Without the accompanying fix in place, this test would have failed with:
// - Expected: green
// - Actual : choose an item
// i.e. the document text was not updated.
CPPUNIT_ASSERT_EQUAL(OUString("green"), pTextNode->GetExpandText(pWrtShell->GetLayout()));
}
CPPUNIT_TEST_SUITE_REGISTRATION(SwTiledRenderingTest);
CPPUNIT_PLUGIN_IMPLEMENT();

View File

@ -697,6 +697,16 @@ void SwSelPaintRects::HighlightContentControl()
tools::JsonWriter aJson;
aJson.put("action", "show");
aJson.put("rectangles", aPayload);
if (pContentControl && pContentControl->HasListItems())
{
tools::ScopedJsonWriterArray aItems = aJson.startArray("items");
for (const auto& rItem : pContentControl->GetListItems())
{
aJson.putSimpleValue(rItem.ToString());
}
}
std::unique_ptr<char, o3tl::free_delete> pJson(aJson.extractData());
GetShell()->GetSfxViewShell()->libreOfficeKitViewCallback(LOK_CALLBACK_CONTENT_CONTROL, pJson.get());
}

View File

@ -163,6 +163,7 @@
#include <IDocumentOutlineNodes.hxx>
#include <SearchResultLocator.hxx>
#include <textcontentcontrol.hxx>
using namespace ::com::sun::star;
using namespace ::com::sun::star::text;
@ -3363,6 +3364,51 @@ SwXTextDocument::getSearchResultRectangles(const char* pPayload)
return std::vector<basegfx::B2DRange>();
}
void SwXTextDocument::executeContentControlEvent(const StringMap& rArguments)
{
SwWrtShell* pWrtShell = m_pDocShell->GetWrtShell();
const SwPosition* pStart = pWrtShell->GetCursor()->Start();
SwTextNode* pTextNode = pStart->nNode.GetNode().GetTextNode();
if (!pTextNode)
{
return;
}
SwTextAttr* pAttr = pTextNode->GetTextAttrAt(pStart->nContent.GetIndex(),
RES_TXTATR_CONTENTCONTROL, SwTextNode::PARENT);
if (!pAttr)
{
return;
}
auto pTextContentControl = static_txtattr_cast<SwTextContentControl*>(pAttr);
const SwFormatContentControl& rFormatContentControl = pTextContentControl->GetContentControl();
auto pContentControl = const_cast<SwContentControl*>(rFormatContentControl.GetContentControl());
auto it = rArguments.find("type");
if (it == rArguments.end())
{
return;
}
if (it->second == "drop-down")
{
if (!pContentControl->HasListItems())
{
return;
}
it = rArguments.find("selected");
if (it == rArguments.end())
{
return;
}
sal_Int32 nSelection = it->second.toInt32();
pContentControl->SetSelectedListItem(nSelection);
pWrtShell->GotoContentControl(rFormatContentControl);
}
}
int SwXTextDocument::getPart()
{
SolarMutexGuard aGuard;

View File

@ -29,10 +29,12 @@ public:
void test1();
void test2();
void testArray();
CPPUNIT_TEST_SUITE(JsonWriterTest);
CPPUNIT_TEST(test1);
CPPUNIT_TEST(test2);
CPPUNIT_TEST(testArray);
CPPUNIT_TEST_SUITE_END();
};
@ -81,6 +83,21 @@ void JsonWriterTest::test2()
std::string(result.get()));
}
void JsonWriterTest::testArray()
{
tools::JsonWriter aJson;
{
tools::ScopedJsonWriterArray aArray = aJson.startArray("items");
aJson.putSimpleValue("foo");
aJson.putSimpleValue("bar");
}
std::unique_ptr<char, o3tl::free_delete> aResult(aJson.extractData());
CPPUNIT_ASSERT_EQUAL(std::string("{ \"items\": [ \"foo\", \"bar\"]}"),
std::string(aResult.get()));
}
CPPUNIT_TEST_SUITE_REGISTRATION(JsonWriterTest);
}