diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
commit | ed5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch) | |
tree | 7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /libreofficekit | |
parent | Initial commit. (diff) | |
download | libreoffice-upstream.tar.xz libreoffice-upstream.zip |
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
48 files changed, 11034 insertions, 0 deletions
diff --git a/libreofficekit/CppunitTest_libreofficekit_checkapi.mk b/libreofficekit/CppunitTest_libreofficekit_checkapi.mk new file mode 100644 index 000000000..1d56df8b7 --- /dev/null +++ b/libreofficekit/CppunitTest_libreofficekit_checkapi.mk @@ -0,0 +1,31 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +$(eval $(call gb_CppunitTest_CppunitTest,libreofficekit_checkapi)) + +$(eval $(call gb_CppunitTest_add_cxxflags,libreofficekit_checkapi, \ + $(gb_CXX03FLAGS) \ +)) + +$(eval $(call gb_CppunitTest_add_exception_objects,libreofficekit_checkapi, \ + libreofficekit/qa/unit/checkapi \ +)) +$(eval $(call gb_CppunitTest_add_cobjects,libreofficekit_checkapi,\ + libreofficekit/qa/unit/compile_test \ +)) + +$(eval $(call gb_CppunitTest_set_external_code,libreofficekit_checkapi)) + +ifeq ($(OS),LINUX) +$(eval $(call gb_CppunitTest_add_libs,libreofficekit_checkapi, \ + -ldl \ +)) +endif + +# vim: set noet sw=4 ts=4: diff --git a/libreofficekit/CppunitTest_libreofficekit_tiledrendering.mk b/libreofficekit/CppunitTest_libreofficekit_tiledrendering.mk new file mode 100644 index 000000000..da3be044e --- /dev/null +++ b/libreofficekit/CppunitTest_libreofficekit_tiledrendering.mk @@ -0,0 +1,40 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +#************************************************************************* +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +#************************************************************************* + +$(eval $(call gb_CppunitTest_CppunitTest,libreofficekit_tiledrendering)) + +$(eval $(call gb_CppunitTest_add_exception_objects,libreofficekit_tiledrendering, \ + libreofficekit/qa/unit/tiledrendering \ +)) + +$(eval $(call gb_CppunitTest_use_external,libreofficekit_tiledrendering,boost_headers)) + +# We need all these libraries / etc. due for CppunitTest to work, even though +# our test specifically tests LOK only functionality which would otherwise not +# require any normal LO api/libraries. +$(eval $(call gb_CppunitTest_use_libraries,libreofficekit_tiledrendering, \ + sal \ +)) + +ifeq ($(OS),LINUX) +$(eval $(call gb_CppunitTest_add_libs,libreofficekit_tiledrendering,\ + -lm \ + -ldl \ +)) +endif + +$(eval $(call gb_CppunitTest_use_api,libreofficekit_tiledrendering,\ + offapi \ +)) + +$(eval $(call gb_CppunitTest_use_configuration,libreofficekit_tiledrendering)) + +# vim: set noet sw=4 ts=4: diff --git a/libreofficekit/Executable_gtktiledviewer.mk b/libreofficekit/Executable_gtktiledviewer.mk new file mode 100644 index 000000000..f5d6ddfc8 --- /dev/null +++ b/libreofficekit/Executable_gtktiledviewer.mk @@ -0,0 +1,62 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +$(eval $(call gb_Executable_Executable,gtktiledviewer)) + +$(eval $(call gb_Library_use_sdk_api,gtktiledviewer)) + +$(eval $(call gb_Executable_set_include,gtktiledviewer,\ + $$(INCLUDE) \ + -I$(SRCDIR)/desktop/inc \ + -I$(SRCDIR)/libreofficekit/qa/gtktiledviewer/ \ + -I$(WORKDIR)/UnoApiHeadersTarget/offapi/normal/ \ + -I$(WORKDIR)/UnoApiHeadersTarget/udkapi/normal/ \ +)) + +$(eval $(call gb_Executable_use_externals,gtktiledviewer,\ + boost_headers \ +)) + +$(eval $(call gb_Executable_add_cxxflags,gtktiledviewer,\ + $$(GTK3_CFLAGS) \ +)) + +$(eval $(call gb_Executable_add_libs,gtktiledviewer,\ + $(GTK3_LIBS) \ + -lX11 \ + -lXext \ + -lXrender \ + -lSM \ + -lICE \ +)) + +$(eval $(call gb_Executable_use_libraries,gtktiledviewer,\ + libreofficekitgtk \ +)) + +ifeq ($(OS), $(filter LINUX %BSD SOLARIS, $(OS))) +$(eval $(call gb_Executable_add_libs,gtktiledviewer,\ + -lm $(UNIX_DLAPI_LIBS) \ +)) +endif + +$(eval $(call gb_Executable_add_exception_objects,gtktiledviewer,\ + libreofficekit/qa/gtktiledviewer/gtv-main \ + libreofficekit/qa/gtktiledviewer/gtv-application \ + libreofficekit/qa/gtktiledviewer/gtv-application-window \ + libreofficekit/qa/gtktiledviewer/gtv-main-toolbar \ + libreofficekit/qa/gtktiledviewer/gtv-signal-handlers \ + libreofficekit/qa/gtktiledviewer/gtv-helpers \ + libreofficekit/qa/gtktiledviewer/gtv-lokdocview-signal-handlers \ + libreofficekit/qa/gtktiledviewer/gtv-calc-header-bar \ + libreofficekit/qa/gtktiledviewer/gtv-comments-sidebar \ + libreofficekit/qa/gtktiledviewer/gtv-lok-dialog \ +)) + +# vim: set noet sw=4 ts=4: diff --git a/libreofficekit/Executable_tilebench.mk b/libreofficekit/Executable_tilebench.mk new file mode 100644 index 000000000..e4a7908dc --- /dev/null +++ b/libreofficekit/Executable_tilebench.mk @@ -0,0 +1,33 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +$(eval $(call gb_Executable_Executable,tilebench)) + +$(eval $(call gb_Executable_set_include,tilebench,\ + $$(INCLUDE) \ + -I$(SRCDIR)/desktop/inc \ +)) + +$(eval $(call gb_Executable_use_externals,tilebench,\ + boost_headers \ +)) + +$(eval $(call gb_Executable_use_libraries,tilebench,\ + sal \ +)) + +$(eval $(call gb_Executable_add_libs,tilebench,\ + -lm $(UNIX_DLAPI_LIBS) \ +)) + +$(eval $(call gb_Executable_add_exception_objects,tilebench,\ + libreofficekit/qa/tilebench/tilebench \ +)) + +# vim: set noet sw=4 ts=4: diff --git a/libreofficekit/IwyuFilter_libreofficekit.yaml b/libreofficekit/IwyuFilter_libreofficekit.yaml new file mode 100644 index 000000000..128f5a357 --- /dev/null +++ b/libreofficekit/IwyuFilter_libreofficekit.yaml @@ -0,0 +1,2 @@ +--- +assumeFilename: libreofficekit/source/gtk/lokdocview.cxx diff --git a/libreofficekit/Library_libreofficekitgtk.mk b/libreofficekit/Library_libreofficekitgtk.mk new file mode 100644 index 000000000..9dedd0e64 --- /dev/null +++ b/libreofficekit/Library_libreofficekitgtk.mk @@ -0,0 +1,42 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +$(eval $(call gb_Library_Library,libreofficekitgtk)) + +$(eval $(call gb_Library_use_sdk_api,libreofficekitgtk)) + +$(eval $(call gb_Library_add_exception_objects,libreofficekitgtk,\ + libreofficekit/source/gtk/lokdocview \ + libreofficekit/source/gtk/tilebuffer \ +)) + +$(eval $(call gb_Library_use_externals,libreofficekitgtk,\ + boost_headers \ +)) + +$(eval $(call gb_Library_set_include,libreofficekitgtk,\ + $$(INCLUDE) \ + $$(GTK3_CFLAGS) \ +)) + +$(eval $(call gb_Library_add_libs,libreofficekitgtk,\ + $(GTK3_LIBS) \ +)) + +$(eval $(call gb_Library_add_defs,libreofficekitgtk,\ + -DLOK_PATH="\"$(LIBDIR)/libreoffice/$(LIBO_LIB_FOLDER)\"" \ +)) + +ifeq ($(OS),$(filter LINUX %BSD SOLARIS, $(OS))) +$(eval $(call gb_Library_add_libs,libreofficekitgtk,\ + $(UNIX_DLAPI_LIBS) -lm \ +)) +endif + +# vim: set noet sw=4 ts=4: diff --git a/libreofficekit/Makefile b/libreofficekit/Makefile new file mode 100644 index 000000000..0997e6284 --- /dev/null +++ b/libreofficekit/Makefile @@ -0,0 +1,14 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +module_directory:=$(dir $(realpath $(firstword $(MAKEFILE_LIST)))) + +include $(module_directory)/../solenv/gbuild/partial_build.mk + +# vim: set noet sw=4 ts=4: diff --git a/libreofficekit/Module_libreofficekit.mk b/libreofficekit/Module_libreofficekit.mk new file mode 100644 index 000000000..bf75f359e --- /dev/null +++ b/libreofficekit/Module_libreofficekit.mk @@ -0,0 +1,38 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +$(eval $(call gb_Module_Module,libreofficekit)) + +ifeq ($(OS), $(filter LINUX %BSD SOLARIS, $(OS))) + +$(eval $(call gb_Module_add_check_targets,libreofficekit, \ + CppunitTest_libreofficekit_checkapi \ +)) + +# tdf#113311 disabled because it can deadlock on shutdown +#$(eval $(call gb_Module_add_subsequentcheck_targets,libreofficekit,\ + CppunitTest_libreofficekit_tiledrendering \ +)) + +ifneq ($(ENABLE_GTK3),) +$(eval $(call gb_Module_add_targets,libreofficekit,\ + Library_libreofficekitgtk \ + Executable_gtktiledviewer \ +)) +endif # ($(ENABLE_GTK3),) + +$(eval $(call gb_Module_add_targets,libreofficekit,\ + $(if $(DISABLE_DYNLOADING),,Executable_tilebench) \ + Package_selectionhandles \ + UIConfig_libreofficekit \ +)) + +endif + +# vim: set ts=4 sw=4 et: diff --git a/libreofficekit/Package_selectionhandles.mk b/libreofficekit/Package_selectionhandles.mk new file mode 100644 index 000000000..106a1059d --- /dev/null +++ b/libreofficekit/Package_selectionhandles.mk @@ -0,0 +1,18 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +$(eval $(call gb_Package_Package,libreofficekit_selectionhandles,$(SRCDIR)/android/source/res/drawable-xhdpi)) + +$(eval $(call gb_Package_add_files,libreofficekit_selectionhandles,$(LIBO_SHARE_FOLDER)/libreofficekit,\ + handle_image_start.png \ + handle_image_middle.png \ + handle_image_end.png \ +)) + +# vim: set noet sw=4 ts=4: diff --git a/libreofficekit/README.md b/libreofficekit/README.md new file mode 100644 index 000000000..7eb7bb5fd --- /dev/null +++ b/libreofficekit/README.md @@ -0,0 +1,105 @@ +# LibreOfficeKit + +LibreOfficeKit can be used for accessing LibreOffice functionality +through C/C++, without any need to use UNO. + +For now it only offers document conversion (in addition to an experimental +tiled rendering API). + +## Integrating LOK Into Other Software + +LOK functionality can be accessed by including `LibreOfficeKit.h[xx]` in your +program. + +LOK initialisation (`lok_init`) requires the inclusion of `LibreOfficeKitInit.h` in +your program. If you use the C++ `LibreOfficeKit.hxx` header, it already includes +`LibreOfficeKitInit.h` for you. + +(`LibreOfficeKit.hxx` is a simple and fully inlined C++ wrapper for the same +functionality as in `LibreOfficeKit.h`.) + +An example program can be seen on: +<https://gitlab.com/ojwb/lloconv> + +## Tiled Rendering + +To use LOK Tiled Rendering you will need the following before the LOK includes: + + #define LOK_USE_UNSTABLE_API + +(This must be define before ANY LOK header, i.e. including the Init header.) + +Currently only bitmap-buffer rendering is supported, with a 32-bit BGRA +colorspace (further alternatives could feasibly be implemented as needed). +Scanlines are ordered top-down (whereas LibreOffice will internally default +to bottom-up). + +## Tiled Editing + +On top of the tiled rendering API, a set of new methods have been added to the +`lok::Document` class to allow basic editing, too. Communication between the LOK +client and LibreOffice is a two-way channel. The client can initiate an action +by calling the above mentioned methods. The most important methods for the +client -> LibreOffice communication are: + +- `initializeForRendering()`, expected to be called right after + `lok::Office::documentLoad()` returned a `lok::Document*`. +- `postKeyEvent()`, expected to be called when the user provides input on the + (soft-)keyboard. +- `postMouseEvent()`, expected to be called when the user generated a touch or + mouse event. + +In general, all coordinates are always in absolute twips (20th of a point, or: +1" = 1440 twips). See `lok::Document` in `LibreOfficeKit.hxx` for a full list of +methods and their documentation. + +The other way around (LibreOffice -> LOK client) is implemented using a +callback. A LOK client can register a callback using the registerCallback() +method. Whenever editing requires some action on the client side, a callback +event is emitted. The callback types are described using the +`LibreOfficeKitCallbackType` enumeration in `LibreOfficeKitEnums.h`, the callback +function signature itself is provided by the LibreOfficeKitCallback typedef in +`LibreOfficeKitTypes.h`. The most important callback types: + +- `LOK_CALLBACK_INVALIDATE_TILES`: drop all tiles cached on client-side that + intersect with the provided rectangle +- `LOK_CALLBACK_INVALIDATE_VISIBLE_CURSOR`: need to set the position and/or the + size of the cursor +- `LOK_CALLBACK_TEXT_SELECTION`: need to adjust the selection overlay provided + by the client as the set of rectangles describing the selection overlay + changed + +There are currently two known LOK clients supporting tiled editing: + +- `gtktiledviewer` (see below), which allows testing the LOK core implementation + on (desktop) Linux +- (LibreOffice on) Android + +Core has next to no idea what is the LOK client, so for effective development, +it's recommended that the core part is developed against `gtktiledviewer`, and +once a feature works there, then implement the Android part, with its slower +development iteration (slow uploading to the device, the need to link all +object files into a single `.so`, etc). + +* Debugging with gdb and `gtktiledviewer` + +To run `gtktiledviewer`: + + bin/run gtktiledviewer --lo-path=$PWD/instdir/program path/to/test.odt + +To receive all incoming events from core use `G_MESSAGES_DEBUG=all` + + G_MESSAGES_DEBUG=all bin/run gtktiledviewer --lo-path=$PWD/instdir/program ../test.odt + +To debug with `gdb`: + + export LO_TRACE='gdb --tui --args' + +before `bin/run`, this will run gtktiledviewer in the debugger instead. + +## LibreOfficeKitGtk + +Currently consists of only a very basic GTK document viewer widget. + +The widget uses `g_info()` instead of `SAL_INFO()`, use the `G_MESSAGES_DEBUG=all` +environment variable to display those messages. diff --git a/libreofficekit/UIConfig_libreofficekit.mk b/libreofficekit/UIConfig_libreofficekit.mk new file mode 100644 index 000000000..7018506db --- /dev/null +++ b/libreofficekit/UIConfig_libreofficekit.mk @@ -0,0 +1,16 @@ +# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*- +# +# This file is part of the LibreOffice project. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +$(eval $(call gb_UIConfig_UIConfig,libreofficekit)) + +$(eval $(call gb_UIConfig_add_a11yerrors_uifiles,libreofficekit,\ + libreofficekit/qa/gtktiledviewer/gtv \ +)) + +# vim: set noet sw=4 ts=4: diff --git a/libreofficekit/UnoCommands.txt b/libreofficekit/UnoCommands.txt new file mode 100644 index 000000000..b949c4d67 --- /dev/null +++ b/libreofficekit/UnoCommands.txt @@ -0,0 +1,15 @@ +This document describes UNO commands that are (know to be) used with LOK. + +Command: ".uno:InsertGraphic" + +JSON parameters: + +{ + "FileName" : + { + "type" : "string", + "value" : "<fileURL>" + } +} + +where <fileURL> is URL encoded string, e.g. file:///home/user/file.odt diff --git a/libreofficekit/qa/data/blank_presentation.odp b/libreofficekit/qa/data/blank_presentation.odp Binary files differnew file mode 100644 index 000000000..fd68d9a9b --- /dev/null +++ b/libreofficekit/qa/data/blank_presentation.odp diff --git a/libreofficekit/qa/data/blank_text.odt b/libreofficekit/qa/data/blank_text.odt Binary files differnew file mode 100644 index 000000000..00b92d785 --- /dev/null +++ b/libreofficekit/qa/data/blank_text.odt diff --git a/libreofficekit/qa/data/calc_sheetnames.ods b/libreofficekit/qa/data/calc_sheetnames.ods Binary files differnew file mode 100644 index 000000000..f6627a058 --- /dev/null +++ b/libreofficekit/qa/data/calc_sheetnames.ods diff --git a/libreofficekit/qa/data/empty.ods b/libreofficekit/qa/data/empty.ods Binary files differnew file mode 100644 index 000000000..a36d1f97c --- /dev/null +++ b/libreofficekit/qa/data/empty.ods diff --git a/libreofficekit/qa/data/impress_slidenames.odp b/libreofficekit/qa/data/impress_slidenames.odp Binary files differnew file mode 100644 index 000000000..d7cb6aeef --- /dev/null +++ b/libreofficekit/qa/data/impress_slidenames.odp diff --git a/libreofficekit/qa/data/join/README b/libreofficekit/qa/data/join/README new file mode 100644 index 000000000..35762e1f0 --- /dev/null +++ b/libreofficekit/qa/data/join/README @@ -0,0 +1,4 @@ +Files to run through tilebench --join to detect problems. + +bin/run tilebench instdir/program libreofficekit/qa/join/<filename> --join + diff --git a/libreofficekit/qa/data/join/calc-100-textjitter.xlsx b/libreofficekit/qa/data/join/calc-100-textjitter.xlsx Binary files differnew file mode 100644 index 000000000..94a3e5254 --- /dev/null +++ b/libreofficekit/qa/data/join/calc-100-textjitter.xlsx diff --git a/libreofficekit/qa/data/join/calc-object-offset.ods b/libreofficekit/qa/data/join/calc-object-offset.ods Binary files differnew file mode 100644 index 000000000..b86ef3107 --- /dev/null +++ b/libreofficekit/qa/data/join/calc-object-offset.ods diff --git a/libreofficekit/qa/gtktiledviewer/gtv-application-window.cxx b/libreofficekit/qa/gtktiledviewer/gtv-application-window.cxx new file mode 100644 index 000000000..7e2f9f907 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-application-window.cxx @@ -0,0 +1,511 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <gtk/gtk.h> + +#include <memory> + +#include <LibreOfficeKit/LibreOfficeKitGtk.h> +#include <LibreOfficeKit/LibreOfficeKitEnums.h> + +#include "gtv-application-window.hxx" +#include "gtv-main-toolbar.hxx" +#include "gtv-helpers.hxx" +#include "gtv-signal-handlers.hxx" +#include "gtv-lokdocview-signal-handlers.hxx" +#include "gtv-calc-header-bar.hxx" +#include "gtv-comments-sidebar.hxx" +#include "gtv-lok-dialog.hxx" + +#include <boost/property_tree/json_parser.hpp> + +namespace { + +struct GtvApplicationWindowPrivate +{ + GtkWidget* container; + GtkWidget* gridcontainer; + GtkWidget* toolbarcontainer; + GtkWidget* scrolledwindowcontainer; + + bool toolbarBroadcast; + bool partSelectorBroadcast; + + GList* m_pChildWindows; + + // Rendering args; options with which lokdocview was rendered in this window + GtvRenderingArgs* m_pRenderingArgs; +}; + +} + +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +#endif +G_DEFINE_TYPE_WITH_PRIVATE(GtvApplicationWindow, gtv_application_window, GTK_TYPE_APPLICATION_WINDOW); +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic pop +#endif +#endif + +static GtvApplicationWindowPrivate* +getPrivate(GtvApplicationWindow* win) +{ + return static_cast<GtvApplicationWindowPrivate*>(gtv_application_window_get_instance_private(win)); +} + +static void +gtv_application_window_init(GtvApplicationWindow* win) +{ + const std::string uiFilePath = GtvHelpers::getDirPath(__FILE__) + std::string(UI_FILE_NAME); + GtvGtkWrapper<GtkBuilder> builder(gtk_builder_new_from_file(uiFilePath.c_str()), + [](GtkBuilder* pBuilder) { + g_object_unref(pBuilder); + }); + GtvApplicationWindowPrivate* priv = getPrivate(win); + + // This is the parent GtkBox holding everything + priv->container = GTK_WIDGET(gtk_builder_get_object(builder.get(), "container")); + // Toolbar container + priv->toolbarcontainer = gtv_main_toolbar_new(); + + // Attach to the toolbar to main window + gtk_box_pack_start(GTK_BOX(priv->container), priv->toolbarcontainer, false, false, false); + gtk_box_reorder_child(GTK_BOX(priv->container), priv->toolbarcontainer, 0); + + priv->gridcontainer = GTK_WIDGET(gtk_builder_get_object(builder.get(), "maingrid")); + // scrolled window containing the main drawing area + win->scrolledwindow = GTK_WIDGET(gtk_builder_get_object(builder.get(), "scrolledwindow")); + // scrolledwindow container + priv->scrolledwindowcontainer = GTK_WIDGET(gtk_builder_get_object(builder.get(), "scrolledwindowcontainer")); + + GtkAdjustment* pHAdjustment = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(win->scrolledwindow)); + g_signal_connect(pHAdjustment, "value-changed", G_CALLBACK(docAdjustmentChanged), win); + GtkAdjustment* pVAdjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(win->scrolledwindow)); + g_signal_connect(pVAdjustment, "value-changed", G_CALLBACK(docAdjustmentChanged), win); + + // calc header row bar + win->cornerarea = gtv_calc_header_bar_new(); + gtv_calc_header_bar_set_type_and_width(GTV_CALC_HEADER_BAR(win->cornerarea), CalcHeaderType::CORNER); + win->rowbar = gtv_calc_header_bar_new(); + gtv_calc_header_bar_set_type_and_width(GTV_CALC_HEADER_BAR(win->rowbar), CalcHeaderType::ROW); + win->columnbar = gtv_calc_header_bar_new(); + gtv_calc_header_bar_set_type_and_width(GTV_CALC_HEADER_BAR(win->columnbar), CalcHeaderType::COLUMN); + + // attach row/column/corner to the container + gtk_grid_attach(GTK_GRID(priv->gridcontainer), win->cornerarea, 0, 0, 1, 1); + gtk_grid_attach(GTK_GRID(priv->gridcontainer), win->rowbar, 0, 1, 1, 1); + gtk_grid_attach(GTK_GRID(priv->gridcontainer), win->columnbar, 1, 0, 1, 1); + + // statusbar + win->statusbar = GTK_WIDGET(gtk_builder_get_object(builder.get(), "statusbar")); + win->redlinelabel = GTK_WIDGET(gtk_builder_get_object(builder.get(), "redlinelabel")); + win->zoomlabel = GTK_WIDGET(gtk_builder_get_object(builder.get(), "zoomlabel")); + + win->findtoolbar = GTK_WIDGET(gtk_builder_get_object(builder.get(), "findtoolbar")); + win->findbarlabel = GTK_WIDGET(gtk_builder_get_object(builder.get(), "findbar_label")); + win->findbarEntry = GTK_WIDGET(gtk_builder_get_object(builder.get(), "findbar_entry")); + win->findAll = GTK_WIDGET(gtk_builder_get_object(builder.get(), "findbar_findall")); + priv->toolbarBroadcast = true; + priv->partSelectorBroadcast = true; + + gtk_container_add(GTK_CONTAINER(win), priv->container); + + priv->m_pChildWindows = nullptr; + priv->m_pRenderingArgs = new GtvRenderingArgs(); +} + +static void +gtv_application_window_dispose(GObject* object) +{ + GtvApplicationWindowPrivate* priv = getPrivate(GTV_APPLICATION_WINDOW(object)); + + delete priv->m_pRenderingArgs; + priv->m_pRenderingArgs = nullptr; + + G_OBJECT_CLASS (gtv_application_window_parent_class)->dispose (object); +} + +static void +gtv_application_window_class_init(GtvApplicationWindowClass* klass) +{ + G_OBJECT_CLASS(klass)->dispose = gtv_application_window_dispose; +} + +/// Helper function to do some tasks after widget is fully loaded (including +/// document load) +static void initWindow(GtvApplicationWindow* window) +{ + GtvApplicationWindowPrivate* priv = getPrivate(window); + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + GList *focusChain = nullptr; + focusChain = g_list_append( focusChain, window->lokdocview ); + + gtk_container_set_focus_chain ( GTK_CONTAINER (priv->container), focusChain ); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + + // TODO: Implement progressbar in statusbar + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + if (pDocument) + { + LibreOfficeKitDocumentType eDocType = static_cast<LibreOfficeKitDocumentType>(pDocument->pClass->getDocumentType(pDocument)); + if (eDocType == LOK_DOCTYPE_SPREADSHEET) + { + // Align to top left corner, so the tiles are in sync with the + // row/column bar, even when zooming out enough that not all space is + // used. + gtk_widget_set_halign(GTK_WIDGET(window->lokdocview), GTK_ALIGN_START); + gtk_widget_set_valign(GTK_WIDGET(window->lokdocview), GTK_ALIGN_START); + } + + // By default make the document editable in a new window + lok_doc_view_set_edit(LOK_DOC_VIEW(window->lokdocview), true); + // Let toolbar adjust its button accordingly + gtv_main_toolbar_doc_loaded(GTV_MAIN_TOOLBAR(priv->toolbarcontainer), eDocType, true /* Edit button state */); + } + + // Fill our comments sidebar + gboolean bTiledAnnotations; + g_object_get(G_OBJECT(window->lokdocview), "tiled-annotations", &bTiledAnnotations, nullptr); + if (!bTiledAnnotations && pDocument) + { + window->commentssidebar = gtv_comments_sidebar_new(); + gtk_container_add(GTK_CONTAINER(priv->scrolledwindowcontainer), window->commentssidebar); + // fill the comments sidebar + gtv_comments_sidebar_view_annotations(GTV_COMMENTS_SIDEBAR(window->commentssidebar)); + } +} + +static void +gtv_application_open_document_callback(GObject* source_object, GAsyncResult* res, gpointer /*userdata*/) +{ + LOKDocView* pDocView = LOK_DOC_VIEW (source_object); + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView))); + GError* error = nullptr; + if (!lok_doc_view_open_document_finish(pDocView, res, &error)) + { + GtkWidget* pDialog = gtk_message_dialog_new(GTK_WINDOW(window), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + "Error occurred while opening the document: '%s'", + error->message); + gtk_dialog_run(GTK_DIALOG(pDialog)); + gtk_widget_destroy(pDialog); + + g_error_free(error); + gtk_widget_destroy(GTK_WIDGET(pDocView)); + gtk_main_quit(); + return; + } + + initWindow(window); +} + +/// Get the visible area of the scrolled window +void gtv_application_window_get_visible_area(GtvApplicationWindow* pWindow, GdkRectangle* pArea) +{ + GtkAdjustment* pHAdjustment = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(pWindow->scrolledwindow)); + GtkAdjustment* pVAdjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(pWindow->scrolledwindow)); + + pArea->x = lok_doc_view_pixel_to_twip(LOK_DOC_VIEW(pWindow->lokdocview), + gtk_adjustment_get_value(pHAdjustment)); + pArea->y = lok_doc_view_pixel_to_twip(LOK_DOC_VIEW(pWindow->lokdocview), + gtk_adjustment_get_value(pVAdjustment)); + pArea->width = lok_doc_view_pixel_to_twip(LOK_DOC_VIEW(pWindow->lokdocview), + gtk_adjustment_get_page_size(pHAdjustment)); + pArea->height = lok_doc_view_pixel_to_twip(LOK_DOC_VIEW(pWindow->lokdocview), + gtk_adjustment_get_page_size(pVAdjustment)); +} + +void gtv_application_window_toggle_findbar(GtvApplicationWindow* window) +{ + if (gtk_widget_get_visible(window->findtoolbar)) + { + gtk_widget_hide(window->findtoolbar); + } + else + { + gtk_widget_show_all(window->findtoolbar); + gtk_widget_grab_focus(window->findtoolbar); + } +} + +GtkToolItem* gtv_application_window_find_tool_by_unocommand(GtvApplicationWindow* window, const std::string& unoCmd) +{ + GtvApplicationWindowPrivate* priv = getPrivate(window); + GtkToolItem* result = nullptr; + + // Find in the first toolbar + GtkContainer* pToolbar1 = gtv_main_toolbar_get_first_toolbar(GTV_MAIN_TOOLBAR(priv->toolbarcontainer)); + GtvGtkWrapper<GList> pList(gtk_container_get_children(pToolbar1), + [](GList* l) + { + g_list_free(l); + }); + for (GList* l = pList.get(); l != nullptr; l = l->next) + { + if (GTK_IS_TOOL_BUTTON(l->data)) + { + GtkToolButton* pButton = GTK_TOOL_BUTTON(l->data); + const gchar* pLabel = gtk_tool_button_get_label(pButton); + if (g_strcmp0(unoCmd.c_str(), pLabel) == 0) + { + result = GTK_TOOL_ITEM(pButton); + } + } + } + + // Look in second toolbar if not found + GtkContainer* pToolbar2 = gtv_main_toolbar_get_second_toolbar(GTV_MAIN_TOOLBAR(priv->toolbarcontainer)); + pList.reset(gtk_container_get_children(pToolbar2)); + for (GList* l = pList.get(); result == nullptr && l != nullptr; l = l->next) + { + if (GTK_IS_TOOL_BUTTON(l->data)) + { + GtkToolButton* pButton = GTK_TOOL_BUTTON(l->data); + const gchar* pLabel = gtk_tool_button_get_label(pButton); + if (g_strcmp0(unoCmd.c_str(), pLabel) == 0) + { + result = GTK_TOOL_ITEM(pButton); + } + } + } + + return result; +} + +static std::string +createRenderingArgsJSON(const GtvRenderingArgs* pRenderingArgs) +{ + boost::property_tree::ptree aTree; + if (pRenderingArgs->m_bHidePageShadow) + { + aTree.put(boost::property_tree::ptree::path_type(".uno:ShowBorderShadow/type", '/'), "boolean"); + aTree.put(boost::property_tree::ptree::path_type(".uno:ShowBorderShadow/value", '/'), false); + } + if (pRenderingArgs->m_bHideWhiteSpace) + { + aTree.put(boost::property_tree::ptree::path_type(".uno:HideWhitespace/type", '/'), "boolean"); + aTree.put(boost::property_tree::ptree::path_type(".uno:HideWhitespace/value", '/'), true); + } + aTree.put(boost::property_tree::ptree::path_type(".uno:Author/type", '/'), "string"); + aTree.put(boost::property_tree::ptree::path_type(".uno:Author/value", '/'), GtvHelpers::getNextAuthor()); + std::stringstream aStream; + boost::property_tree::write_json(aStream, aTree); + return aStream.str(); +} + +static void setupDocView(GtvApplicationWindow* window) +{ + GtvApplicationWindowPrivate* priv = getPrivate(window); + g_object_set(G_OBJECT(window->lokdocview), + "doc-password", true, + "doc-password-to-modify", true, + "tiled-annotations", priv->m_pRenderingArgs->m_bEnableTiledAnnotations, + nullptr); + +#if GLIB_CHECK_VERSION(2,40,0) + g_assert_nonnull(window->lokdocview); +#endif + g_signal_connect(window->lokdocview, "edit-changed", G_CALLBACK(LOKDocViewSigHandlers::editChanged), nullptr); + g_signal_connect(window->lokdocview, "command-changed", G_CALLBACK(LOKDocViewSigHandlers::commandChanged), nullptr); + g_signal_connect(window->lokdocview, "command-result", G_CALLBACK(LOKDocViewSigHandlers::commandResult), nullptr); + g_signal_connect(window->lokdocview, "search-not-found", G_CALLBACK(LOKDocViewSigHandlers::searchNotFound), nullptr); + 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); + g_signal_connect(window->lokdocview, "password-required", G_CALLBACK(LOKDocViewSigHandlers::passwordRequired), nullptr); + g_signal_connect(window->lokdocview, "comment", G_CALLBACK(LOKDocViewSigHandlers::comment), nullptr); + g_signal_connect(window->lokdocview, "window", G_CALLBACK(LOKDocViewSigHandlers::window), window); + + g_signal_connect(window->lokdocview, "configure-event", G_CALLBACK(LOKDocViewSigHandlers::configureEvent), nullptr); +} + +void +gtv_application_window_create_view_from_window(GtvApplicationWindow* window) +{ + GtvApplicationWindowPrivate* priv = getPrivate(window); + GApplication* app = g_application_get_default(); + + GtvApplicationWindow* newWindow = GTV_APPLICATION_WINDOW(gtv_application_window_new(GTK_APPLICATION(app))); + const std::string aArguments = createRenderingArgsJSON(priv->m_pRenderingArgs); + newWindow->lokdocview = lok_doc_view_new_from_widget(LOK_DOC_VIEW(window->lokdocview), aArguments.c_str()); + setupDocView(newWindow); + + gtk_container_add(GTK_CONTAINER(newWindow->scrolledwindow), newWindow->lokdocview); + gtk_widget_show_all(newWindow->scrolledwindow); + gtk_window_present(GTK_WINDOW(newWindow)); + + initWindow(newWindow); +} + +void +gtv_application_window_load_document(GtvApplicationWindow* window, + const GtvRenderingArgs* aArgs, + const std::string& aDocPath) +{ + GtvApplicationWindowPrivate* priv = getPrivate(window); + // keep a copy of it; we need to use these for creating new views later + *(priv->m_pRenderingArgs) = *aArgs; + + // setup lokdocview + const char *pUserProfile = priv->m_pRenderingArgs->m_aUserProfile.empty() ? + nullptr : priv->m_pRenderingArgs->m_aUserProfile.c_str(); + + window->lokdocview = GTK_WIDGET( + g_initable_new(LOK_TYPE_DOC_VIEW, nullptr, nullptr, + "lopath", priv->m_pRenderingArgs->m_aLoPath.c_str(), + "unipoll", priv->m_pRenderingArgs->m_bUnipoll, + "userprofileurl", pUserProfile, + "halign", GTK_ALIGN_CENTER, + "valign", GTK_ALIGN_CENTER, + nullptr)); + + gtk_container_add(GTK_CONTAINER(window->scrolledwindow), window->lokdocview); + + setupDocView(window); + + // Create argument JSON + const std::string aArguments = createRenderingArgsJSON(priv->m_pRenderingArgs); + lok_doc_view_open_document(LOK_DOC_VIEW(window->lokdocview), aDocPath.c_str(), + aArguments.c_str(), nullptr, + gtv_application_open_document_callback, window->lokdocview); + + gtk_widget_show_all(GTK_WIDGET(window->scrolledwindow)); +} + +GtvMainToolbar* +gtv_application_window_get_main_toolbar(GtvApplicationWindow* window) +{ + GtvApplicationWindowPrivate* priv = getPrivate(window); + return GTV_MAIN_TOOLBAR(priv->toolbarcontainer); +} + +void +gtv_application_window_set_toolbar_broadcast(GtvApplicationWindow* window, bool broadcast) +{ + GtvApplicationWindowPrivate* priv = getPrivate(window); + priv->toolbarBroadcast = broadcast; +} + +gboolean +gtv_application_window_get_toolbar_broadcast(GtvApplicationWindow* window) +{ + GtvApplicationWindowPrivate* priv = getPrivate(window); + return priv->toolbarBroadcast; +} + +void +gtv_application_window_set_part_broadcast(GtvApplicationWindow* window, bool broadcast) +{ + GtvApplicationWindowPrivate* priv = getPrivate(window); + priv->partSelectorBroadcast = broadcast; +} + +gboolean +gtv_application_window_get_part_broadcast(GtvApplicationWindow* window) +{ + GtvApplicationWindowPrivate* priv = getPrivate(window); + return priv->partSelectorBroadcast; +} + +void +gtv_application_window_register_child_window(GtvApplicationWindow* window, GtkWindow* pChildWin) +{ + guint dialogid = 0; + g_object_get(G_OBJECT(pChildWin), "dialogid", &dialogid, nullptr); + g_debug("Register child window: dialogid [%d] in window[%p]", dialogid, window); + GtvApplicationWindowPrivate* priv = getPrivate(window); + if (pChildWin) + priv->m_pChildWindows = g_list_append(priv->m_pChildWindows, pChildWin); +} + +void +gtv_application_window_unregister_child_window(GtvApplicationWindow* window, GtkWindow* pChildWin) +{ + guint dialogid = 0; + g_object_get(G_OBJECT(pChildWin), "dialogid", &dialogid, nullptr); + g_debug("Unregister child window: dialogid [%d] in window[%p]", dialogid, window); + GtvApplicationWindowPrivate* priv = getPrivate(window); + if (pChildWin) + { + priv->m_pChildWindows = g_list_remove(priv->m_pChildWindows, pChildWin); + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + guint dialogId = 0; + g_object_get(G_OBJECT(pChildWin), "dialogid", &dialogId, nullptr); + pDocument->pClass->postWindow(pDocument, dialogId, LOK_WINDOW_CLOSE, nullptr); + } +} + +GtkWindow* +gtv_application_window_get_child_window_by_id(GtvApplicationWindow* window, guint nWinId) +{ + GtvApplicationWindowPrivate* priv = getPrivate(window); + GList* pIt = nullptr; + GtkWindow* ret = nullptr; + // For now, only dialogs are registered as child window + for (pIt = priv->m_pChildWindows; pIt != nullptr; pIt = pIt->next) + { + guint dialogId = 0; + g_object_get(G_OBJECT(pIt->data), "dialogid", &dialogId, nullptr); + if (dialogId == nWinId) + { + ret = GTK_WINDOW(pIt->data); + break; + } + } + + return ret; +} + +GtkWidget* +gtv_application_window_get_parent(GtvApplicationWindow* window, guint nWinId) +{ + GtvApplicationWindowPrivate* priv = getPrivate(window); + GList* pIt = nullptr; + for (pIt = priv->m_pChildWindows; pIt != nullptr; pIt = pIt->next) + { + if (gtv_lok_dialog_is_parent_of(GTV_LOK_DIALOG(pIt->data), nWinId)) + return GTK_WIDGET(pIt->data); + } + return nullptr; +} + +GtvApplicationWindow* +gtv_application_window_new(GtkApplication* app) +{ + g_return_val_if_fail(GTK_IS_APPLICATION(app), nullptr); + + return GTV_APPLICATION_WINDOW(g_object_new(GTV_TYPE_APPLICATION_WINDOW, + "application", app, + "width-request", 1024, + "height-request", 768, + "title", "LibreOffice GtkTiledViewer", + "window-position", GTK_WIN_POS_CENTER, + "show-menubar", false, + nullptr)); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-application-window.hxx b/libreofficekit/qa/gtktiledviewer/gtv-application-window.hxx new file mode 100644 index 000000000..9d3d51962 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-application-window.hxx @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef GTV_APPLICATION_WINDOW_H +#define GTV_APPLICATION_WINDOW_H + +#include <gtk/gtk.h> + +#include <LibreOfficeKit/LibreOfficeKitEnums.h> + +#include "gtv-main-toolbar.hxx" + +#include <string> + +struct GtvRenderingArgs +{ + std::string m_aLoPath; + std::string m_aUserProfile; + bool m_bEnableTiledAnnotations; + bool m_bUnipoll; + + std::string m_aBackgroundColor; + bool m_bHidePageShadow; + bool m_bHideWhiteSpace; + + GtvRenderingArgs() + : m_bEnableTiledAnnotations(false), + m_bUnipoll(false), + m_bHidePageShadow(false), + m_bHideWhiteSpace(false) + { } +}; + +G_BEGIN_DECLS + +#define GTV_TYPE_APPLICATION_WINDOW (gtv_application_window_get_type()) +#define GTV_APPLICATION_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GTV_TYPE_APPLICATION_WINDOW, GtvApplicationWindow)) +#define GTV_IS_APPLICATION_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTV_TYPE_APPLICATION_WINDOW)) +#define GTV_APPLICATION_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GTV_TYPE_APPLICATION_WINDOW, GtvApplicationWindowClass)) +#define GTV_IS_APPLICATION_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GTV_TYPE_APPLICATION_WINDOW)) +#define GTV_APPLICATION_WINDOW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), GTV_TYPE_APPLICATION_WINDOW, GtvApplicationWindowClass)) + +struct GtvApplicationWindow +{ + GtkApplicationWindow parent_instance; + + GtkWidget* scrolledwindow; + GtkWidget* lokdocview; + LibreOfficeKitDocumentType doctype; + + GtkWidget* rowbar; + GtkWidget* columnbar; + GtkWidget* cornerarea; + + GtkWidget* commentssidebar; + GtkWidget* statusbar; + GtkWidget* zoomlabel; + GtkWidget* redlinelabel; + GtkWidget* findbarlabel; + GtkWidget* findbarEntry; + GtkWidget* findAll; + + GtkWidget* findtoolbar; +}; + +struct GtvApplicationWindowClass +{ + GtkApplicationWindowClass parentClass; +}; + +GType gtv_application_window_get_type (void) G_GNUC_CONST; + +GtvApplicationWindow* gtv_application_window_new(GtkApplication* application); + +void gtv_application_window_load_document(GtvApplicationWindow* application, + const GtvRenderingArgs* aArgs, + const std::string& aDocPath); + +void gtv_application_window_create_view_from_window(GtvApplicationWindow* window); + +void gtv_application_window_get_visible_area(GtvApplicationWindow* pWindow, GdkRectangle* pArea); + +void gtv_application_window_toggle_findbar(GtvApplicationWindow* window); + +GtkToolItem* gtv_application_window_find_tool_by_unocommand(GtvApplicationWindow* window, const std::string& unoCmd); + +GtvMainToolbar* gtv_application_window_get_main_toolbar(GtvApplicationWindow* window); + +void gtv_application_window_set_toolbar_broadcast(GtvApplicationWindow* window, bool broadcast); + +gboolean gtv_application_window_get_toolbar_broadcast(GtvApplicationWindow* window); + +void gtv_application_window_set_part_broadcast(GtvApplicationWindow* window, bool broadcast); + +gboolean gtv_application_window_get_part_broadcast(GtvApplicationWindow* window); + +void gtv_application_window_register_child_window(GtvApplicationWindow* window, GtkWindow* pChildWin); + +void gtv_application_window_unregister_child_window(GtvApplicationWindow* window, GtkWindow* pChildWin); + +GtkWindow* gtv_application_window_get_child_window_by_id(GtvApplicationWindow* window, guint nWinId); + +GtkWidget* gtv_application_window_get_parent(GtvApplicationWindow* window, guint nWinId); + +G_END_DECLS + +#endif /* GTV_APPLICATION_WINDOW_H */ + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-application.cxx b/libreofficekit/qa/gtktiledviewer/gtv-application.cxx new file mode 100644 index 000000000..4268d4e19 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-application.cxx @@ -0,0 +1,188 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <gtk/gtk.h> + +#include "gtv-application.hxx" +#include "gtv-application-window.hxx" + +#include <LibreOfficeKit/LibreOfficeKitGtk.h> + +#include <string> + +namespace { + +struct GtvApplicationPrivate +{ + GtvRenderingArgs* m_pRenderingArgs; +}; + +} + +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +#endif +G_DEFINE_TYPE_WITH_PRIVATE(GtvApplication, gtv_application, GTK_TYPE_APPLICATION); +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic pop +#endif +#endif + +static GtvApplicationPrivate* +getPrivate(GtvApplication* app) +{ + return static_cast<GtvApplicationPrivate*>(gtv_application_get_instance_private(app)); +} + +static void +gtv_application_activate(GApplication*) +{ + // If this isn't provided, some GTK versions fail to run us at all. +} + +static void +gtv_application_open(GApplication* app, GFile** file, gint nFiles, const gchar* /*hint*/) +{ + for (gint i = 0; i < nFiles; i++) + { + // TODO: add some option to create a new view for existing document + // For now, this just opens a new document + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtv_application_window_new(GTK_APPLICATION(app))); + gtk_window_present(GTK_WINDOW(window)); + + GtvApplicationPrivate* priv = getPrivate(GTV_APPLICATION(app)); + gtv_application_window_load_document(window, priv->m_pRenderingArgs, std::string(g_file_get_path(file[i]))); + } +} + +static void +gtv_application_init(GtvApplication* app) +{ + static const GOptionEntry commandLineOptions[] = + { + { "version", 0, 0, G_OPTION_ARG_NONE, nullptr, "Show LOkit version", nullptr }, + { "lo-path", 0, 0, G_OPTION_ARG_STRING, nullptr, "LO path", nullptr }, + { "unipoll", 0, 0, G_OPTION_ARG_NONE, nullptr, "Enable unified polling loop", nullptr }, + { "user-profile", 0, 0, G_OPTION_ARG_STRING, nullptr, "User profile to use", nullptr }, + { "enable-tiled-annotations", 0, 0, G_OPTION_ARG_NONE, nullptr, "Whether tiled annotations should be enabled", nullptr }, + { "background-color", 0, 0, G_OPTION_ARG_STRING, nullptr, "Background color", nullptr }, + { "hide-page-shadow", 0, 0, G_OPTION_ARG_NONE, nullptr, "Hide page shadow", nullptr }, + { "hide-whitespace", 0, 0, G_OPTION_ARG_NONE, nullptr, "Hide whitespace", nullptr }, + { nullptr, 0, 0, G_OPTION_ARG_NONE, nullptr, nullptr, nullptr }, + }; + + g_application_add_main_option_entries(G_APPLICATION(app), commandLineOptions); + + GtvApplicationPrivate* priv = getPrivate(GTV_APPLICATION(app)); + priv->m_pRenderingArgs = new GtvRenderingArgs(); +} + +static void +gtv_application_dispose (GObject* object) +{ + GtvApplicationPrivate* priv = getPrivate(GTV_APPLICATION(object)); + + delete priv->m_pRenderingArgs; + priv->m_pRenderingArgs = nullptr; + + G_OBJECT_CLASS (gtv_application_parent_class)->dispose (object); +} + +static gint +gtv_application_handle_local_options(GApplication* app, GVariantDict* options) +{ + GtvApplicationPrivate* priv = getPrivate(GTV_APPLICATION(app)); + // This is mandatory + if (g_variant_dict_contains(options, "lo-path")) + { + gchar* loPath = nullptr; + g_variant_dict_lookup(options, "lo-path", "s", &loPath); + if (loPath) + { + priv->m_pRenderingArgs->m_aLoPath = std::string(loPath); + g_free(loPath); + } + } + else + { + g_print("--lo-path= is mandatory. Please provide the path to LO installation.\n"); + return 1; // Cannot afford to continue in absence of this param + } + + if (g_variant_dict_contains(options, "unipoll")) + priv->m_pRenderingArgs->m_bUnipoll = true; + + if (g_variant_dict_contains(options, "version")) + { + if (!priv->m_pRenderingArgs->m_aLoPath.empty()) + { + GtkWidget* pDocView = lok_doc_view_new(priv->m_pRenderingArgs->m_aLoPath.c_str(), nullptr, nullptr); + const gchar* versionInfo = lok_doc_view_get_version_info(LOK_DOC_VIEW(pDocView)); + if (versionInfo) + g_print("LOKit version: %s", versionInfo); + } + + return 1; // exit anyway + } + + // Optional args + if (g_variant_dict_contains(options, "user-profile")) + { + gchar* userProfile = nullptr; + g_variant_dict_lookup(options, "user-profile", "s", &userProfile); + if (userProfile) + { + priv->m_pRenderingArgs->m_aUserProfile = std::string("vnd.sun.star.pathname:") + std::string(userProfile); + g_free(userProfile); + } + } + + if (g_variant_dict_contains(options, "background-color")) + { + gchar* backgroundColor = nullptr; + g_variant_dict_lookup(options, "background-color", "s", &backgroundColor); + if (backgroundColor) + { + priv->m_pRenderingArgs->m_aBackgroundColor = std::string(backgroundColor); + g_free(backgroundColor); + } + } + + if (g_variant_dict_contains(options, "enable-tiled-annotations")) + priv->m_pRenderingArgs->m_bEnableTiledAnnotations = true; + if (g_variant_dict_contains(options, "hide-page-shadow")) + priv->m_pRenderingArgs->m_bHidePageShadow = true; + if (g_variant_dict_contains(options, "hide-whitespace")) + priv->m_pRenderingArgs->m_bHideWhiteSpace = true; + + return -1; +} + +static void +gtv_application_class_init(GtvApplicationClass* klass) +{ + G_APPLICATION_CLASS(klass)->activate = gtv_application_activate; + G_APPLICATION_CLASS(klass)->open = gtv_application_open; + G_APPLICATION_CLASS(klass)->handle_local_options = gtv_application_handle_local_options; + G_OBJECT_CLASS(klass)->dispose = gtv_application_dispose; +} + +GtvApplication* gtv_application_new() +{ + return GTV_APPLICATION(g_object_new(GTV_TYPE_APPLICATION, + "application-id", "org.libreoffice.gtktiledviewer", + "flags", G_APPLICATION_HANDLES_OPEN, + nullptr)); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-application.hxx b/libreofficekit/qa/gtktiledviewer/gtv-application.hxx new file mode 100644 index 000000000..660350366 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-application.hxx @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef GTV_APPLICATION_H +#define GTV_APPLICATION_H + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTV_TYPE_APPLICATION (gtv_application_get_type()) +#define GTV_APPLICATION(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GTV_TYPE_APPLICATION, GtvApplication)) +#define GTV_IS_APPLICATION(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTV_TYPE_APPLICATION)) +#define GTV_APPLICATION_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GTV_TYPE_APPLICATION, GtvApplicationClass)) +#define GTV_IS_APPLICATION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GTV_TYPE_APPLICATION)) +#define GTV_APPLICATION_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), GTV_TYPE_APPLICATION, GtvApplicationClass)) + +struct GtvApplication +{ + GtkApplication parent; +}; + +struct GtvApplicationClass +{ + GtkApplicationClass parentClass; +}; + +GType gtv_application_get_type (void) G_GNUC_CONST; + +GtvApplication* gtv_application_new(); + +G_END_DECLS + +#endif /* GTV_APPLICATION_H */ + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-calc-header-bar.cxx b/libreofficekit/qa/gtktiledviewer/gtv-calc-header-bar.cxx new file mode 100644 index 000000000..dc106e58d --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-calc-header-bar.cxx @@ -0,0 +1,234 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <gtk/gtk.h> + +#include <cmath> +#include <iostream> + +#include "gtv-calc-header-bar.hxx" + +#include <boost/property_tree/ptree.hpp> +#include <o3tl/unreachable.hxx> + +namespace { + +struct GtvCalcHeaderBarPrivateImpl +{ + /// Stores size and content of a single row header. + struct Header + { + int m_nSize; + std::string m_aText; + Header(int nSize, const std::string& rText) + : m_nSize(nSize), + m_aText(rText) + { } + }; + + std::vector<Header> m_aHeaders; + CalcHeaderType m_eType; + + GtvCalcHeaderBarPrivateImpl() + : m_eType(CalcHeaderType::NONE) + { } +}; + +struct GtvCalcHeaderBarPrivate +{ + GtvCalcHeaderBarPrivateImpl* m_pImpl; + + GtvCalcHeaderBarPrivateImpl* operator->() + { + return m_pImpl; + } +}; + +} + +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +#endif +G_DEFINE_TYPE_WITH_PRIVATE(GtvCalcHeaderBar, gtv_calc_header_bar, GTK_TYPE_DRAWING_AREA); +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic pop +#endif +#endif + +const int ROW_HEADER_WIDTH = 50; +const int COLUMN_HEADER_HEIGHT = 20; + +static GtvCalcHeaderBarPrivate& +getPrivate(GtvCalcHeaderBar* headerbar) +{ + return *static_cast<GtvCalcHeaderBarPrivate*>(gtv_calc_header_bar_get_instance_private(headerbar)); +} + +static void +gtv_calc_header_bar_init(GtvCalcHeaderBar* bar) +{ + GtvCalcHeaderBarPrivate& priv = getPrivate(bar); + priv.m_pImpl = new GtvCalcHeaderBarPrivateImpl(); +} + +static void +gtv_calc_header_bar_finalize(GObject* object) +{ + GtvCalcHeaderBarPrivate& priv = getPrivate(GTV_CALC_HEADER_BAR(object)); + + delete priv.m_pImpl; + priv.m_pImpl = nullptr; + + G_OBJECT_CLASS (gtv_calc_header_bar_parent_class)->finalize (object); +} + +static void gtv_calc_header_bar_draw_text(cairo_t* pCairo, const GdkRectangle& rRectangle, const std::string& rText) +{ + cairo_text_extents_t extents; + cairo_text_extents(pCairo, rText.c_str(), &extents); + // Cairo reference point for text is the bottom left corner. + cairo_move_to(pCairo, rRectangle.x + rRectangle.width / 2 - extents.width / 2, rRectangle.y + rRectangle.height / 2 + extents.height / 2); + cairo_show_text(pCairo, rText.c_str()); +} + +static bool gtv_calc_header_bar_draw_impl(GtkWidget* pWidget, cairo_t* pCairo) +{ + GtvCalcHeaderBar* self = GTV_CALC_HEADER_BAR(pWidget); + GtvCalcHeaderBarPrivate& priv = getPrivate(GTV_CALC_HEADER_BAR(self)); + cairo_set_source_rgb(pCairo, 0, 0, 0); + + int nPrevious = 0; + for (const GtvCalcHeaderBarPrivateImpl::Header& rHeader : priv->m_aHeaders) + { + GdkRectangle aRectangle; + if (priv->m_eType == CalcHeaderType::ROW) + { + aRectangle.x = 0; + aRectangle.y = nPrevious; + aRectangle.width = ROW_HEADER_WIDTH - 1; + aRectangle.height = rHeader.m_nSize - nPrevious; + // Left line. + cairo_rectangle(pCairo, aRectangle.x, aRectangle.y, 1, aRectangle.height); + cairo_fill(pCairo); + // Bottom line. + cairo_rectangle(pCairo, aRectangle.x, aRectangle.y + aRectangle.height, aRectangle.width, 1); + cairo_fill(pCairo); + // Right line. + cairo_rectangle(pCairo, aRectangle.width, aRectangle.y, 1, aRectangle.height); + cairo_fill(pCairo); + } + else if (priv->m_eType == CalcHeaderType::COLUMN) + { + aRectangle.x = nPrevious; + aRectangle.y = 0; + aRectangle.width = rHeader.m_nSize - nPrevious; + aRectangle.height = COLUMN_HEADER_HEIGHT - 1; + // Top line. + cairo_rectangle(pCairo, aRectangle.x, aRectangle.y, aRectangle.width, 1); + cairo_fill(pCairo); + // Right line. + cairo_rectangle(pCairo, aRectangle.x + aRectangle.width , aRectangle.y, 1, aRectangle.height); + cairo_fill(pCairo); + // Bottom line. + cairo_rectangle(pCairo, aRectangle.x, aRectangle.height, aRectangle.width, 1); + cairo_fill(pCairo); + } + else + { + O3TL_UNREACHABLE; // should never happen + } + + gtv_calc_header_bar_draw_text(pCairo, aRectangle, rHeader.m_aText); + nPrevious = rHeader.m_nSize; + if (rHeader.m_nSize > self->m_nSizePixel) + break; + } + + if (priv->m_aHeaders.empty() && priv->m_eType == CalcHeaderType::CORNER) + { + GdkRectangle aRectangle; + aRectangle.x = 0; + aRectangle.y = 0; + aRectangle.width = ROW_HEADER_WIDTH - 1; + aRectangle.height = COLUMN_HEADER_HEIGHT - 1; + cairo_rectangle(pCairo, aRectangle.x, aRectangle.y, aRectangle.width, aRectangle.height); + cairo_stroke(pCairo); + } + + return false; +} + +static gboolean +gtv_calc_header_bar_draw(GtkWidget* bar, cairo_t* pCairo) +{ + return gtv_calc_header_bar_draw_impl(bar, pCairo); +} + +static void +gtv_calc_header_bar_class_init(GtvCalcHeaderBarClass* klass) +{ + GTK_WIDGET_CLASS(klass)->draw = gtv_calc_header_bar_draw; + G_OBJECT_CLASS(klass)->finalize = gtv_calc_header_bar_finalize; +} + +void gtv_calc_header_bar_configure(GtvCalcHeaderBar* bar, const boost::property_tree::ptree* values) +{ + GtvCalcHeaderBarPrivate& priv = getPrivate(bar); + priv->m_aHeaders.clear(); + + if (values) + { + boost::property_tree::ptree val = *values; + try + { + for (const boost::property_tree::ptree::value_type& rValue : val) + { + int nSize = std::round(std::atof(rValue.second.get<std::string>("size").c_str())); + if (nSize >= bar->m_nPositionPixel) + { + const int nScrolledSize = nSize - bar->m_nPositionPixel; + GtvCalcHeaderBarPrivateImpl::Header aHeader(nScrolledSize, rValue.second.get<std::string>("text")); + priv->m_aHeaders.push_back(aHeader); + } + } + } + catch (boost::property_tree::ptree_bad_path& rException) + { + std::cerr << "gtv_calc_header_bar_configure: " << rException.what() << std::endl; + } + } + gtk_widget_show(GTK_WIDGET(bar)); + gtk_widget_queue_draw(GTK_WIDGET(bar)); +} + +void +gtv_calc_header_bar_set_type_and_width(GtvCalcHeaderBar* bar, CalcHeaderType eType) +{ + // TODO: Install type property for this class + GtvCalcHeaderBarPrivate& priv = getPrivate(bar); + priv->m_eType = eType; + + if (eType == CalcHeaderType::ROW) + gtk_widget_set_size_request(GTK_WIDGET(bar), ROW_HEADER_WIDTH, -1); + else if (eType == CalcHeaderType::COLUMN) + gtk_widget_set_size_request(GTK_WIDGET(bar), -1, COLUMN_HEADER_HEIGHT); +} + +GtkWidget* +gtv_calc_header_bar_new() +{ + return GTK_WIDGET(g_object_new(GTV_TYPE_CALC_HEADER_BAR, + nullptr)); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-calc-header-bar.hxx b/libreofficekit/qa/gtktiledviewer/gtv-calc-header-bar.hxx new file mode 100644 index 000000000..a1a4d37a3 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-calc-header-bar.hxx @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef GTV_CALC_HEADER_BAR_H +#define GTV_CALC_HEADER_BAR_H + +#include <gtk/gtk.h> + +#include <boost/property_tree/ptree_fwd.hpp> + +G_BEGIN_DECLS + +#define GTV_TYPE_CALC_HEADER_BAR (gtv_calc_header_bar_get_type()) +#define GTV_CALC_HEADER_BAR(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GTV_TYPE_CALC_HEADER_BAR, GtvCalcHeaderBar)) +#define GTV_IS_CALC_HEADER_BAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTV_TYPE_CALC_HEADER_BAR)) +#define GTV_CALC_HEADER_BAR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GTV_TYPE_CALC_HEADER_BAR, GtvCalcHeaderBarClass)) +#define GTV_IS_CALC_HEADER_BAR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GTV_TYPE_CALC_HEADER_BAR)) +#define GTV_CALC_HEADER_BAR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), GTV_TYPE_CALC_HEADER_BAR, GtvCalcHeaderBarClass)) + +struct GtvCalcHeaderBar +{ + GtkDrawingArea parent; + /// Height for row bar, width for column bar. + int m_nSizePixel; + /// Left/top position for the column/row bar -- initially 0, then may grow due to scrolling. + int m_nPositionPixel; +}; + +struct GtvCalcHeaderBarClass +{ + GtkDrawingAreaClass parentClass; +}; + +GType gtv_calc_header_bar_get_type (void) G_GNUC_CONST; + +enum CalcHeaderType { ROW, COLUMN, CORNER, NONE }; + +GtkWidget* gtv_calc_header_bar_new(); + +void gtv_calc_header_bar_configure(GtvCalcHeaderBar* bar, const boost::property_tree::ptree* values); + +int gtv_calc_header_bar_get_pos_pixel(GtvCalcHeaderBar* bar); + +int gtv_calc_header_bar_get_size_pixel(GtvCalcHeaderBar* bar); + +void gtv_calc_header_bar_set_type_and_width(GtvCalcHeaderBar* bar, CalcHeaderType eType); + +G_END_DECLS + +#endif /* GTV_CALC_HEADER_BAR_H */ + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-comments-sidebar.cxx b/libreofficekit/qa/gtktiledviewer/gtv-comments-sidebar.cxx new file mode 100644 index 000000000..f63e77fd1 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-comments-sidebar.cxx @@ -0,0 +1,114 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <gtk/gtk.h> + +#include <iostream> + +#include "gtv-application-window.hxx" +#include "gtv-helpers.hxx" +#include "gtv-comments-sidebar.hxx" + +#include <LibreOfficeKit/LibreOfficeKitGtk.h> + +#include <boost/property_tree/json_parser.hpp> + +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +#endif +G_DEFINE_TYPE(GtvCommentsSidebar, gtv_comments_sidebar, GTK_TYPE_BOX); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +void +gtv_comments_sidebar_view_annotations(GtvCommentsSidebar* sidebar) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(sidebar))); + + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + char* pValues = pDocument->pClass->getCommandValues(pDocument, ".uno:ViewAnnotations"); + g_info("lok::Document::getCommandValues(%s) : %s", ".uno:ViewAnnotations", pValues); + std::stringstream aStream(pValues); + free(pValues); + + // empty the comments grid + GtvGtkWrapper<GList> children(gtk_container_get_children(GTK_CONTAINER(sidebar->commentsgrid)), + [](GList* l) + { + g_list_free(l); + }); + GList* iter; + for (iter = children.get(); iter != nullptr; iter = g_list_next(iter)) + gtk_widget_destroy(GTK_WIDGET(iter->data)); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + try + { + for (const boost::property_tree::ptree::value_type& rValue : aTree.get_child("comments")) + { + GtkWidget* pCommentBox = GtvHelpers::createCommentBox(rValue.second); + gtk_container_add(GTK_CONTAINER(sidebar->commentsgrid), pCommentBox); + } + gtk_widget_show_all(sidebar->scrolledwindow); + } + catch(boost::property_tree::ptree_bad_path& rException) + { + std::cerr << "CommentsSidebar::unoViewAnnotations: failed to get comments" << rException.what() << std::endl; + } +} + +static void +gtv_comments_sidebar_view_annotations_cb(GtkWidget* pWidget, gpointer) +{ + GtvCommentsSidebar* sidebar = GTV_COMMENTS_SIDEBAR(pWidget); + gtv_comments_sidebar_view_annotations(sidebar); +} + +static void +gtv_comments_sidebar_init(GtvCommentsSidebar* sidebar) +{ + sidebar->scrolledwindow = gtk_scrolled_window_new(nullptr, nullptr); + gtk_widget_set_vexpand(sidebar->scrolledwindow, true); + sidebar->commentsgrid = gtk_grid_new(); + g_object_set(sidebar->commentsgrid, "orientation", GTK_ORIENTATION_VERTICAL, nullptr); + + sidebar->viewannotationsButton = gtk_button_new_with_label(".uno:ViewAnnotations"); + // Hack to make sidebar grid wide enough to not need any horizontal scrollbar + gtk_widget_set_margin_start(sidebar->viewannotationsButton, 20); + gtk_widget_set_margin_end(sidebar->viewannotationsButton, 20); + gtk_container_add(GTK_CONTAINER(sidebar), sidebar->viewannotationsButton); + g_signal_connect_swapped(sidebar->viewannotationsButton, "clicked", G_CALLBACK(gtv_comments_sidebar_view_annotations_cb), sidebar); + + gtk_container_add(GTK_CONTAINER(sidebar), sidebar->scrolledwindow); + gtk_container_add(GTK_CONTAINER(sidebar->scrolledwindow), sidebar->commentsgrid); + + gtk_widget_show_all(GTK_WIDGET(sidebar)); +} + +static void +gtv_comments_sidebar_class_init(GtvCommentsSidebarClass* /*klass*/) +{ +} + +GtkWidget* +gtv_comments_sidebar_new() +{ + return GTK_WIDGET(g_object_new(GTV_TYPE_COMMENTS_SIDEBAR, + "orientation", GTK_ORIENTATION_VERTICAL, + nullptr)); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-comments-sidebar.hxx b/libreofficekit/qa/gtktiledviewer/gtv-comments-sidebar.hxx new file mode 100644 index 000000000..8ed096423 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-comments-sidebar.hxx @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef GTV_COMMENTS_SIDEBAR_H +#define GTV_COMMENTS_SIDEBAR_H + +#include <gtk/gtk.h> + +G_BEGIN_DECLS + +#define GTV_TYPE_COMMENTS_SIDEBAR (gtv_comments_sidebar_get_type()) +#define GTV_COMMENTS_SIDEBAR(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GTV_TYPE_COMMENTS_SIDEBAR, GtvCommentsSidebar)) +#define GTV_IS_COMMENTS_SIDEBAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTV_TYPE_COMMENTS_SIDEBAR)) +#define GTV_COMMENTS_SIDEBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GTV_TYPE_COMMENTS_SIDEBAR, GtvCommentsSidebarClass)) +#define GTV_IS_COMMENTS_SIDEBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GTV_TYPE_COMMENTS_SIDEBAR)) +#define GTV_COMMENTS_SIDEBAR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), GTV_TYPE_COMMENTS_SIDEBAR, GtvCommentsSidebarClass)) + +struct GtvCommentsSidebar +{ + GtkBox parent; + + GtkWidget* viewannotationsButton; + GtkWidget* scrolledwindow; + GtkWidget* commentsgrid; +}; + +struct GtvCommentsSidebarClass +{ + GtkBoxClass parentClass; +}; + +GType gtv_comments_sidebar_get_type (void) G_GNUC_CONST; + +GtkWidget* gtv_comments_sidebar_new(); + +void gtv_comments_sidebar_view_annotations(GtvCommentsSidebar* sidebar); + +G_END_DECLS + +#endif /* GTV_COMMENTS_SIDEBAR_H */ + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-helpers.cxx b/libreofficekit/qa/gtktiledviewer/gtv-helpers.cxx new file mode 100644 index 000000000..b557fe4ba --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-helpers.cxx @@ -0,0 +1,152 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <gtk/gtk.h> + +#include <pwd.h> + +#include <cstring> + +#include "gtv-helpers.hxx" +#include "gtv-signal-handlers.hxx" + +#include <boost/property_tree/ptree.hpp> + +void GtvHelpers::userPromptDialog(GtkWindow* pWindow, const std::string& aTitle, std::map<std::string, std::string>& aEntries) +{ + GtkWidget* pDialog = gtk_dialog_new_with_buttons (aTitle.c_str(), + pWindow, + GTK_DIALOG_MODAL, + "Ok", + GTK_RESPONSE_OK, + nullptr); + + GtkWidget* pDialogMessageArea = gtk_dialog_get_content_area (GTK_DIALOG (pDialog)); + GtkWidget* pEntryArea = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_container_add(GTK_CONTAINER(pDialogMessageArea), pEntryArea); + for (const auto& entry : aEntries) + { + GtkWidget* pEntry = gtk_entry_new(); + gtk_entry_set_placeholder_text(GTK_ENTRY(pEntry), entry.first.c_str()); + gtk_container_add(GTK_CONTAINER(pEntryArea), pEntry); + } + + gtk_widget_show_all(pDialog); + + gint res = gtk_dialog_run(GTK_DIALOG(pDialog)); + switch(res) + { + case GTK_RESPONSE_OK: + GtvGtkWrapper<GList> pList(gtk_container_get_children(GTK_CONTAINER(pEntryArea)), + [](GList* l) + { + g_list_free(l); + }); + + for (GList* l = pList.get(); l != nullptr; l = l->next) + { + const gchar* pKey = gtk_entry_get_placeholder_text(GTK_ENTRY(l->data)); + aEntries[std::string(pKey)] = std::string(gtk_entry_get_text(GTK_ENTRY(l->data))); + } + break; + } + + gtk_widget_destroy(pDialog); +} + +/// Our GtkClipboardGetFunc implementation for HTML. +static void htmlGetFunc(GtkClipboard* /*pClipboard*/, GtkSelectionData* pSelectionData, guint /*info*/, gpointer pUserData) +{ + GdkAtom aAtom(gdk_atom_intern("text/html", false)); + const gchar* pSelection = static_cast<const gchar*>(pUserData); + gtk_selection_data_set(pSelectionData, aAtom, 8, reinterpret_cast<const guchar *>(pSelection), strlen(pSelection)); +} + +/// Our GtkClipboardClearFunc implementation for HTML. +static void htmlClearFunc(GtkClipboard* /*pClipboard*/, gpointer pData) +{ + g_free(pData); +} + +void GtvHelpers::clipboardSetHtml(GtkClipboard* pClipboard, const char* pSelection) +{ + GtvGtkWrapper<GtkTargetList> pList(gtk_target_list_new(nullptr, 0), + [](GtkTargetList* pTargetList) + { + gtk_target_list_unref(pTargetList); + }); + GdkAtom aAtom(gdk_atom_intern("text/html", false)); + gtk_target_list_add(pList.get(), aAtom, 0, 0); + gint nTargets = 0; + GtkTargetEntry* pTargets = gtk_target_table_new_from_list(pList.get(), &nTargets); + + gtk_clipboard_set_with_data(pClipboard, pTargets, nTargets, htmlGetFunc, htmlClearFunc, g_strdup(pSelection)); + + gtk_target_table_free(pTargets, nTargets); +} + +std::string GtvHelpers::getNextAuthor() +{ + static int nCounter = 0; + struct passwd* pPasswd = getpwuid(getuid()); + return std::string(pPasswd->pw_gecos) + " #" + std::to_string(++nCounter); +} + +GtkWidget* GtvHelpers::createCommentBox(const boost::property_tree::ptree& aComment) +{ + GtkWidget* pCommentVBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1); + gchar *id = g_strndup(aComment.get<std::string>("id").c_str(), 20); + g_object_set_data_full(G_OBJECT(pCommentVBox), "id", id, g_free); + + // Set background if it's a reply comment + if (aComment.get("parent", -1) > 0) + { + GtkStyleContext* pStyleContext = gtk_widget_get_style_context(pCommentVBox); + GtkCssProvider* pCssProvider = gtk_css_provider_new(); + gtk_style_context_add_class(pStyleContext, "commentbox"); + gtk_style_context_add_provider(pStyleContext, GTK_STYLE_PROVIDER(pCssProvider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + gtk_css_provider_load_from_data(pCssProvider, ".commentbox {background-color: lightgreen;}", -1, nullptr); + } + + GtkWidget* pCommentText = gtk_label_new(aComment.get<std::string>("text").c_str()); + GtkWidget* pCommentAuthor = gtk_label_new(aComment.get<std::string>("author").c_str()); + GtkWidget* pCommentDate = gtk_label_new(aComment.get<std::string>("dateTime").c_str()); + GtkWidget* pControlsHBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + GtkWidget* pEditButton = gtk_button_new_with_label("Edit"); + GtkWidget* pReplyButton = gtk_button_new_with_label("Reply"); + GtkWidget* pDeleteButton = gtk_button_new_with_label("Delete"); + g_signal_connect(G_OBJECT(pEditButton), "clicked", G_CALLBACK(editButtonClicked), pCommentVBox); + g_signal_connect(G_OBJECT(pReplyButton), "clicked", G_CALLBACK(replyButtonClicked), pCommentVBox); + g_signal_connect(G_OBJECT(pDeleteButton), "clicked", G_CALLBACK(deleteCommentButtonClicked), pCommentVBox); + + gtk_container_add(GTK_CONTAINER(pControlsHBox), pEditButton); + gtk_container_add(GTK_CONTAINER(pControlsHBox), pReplyButton); + gtk_container_add(GTK_CONTAINER(pControlsHBox), pDeleteButton); + GtkWidget* pCommentSeparator = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL); + + gtk_container_add(GTK_CONTAINER(pCommentVBox), pCommentText); + gtk_container_add(GTK_CONTAINER(pCommentVBox), pCommentAuthor); + gtk_container_add(GTK_CONTAINER(pCommentVBox), pCommentDate); + gtk_container_add(GTK_CONTAINER(pCommentVBox), pControlsHBox); + gtk_container_add(GTK_CONTAINER(pCommentVBox), pCommentSeparator); + + gtk_label_set_line_wrap(GTK_LABEL(pCommentText), true); + gtk_label_set_max_width_chars(GTK_LABEL(pCommentText), 35); + + return pCommentVBox; +} + +std::string GtvHelpers::getDirPath(const std::string& filePath) +{ + int position = filePath.find_last_of('/'); + const std::string dirPath = filePath.substr(0, position + 1); + return dirPath; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-helpers.hxx b/libreofficekit/qa/gtktiledviewer/gtv-helpers.hxx new file mode 100644 index 000000000..4b304ab89 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-helpers.hxx @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef GTV_HELPERS_H +#define GTV_HELPERS_H + +#include <gtk/gtk.h> + +#include <map> +#include <sstream> +#include <string> +#include <vector> +#include <memory> + +#include <boost/property_tree/ptree_fwd.hpp> + +#define UI_FILE_NAME "gtv.ui" + +// Wrapper with custom deleter to use for Gtk objects +template <class T> +using GtvGtkWrapper = std::unique_ptr<T, void(*)(T*)>; + +namespace GtvHelpers +{ + void userPromptDialog(GtkWindow* pWindow, const std::string& aTitle, std::map<std::string, std::string>& aEntries); + + void clipboardSetHtml(GtkClipboard* pClipboard, const char* pSelection); + + /// Generate an author string for multiple views. + std::string getNextAuthor(); + + GtkWidget* createCommentBox(const boost::property_tree::ptree& aComment); + + std::string getDirPath(const std::string& filePath); + + template<typename T> + std::vector<T> split(const std::string& aPayload, const std::string& aDelim, const int nItems) + { + std::vector<T> aRet; + + if (!aPayload.empty()) + { + gchar** ppCoordinates = g_strsplit(aPayload.c_str(), aDelim.c_str(), nItems); + gchar** ppCoordinate = ppCoordinates; + while (*ppCoordinate) + { + std::stringstream strstream(*ppCoordinate); + T item; + strstream >> item; + aRet.push_back(item); + ++ppCoordinate; + } + g_strfreev(ppCoordinates); + } + + return aRet; + } +} + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-lok-dialog.cxx b/libreofficekit/qa/gtktiledviewer/gtv-lok-dialog.cxx new file mode 100644 index 000000000..f23148eaf --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-lok-dialog.cxx @@ -0,0 +1,717 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> + +#include <iostream> + +#include <LibreOfficeKit/LibreOfficeKitGtk.h> +#include <LibreOfficeKit/LibreOfficeKitEnums.h> + +#include "gtv-application-window.hxx" +#include "gtv-lok-dialog.hxx" + +#include <com/sun/star/awt/Key.hpp> + +#include <o3tl/unit_conversion.hxx> +#include <vcl/event.hxx> + +namespace { + +struct GtvLokDialogPrivate +{ + LOKDocView* lokdocview; + GtkWidget* pDialogDrawingArea; + GtkWidget* pFloatingWin; + + // state for dialog + guint32 m_nLastButtonPressTime; + guint32 m_nLastButtonReleaseTime; + guint32 m_nKeyModifier; + guint32 m_nLastButtonPressed; + guint32 m_nWidth; + guint32 m_nHeight; + + // state for child floating windows + guint32 m_nChildId; + guint32 m_nChildWidth; + guint32 m_nChildHeight; + guint32 m_nChildLastButtonPressTime; + guint32 m_nChildLastButtonReleaseTime; + guint32 m_nChildKeyModifier; + guint32 m_nChildLastButtonPressed; + + guint dialogid; +}; + +} + +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +#endif +G_DEFINE_TYPE_WITH_PRIVATE(GtvLokDialog, gtv_lok_dialog, GTK_TYPE_DIALOG); +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic pop +#endif +#endif + +enum +{ + PROP_0, + PROP_LOKDOCVIEW_CONTEXT, + PROP_DIALOG_ID, + PROP_DIALOG_WIDTH, + PROP_DIALOG_HEIGHT, + PROP_LAST +}; + +static GParamSpec* properties[PROP_LAST]; + +static GtvLokDialogPrivate* +getPrivate(GtvLokDialog* dialog) +{ + return static_cast<GtvLokDialogPrivate*>(gtv_lok_dialog_get_instance_private(dialog)); +} + +static void +gtv_lok_dialog_draw(GtkWidget* pDialogDrawingArea, cairo_t* pCairo, gpointer) +{ + GtvLokDialog* pDialog = GTV_LOK_DIALOG(gtk_widget_get_toplevel(pDialogDrawingArea)); + GtvLokDialogPrivate* priv = getPrivate(pDialog); + + GdkRectangle aRect; + gdk_cairo_get_clip_rectangle(pCairo, &aRect); + g_info("Painting dialog region: %d, %d, %d, %d", aRect.x, aRect.y, aRect.width, aRect.height); + + int nWidth = priv->m_nWidth; + int nHeight = priv->m_nHeight; + if (aRect.width != 0 && aRect.height != 0) + { + nWidth = aRect.width; + nHeight = aRect.height; + } + + cairo_surface_t* pSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, nWidth, nHeight); + unsigned char* pBuffer = cairo_image_surface_get_data(pSurface); + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(priv->lokdocview)); + pDocument->pClass->paintWindow(pDocument, priv->dialogid, pBuffer, aRect.x, aRect.y, nWidth, nHeight); + + gtk_widget_set_size_request(GTK_WIDGET(pDialogDrawingArea), priv->m_nWidth, priv->m_nHeight); + + cairo_surface_flush(pSurface); + cairo_surface_mark_dirty(pSurface); + + cairo_set_source_surface(pCairo, pSurface, aRect.x, aRect.y); + // paint the dialog image + cairo_paint(pCairo); + + // debug red-colored border around the painted region + cairo_set_source_rgb(pCairo, 1.0, 0, 0); + cairo_rectangle(pCairo, aRect.x, aRect.y, nWidth, nHeight); + cairo_stroke(pCairo); +} + +static gboolean +gtv_lok_dialog_signal_button(GtkWidget* pDialogDrawingArea, GdkEventButton* pEvent) +{ + GtvLokDialog* pDialog = GTV_LOK_DIALOG(gtk_widget_get_toplevel(pDialogDrawingArea)); + GtvLokDialogPrivate* priv = getPrivate(pDialog); + + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_window_get_transient_for(GTK_WINDOW(pDialog))); + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + + std::string aEventType = "unknown"; + if (pEvent->type == GDK_BUTTON_PRESS) + aEventType = "BUTTON_PRESS"; + else if (pEvent->type == GDK_BUTTON_RELEASE) + aEventType = "BUTTON_RELEASE"; + + g_info("lok_dialog_signal_button (type: %s): %d, %d", + aEventType.c_str(), + static_cast<int>(pEvent->x), static_cast<int>(pEvent->y)); + gtk_widget_grab_focus(pDialogDrawingArea); + + switch (pEvent->type) + { + case GDK_BUTTON_PRESS: + { + int nCount = 1; + if ((pEvent->time - priv->m_nLastButtonPressTime) < 250) + nCount++; + priv->m_nLastButtonPressTime = pEvent->time; + int nEventButton = 0; + switch (pEvent->button) + { + case 1: + nEventButton = MOUSE_LEFT; + break; + case 2: + nEventButton = MOUSE_MIDDLE; + break; + case 3: + nEventButton = MOUSE_RIGHT; + break; + } + priv->m_nLastButtonPressed = nEventButton; + pDocument->pClass->postWindowMouseEvent(pDocument, + priv->dialogid, + LOK_MOUSEEVENT_MOUSEBUTTONDOWN, + (pEvent->x), + (pEvent->y), + nCount, + nEventButton, + priv->m_nKeyModifier); + + break; + } + case GDK_BUTTON_RELEASE: + { + int nCount = 1; + if ((pEvent->time - priv->m_nLastButtonReleaseTime) < 250) + nCount++; + priv->m_nLastButtonReleaseTime = pEvent->time; + int nEventButton = 0; + switch (pEvent->button) + { + case 1: + nEventButton = MOUSE_LEFT; + break; + case 2: + nEventButton = MOUSE_MIDDLE; + break; + case 3: + nEventButton = MOUSE_RIGHT; + break; + } + priv->m_nLastButtonPressed = nEventButton; + pDocument->pClass->postWindowMouseEvent(pDocument, + priv->dialogid, + LOK_MOUSEEVENT_MOUSEBUTTONUP, + (pEvent->x), + (pEvent->y), + nCount, + nEventButton, + priv->m_nKeyModifier); + break; + } + default: + break; + } + return FALSE; +} + +static gboolean +gtv_lok_dialog_signal_motion(GtkWidget* pDialogDrawingArea, GdkEventButton* pEvent) +{ + GtvLokDialog* pDialog = GTV_LOK_DIALOG(gtk_widget_get_toplevel(pDialogDrawingArea)); + GtvLokDialogPrivate* priv = getPrivate(pDialog); + + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_window_get_transient_for(GTK_WINDOW(pDialog))); + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + + g_info("lok_dialog_signal_motion: %d, %d (in twips: %d, %d)", + static_cast<int>(pEvent->x), static_cast<int>(pEvent->y), + static_cast<int>(o3tl::toTwips(pEvent->x, o3tl::Length::px)), + static_cast<int>(o3tl::toTwips(pEvent->y, o3tl::Length::px))); + + pDocument->pClass->postWindowMouseEvent(pDocument, + priv->dialogid, + LOK_MOUSEEVENT_MOUSEMOVE, + (pEvent->x), + (pEvent->y), + 1, + priv->m_nLastButtonPressed, + priv->m_nKeyModifier); + + return FALSE; +} + +static gboolean +gtv_lok_dialog_signal_key(GtkWidget* pDialogDrawingArea, GdkEventKey* pEvent) +{ + GtvLokDialog* pDialog = GTV_LOK_DIALOG(gtk_widget_get_toplevel(pDialogDrawingArea)); + GtvLokDialogPrivate* priv = getPrivate(pDialog); + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_window_get_transient_for(GTK_WINDOW(pDialog))); + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + + g_info("lok_dialog_signal_key"); + int nCharCode = 0; + int nKeyCode = 0; + priv->m_nKeyModifier &= KEY_MOD2; + switch (pEvent->keyval) + { + case GDK_KEY_BackSpace: + nKeyCode = com::sun::star::awt::Key::BACKSPACE; + break; + case GDK_KEY_Delete: + nKeyCode = com::sun::star::awt::Key::DELETE; + break; + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + nKeyCode = com::sun::star::awt::Key::RETURN; + break; + case GDK_KEY_Escape: + nKeyCode = com::sun::star::awt::Key::ESCAPE; + break; + case GDK_KEY_Tab: + nKeyCode = com::sun::star::awt::Key::TAB; + break; + case GDK_KEY_Down: + nKeyCode = com::sun::star::awt::Key::DOWN; + break; + case GDK_KEY_Up: + nKeyCode = com::sun::star::awt::Key::UP; + break; + case GDK_KEY_Left: + nKeyCode = com::sun::star::awt::Key::LEFT; + break; + case GDK_KEY_Right: + nKeyCode = com::sun::star::awt::Key::RIGHT; + break; + case GDK_KEY_Page_Down: + nKeyCode = com::sun::star::awt::Key::PAGEDOWN; + break; + case GDK_KEY_Page_Up: + nKeyCode = com::sun::star::awt::Key::PAGEUP; + break; + case GDK_KEY_Insert: + nKeyCode = com::sun::star::awt::Key::INSERT; + break; + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + if (pEvent->type == GDK_KEY_PRESS) + priv->m_nKeyModifier |= KEY_SHIFT; + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + if (pEvent->type == GDK_KEY_PRESS) + priv->m_nKeyModifier |= KEY_MOD1; + break; + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + if (pEvent->type == GDK_KEY_PRESS) + priv->m_nKeyModifier |= KEY_MOD2; + else + priv->m_nKeyModifier &= ~KEY_MOD2; + break; + default: + if (pEvent->keyval >= GDK_KEY_F1 && pEvent->keyval <= GDK_KEY_F26) + nKeyCode = com::sun::star::awt::Key::F1 + (pEvent->keyval - GDK_KEY_F1); + else + nCharCode = gdk_keyval_to_unicode(pEvent->keyval); + } + + // rsc is not public API, but should be good enough for debugging purposes. + // If this is needed for real, then probably a new param of type + // css::awt::KeyModifier is needed in postKeyEvent(). + if (pEvent->state & GDK_SHIFT_MASK) + nKeyCode |= KEY_SHIFT; + + if (pEvent->state & GDK_CONTROL_MASK) + nKeyCode |= KEY_MOD1; + + if (priv->m_nKeyModifier & KEY_MOD2) + nKeyCode |= KEY_MOD2; + + if (nKeyCode & (KEY_SHIFT | KEY_MOD1 | KEY_MOD2)) { + if (pEvent->keyval >= GDK_KEY_a && pEvent->keyval <= GDK_KEY_z) + { + nKeyCode |= 512 + (pEvent->keyval - GDK_KEY_a); + } + else if (pEvent->keyval >= GDK_KEY_A && pEvent->keyval <= GDK_KEY_Z) { + nKeyCode |= 512 + (pEvent->keyval - GDK_KEY_A); + } + else if (pEvent->keyval >= GDK_KEY_0 && pEvent->keyval <= GDK_KEY_9) { + nKeyCode |= 256 + (pEvent->keyval - GDK_KEY_0); + } + } + + std::stringstream ss; + ss << "gtv_lok_dialog::postKey(" << pEvent->type << ", " << nCharCode << ", " << nKeyCode << ")"; + g_info("%s", ss.str().c_str()); + + pDocument->pClass->postWindowKeyEvent(pDocument, + priv->dialogid, + pEvent->type == GDK_KEY_RELEASE ? LOK_KEYEVENT_KEYUP : LOK_KEYEVENT_KEYINPUT, + nCharCode, + nKeyCode); + + return FALSE; +} + +static void +gtv_lok_dialog_init(GtvLokDialog* dialog) +{ + GtvLokDialogPrivate* priv = getPrivate(dialog); + + GtkWidget* pContentArea = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + priv->pDialogDrawingArea = gtk_drawing_area_new(); + priv->pFloatingWin = nullptr; + priv->m_nChildId = 0; + priv->m_nChildWidth = 0; + priv->m_nChildHeight = 0; + + priv->m_nLastButtonPressTime = 0; + priv->m_nLastButtonReleaseTime = 0; + priv->m_nKeyModifier = 0; + priv->m_nLastButtonPressed = 0; + + gtk_widget_add_events(priv->pDialogDrawingArea, + GDK_BUTTON_PRESS_MASK + |GDK_BUTTON_RELEASE_MASK + |GDK_BUTTON_MOTION_MASK + |GDK_KEY_PRESS_MASK + |GDK_KEY_RELEASE_MASK); + // This is required to be able to capture key events on the drawing area + gtk_widget_set_can_focus(priv->pDialogDrawingArea, true); + + g_signal_connect(G_OBJECT(priv->pDialogDrawingArea), "draw", G_CALLBACK(gtv_lok_dialog_draw), nullptr); + g_signal_connect(G_OBJECT(priv->pDialogDrawingArea), "button-press-event", G_CALLBACK(gtv_lok_dialog_signal_button), nullptr); + g_signal_connect(G_OBJECT(priv->pDialogDrawingArea), "button-release-event", G_CALLBACK(gtv_lok_dialog_signal_button), nullptr); + g_signal_connect(G_OBJECT(priv->pDialogDrawingArea), "motion-notify-event", G_CALLBACK(gtv_lok_dialog_signal_motion), nullptr); + g_signal_connect(G_OBJECT(priv->pDialogDrawingArea), "key-press-event", G_CALLBACK(gtv_lok_dialog_signal_key), nullptr); + g_signal_connect(G_OBJECT(priv->pDialogDrawingArea), "key-release-event", G_CALLBACK(gtv_lok_dialog_signal_key), nullptr); + gtk_container_add(GTK_CONTAINER(pContentArea), priv->pDialogDrawingArea); +} + +static void +gtv_lok_dialog_set_property(GObject* object, guint propId, const GValue* value, GParamSpec* pspec) +{ + GtvLokDialog* self = GTV_LOK_DIALOG(object); + GtvLokDialogPrivate* priv = getPrivate(self); + + switch(propId) + { + case PROP_LOKDOCVIEW_CONTEXT: + priv->lokdocview = LOK_DOC_VIEW(g_value_get_object(value)); + break; + case PROP_DIALOG_ID: + priv->dialogid = g_value_get_uint(value); + break; + case PROP_DIALOG_WIDTH: + priv->m_nWidth = g_value_get_uint(value); + break; + case PROP_DIALOG_HEIGHT: + priv->m_nHeight = g_value_get_uint(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propId, pspec); + } + + //if (propId == PROP_DIALOG_WIDTH || propId == PROP_DIALOG_HEIGHT) + // gtk_widget_set_size_request(GTK_WIDGET(priv->pDialogDrawingArea), priv->m_nWidth, priv->m_nHeight); +} + +static void +gtv_lok_dialog_get_property(GObject* object, guint propId, GValue* value, GParamSpec* pspec) +{ + GtvLokDialog* self = GTV_LOK_DIALOG(object); + GtvLokDialogPrivate* priv = getPrivate(self); + + switch(propId) + { + case PROP_LOKDOCVIEW_CONTEXT: + g_value_set_object(value, priv->lokdocview); + break; + case PROP_DIALOG_ID: + g_value_set_uint(value, priv->dialogid); + break; + case PROP_DIALOG_WIDTH: + g_value_set_uint(value, priv->m_nWidth); + break; + case PROP_DIALOG_HEIGHT: + g_value_set_uint(value, priv->m_nHeight); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propId, pspec); + } +} + +static void +gtv_lok_dialog_class_init(GtvLokDialogClass* klass) +{ + G_OBJECT_CLASS(klass)->get_property = gtv_lok_dialog_get_property; + G_OBJECT_CLASS(klass)->set_property = gtv_lok_dialog_set_property; + + properties[PROP_LOKDOCVIEW_CONTEXT] = g_param_spec_object("lokdocview", + "LOKDocView Context", + "The LOKDocView context object to be used for dialog rendering", + LOK_TYPE_DOC_VIEW, + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + properties[PROP_DIALOG_ID] = g_param_spec_uint("dialogid", + "Dialog identifier", + "Unique dialog identifier", + 0, G_MAXUINT, 0, + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + properties[PROP_DIALOG_WIDTH] = g_param_spec_uint("width", + "Dialog width", + "Dialog width", + 0, 4096, 0, + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + properties[PROP_DIALOG_HEIGHT] = g_param_spec_uint("height", + "Dialog height", + "Dialog height", + 0, 2048, 0, + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (G_OBJECT_CLASS(klass), PROP_LAST, properties); +} + +static void +gtv_lok_dialog_floating_win_draw(GtkWidget* pDrawingArea, cairo_t* pCairo, gpointer userdata) +{ + GtvLokDialog* pDialog = GTV_LOK_DIALOG(userdata); + GtvLokDialogPrivate* priv = getPrivate(pDialog); + + g_info("gtv_lok_dialog_floating_win_draw triggered"); + cairo_surface_t* pSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, priv->m_nChildWidth, priv->m_nChildHeight); + unsigned char* pBuffer = cairo_image_surface_get_data(pSurface); + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(priv->lokdocview)); + pDocument->pClass->paintWindow(pDocument, priv->m_nChildId, pBuffer, 0, 0, priv->m_nChildWidth, priv->m_nChildHeight); + + gtk_widget_set_size_request(GTK_WIDGET(pDrawingArea), priv->m_nChildWidth, priv->m_nChildHeight); + //gtk_widget_set_size_request(GTK_WIDGET(pDialog), nWidth, nHeight); + //gtk_window_resize(GTK_WINDOW(pDialog), nWidth, nHeight); + + cairo_surface_flush(pSurface); + cairo_surface_mark_dirty(pSurface); + + cairo_set_source_surface(pCairo, pSurface, 0, 0); + cairo_paint(pCairo); +} + +static gboolean +gtv_lok_dialog_floating_win_signal_button(GtkWidget* /*pDialogChildDrawingArea*/, GdkEventButton* pEvent, gpointer userdata) +{ + GtvLokDialog* pDialog = GTV_LOK_DIALOG(userdata); + GtvLokDialogPrivate* priv = getPrivate(pDialog); + + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_window_get_transient_for(GTK_WINDOW(pDialog))); + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + + std::string aEventType = "unknown"; + if (pEvent->type == GDK_BUTTON_PRESS) + aEventType = "BUTTON_PRESS"; + else if (pEvent->type == GDK_BUTTON_RELEASE) + aEventType = "BUTTON_RELEASE"; + + g_info("lok_dialog_floating_win_signal_button (type: %s): %d, %d (in twips: %d, %d)", + aEventType.c_str(), + static_cast<int>(pEvent->x), static_cast<int>(pEvent->y), + static_cast<int>(o3tl::toTwips(pEvent->x, o3tl::Length::px)), + static_cast<int>(o3tl::toTwips(pEvent->y, o3tl::Length::px))); + + switch (pEvent->type) + { + case GDK_BUTTON_PRESS: + { + int nCount = 1; + if ((pEvent->time - priv->m_nChildLastButtonPressTime) < 250) + nCount++; + priv->m_nChildLastButtonPressTime = pEvent->time; + int nEventButton = 0; + switch (pEvent->button) + { + case 1: + nEventButton = MOUSE_LEFT; + break; + case 2: + nEventButton = MOUSE_MIDDLE; + break; + case 3: + nEventButton = MOUSE_RIGHT; + break; + } + priv->m_nChildLastButtonPressed = nEventButton; + pDocument->pClass->postWindowMouseEvent(pDocument, + priv->m_nChildId, + LOK_MOUSEEVENT_MOUSEBUTTONDOWN, + (pEvent->x), + (pEvent->y), + nCount, + nEventButton, + priv->m_nChildKeyModifier); + + break; + } + case GDK_BUTTON_RELEASE: + { + int nCount = 1; + if ((pEvent->time - priv->m_nChildLastButtonReleaseTime) < 250) + nCount++; + priv->m_nChildLastButtonReleaseTime = pEvent->time; + int nEventButton = 0; + switch (pEvent->button) + { + case 1: + nEventButton = MOUSE_LEFT; + break; + case 2: + nEventButton = MOUSE_MIDDLE; + break; + case 3: + nEventButton = MOUSE_RIGHT; + break; + } + priv->m_nChildLastButtonPressed = nEventButton; + pDocument->pClass->postWindowMouseEvent(pDocument, + priv->m_nChildId, + LOK_MOUSEEVENT_MOUSEBUTTONUP, + (pEvent->x), + (pEvent->y), + nCount, + nEventButton, + priv->m_nChildKeyModifier); + break; + } + default: + break; + } + return FALSE; +} + +static gboolean +gtv_lok_dialog_floating_win_signal_motion(GtkWidget* /*pDialogDrawingArea*/, GdkEventButton* pEvent, gpointer userdata) +{ + GtvLokDialog* pDialog = GTV_LOK_DIALOG(userdata); + GtvLokDialogPrivate* priv = getPrivate(pDialog); + + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_window_get_transient_for(GTK_WINDOW(pDialog))); + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + + g_info("lok_dialog_floating_win_signal_motion: %d, %d (in twips: %d, %d)", + static_cast<int>(pEvent->x), static_cast<int>(pEvent->y), + static_cast<int>(o3tl::toTwips(pEvent->x, o3tl::Length::px)), + static_cast<int>(o3tl::toTwips(pEvent->y, o3tl::Length::px))); + + pDocument->pClass->postWindowMouseEvent(pDocument, + priv->m_nChildId, + LOK_MOUSEEVENT_MOUSEMOVE, + (pEvent->x), + (pEvent->y), + 1, + priv->m_nChildLastButtonPressed, + priv->m_nChildKeyModifier); + + return FALSE; +} + +// Public methods below + +void gtv_lok_dialog_invalidate(GtvLokDialog* dialog, const GdkRectangle& aRectangle) +{ + GtvLokDialogPrivate* priv = getPrivate(dialog); + if (aRectangle.width != 0 && aRectangle.height != 0) + gtk_widget_queue_draw_area(priv->pDialogDrawingArea, aRectangle.x, aRectangle.y, aRectangle.width, aRectangle.height); + else + gtk_widget_queue_draw(priv->pDialogDrawingArea); +} + +// checks if we are the parent of given childId +gboolean gtv_lok_dialog_is_parent_of(GtvLokDialog* dialog, guint childId) +{ + GtvLokDialogPrivate* priv = getPrivate(dialog); + + return priv->m_nChildId == childId; +} + +void gtv_lok_dialog_child_create(GtvLokDialog* dialog, guint childId, guint nX, guint nY, guint width, guint height) +{ + GtvLokDialogPrivate* priv = getPrivate(dialog); + + g_debug("Dialog [ %d ] child window [ %d] being created, with dimensions [%dx%d]@(%d,%d)", priv->dialogid, childId, width, height, nX, nY); + priv->pFloatingWin = gtk_window_new(GTK_WINDOW_POPUP); + priv->m_nChildId = childId; + priv->m_nChildWidth = width; + priv->m_nChildHeight = height; + GtkWidget* pDrawingArea = gtk_drawing_area_new(); + gtk_container_add(GTK_CONTAINER(priv->pFloatingWin), pDrawingArea); + + gtk_window_set_transient_for(GTK_WINDOW(priv->pFloatingWin), GTK_WINDOW(dialog)); + gtk_window_set_destroy_with_parent(GTK_WINDOW(priv->pFloatingWin), true); + + gtk_widget_add_events(pDrawingArea, + GDK_BUTTON_PRESS_MASK + |GDK_POINTER_MOTION_MASK + |GDK_BUTTON_RELEASE_MASK + |GDK_BUTTON_MOTION_MASK); + + g_signal_connect(G_OBJECT(pDrawingArea), "draw", G_CALLBACK(gtv_lok_dialog_floating_win_draw), dialog); + g_signal_connect(G_OBJECT(pDrawingArea), "button-press-event", G_CALLBACK(gtv_lok_dialog_floating_win_signal_button), dialog); + g_signal_connect(G_OBJECT(pDrawingArea), "button-release-event", G_CALLBACK(gtv_lok_dialog_floating_win_signal_button), dialog); + g_signal_connect(G_OBJECT(pDrawingArea), "motion-notify-event", G_CALLBACK(gtv_lok_dialog_floating_win_signal_motion), dialog); + + gtk_widget_set_size_request(priv->pFloatingWin, 1, 1); + gtk_window_set_type_hint(GTK_WINDOW(priv->pFloatingWin), GDK_WINDOW_TYPE_HINT_POPUP_MENU); + gtk_window_set_screen(GTK_WINDOW(priv->pFloatingWin), gtk_window_get_screen(GTK_WINDOW(dialog))); + + gtk_widget_show_all(priv->pFloatingWin); + gtk_window_present(GTK_WINDOW(priv->pFloatingWin)); + gtk_widget_grab_focus(pDrawingArea); + + // Get the root coords of our new floating window + GdkWindow* pGdkWin = gtk_widget_get_window(GTK_WIDGET(dialog)); + int nrX = 0; + int nrY = 0; + gdk_window_get_root_coords(pGdkWin, nX, nY, &nrX, &nrY); + gtk_window_move(GTK_WINDOW(priv->pFloatingWin), nrX, nrY); +} + +void gtv_lok_dialog_child_invalidate(GtvLokDialog* dialog) +{ + GtvLokDialogPrivate* priv = getPrivate(dialog); + g_debug("Dialog [ %d ] child invalidate request", priv->dialogid); + gtk_widget_queue_draw(priv->pFloatingWin); +} + +void gtv_lok_dialog_child_close(GtvLokDialog* dialog) +{ + g_info("Dialog's floating window close"); + + GtvLokDialogPrivate* priv = getPrivate(dialog); + if (priv->pFloatingWin) + { + gtk_widget_destroy(priv->pFloatingWin); + priv->pFloatingWin = nullptr; + priv->m_nChildId = 0; + priv->m_nChildWidth = 0; + priv->m_nChildHeight = 0; + } +} + +GtkWidget* gtv_lok_dialog_new(LOKDocView* pDocView, guint dialogId, guint width, guint height) +{ + g_debug("Dialog [ %d ] of size: %d x %d created", dialogId, width, height); + GtkWindow* pWindow = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView))); + return GTK_WIDGET(g_object_new(GTV_TYPE_LOK_DIALOG, + "lokdocview", pDocView, + "dialogid", dialogId, + "width", width, + "height", height, + "title", "LOK Dialog", + "modal", false, + "transient-for", pWindow, + nullptr)); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-lok-dialog.hxx b/libreofficekit/qa/gtktiledviewer/gtv-lok-dialog.hxx new file mode 100644 index 000000000..2a5bfb595 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-lok-dialog.hxx @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef GTV_LOK_DIALOG_H +#define GTV_LOK_DIALOG_H + +#include <gtk/gtk.h> + +#include <LibreOfficeKit/LibreOfficeKitGtk.h> + +G_BEGIN_DECLS + +#define GTV_TYPE_LOK_DIALOG (gtv_lok_dialog_get_type()) +#define GTV_LOK_DIALOG(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GTV_TYPE_LOK_DIALOG, GtvLokDialog)) +#define GTV_IS_LOK_DIALOG(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTV_TYPE_LOK_DIALOG)) +#define GTV_LOK_DIALOG_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GTV_TYPE_LOK_DIALOG, GtvLokDialogClass)) +#define GTV_IS_LOK_DIALOG_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GTV_TYPE_LOK_DIALOG)) +#define GTV_LOK_DIALOG_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), GTV_TYPE_LOK_DIALOG, GtvLokDialogClass)) + +struct GtvLokDialog +{ + GtkDialog parent; +}; + +struct GtvLokDialogClass +{ + GtkDialogClass parentClass; +}; + +GType gtv_lok_dialog_get_type (void) G_GNUC_CONST; + +GtkWidget* gtv_lok_dialog_new(LOKDocView* pDocView, guint dialogId, guint width, guint height); + +void gtv_lok_dialog_invalidate(GtvLokDialog* dialog, const GdkRectangle& aRectangle); + +void gtv_lok_dialog_child_create(GtvLokDialog* dialog, guint childId, guint nX, guint nY, guint width, guint height); + +void gtv_lok_dialog_child_invalidate(GtvLokDialog* dialog); + +void gtv_lok_dialog_child_close(GtvLokDialog* dialog); + +gboolean gtv_lok_dialog_is_parent_of(GtvLokDialog* dialog, guint childId); + +G_END_DECLS + +#endif /* GTV_LOK_DIALOG_H */ + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-lokdocview-signal-handlers.cxx b/libreofficekit/qa/gtktiledviewer/gtv-lokdocview-signal-handlers.cxx new file mode 100644 index 000000000..8d1b3eb67 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-lokdocview-signal-handlers.cxx @@ -0,0 +1,479 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <gtk/gtk.h> + +#include "gtv-application-window.hxx" +#include "gtv-helpers.hxx" +#include "gtv-calc-header-bar.hxx" +#include "gtv-comments-sidebar.hxx" +#include "gtv-lokdocview-signal-handlers.hxx" +#include "gtv-lok-dialog.hxx" + +#include <boost/property_tree/json_parser.hpp> + +static gboolean deleteLokDialog(GtkWidget* pWidget, GdkEvent* /*event*/, gpointer userdata) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(userdata); + g_info("deleteLokDialog"); + gtv_application_window_unregister_child_window(window, GTK_WINDOW(pWidget)); + + return FALSE; +} + +static gboolean destroyLokDialog(GtkWidget* pWidget, gpointer userdata) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(userdata); + g_info("destroyLokDialog"); + gtv_application_window_unregister_child_window(window, GTK_WINDOW(pWidget)); + + return FALSE; +} + +void LOKDocViewSigHandlers::editChanged(LOKDocView* pDocView, gboolean bWasEdit, gpointer) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView))); + bool bEdit = lok_doc_view_get_edit(LOK_DOC_VIEW(window->lokdocview)); + g_info("signalEdit: %d -> %d", bWasEdit, bEdit); + + // Let the main toolbar know, so that it can enable disable the button + GtvMainToolbar* pMainToolbar = gtv_application_window_get_main_toolbar(GTV_APPLICATION_WINDOW(window)); + gtv_main_toolbar_set_edit(pMainToolbar, bEdit); +} + +void LOKDocViewSigHandlers::commandChanged(LOKDocView* pDocView, char* pPayload, gpointer) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView))); + std::string aPayload(pPayload); + size_t nPosition = aPayload.find('='); + if (nPosition == std::string::npos) + return; + + const std::string aKey = aPayload.substr(0, nPosition); + const std::string aValue = aPayload.substr(nPosition + 1); + GtkToolItem* pItem = gtv_application_window_find_tool_by_unocommand(window, aKey); + if (pItem != nullptr) + { + if (aValue == "true" || aValue == "false") { + bool bEdit = aValue == "true"; + if (bool(gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(pItem))) != bEdit) + { + // Avoid invoking lok_doc_view_post_command(). + // FIXME: maybe block/unblock the signal (see + // g_signal_handlers_block_by_func) ? + gtv_application_window_set_toolbar_broadcast(window, false); + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(pItem), bEdit); + gtv_application_window_set_toolbar_broadcast(window, true); + + } + } else if (aValue == "enabled" || aValue == "disabled") { + bool bSensitive = aValue == "enabled"; + gtk_widget_set_sensitive(GTK_WIDGET(pItem), bSensitive); + + // Remember state, so in case edit is disable and enabled + // later, the correct sensitivity can be restored. + GtvMainToolbar* pMainToolbar = gtv_application_window_get_main_toolbar(window); + gtv_main_toolbar_set_sensitive_internal(pMainToolbar, pItem, bSensitive); + } + } + else if (aKey == ".uno:TrackedChangeIndex") + { + std::string aText("Current redline: "); + if (aValue.empty()) + aText += "none"; + else + aText += aValue; + gtk_label_set_text(GTK_LABEL(window->redlinelabel), aText.c_str()); + } +} + +void LOKDocViewSigHandlers::commandResult(LOKDocView*, char* pPayload, gpointer) +{ + fprintf(stderr, "Command finished: %s\n", pPayload); +} + +void LOKDocViewSigHandlers::searchNotFound(LOKDocView* pDocView, char* , gpointer) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView))); + gtk_label_set_text(GTK_LABEL(window->findbarlabel), "Search key not found"); +} + +void LOKDocViewSigHandlers::searchResultCount(LOKDocView* pDocView, char* pPayload, gpointer) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView))); + std::stringstream ss; + ss << pPayload << " match(es)"; + gtk_label_set_text(GTK_LABEL(window->findbarlabel), ss.str().c_str()); +} + +void LOKDocViewSigHandlers::partChanged(LOKDocView* /*pDocView*/, int, gpointer) +{ +// GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView))); + //rWindow.m_bPartSelectorBroadcast = false; +// gtk_combo_box_set_active(GTK_COMBO_BOX(rWindow.m_pPartSelector), nPart); + // rWindow.m_bPartSelectorBroadcast = true; +} + +void LOKDocViewSigHandlers::hyperlinkClicked(LOKDocView* pDocView, char* pPayload, gpointer) +{ + GError* pError = nullptr; +#if GTK_CHECK_VERSION(3,22,0) + gtk_show_uri_on_window( + GTK_WINDOW (gtk_widget_get_toplevel(GTK_WIDGET(pDocView))), + pPayload, GDK_CURRENT_TIME, &pError); +#else + (void) pDocView; + gtk_show_uri(nullptr, pPayload, GDK_CURRENT_TIME, &pError); +#endif + if (pError != nullptr) + { + g_warning("Unable to show URI %s : %s", pPayload, pError->message); + g_error_free(pError); + } +} + +void LOKDocViewSigHandlers::cursorChanged(LOKDocView* pDocView, gint nX, gint nY, + gint /*nWidth*/, gint /*nHeight*/, gpointer /*pData*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView))); + GtkAdjustment* vadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(window->scrolledwindow)); + GtkAdjustment* hadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(window->scrolledwindow)); + GdkRectangle visArea; + gdouble upper; + gint x = -1, y = -1; + + gtv_application_window_get_visible_area(window, &visArea); + + // check vertically + if (nY < visArea.y) + { + y = nY - visArea.height/2; + if (y < 0) + y = gtk_adjustment_get_lower(vadj); + } + else if (nY > visArea.y + visArea.height) + { + y = nY - visArea.height/2; + upper = lok_doc_view_pixel_to_twip(pDocView, gtk_adjustment_get_upper(vadj)); + if (y > upper) + y = upper; + + } + + if (nX < visArea.x) + { + x = nX - visArea.width/2; + if (x < 0) + x = gtk_adjustment_get_lower(hadj); + } + else if (nX > visArea.x + visArea.width) + { + x = nX - visArea.width/2; + upper = lok_doc_view_pixel_to_twip(pDocView, gtk_adjustment_get_upper(hadj)); + if (x > upper) + x = upper; + } + + if (y!=-1) + gtk_adjustment_set_value(vadj, lok_doc_view_twip_to_pixel(pDocView, y)); + if (x!=-1) + gtk_adjustment_set_value(hadj, lok_doc_view_twip_to_pixel(pDocView, x)); +} + +void LOKDocViewSigHandlers::addressChanged(LOKDocView* pDocView, char* pPayload, gpointer) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView))); + GtvMainToolbar* toolbar = gtv_application_window_get_main_toolbar(window); + GtkEntry* pAddressbar = GTK_ENTRY(toolbar->m_pAddressbar); + gtk_entry_set_text(pAddressbar, pPayload); +} + +void LOKDocViewSigHandlers::formulaChanged(LOKDocView* pDocView, char* pPayload, gpointer) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView))); + GtvMainToolbar* toolbar = gtv_application_window_get_main_toolbar(window); + GtkEntry* pFormulabar = GTK_ENTRY(toolbar->m_pFormulabar); + 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()); + } + } + + boost::optional<boost::property_tree::ptree&> oDate = aTree.get_child_optional("date"); + gtk_widget_set_sensitive(GTK_WIDGET(toolbar->m_pContentControlDateSelector), bool(oDate)); + + 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))); + GtkWidget* pPasswordDialog = gtk_dialog_new_with_buttons ("Password required", + GTK_WINDOW (window), + GTK_DIALOG_MODAL, + "OK", + GTK_RESPONSE_OK, + nullptr); + g_object_set(G_OBJECT(pPasswordDialog), "resizable", FALSE, nullptr); + GtkWidget* pDialogMessageArea = gtk_dialog_get_content_area (GTK_DIALOG (pPasswordDialog)); + GtkWidget* pPasswordEntry = gtk_entry_new (); + gtk_entry_set_visibility (GTK_ENTRY(pPasswordEntry), FALSE); + gtk_entry_set_invisible_char (GTK_ENTRY(pPasswordEntry), '*'); + gtk_box_pack_end(GTK_BOX(pDialogMessageArea), pPasswordEntry, true, true, 2); + if (bModify) + { + GtkWidget* pSecondaryLabel = gtk_label_new ("Document requires password to edit"); + gtk_box_pack_end(GTK_BOX(pDialogMessageArea), pSecondaryLabel, true, true, 2); + gtk_dialog_add_button (GTK_DIALOG (pPasswordDialog), "Open as read-only", GTK_RESPONSE_ACCEPT); + } + gtk_widget_show_all(pPasswordDialog); + + gint res = gtk_dialog_run (GTK_DIALOG(pPasswordDialog)); + switch (res) + { + case GTK_RESPONSE_OK: + lok_doc_view_set_document_password (LOK_DOC_VIEW(window->lokdocview), pUrl, gtk_entry_get_text(GTK_ENTRY(pPasswordEntry))); + break; + case GTK_RESPONSE_ACCEPT: + // User accepts to open this document as read-only + case GTK_RESPONSE_DELETE_EVENT: + lok_doc_view_set_document_password (LOK_DOC_VIEW(window->lokdocview), pUrl, nullptr); + break; + } + + gtk_widget_destroy(pPasswordDialog); +} + +void LOKDocViewSigHandlers::comment(LOKDocView* pDocView, gchar* pComment, gpointer) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView))); + + std::stringstream aStream(pComment); + boost::property_tree::ptree aRoot; + boost::property_tree::read_json(aStream, aRoot); + boost::property_tree::ptree aComment = aRoot.get_child("comment"); + GtvCommentsSidebar* sidebar = GTV_COMMENTS_SIDEBAR(window->commentssidebar); + GtkWidget* pCommentsGrid = sidebar->commentsgrid; + GtvGtkWrapper<GList> pChildren(gtk_container_get_children(GTK_CONTAINER(pCommentsGrid)), + [](GList* l) + { + g_list_free(l); + }); + GtkWidget* pSelf = nullptr; + GtkWidget* pParent = nullptr; + for (GList* l = pChildren.get(); l != nullptr; l = l->next) + { + gchar *id = static_cast<gchar*>(g_object_get_data(G_OBJECT(l->data), "id")); + + if (g_strcmp0(id, aComment.get<std::string>("id").c_str()) == 0) + pSelf = GTK_WIDGET(l->data); + + // There is no 'parent' in Remove callbacks + if (g_strcmp0(id, aComment.get("parent", std::string("0")).c_str()) == 0) + pParent = GTK_WIDGET(l->data); + } + + if (aComment.get<std::string>("action") == "Remove") + { + if (pSelf) + gtk_widget_destroy(pSelf); + else + g_warning("Can't find the comment to remove in the list !!"); + } + else if (aComment.get<std::string>("action") == "Add" || aComment.get<std::string>("action") == "Modify") + { + GtkWidget* pCommentBox = GtvHelpers::createCommentBox(aComment); + if (pSelf != nullptr || pParent != nullptr) + { + gtk_grid_insert_next_to(GTK_GRID(pCommentsGrid), pSelf != nullptr ? pSelf : pParent, GTK_POS_BOTTOM); + gtk_grid_attach_next_to(GTK_GRID(pCommentsGrid), pCommentBox, pSelf != nullptr ? pSelf : pParent, GTK_POS_BOTTOM, 1, 1); + } + else + gtk_container_add(GTK_CONTAINER(pCommentsGrid), pCommentBox); + + gtk_widget_show_all(pCommentBox); + + // We added the widget already below the existing one, so destroy the + // already existing one now + if (pSelf) + gtk_widget_destroy(pSelf); + } +} + +void LOKDocViewSigHandlers::window(LOKDocView* pDocView, gchar* pPayload, gpointer pData) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(pData); + + std::stringstream aStream(pPayload); + boost::property_tree::ptree aRoot; + boost::property_tree::read_json(aStream, aRoot); + const unsigned nWinId = aRoot.get<unsigned>("id"); + const std::string aAction = aRoot.get<std::string>("action"); + + if (aAction == "created") + { + const std::string aType = aRoot.get<std::string>("type"); + const std::string aSize = aRoot.get<std::string>("size"); + const std::string aTitle = aRoot.get<std::string>("title", ""); + std::vector<int> aSizePoints = GtvHelpers::split<int>(aSize, ", ", 2); + + if (aType == "dialog") + { + GtkWidget* pDialog = gtv_lok_dialog_new(pDocView, nWinId, aSizePoints[0], aSizePoints[1]); + g_info("created dialog, for dialogid: %d with size: %s", nWinId, aSize.c_str()); + + gtv_application_window_register_child_window(window, GTK_WINDOW(pDialog)); + g_signal_connect(pDialog, "destroy", G_CALLBACK(destroyLokDialog), window); + g_signal_connect(pDialog, "delete-event", G_CALLBACK(deleteLokDialog), window); + + if (!aTitle.empty()) + gtk_window_set_title(GTK_WINDOW(pDialog), aTitle.c_str()); + + gtk_window_set_resizable(GTK_WINDOW(pDialog), false); + gtk_widget_show_all(GTK_WIDGET(pDialog)); + gtk_window_present(GTK_WINDOW(pDialog)); + } + else if (aType == "child") + { + const unsigned nParentId = std::atoi(aRoot.get<std::string>("parentId").c_str()); + GtkWindow* pDialog = gtv_application_window_get_child_window_by_id(window, nParentId); + const std::string aPos = aRoot.get<std::string>("position"); + std::vector<int> aPosPoints = GtvHelpers::split<int>(aPos, ", ", 2); + gtv_lok_dialog_child_create(GTV_LOK_DIALOG(pDialog), nWinId, aPosPoints[0], aPosPoints[1], aSizePoints[0], aSizePoints[1]); + } + } + else + { + // check if it's a child window + GtkWidget* pParent = gtv_application_window_get_parent(window, nWinId); + if (pParent) // it's a floating window in the dialog + { + if (aAction == "invalidate") + gtv_lok_dialog_child_invalidate(GTV_LOK_DIALOG(pParent)); + else if (aAction == "close") + gtv_lok_dialog_child_close(GTV_LOK_DIALOG(pParent)); + } + else if (GtkWindow* pDialog = gtv_application_window_get_child_window_by_id(window, nWinId)) + { // it's the dialog window itself + if (aAction == "close") + gtk_widget_destroy(GTK_WIDGET(pDialog)); + else if (aAction == "size_changed") + { + const std::string aSize = aRoot.get<std::string>("size"); + std::vector<int> aSizePoints = GtvHelpers::split<int>(aSize, ", ", 2); + if (aSizePoints.size() != 2) + { + g_error("Malformed size_changed callback"); + return; + } + + g_object_set(G_OBJECT(pDialog), + "width", aSizePoints[0], + "height", aSizePoints[1], + nullptr); + + GdkRectangle aGdkRectangle = {0, 0, 0, 0}; + gtv_lok_dialog_invalidate(GTV_LOK_DIALOG(pDialog), aGdkRectangle); + } + else if (aAction == "invalidate") + { + GdkRectangle aGdkRectangle = {0, 0, 0, 0}; + try + { + const std::string aRectangle = aRoot.get<std::string>("rectangle"); + std::vector<int> aRectPoints = GtvHelpers::split<int>(aRectangle, ", ", 4); + if (aRectPoints.size() == 4) + aGdkRectangle = {aRectPoints[0], aRectPoints[1], aRectPoints[2], aRectPoints[3]}; + } + catch(const std::exception&) + {} + + gtv_lok_dialog_invalidate(GTV_LOK_DIALOG(pDialog), aGdkRectangle); + } + else if (aAction == "title_changed") + { + const std::string aTitle = aRoot.get<std::string>("title", ""); + gtk_window_set_title(pDialog, aTitle.c_str()); + } + } + } +} + +gboolean LOKDocViewSigHandlers::configureEvent(GtkWidget* pWidget, GdkEventConfigure* /*pEvent*/, gpointer /*pData*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pWidget))); + + gboolean isInit = false; + g_object_get(G_OBJECT(window->lokdocview), "is-initialized", &isInit, nullptr); + if (!isInit) + { + g_info("Ignoring configure event; document not yet ready"); + return false; + } + + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + if (!pDocument || pDocument->pClass->getDocumentType(pDocument) != LOK_DOCTYPE_SPREADSHEET) + return true; + + GtkAdjustment* pVAdjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(window->scrolledwindow)); + int rowSizePixel = GTV_CALC_HEADER_BAR(window->rowbar)->m_nSizePixel = gtk_adjustment_get_page_size(pVAdjustment); + int rowPosPixel = GTV_CALC_HEADER_BAR(window->rowbar)->m_nPositionPixel = gtk_adjustment_get_value(pVAdjustment); + GtkAdjustment* pHAdjustment = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(window->scrolledwindow)); + int colSizePixel = GTV_CALC_HEADER_BAR(window->columnbar)->m_nSizePixel = gtk_adjustment_get_page_size(pHAdjustment); + int colPosPixel = GTV_CALC_HEADER_BAR(window->columnbar)->m_nPositionPixel = gtk_adjustment_get_value(pHAdjustment); + + std::stringstream aCommand; + aCommand << ".uno:ViewRowColumnHeaders"; + aCommand << "?x=" << int(lok_doc_view_pixel_to_twip(LOK_DOC_VIEW(window->lokdocview), colPosPixel)); + aCommand << "&width=" << int(lok_doc_view_pixel_to_twip(LOK_DOC_VIEW(window->lokdocview), colSizePixel)); + aCommand << "&y=" << int(lok_doc_view_pixel_to_twip(LOK_DOC_VIEW(window->lokdocview), rowPosPixel)); + aCommand << "&height=" << int(lok_doc_view_pixel_to_twip(LOK_DOC_VIEW(window->lokdocview), rowSizePixel)); + std::stringstream ss; + ss << "lok::Document::getCommandValues(" << aCommand.str() << ")"; + g_info("%s", ss.str().c_str()); + char* pValues = pDocument->pClass->getCommandValues(pDocument, aCommand.str().c_str()); + g_info("lok::Document::getCommandValues() returned '%s'", pValues); + std::stringstream aStream(pValues); + free(pValues); + assert(!aStream.str().empty()); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + + gtv_calc_header_bar_configure(GTV_CALC_HEADER_BAR(window->rowbar), &aTree.get_child("rows")); + gtv_calc_header_bar_configure(GTV_CALC_HEADER_BAR(window->columnbar), &aTree.get_child("columns")); + gtv_calc_header_bar_configure(GTV_CALC_HEADER_BAR(window->cornerarea), nullptr); + + return true; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-lokdocview-signal-handlers.hxx b/libreofficekit/qa/gtktiledviewer/gtv-lokdocview-signal-handlers.hxx new file mode 100644 index 000000000..0c5bb7113 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-lokdocview-signal-handlers.hxx @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef GTV_LOKDOCVIEW_SIGNAL_HANDLERS_H +#define GTV_LOKDOCVIEW_SIGNAL_HANDLERS_H + +#include <gtk/gtk.h> +#include <LibreOfficeKit/LibreOfficeKitGtk.h> + +namespace LOKDocViewSigHandlers { + void editChanged(LOKDocView* pDocView, gboolean bWasEdit, gpointer); + void commandChanged(LOKDocView* pDocView, char* pPayload, gpointer); + void commandResult(LOKDocView*, char*, gpointer); + void searchNotFound(LOKDocView*, char*, gpointer); + void searchResultCount(LOKDocView*, char*, gpointer); + void partChanged(LOKDocView*, int, gpointer); + void hyperlinkClicked(LOKDocView*, char*, gpointer); + void cursorChanged(LOKDocView* pDocView, gint nX, gint nY, gint nWidth, gint nHeight, gpointer); + void addressChanged(LOKDocView* pDocView, char* pPayload, gpointer); + void formulaChanged(LOKDocView* pDocView, char* pPayload, gpointer); + 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); +} + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-main-toolbar.cxx b/libreofficekit/qa/gtktiledviewer/gtv-main-toolbar.cxx new file mode 100644 index 000000000..84c5335b3 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-main-toolbar.cxx @@ -0,0 +1,365 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <gtk/gtk.h> + +#include "gtv-application-window.hxx" +#include "gtv-main-toolbar.hxx" +#include "gtv-signal-handlers.hxx" +#include "gtv-helpers.hxx" + +#include <LibreOfficeKit/LibreOfficeKitGtk.h> + +#include <fstream> +#include <map> +#include <memory> + +namespace { + +struct GtvMainToolbarPrivateImpl +{ + GtkWidget* toolbar1; + GtkWidget* toolbar2; + + GtkWidget* m_pEnableEditing; + GtkWidget* m_pLeftpara; + GtkWidget* m_pCenterpara; + GtkWidget* m_pRightpara; + GtkWidget* m_pJustifypara; + GtkWidget* m_pDeleteComment; + GtkWidget* m_pPartSelector; + GtkWidget* m_pPartModeSelector; + GtkWidget* m_pRecentUnoSelector; + std::map<std::string, std::string> m_pRecentUnoCommands; + + /// Sensitivity (enabled or disabled) for each tool item, ignoring edit state + std::map<GtkToolItem*, bool> m_aToolItemSensitivities; + + GtvMainToolbarPrivateImpl() : + toolbar1(nullptr), + toolbar2(nullptr), + m_pEnableEditing(nullptr), + m_pLeftpara(nullptr), + m_pCenterpara(nullptr), + m_pRightpara(nullptr), + m_pJustifypara(nullptr), + m_pDeleteComment(nullptr), + m_pPartSelector(nullptr), + m_pPartModeSelector(nullptr), + m_pRecentUnoSelector(nullptr) + { } +}; + +struct GtvMainToolbarPrivate +{ + GtvMainToolbarPrivateImpl* m_pImpl; + + GtvMainToolbarPrivateImpl* operator->() + { + return m_pImpl; + } +}; + +} + +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +#endif +G_DEFINE_TYPE_WITH_PRIVATE(GtvMainToolbar, gtv_main_toolbar, GTK_TYPE_BOX); +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic pop +#endif +#endif + +static GtvMainToolbarPrivate& +getPrivate(GtvMainToolbar* toolbar) +{ + return *static_cast<GtvMainToolbarPrivate*>(gtv_main_toolbar_get_instance_private(toolbar)); +} + +static void +gtv_main_toolbar_init(GtvMainToolbar* toolbar) +{ + GtvMainToolbarPrivate& priv = getPrivate(toolbar); + priv.m_pImpl = new GtvMainToolbarPrivateImpl(); + + const std::string uiFilePath = GtvHelpers::getDirPath(__FILE__) + std::string(UI_FILE_NAME); + GtvGtkWrapper<GtkBuilder> builder(gtk_builder_new_from_file(uiFilePath.c_str()), + [](GtkBuilder* pBuilder) { + g_object_unref(pBuilder); + }); + + priv->toolbar1 = GTK_WIDGET(gtk_builder_get_object(builder.get(), "toolbar1")); + gtk_box_pack_start(GTK_BOX(toolbar), priv->toolbar1, false, false, false); + priv->toolbar2 = GTK_WIDGET(gtk_builder_get_object(builder.get(), "toolbar2")); + gtk_box_pack_start(GTK_BOX(toolbar), priv->toolbar2, false, false, false); + + priv->m_pEnableEditing = GTK_WIDGET(gtk_builder_get_object(builder.get(), "btn_editmode")); + priv->m_pLeftpara = GTK_WIDGET(gtk_builder_get_object(builder.get(), "btn_justifyleft")); + priv->m_pCenterpara = GTK_WIDGET(gtk_builder_get_object(builder.get(), "btn_justifycenter")); + priv->m_pRightpara = GTK_WIDGET(gtk_builder_get_object(builder.get(), "btn_justifyright")); + priv->m_pJustifypara = GTK_WIDGET(gtk_builder_get_object(builder.get(), "btn_justifyfill")); + priv->m_pDeleteComment = GTK_WIDGET(gtk_builder_get_object(builder.get(), "btn_removeannotation")); + priv->m_pPartSelector = GTK_WIDGET(gtk_builder_get_object(builder.get(), "combo_partselector")); + priv->m_pPartModeSelector = GTK_WIDGET(gtk_builder_get_object(builder.get(), "combo_partsmodeselector")); + priv->m_pRecentUnoSelector = GTK_WIDGET(gtk_builder_get_object(builder.get(), "combo_recentunoselector")); + + 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")); + toolbar->m_pContentControlDateSelector + = GTK_WIDGET(gtk_builder_get_object(builder.get(), "menu_contentcontroldateselector")); + + // TODO: compile with -rdynamic and get rid of it + gtk_builder_add_callback_symbol(builder.get(), "btn_clicked", G_CALLBACK(btn_clicked)); + gtk_builder_add_callback_symbol(builder.get(), "doCopy", G_CALLBACK(doCopy)); + gtk_builder_add_callback_symbol(builder.get(), "doPaste", G_CALLBACK(doPaste)); + gtk_builder_add_callback_symbol(builder.get(), "createView", G_CALLBACK(createView)); + gtk_builder_add_callback_symbol(builder.get(), "getRulerState", G_CALLBACK(getRulerState)); + gtk_builder_add_callback_symbol(builder.get(), "recentUnoChanged", G_CALLBACK(recentUnoChanged)); + gtk_builder_add_callback_symbol(builder.get(), "unoCommandDebugger", G_CALLBACK(unoCommandDebugger)); + 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(), "changeDateContentControl", + G_CALLBACK(changeDateContentControl)); + 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)); + gtk_builder_add_callback_symbol(builder.get(), "documentRepair", G_CALLBACK(documentRepair)); + gtk_builder_add_callback_symbol(builder.get(), "signalAddressbar", G_CALLBACK(signalAddressbar)); + gtk_builder_add_callback_symbol(builder.get(), "signalFormulabar", G_CALLBACK(signalFormulabar)); + + // find toolbar + // Note: These buttons are not the part of GtvMainToolbar + gtk_builder_add_callback_symbol(builder.get(), "signalSearchNext", G_CALLBACK(signalSearchNext)); + gtk_builder_add_callback_symbol(builder.get(), "signalSearchPrev", G_CALLBACK(signalSearchPrev)); + gtk_builder_add_callback_symbol(builder.get(), "signalFindbar", G_CALLBACK(signalFindbar)); + gtk_builder_add_callback_symbol(builder.get(), "toggleFindAll", G_CALLBACK(toggleFindAll)); + + gtk_builder_connect_signals(builder.get(), nullptr); + + gtk_widget_show_all(GTK_WIDGET(toolbar)); +} + +static void +gtv_main_toolbar_finalize(GObject* object) +{ + GtvMainToolbarPrivate& priv = getPrivate(GTV_MAIN_TOOLBAR(object)); + + delete priv.m_pImpl; + priv.m_pImpl = nullptr; + + G_OBJECT_CLASS (gtv_main_toolbar_parent_class)->finalize (object); +} + +static void +gtv_main_toolbar_class_init(GtvMainToolbarClass* klass) +{ + G_OBJECT_CLASS(klass)->finalize = gtv_main_toolbar_finalize; +} + +static void populatePartSelector(GtvMainToolbar* toolbar) +{ + GtvMainToolbarPrivate& priv = getPrivate(toolbar); + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(toolbar))); + gtv_application_window_set_part_broadcast(window, false); + gtk_list_store_clear( GTK_LIST_STORE( + gtk_combo_box_get_model( + GTK_COMBO_BOX(priv->m_pPartSelector) )) ); + + if (!window->lokdocview) + { + return; + } + + const int nMaxLength = 50; + char sText[nMaxLength]; + + int nParts = lok_doc_view_get_parts(LOK_DOC_VIEW(window->lokdocview)); + for ( int i = 0; i < nParts; i++ ) + { + char* pName = lok_doc_view_get_part_name(LOK_DOC_VIEW(window->lokdocview), i); + assert( pName ); + snprintf( sText, nMaxLength, "%i (%s)", i+1, pName ); + free( pName ); + + gtk_combo_box_text_append_text( GTK_COMBO_BOX_TEXT(priv->m_pPartSelector), sText ); + } + gtk_combo_box_set_active(GTK_COMBO_BOX(priv->m_pPartSelector), lok_doc_view_get_part(LOK_DOC_VIEW(window->lokdocview))); + + gtv_application_window_set_part_broadcast(window, true); +} + +static void populateRecentUnoSelector(GtvMainToolbar* toolbar) +{ + GtvMainToolbarPrivate& priv = getPrivate(toolbar); + GtkComboBoxText* pSelector = GTK_COMBO_BOX_TEXT(priv->m_pRecentUnoSelector); + + unsigned counter = 0; + std::ifstream is("/tmp/gtv-recentunos.txt"); + while (is.good() && counter < 10) + { + std::string unoCommandStr; + std::getline(is, unoCommandStr); + std::vector<std::string> aUnoCmd = GtvHelpers::split<std::string>(unoCommandStr, " | ", 2); + if (aUnoCmd.size() != 2) + continue; + auto it = priv->m_pRecentUnoCommands.emplace(aUnoCmd[0], aUnoCmd[1]); + if (it.second) + { + gtk_combo_box_text_append_text(pSelector, aUnoCmd[0].c_str()); + ++counter; + } + } +} + +void +gtv_main_toolbar_doc_loaded(GtvMainToolbar* toolbar, LibreOfficeKitDocumentType eDocType, bool bEditMode) +{ + GtvMainToolbarPrivate& priv = getPrivate(toolbar); + gtk_widget_set_visible(toolbar->m_pAddressbar, false); + gtk_widget_set_visible(toolbar->m_pFormulabar, false); + if (eDocType == LOK_DOCTYPE_SPREADSHEET) + { + gtk_tool_button_set_label(GTK_TOOL_BUTTON(priv->m_pLeftpara), ".uno:AlignLeft"); + gtk_tool_button_set_label(GTK_TOOL_BUTTON(priv->m_pCenterpara), ".uno:AlignHorizontalCenter"); + gtk_tool_button_set_label(GTK_TOOL_BUTTON(priv->m_pRightpara), ".uno:AlignRight"); + gtk_widget_hide(priv->m_pJustifypara); + gtk_tool_button_set_label(GTK_TOOL_BUTTON(priv->m_pDeleteComment), ".uno:DeleteNote"); + + gtk_widget_set_visible(toolbar->m_pAddressbar, true); + gtk_widget_set_visible(toolbar->m_pFormulabar, true); + } + else if (eDocType == LOK_DOCTYPE_PRESENTATION) + { + gtk_tool_button_set_label(GTK_TOOL_BUTTON(priv->m_pDeleteComment), ".uno:DeleteAnnotation"); + } + + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(priv->m_pEnableEditing), bEditMode); + + // populate combo boxes + populatePartSelector(toolbar); + + // populate recent uno selector + populateRecentUnoSelector(toolbar); +} + +void +gtv_main_toolbar_add_recent_uno(GtvMainToolbar* toolbar, const std::string& rUnoCmdStr) +{ + GtvMainToolbarPrivate& priv = getPrivate(toolbar); + GtkComboBoxText* pSelector = GTK_COMBO_BOX_TEXT(priv->m_pRecentUnoSelector); + + const std::vector<std::string> aUnoCmd = GtvHelpers::split<std::string>(rUnoCmdStr, " | ", 2); + priv->m_pRecentUnoCommands[aUnoCmd[0]] = aUnoCmd[1]; + // keep placeholder string at the top + gtk_combo_box_text_insert_text(pSelector, 1, aUnoCmd[0].c_str()); + // TODO: Remove other text entries with same key +} + +std::string +gtv_main_toolbar_get_recent_uno_args(GtvMainToolbar* toolbar, const std::string& rUnoCmd) +{ + GtvMainToolbarPrivate& priv = getPrivate(toolbar); + auto it = std::find_if(priv->m_pRecentUnoCommands.begin(), priv->m_pRecentUnoCommands.end(), + [&rUnoCmd](const std::pair<std::string, std::string>& pair) { + return rUnoCmd == pair.first; + }); + std::string ret; + if (it != priv->m_pRecentUnoCommands.end()) + ret = it->second; + return ret; +} + +GtkContainer* +gtv_main_toolbar_get_first_toolbar(GtvMainToolbar* toolbar) +{ + GtvMainToolbarPrivate& priv = getPrivate(toolbar); + return GTK_CONTAINER(priv->toolbar1); +} + +GtkContainer* +gtv_main_toolbar_get_second_toolbar(GtvMainToolbar* toolbar) +{ + GtvMainToolbarPrivate& priv = getPrivate(toolbar); + return GTK_CONTAINER(priv->toolbar2); +} + +void +gtv_main_toolbar_set_sensitive_internal(GtvMainToolbar* toolbar, GtkToolItem* pItem, bool isSensitive) +{ + GtvMainToolbarPrivate& priv = getPrivate(toolbar); + priv->m_aToolItemSensitivities[pItem] = isSensitive; +} + +static void setSensitiveIfEdit(GtvMainToolbar* toolbar, GtkToolItem* pItem, bool bEdit) +{ + GtvMainToolbarPrivate& priv = getPrivate(toolbar); + // some buttons remain enabled always + const gchar* pIconName = gtk_tool_button_get_icon_name(GTK_TOOL_BUTTON(pItem)); + if (g_strcmp0(pIconName, "zoom-in-symbolic") != 0 && + g_strcmp0(pIconName, "zoom-original-symbolic") != 0 && + g_strcmp0(pIconName, "zoom-out-symbolic") != 0 && + g_strcmp0(pIconName, "insert-text-symbolic") != 0 && + g_strcmp0(pIconName, "view-continuous-symbolic") != 0 && + g_strcmp0(pIconName, "document-properties") != 0 && + g_strcmp0(pIconName, "system-run") != 0) + { + bool state = true; + if (priv->m_aToolItemSensitivities.find(pItem) != priv->m_aToolItemSensitivities.end()) + state = priv->m_aToolItemSensitivities[pItem]; + + gtk_widget_set_sensitive(GTK_WIDGET(pItem), bEdit && state); + } +} + +void +gtv_main_toolbar_set_edit(GtvMainToolbar* toolbar, gboolean bEdit) +{ + GtvMainToolbarPrivate& priv = getPrivate(toolbar); + GtvGtkWrapper<GList> pList(gtk_container_get_children(GTK_CONTAINER(priv->toolbar1)), + [](GList* l) + { + g_list_free(l); + }); + for (GList* l = pList.get(); l != nullptr; l = l->next) + { + if (GTK_IS_TOOL_BUTTON(l->data)) + { + setSensitiveIfEdit(toolbar, GTK_TOOL_ITEM(l->data), bEdit); + } + } + + pList.reset(gtk_container_get_children(GTK_CONTAINER(priv->toolbar2))); + for (GList* l = pList.get(); l != nullptr; l = l->next) + { + if (GTK_IS_TOOL_BUTTON(l->data)) + { + setSensitiveIfEdit(toolbar, GTK_TOOL_ITEM(l->data), bEdit); + } + } +} + +GtkWidget* +gtv_main_toolbar_new() +{ + return GTK_WIDGET(g_object_new(GTV_TYPE_MAIN_TOOLBAR, + "orientation", GTK_ORIENTATION_VERTICAL, + nullptr)); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-main-toolbar.hxx b/libreofficekit/qa/gtktiledviewer/gtv-main-toolbar.hxx new file mode 100644 index 000000000..91827ef92 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-main-toolbar.hxx @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef GTV_MAIN_TOOLBAR_H +#define GTV_MAIN_TOOLBAR_H + +#include <gtk/gtk.h> + +#include <string> + +#include <LibreOfficeKit/LibreOfficeKitEnums.h> + +#define GTV_TYPE_MAIN_TOOLBAR (gtv_main_toolbar_get_type()) +#define GTV_MAIN_TOOLBAR(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GTV_TYPE_MAIN_TOOLBAR, GtvMainToolbar)) +#define GTV_IS_MAIN_TOOLBAR(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTV_TYPE_MAIN_TOOLBAR)) +#define GTV_MAIN_TOOLBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GTV_TYPE_MAIN_TOOLBAR, GtvMainToolbarClass)) +#define GTV_IS_MAIN_TOOLBAR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GTV_TYPE_MAIN_TOOLBAR)) +#define GTV_MAIN_TOOLBAR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), GTV_TYPE_MAIN_TOOLBAR, GtvMainToolbarClass)) + +struct GtvMainToolbar +{ + GtkBox parent; + + GtkWidget* m_pAddressbar; + GtkWidget* m_pFormulabar; + GtkWidget* m_pContentControlSelector; + GtkWidget* m_pContentControlDateSelector; +}; + +struct GtvMainToolbarClass +{ + GtkBoxClass parentClass; +}; + +GType gtv_main_toolbar_get_type (void) G_GNUC_CONST; + +GtkWidget* gtv_main_toolbar_new(); + +GtkContainer* gtv_main_toolbar_get_first_toolbar(GtvMainToolbar* toolbar); + +GtkContainer* gtv_main_toolbar_get_second_toolbar(GtvMainToolbar* toolbar); + +void gtv_main_toolbar_set_sensitive_internal(GtvMainToolbar* toolbar, GtkToolItem* pItem, bool isSensitive); + +/// Use internal sensitivity map to set actual widget's sensitiveness +void gtv_main_toolbar_set_edit(GtvMainToolbar* toolbar, gboolean bEdit); + +void gtv_main_toolbar_doc_loaded(GtvMainToolbar* toolbar, LibreOfficeKitDocumentType eDocType, bool bEditMode); + +void gtv_main_toolbar_add_recent_uno(GtvMainToolbar* toolbar, const std::string& rUnoCmdStr); + +std::string gtv_main_toolbar_get_recent_uno_args(GtvMainToolbar* toolbar, const std::string& rUnoCmd); + +#endif /* GTV_MAIN_TOOLBAR_H */ + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-main.cxx b/libreofficekit/qa/gtktiledviewer/gtv-main.cxx new file mode 100644 index 000000000..a1bf7ed92 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-main.cxx @@ -0,0 +1,19 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <gio/gio.h> + +#include "gtv-application.hxx" + +int main(int argc, char* argv[]) +{ + return g_application_run(G_APPLICATION(gtv_application_new()), argc, argv); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-signal-handlers.cxx b/libreofficekit/qa/gtktiledviewer/gtv-signal-handlers.cxx new file mode 100644 index 000000000..bb0e7edd3 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-signal-handlers.cxx @@ -0,0 +1,809 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <gtk/gtk.h> + +#include "gtv-application-window.hxx" +#include "gtv-helpers.hxx" +#include "gtv-lokdocview-signal-handlers.hxx" +#include "gtv-signal-handlers.hxx" + +#include <sal/macros.h> + +#include <cassert> +#include <map> +#include <vector> + +#include <boost/property_tree/json_parser.hpp> +#include <optional> + +void btn_clicked(GtkWidget* pButton, gpointer) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + GtkToolButton* pItem = GTK_TOOL_BUTTON(pButton); + const gchar* label = gtk_tool_button_get_label(pItem); + if (!(gtv_application_window_get_toolbar_broadcast(window) && g_str_has_prefix(label, ".uno:"))) + return; + + std::string aArguments; + if (g_strcmp0(label, ".uno:InsertAnnotation") == 0) + { + std::map<std::string, std::string> aEntries; + aEntries["Text"] = ""; + GtvHelpers::userPromptDialog(GTK_WINDOW(window), "Insert Comment", aEntries); + + boost::property_tree::ptree aTree; + aTree.put(boost::property_tree::ptree::path_type("Text/type", '/'), "string"); + aTree.put(boost::property_tree::ptree::path_type("Text/value", '/'), aEntries["Text"]); + + std::stringstream aStream; + boost::property_tree::write_json(aStream, aTree); + aArguments = aStream.str(); + } + + bool bNotify = g_strcmp0(label, ".uno:Save") == 0; + if (window->lokdocview) + lok_doc_view_post_command(LOK_DOC_VIEW(window->lokdocview), label, aArguments.c_str(), bNotify); +} + +void doCopy(GtkWidget* pButton, gpointer /*pItem*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + char* pUsedFormat = nullptr; + // TODO: Should check `text-selection` signal before trying to copy + char* pSelection = lok_doc_view_copy_selection(LOK_DOC_VIEW(window->lokdocview), "text/html", &pUsedFormat); + if (!pSelection) + return; + + GtkClipboard* pClipboard = gtk_clipboard_get_for_display(gtk_widget_get_display(pButton), GDK_SELECTION_CLIPBOARD); + std::string aUsedFormat(pUsedFormat); + if (aUsedFormat == "text/plain;charset=utf-8") + gtk_clipboard_set_text(pClipboard, pSelection, -1); + else + GtvHelpers::clipboardSetHtml(pClipboard, pSelection); + + free(pSelection); + free(pUsedFormat); +} + +void doPaste(GtkWidget* pButton, gpointer /*pItem*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + GtkClipboard* pClipboard = gtk_clipboard_get_for_display(gtk_widget_get_display(pButton), GDK_SELECTION_CLIPBOARD); + GdkAtom* pTargets; + gint nTargets; + std::map<std::string, GdkAtom> aTargets; + if (gtk_clipboard_wait_for_targets(pClipboard, &pTargets, &nTargets)) + { + for (gint i = 0; i < nTargets; ++i) + { + gchar* pName = gdk_atom_name(pTargets[i]); + aTargets[pName] = pTargets[i]; + g_free(pName); + } + g_free(pTargets); + } + + std::optional<GdkAtom> oTarget; + std::string aTargetName; + + std::vector<std::string> aPreferredNames = + { + std::string("image/png"), + std::string("text/html") + }; + for (const std::string& rName : aPreferredNames) + { + std::map<std::string, GdkAtom>::iterator it = aTargets.find(rName); + if (it != aTargets.end()) + { + aTargetName = it->first; + oTarget = it->second; + break; + } + } + + if (oTarget) + { + GtkSelectionData* pSelectionData = gtk_clipboard_wait_for_contents(pClipboard, *oTarget); + if (!pSelectionData) + { + return; + } + gint nLength; + const guchar* pData = gtk_selection_data_get_data_with_length(pSelectionData, &nLength); + bool bSuccess = lok_doc_view_paste(LOK_DOC_VIEW(window->lokdocview), aTargetName.c_str(), reinterpret_cast<const char*>(pData), nLength); + gtk_selection_data_free(pSelectionData); + if (bSuccess) + return; + } + + gchar* pText = gtk_clipboard_wait_for_text(pClipboard); + if (pText) + lok_doc_view_paste(LOK_DOC_VIEW(window->lokdocview), "text/plain;charset=utf-8", pText, strlen(pText)); +} + +void createView(GtkWidget* pButton, gpointer /*pItem*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + gtv_application_window_create_view_from_window(GTV_APPLICATION_WINDOW(window)); +} + +void getRulerState(GtkWidget* pButton, gpointer /*pItem*/) +{ + const std::string type = ".uno:RulerState"; + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + pDocument->pClass->getCommandValues(pDocument, type.c_str()); +} + +static void removeUnoParam(GtkWidget* pWidget, gpointer userdata) +{ + GtkWidget* pParamAreaBox = GTK_WIDGET(userdata); + GtkWidget* pParamContainer = gtk_widget_get_parent(pWidget); + + gtk_container_remove(GTK_CONTAINER(pParamAreaBox), pParamContainer); +} + +static void addMoreUnoParam(GtkWidget* /*pWidget*/, gpointer userdata) +{ + GtkWidget* pUnoParamAreaBox = GTK_WIDGET(userdata); + + GtkWidget* pParamContainer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start(GTK_BOX(pUnoParamAreaBox), pParamContainer, true, true, 2); + + GtkWidget* pTypeEntry = gtk_entry_new(); + gtk_box_pack_start(GTK_BOX(pParamContainer), pTypeEntry, true, true, 2); + gtk_entry_set_placeholder_text(GTK_ENTRY(pTypeEntry), "Param type (Eg. boolean, string etc.)"); + + GtkWidget* pNameEntry = gtk_entry_new(); + gtk_box_pack_start(GTK_BOX(pParamContainer), pNameEntry, true, true, 2); + gtk_entry_set_placeholder_text(GTK_ENTRY(pNameEntry), "Param name"); + + GtkWidget* pValueEntry = gtk_entry_new(); + gtk_box_pack_start(GTK_BOX(pParamContainer), pValueEntry, true, true, 2); + gtk_entry_set_placeholder_text(GTK_ENTRY(pValueEntry), "Param value"); + + GtkWidget* pRemoveButton = gtk_button_new_from_icon_name("list-remove-symbolic", GTK_ICON_SIZE_BUTTON); + g_signal_connect(pRemoveButton, "clicked", G_CALLBACK(removeUnoParam), pUnoParamAreaBox); + gtk_box_pack_start(GTK_BOX(pParamContainer), pRemoveButton, true, true, 2); + + gtk_widget_show_all(pUnoParamAreaBox); +} + +static void iterateUnoParams(GtkWidget* pWidget, gpointer userdata) +{ + boost::property_tree::ptree *pTree = static_cast<boost::property_tree::ptree*>(userdata); + GtvGtkWrapper<GList> pChildren(gtk_container_get_children(GTK_CONTAINER(pWidget)), + [](GList* pList) { + g_list_free(pList); + }); + GList* pIt = nullptr; + guint i = 0; + const gchar* unoParam[3]; + for (pIt = pChildren.get(), i = 0; i < 3; pIt = pIt->next, i++) + { + assert(pIt != nullptr); + unoParam[i] = gtk_entry_get_text(GTK_ENTRY(pIt->data)); + } + + gchar* pPath = g_strconcat(unoParam[1], "/", "type", nullptr); + pTree->put(boost::property_tree::ptree::path_type(pPath, '/'), unoParam[0]); + g_free(pPath); + pPath = g_strconcat(unoParam[1], "/", "value", nullptr); + pTree->put(boost::property_tree::ptree::path_type(pPath, '/'), unoParam[2]); + g_free(pPath); +} + +void recentUnoChanged( GtkWidget* pSelector, gpointer /* pItem */ ) +{ + GtvApplicationWindow* pWindow = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pSelector)); + gchar* pUnoCmd = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(pSelector)); + + GtvMainToolbar* pToolbar = gtv_application_window_get_main_toolbar(pWindow); + const std::string aUnoArgs = gtv_main_toolbar_get_recent_uno_args(pToolbar, pUnoCmd); + // this will also discard our default placeholder string, "Recent UNO" + if (aUnoArgs.empty()) + return; + + lok_doc_view_post_command(LOK_DOC_VIEW(pWindow->lokdocview), pUnoCmd, (aUnoArgs.empty() ? nullptr : aUnoArgs.c_str()), false); + g_free(pUnoCmd); +} + +static void addToRecentUnoCommands(GtvApplicationWindow* pWindow, const std::string& rUnoCmd, std::string rArgs) +{ + GtvMainToolbar* pToolbar = gtv_application_window_get_main_toolbar(pWindow); + rArgs.erase(std::find(rArgs.begin(), rArgs.end(), '\n')); + const std::string rUnoCmdStr = rUnoCmd + " | " + rArgs; + + + // add to file + std::ofstream outfile("/tmp/gtv-recentunos.txt", std::ios_base::app | std::ios_base::out); + if (outfile.good()) + outfile << rUnoCmdStr << '\n'; + + // add to combo box + gtv_main_toolbar_add_recent_uno(pToolbar, rUnoCmdStr); +} + +void unoCommandDebugger(GtkWidget* pButton, gpointer /* pItem */) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + GtkWidget* pUnoCmdDialog = gtk_dialog_new_with_buttons ("Execute UNO command", + GTK_WINDOW (window), + GTK_DIALOG_MODAL, + "Execute", + GTK_RESPONSE_OK, + nullptr); + g_object_set(G_OBJECT(pUnoCmdDialog), "resizable", FALSE, nullptr); + GtkWidget* pDialogMessageArea = gtk_dialog_get_content_area (GTK_DIALOG (pUnoCmdDialog)); + GtkWidget* pUnoCmdAreaBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start(GTK_BOX(pDialogMessageArea), pUnoCmdAreaBox, true, true, 2); + + GtkWidget* pUnoCmdLabel = gtk_label_new("Enter UNO command"); + gtk_box_pack_start(GTK_BOX(pUnoCmdAreaBox), pUnoCmdLabel, true, true, 2); + + GtkWidget* pUnoCmdEntry = gtk_entry_new (); + gtk_box_pack_start(GTK_BOX(pUnoCmdAreaBox), pUnoCmdEntry, true, true, 2); + gtk_entry_set_placeholder_text(GTK_ENTRY(pUnoCmdEntry), "UNO command (Eg. Bold, Italic etc.)"); + GtkWidget* pUnoParamAreaBox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + gtk_box_pack_start(GTK_BOX(pDialogMessageArea), pUnoParamAreaBox, true, true, 2); + + GtkWidget* pAddMoreButton = gtk_button_new_with_label("Add UNO parameter"); + gtk_box_pack_start(GTK_BOX(pDialogMessageArea), pAddMoreButton, true, true, 2); + g_signal_connect(G_OBJECT(pAddMoreButton), "clicked", G_CALLBACK(addMoreUnoParam), pUnoParamAreaBox); + + gtk_widget_show_all(pUnoCmdDialog); + + gint res = gtk_dialog_run (GTK_DIALOG(pUnoCmdDialog)); + if (res == GTK_RESPONSE_OK) + { + gchar* sUnoCmd = g_strconcat(".uno:", gtk_entry_get_text(GTK_ENTRY(pUnoCmdEntry)), nullptr); + + boost::property_tree::ptree aTree; + gtk_container_foreach(GTK_CONTAINER(pUnoParamAreaBox), iterateUnoParams, &aTree); + + std::stringstream aStream; + boost::property_tree::write_json(aStream, aTree, false); + std::string aArguments = aStream.str(); + + g_info("Generated UNO command: %s %s", sUnoCmd, aArguments.c_str()); + + lok_doc_view_post_command(LOK_DOC_VIEW(window->lokdocview), sUnoCmd, (aArguments.empty() ? nullptr : aArguments.c_str()), false); + addToRecentUnoCommands(window, sUnoCmd, aArguments); + + g_free(sUnoCmd); + } + + gtk_widget_destroy(pUnoCmdDialog); +} + +void toggleEditing(GtkWidget* pButton, gpointer /*pItem*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + bool bActive = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(pButton)); + if (bool(lok_doc_view_get_edit(LOK_DOC_VIEW(window->lokdocview))) != bActive) + lok_doc_view_set_edit(LOK_DOC_VIEW(window->lokdocview), bActive); +} + +void changePart( GtkWidget* pSelector, gpointer /* pItem */ ) +{ + int nPart = gtk_combo_box_get_active( GTK_COMBO_BOX(pSelector) ); + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pSelector)); + + if (gtv_application_window_get_part_broadcast(window) && window->lokdocview) + { + lok_doc_view_set_part( LOK_DOC_VIEW(window->lokdocview), nPart ); + lok_doc_view_reset_view(LOK_DOC_VIEW(window->lokdocview)); + } +} + +void changePartMode( GtkWidget* pSelector, gpointer /* pItem */ ) +{ + // Just convert directly back to the LibreOfficeKitPartMode enum. + // I.e. the ordering above should match the enum member ordering. + LibreOfficeKitPartMode ePartMode = + LibreOfficeKitPartMode( gtk_combo_box_get_active( GTK_COMBO_BOX(pSelector) ) ); + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pSelector)); + + if ( window->lokdocview ) + { + lok_doc_view_set_partmode( LOK_DOC_VIEW(window->lokdocview), ePartMode ); + } +} + +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 changeDateContentControl(GtkWidget* pSelector, gpointer /*pItem*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pSelector)); + if (gtv_application_window_get_part_broadcast(window) && window->lokdocview) + { + GtkPopover* pPopover = GTK_POPOVER(gtk_widget_get_parent(gtk_widget_get_parent(pSelector))); + guint nYear, nMonth, nDay; + gtk_calendar_get_date(GTK_CALENDAR(pSelector), &nYear, &nMonth, &nDay); + gtk_popover_popdown(pPopover); + + std::stringstream aDate; + aDate << std::setfill('0') << std::setw(4) << nYear; + aDate << "-"; + aDate << std::setfill('0') << std::setw(2) << (nMonth + 1); + aDate << "-"; + aDate << std::setfill('0') << std::setw(2) << nDay; + aDate << "T00:00:00Z"; + boost::property_tree::ptree aValues; + aValues.put("type", "date"); + aValues.put("selected", aDate.str()); + 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 }; + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + const char *sName = gtk_tool_button_get_icon_name( GTK_TOOL_BUTTON(pButton) ); + + float fZoom = 0; + float fCurrentZoom = 0; + + if ( window->lokdocview ) + { + fCurrentZoom = lok_doc_view_get_zoom( LOK_DOC_VIEW(window->lokdocview) ); + } + + if ( strcmp(sName, "zoom-in-symbolic") == 0) + { + for ( size_t i = 0; i < SAL_N_ELEMENTS( fZooms ); i++ ) + { + if ( fCurrentZoom < fZooms[i] ) + { + fZoom = fZooms[i]; + break; + } + } + } + else if ( strcmp(sName, "zoom-original-symbolic") == 0) + { + fZoom = 1; + } + else if ( strcmp(sName, "zoom-out-symbolic") == 0) + { + for ( size_t i = 0; i < SAL_N_ELEMENTS( fZooms ); i++ ) + { + if ( fCurrentZoom > fZooms[i] ) + { + fZoom = fZooms[i]; + } + } + } + + if ( fZoom != 0 && window->lokdocview ) + { + lok_doc_view_set_zoom( LOK_DOC_VIEW(window->lokdocview), fZoom ); + GdkRectangle aVisibleArea; + gtv_application_window_get_visible_area(window, &aVisibleArea); + lok_doc_view_set_visible_area(LOK_DOC_VIEW(window->lokdocview), &aVisibleArea); + } + const std::string aZoom = std::string("Zoom: ") + std::to_string(int(fZoom * 100)) + std::string("%"); + gtk_label_set_text(GTK_LABEL(window->zoomlabel), aZoom.c_str()); +} + +void documentRedline(GtkWidget* pButton, gpointer /*pItem*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + // Get the data. + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + char* pValues = pDocument->pClass->getCommandValues(pDocument, ".uno:AcceptTrackedChanges"); + if (!pValues) + return; + + std::stringstream aInfo; + aInfo << "lok::Document::getCommandValues('.uno:AcceptTrackedChanges') returned '" << pValues << "'" << std::endl; + g_info("%s", aInfo.str().c_str()); + std::stringstream aStream(pValues); + free(pValues); + assert(!aStream.str().empty()); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + + // Create the dialog. + GtkWidget* pDialog = gtk_dialog_new_with_buttons("Manage Changes", + GTK_WINDOW (window), + GTK_DIALOG_MODAL, + "Accept", + GTK_RESPONSE_YES, + "Reject", + GTK_RESPONSE_NO, + "Jump", + GTK_RESPONSE_APPLY, + nullptr); + gtk_window_set_default_size(GTK_WINDOW(pDialog), 800, 600); + GtkWidget* pContentArea = gtk_dialog_get_content_area(GTK_DIALOG (pDialog)); + GtkWidget* pScrolledWindow = gtk_scrolled_window_new(nullptr, nullptr); + + // Build the table. + GtkTreeStore* pTreeStore = gtk_tree_store_new(6, G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); + for (const auto& rValue : aTree.get_child("redlines")) + { + GtkTreeIter aTreeIter; + gtk_tree_store_append(pTreeStore, &aTreeIter, nullptr); + gtk_tree_store_set(pTreeStore, &aTreeIter, + 0, rValue.second.get<int>("index"), + 1, rValue.second.get<std::string>("author").c_str(), + 2, rValue.second.get<std::string>("type").c_str(), + 3, rValue.second.get<std::string>("comment").c_str(), + 4, rValue.second.get<std::string>("description").c_str(), + 5, rValue.second.get<std::string>("dateTime").c_str(), + -1); + } + GtkWidget* pTreeView = gtk_tree_view_new_with_model(GTK_TREE_MODEL(pTreeStore)); + std::vector<std::string> aColumns = {"Index", "Author", "Type", "Comment", "Description", "Timestamp"}; + for (size_t nColumn = 0; nColumn < aColumns.size(); ++nColumn) + { + GtkCellRenderer* pRenderer = gtk_cell_renderer_text_new(); + GtkTreeViewColumn* pColumn = gtk_tree_view_column_new_with_attributes(aColumns[nColumn].c_str(), + pRenderer, + "text", nColumn, + nullptr); + gtk_tree_view_append_column(GTK_TREE_VIEW(pTreeView), pColumn); + } + gtk_container_add(GTK_CONTAINER(pScrolledWindow), pTreeView); + gtk_box_pack_start(GTK_BOX(pContentArea), pScrolledWindow, true, true, 2); + + // Show the dialog. + gtk_widget_show_all(pDialog); + gint res = gtk_dialog_run(GTK_DIALOG(pDialog)); + + // Dispatch the matching command, if necessary. + if (res == GTK_RESPONSE_YES || res == GTK_RESPONSE_NO || res == GTK_RESPONSE_APPLY) + { + GtkTreeSelection* pSelection = gtk_tree_view_get_selection(GTK_TREE_VIEW(pTreeView)); + GtkTreeIter aTreeIter; + GtkTreeModel* pTreeModel; + if (gtk_tree_selection_get_selected(pSelection, &pTreeModel, &aTreeIter)) + { + gint nIndex = 0; + // 0: index + gtk_tree_model_get(pTreeModel, &aTreeIter, 0, &nIndex, -1); + std::string aCommand; + if (res == GTK_RESPONSE_YES) + aCommand = ".uno:AcceptTrackedChange"; + else if (res == GTK_RESPONSE_NO) + aCommand = ".uno:RejectTrackedChange"; + else + // Just select the given redline, don't accept or reject it. + aCommand = ".uno:NextTrackedChange"; + // Without the '.uno:' prefix. + std::string aKey = aCommand.substr(strlen(".uno:")); + + // Post the command. + boost::property_tree::ptree aCommandTree; + aCommandTree.put(boost::property_tree::ptree::path_type(aKey + "/type", '/'), "unsigned short"); + aCommandTree.put(boost::property_tree::ptree::path_type(aKey + "/value", '/'), nIndex); + + aStream.str(std::string()); + boost::property_tree::write_json(aStream, aCommandTree); + std::string aArguments = aStream.str(); + lok_doc_view_post_command(LOK_DOC_VIEW(window->lokdocview), aCommand.c_str(), aArguments.c_str(), false); + } + } + + gtk_widget_destroy(pDialog); +} + +void documentRepair(GtkWidget* pButton, gpointer /*pItem*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + // Get the data. + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + // Show it in linear time, so first redo in reverse order, then undo. + std::vector<std::string> aTypes = {".uno:Redo", ".uno:Undo"}; + std::vector<boost::property_tree::ptree> aTrees; + for (size_t nType = 0; nType < aTypes.size(); ++nType) + { + const std::string& rType = aTypes[nType]; + char* pValues = pDocument->pClass->getCommandValues(pDocument, rType.c_str()); + std::stringstream aInfo; + aInfo << "lok::Document::getCommandValues('" << rType << "') returned '" << pValues << "'" << std::endl; + g_info("%s", aInfo.str().c_str()); + std::stringstream aStream(pValues); + free(pValues); + assert(!aStream.str().empty()); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + aTrees.push_back(aTree); + } + + // Create the dialog. + GtkWidget* pDialog = gtk_dialog_new_with_buttons("Repair document", + GTK_WINDOW (window), + GTK_DIALOG_MODAL, + "Jump to state", + GTK_RESPONSE_OK, + nullptr); + gtk_window_set_default_size(GTK_WINDOW(pDialog), 800, 600); + GtkWidget* pContentArea = gtk_dialog_get_content_area(GTK_DIALOG (pDialog)); + GtkWidget* pScrolledWindow = gtk_scrolled_window_new(nullptr, nullptr); + + // Build the table. + GtkTreeStore* pTreeStore = gtk_tree_store_new(5, G_TYPE_STRING, G_TYPE_INT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); + for (size_t nTree = 0; nTree < aTrees.size(); ++nTree) + { + const auto& rTree = aTrees[nTree]; + for (const auto& rValue : rTree.get_child("actions")) + { + GtkTreeIter aTreeIter; + gtk_tree_store_append(pTreeStore, &aTreeIter, nullptr); + gtk_tree_store_set(pTreeStore, &aTreeIter, + 0, aTypes[nTree].c_str(), + 1, rValue.second.get<int>("index"), + 2, rValue.second.get<std::string>("comment").c_str(), + 3, rValue.second.get<std::string>("viewId").c_str(), + 4, rValue.second.get<std::string>("dateTime").c_str(), + -1); + } + } + GtkWidget* pTreeView = gtk_tree_view_new_with_model(GTK_TREE_MODEL(pTreeStore)); + std::vector<std::string> aColumns = {"Type", "Index", "Comment", "View ID", "Timestamp"}; + for (size_t nColumn = 0; nColumn < aColumns.size(); ++nColumn) + { + GtkCellRenderer* pRenderer = gtk_cell_renderer_text_new(); + GtkTreeViewColumn* pColumn = gtk_tree_view_column_new_with_attributes(aColumns[nColumn].c_str(), + pRenderer, + "text", nColumn, + nullptr); + gtk_tree_view_append_column(GTK_TREE_VIEW(pTreeView), pColumn); + } + gtk_container_add(GTK_CONTAINER(pScrolledWindow), pTreeView); + gtk_box_pack_start(GTK_BOX(pContentArea), pScrolledWindow, true, true, 2); + + // Show the dialog. + gtk_widget_show_all(pDialog); + gint res = gtk_dialog_run(GTK_DIALOG(pDialog)); + + // Dispatch the matching command, if necessary. + if (res == GTK_RESPONSE_OK) + { + GtkTreeSelection* pSelection = gtk_tree_view_get_selection(GTK_TREE_VIEW(pTreeView)); + GtkTreeIter aTreeIter; + GtkTreeModel* pTreeModel; + if (gtk_tree_selection_get_selected(pSelection, &pTreeModel, &aTreeIter)) + { + gchar* pType = nullptr; + gint nIndex = 0; + // 0: type, 1: index + gtk_tree_model_get(pTreeModel, &aTreeIter, 0, &pType, 1, &nIndex, -1); + // '.uno:Undo' or '.uno:Redo' + const std::string aType(pType); + // Without the '.uno:' prefix. + std::string aKey = aType.substr(strlen(".uno:")); + g_free(pType); + + // Post the command. + boost::property_tree::ptree aTree; + aTree.put(boost::property_tree::ptree::path_type(aKey + "/type", '/'), "unsigned short"); + aTree.put(boost::property_tree::ptree::path_type(aKey + "/value", '/'), nIndex + 1); + + // Without this, we could only undo our own commands. + aTree.put(boost::property_tree::ptree::path_type("Repair/type", '/'), "boolean"); + aTree.put(boost::property_tree::ptree::path_type("Repair/value", '/'), true); + + std::stringstream aStream; + boost::property_tree::write_json(aStream, aTree); + std::string aArguments = aStream.str(); + lok_doc_view_post_command(LOK_DOC_VIEW(window->lokdocview), aType.c_str(), aArguments.c_str(), false); + } + } + + gtk_widget_destroy(pDialog); +} + +void toggleFindbar(GtkWidget* pButton, gpointer /*pItem*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + gtv_application_window_toggle_findbar(window); +} + +void docAdjustmentChanged(GtkAdjustment*, gpointer pData) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(pData); + if (window->lokdocview) + LOKDocViewSigHandlers::configureEvent(window->lokdocview, nullptr, nullptr); +} + +void signalSearchNext(GtkWidget* pButton, gpointer /*pItem*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + GtkEntry* pEntry = GTK_ENTRY(window->findbarEntry); + const char* pText = gtk_entry_get_text(pEntry); + bool findAll = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(window->findAll)); + lok_doc_view_find_next(LOK_DOC_VIEW(window->lokdocview), pText, findAll); +} + +void signalSearchPrev(GtkWidget* pButton, gpointer /*pItem*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + GtkEntry* pEntry = GTK_ENTRY(window->findbarEntry); + const char* pText = gtk_entry_get_text(pEntry); + bool findAll = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(window->findAll)); + lok_doc_view_find_prev(LOK_DOC_VIEW(window->lokdocview), pText, findAll); +} + +gboolean signalFindbar(GtkWidget* pWidget, GdkEventKey* pEvent, gpointer /*pData*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pWidget)); + gtk_label_set_text(GTK_LABEL(window->findbarlabel), ""); + switch(pEvent->keyval) + { + case GDK_KEY_Return: + { + // Search forward. + signalSearchNext(pWidget, nullptr); + return true; + } + case GDK_KEY_Escape: + { + // Hide the findbar. + gtk_widget_hide(GTK_WIDGET(window->findtoolbar)); + return true; + } + } + return FALSE; +} + +void toggleFindAll(GtkWidget* pButton, gpointer /*pItem*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pButton)); + GtkEntry* pEntry = GTK_ENTRY(window->findbarEntry); + const char* pText = gtk_entry_get_text(pEntry); + bool findAll = gtk_toggle_tool_button_get_active(GTK_TOGGLE_TOOL_BUTTON(window->findAll)); + gtk_toggle_tool_button_set_active(GTK_TOGGLE_TOOL_BUTTON(window->findAll), !findAll); + lok_doc_view_highlight_all(LOK_DOC_VIEW(window->lokdocview), pText); +} + +void editButtonClicked(GtkWidget* pWidget, gpointer userdata) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pWidget)); + std::map<std::string, std::string> aEntries; + aEntries["Text"] = ""; + + GtvHelpers::userPromptDialog(GTK_WINDOW(window), "Edit comment", aEntries); + + gchar *commentId = static_cast<gchar*>(g_object_get_data(G_OBJECT(userdata), "id")); + + boost::property_tree::ptree aTree; + aTree.put(boost::property_tree::ptree::path_type("Id/type", '/'), "string"); + aTree.put(boost::property_tree::ptree::path_type("Id/value", '/'), std::string(commentId)); + + aTree.put(boost::property_tree::ptree::path_type("Text/type", '/'), "string"); + aTree.put(boost::property_tree::ptree::path_type("Text/value", '/'), aEntries["Text"]); + + std::stringstream aStream; + boost::property_tree::write_json(aStream, aTree); + std::string aArguments = aStream.str(); + + lok_doc_view_post_command(LOK_DOC_VIEW(window->lokdocview), ".uno:EditAnnotation", aArguments.c_str(), false); +} + +void replyButtonClicked(GtkWidget* pWidget, gpointer userdata) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pWidget)); + std::map<std::string, std::string> aEntries; + aEntries["Text"] = ""; + + GtvHelpers::userPromptDialog(GTK_WINDOW(window), "Reply comment", aEntries); + + gchar *commentId = static_cast<gchar*>(g_object_get_data(G_OBJECT(userdata), "id")); + + boost::property_tree::ptree aTree; + aTree.put(boost::property_tree::ptree::path_type("Id/type", '/'), "string"); + aTree.put(boost::property_tree::ptree::path_type("Id/value", '/'), std::string(commentId)); + + aTree.put(boost::property_tree::ptree::path_type("Text/type", '/'), "string"); + aTree.put(boost::property_tree::ptree::path_type("Text/value", '/'), aEntries["Text"]); + + std::stringstream aStream; + boost::property_tree::write_json(aStream, aTree); + std::string aArguments = aStream.str(); + + // Different reply UNO command for impress + std::string replyCommand = ".uno:ReplyComment"; + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + if (pDocument && pDocument->pClass->getDocumentType(pDocument) == LOK_DOCTYPE_PRESENTATION) + replyCommand = ".uno:ReplyToAnnotation"; + lok_doc_view_post_command(LOK_DOC_VIEW(window->lokdocview), replyCommand.c_str(), aArguments.c_str(), false); +} + +void deleteCommentButtonClicked(GtkWidget* pWidget, gpointer userdata) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pWidget)); + gchar *commentid = static_cast<gchar*>(g_object_get_data(G_OBJECT(userdata), "id")); + + boost::property_tree::ptree aTree; + aTree.put(boost::property_tree::ptree::path_type("Id/type", '/'), "string"); + aTree.put(boost::property_tree::ptree::path_type("Id/value", '/'), std::string(commentid)); + + std::stringstream aStream; + boost::property_tree::write_json(aStream, aTree); + std::string aArguments = aStream.str(); + + // Different reply UNO command for impress + std::string deleteCommand = ".uno:DeleteComment"; + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(window->lokdocview)); + if (pDocument) + { + if (pDocument->pClass->getDocumentType(pDocument) == LOK_DOCTYPE_PRESENTATION) + deleteCommand = ".uno:DeleteAnnotation"; + else if (pDocument->pClass->getDocumentType(pDocument) == LOK_DOCTYPE_SPREADSHEET) + deleteCommand = ".uno:DeleteNote"; + } + + lok_doc_view_post_command(LOK_DOC_VIEW(window->lokdocview), deleteCommand.c_str(), aArguments.c_str(), false); +} + +/// Handles the key-press-event of the address entry widget. +gboolean signalAddressbar(GtkWidget* pWidget, GdkEventKey* pEvent, gpointer /*pData*/) +{ + GtvApplicationWindow* window = GTV_APPLICATION_WINDOW(gtk_widget_get_toplevel(pWidget)); + switch(pEvent->keyval) + { + case GDK_KEY_Return: + { + GtkEntry* pEntry = GTK_ENTRY(pWidget); + const char* pText = gtk_entry_get_text(pEntry); + + boost::property_tree::ptree aTree; + aTree.put(boost::property_tree::ptree::path_type("ToPoint/type", '/'), "string"); + aTree.put(boost::property_tree::ptree::path_type("ToPoint/value", '/'), pText); + std::stringstream aStream; + boost::property_tree::write_json(aStream, aTree); + std::string aArguments = aStream.str(); + + lok_doc_view_post_command(LOK_DOC_VIEW(window->lokdocview), ".uno:GoToCell", aArguments.c_str(), false); + gtk_widget_grab_focus(window->lokdocview); + return true; + } + case GDK_KEY_Escape: + { + std::string aArguments; + lok_doc_view_post_command(LOK_DOC_VIEW(window->lokdocview), ".uno:Cancel", aArguments.c_str(), false); + gtk_widget_grab_focus(window->lokdocview); + return true; + } + } + return FALSE; +} + +/// Handles the key-press-event of the formula entry widget. +gboolean signalFormulabar(GtkWidget* /*pWidget*/, GdkEventKey* /*pEvent*/, gpointer /*pData*/) +{ + // for now it just displays the callback + // TODO - submit the edited formula + return true; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv-signal-handlers.hxx b/libreofficekit/qa/gtktiledviewer/gtv-signal-handlers.hxx new file mode 100644 index 000000000..c06017d87 --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv-signal-handlers.hxx @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef GTV_SIGNAL_HANDLERS_H +#define GTV_SIGNAL_HANDLERS_H + +#include <gtk/gtk.h> + +void btn_clicked(GtkWidget* pWidget, gpointer); + +void doCopy(GtkWidget* pButton, gpointer /*pItem*/); + +void doPaste(GtkWidget* pButton, gpointer /*pItem*/); + +void createView(GtkWidget* pButton, gpointer /*pItem*/); + +void getRulerState(GtkWidget* pButton, gpointer /*pItem*/); + +void recentUnoChanged(GtkWidget* pSelector, gpointer /* pItem */); + +void unoCommandDebugger(GtkWidget* pButton, gpointer /* pItem */); + +void toggleEditing(GtkWidget* pButton, gpointer /*pItem*/); + +void changePartMode(GtkWidget* pSelector, gpointer /* pItem */); + +void changePart(GtkWidget* pSelector, gpointer /*pItem*/); + +void openLokDialog(GtkWidget* pSelector, gpointer /*pItem*/); + +void changeZoom(GtkWidget* pButton, gpointer /* pItem */); + +void toggleFindbar(GtkWidget* pButton, gpointer /*pItem*/); + +void documentRedline(GtkWidget* pButton, gpointer /*pItem*/); + +void documentRepair(GtkWidget* pButton, gpointer /*pItem*/); + +void docAdjustmentChanged(GtkAdjustment*, gpointer); + +/// Click handler for the search next button. +void signalSearchNext(GtkWidget* pButton, gpointer /*pItem*/); + +/// Click handler for the search previous button. +void signalSearchPrev(GtkWidget* pButton, gpointer /*pItem*/); + +/// Handles the key-press-event of the search entry widget. +gboolean signalFindbar(GtkWidget* pWidget, GdkEventKey* pEvent, gpointer /*pData*/); + +void toggleFindAll(GtkWidget* pButton, gpointer /*pItem*/); + +void editButtonClicked(GtkWidget*, gpointer); + +void replyButtonClicked(GtkWidget*, gpointer); + +void deleteCommentButtonClicked(GtkWidget*, gpointer); + +/// Handles the key-press-event of the address bar entry widget. +gboolean signalAddressbar(GtkWidget* pWidget, GdkEventKey* pEvent, gpointer /*pData*/); + +/// Handles the key-press-event of the formula entry widget. +gboolean signalFormulabar(GtkWidget* /*pWidget*/, GdkEventKey* /*pEvent*/, gpointer /*pData*/); + +void changeContentControl(GtkWidget* pSelector, gpointer /*pItem*/); + +void changeDateContentControl(GtkWidget* pSelector, gpointer /*pItem*/); +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/gtktiledviewer/gtv.ui b/libreofficekit/qa/gtktiledviewer/gtv.ui new file mode 100644 index 000000000..79cb9a9ec --- /dev/null +++ b/libreofficekit/qa/gtktiledviewer/gtv.ui @@ -0,0 +1,827 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.20.0 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <object class="GtkBox" id="container"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <placeholder/> + </child> + <child> + <!-- n-columns=1 n-rows=1 --> + <object class="GtkGrid" id="maingrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkBox" id="scrolledwindowcontainer"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkScrolledWindow" id="scrolledwindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="hexpand">True</property> + <property name="vexpand">True</property> + <property name="shadow_type">in</property> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + <child> + <placeholder/> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkStatusbar" id="statusbar"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">10</property> + <property name="margin_start">10</property> + <property name="margin_end">10</property> + <property name="margin_bottom">6</property> + <property name="spacing">2</property> + <child> + <object class="GtkLabel" id="zoomlabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">22</property> + <property name="label" translatable="yes">100%</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel" id="redlinelabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">22</property> + <property name="label" translatable="yes">Current redline: </property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="pack_type">end</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkToolbar" id="findtoolbar"> + <property name="can_focus">True</property> + <property name="toolbar_style">both-horiz</property> + <child> + <object class="GtkToolButton" id="findbar_close"> + <property name="visible">True</property> + <property name="label" translatable="yes">__glade_unnamed_1</property> + <property name="use_underline">True</property> + <property name="icon_name">window-close-symbolic</property> + <signal name="clicked" handler="toggleFindbar" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolItem" id="findbar_entrytoolitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkEntry" id="findbar_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="truncate-multiline">True</property> + <signal name="key-press-event" handler="signalFindbar" swapped="no"/> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">False</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="findbar_next"> + <property name="visible">True</property> + <property name="label" translatable="yes">__glade_unnamed_3</property> + <property name="use_underline">True</property> + <property name="icon_name">go-down-symbolic</property> + <signal name="clicked" handler="signalSearchNext" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="findbar_prev"> + <property name="visible">True</property> + <property name="label" translatable="yes">toolbutton</property> + <property name="use_underline">True</property> + <property name="icon_name">go-up-symbolic</property> + <signal name="clicked" handler="signalSearchPrev" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="findbar_findall"> + <property name="visible">True</property> + <property name="label" translatable="yes">Highlight all</property> + <property name="use_underline">True</property> + <signal name="clicked" handler="toggleFindAll" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolItem" id="findbar_labeltoolitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel" id="findbar_label"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Search not found</property> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + <object class="GtkToolbar" id="toolbar1"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="toolbar_style">icons</property> + <child> + <object class="GtkToolButton" id="btn_save"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:Save</property> + <property name="icon_name">document-save-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separator1"> + <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> + <child> + <object class="GtkToolButton" id="btn_copy"> + <property name="visible">True</property> + <property name="label" translatable="yes">Copy</property> + <property name="use_underline">True</property> + <property name="icon_name">edit-copy-symbolic</property> + <signal name="clicked" handler="doCopy" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="btn_paste"> + <property name="visible">True</property> + <property name="label" translatable="yes">Paste</property> + <property name="use_underline">True</property> + <property name="icon_name">edit-paste-symbolic</property> + <signal name="clicked" handler="doPaste" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separator2"> + <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> + <child> + <object class="GtkToolButton" id="btn_undo"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:Undo</property> + <property name="use_underline">True</property> + <property name="icon_name">edit-undo-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="btn_redo"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:Redo</property> + <property name="use_underline">True</property> + <property name="icon_name">edit-redo-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="btn_docrepair"> + <property name="visible">True</property> + <property name="label" translatable="yes">Document Repair</property> + <property name="use_underline">True</property> + <property name="icon_name">document-properties</property> + <signal name="clicked" handler="documentRepair" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="btn_docredlines"> + <property name="visible">True</property> + <property name="label" translatable="yes">Document redlines</property> + <property name="use_underline">True</property> + <property name="icon_name">system-run</property> + <signal name="clicked" handler="documentRedline" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separator3"> + <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> + <child> + <object class="GtkToggleToolButton" id="btn_find"> + <property name="visible">True</property> + <property name="label" translatable="yes">Find</property> + <property name="use_underline">True</property> + <property name="icon_name">edit-find-symbolic</property> + <signal name="clicked" handler="toggleFindbar" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separator4"> + <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> + <child> + <object class="GtkToolButton" id="btn_zoomin"> + <property name="visible">True</property> + <property name="label" translatable="yes">Zoom In</property> + <property name="use_underline">True</property> + <property name="icon_name">zoom-in-symbolic</property> + <signal name="clicked" handler="changeZoom" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="btn_zoomoriginal"> + <property name="visible">True</property> + <property name="label" translatable="yes">Zoom Original</property> + <property name="use_underline">True</property> + <property name="icon_name">zoom-original-symbolic</property> + <signal name="clicked" handler="changeZoom" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="btn_zoomout"> + <property name="visible">True</property> + <property name="label" translatable="yes">Zoom out</property> + <property name="use_underline">True</property> + <property name="icon_name">zoom-out-symbolic</property> + <signal name="clicked" handler="changeZoom" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolItem" id="partselectortoolitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkComboBoxText" id="combo_partselector"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <signal name="changed" handler="changePart" swapped="no"/> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolItem" id="partmodeselectortoolitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkComboBoxText" id="combo_partsmodeselector"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="active">0</property> + <items> + <item translatable="yes">Standard</item> + <item translatable="yes">Notes</item> + </items> + <signal name="changed" handler="changePartMode" swapped="no"/> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <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="GtkToolItem" id="contentcontroldateselectortoolitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkMenuButton" id="menu_contentcontroldateselector"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text">Content control date</property> + <property name="popover">calendar</property> + <child> + <placeholder/> + </child> + </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> + <property name="label" translatable="yes">Turn on/off edit mode</property> + <property name="use_underline">True</property> + <property name="icon_name">insert-text-symbolic</property> + <signal name="clicked" handler="toggleEditing" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separator5"> + <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> + <child> + <object class="GtkToolItem" id="recentunoselectortoolitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkComboBoxText" id="combo_recentunoselector"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="active">0</property> + <items> + <item translatable="no">Select UNO</item> + </items> + <signal name="changed" handler="recentUnoChanged" swapped="no"/> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="btn_unodebugger"> + <property name="visible">True</property> + <property name="label" translatable="yes">Uno Command Debugger</property> + <property name="use_underline">True</property> + <property name="icon_name">dialog-question-symbolic</property> + <signal name="clicked" handler="unoCommandDebugger" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separator6"> + <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> + <child> + <object class="GtkToolButton" id="btn_createview"> + <property name="visible">True</property> + <property name="label" translatable="yes">Create new view</property> + <property name="use_underline">True</property> + <property name="icon_name">view-continuous-symbolic</property> + <signal name="clicked" handler="createView" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="btn_rulerstate"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:UpdateRuler</property> + <property name="use_underline">True</property> + <property name="icon_name">text-x-generic</property> + <signal name="clicked" handler="getRulerState" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + </object> + <object class="GtkToolbar" id="toolbar2"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="toolbar_style">icons</property> + <child> + <object class="GtkToggleToolButton" id="btn_bold"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:Bold</property> + <property name="use_underline">True</property> + <property name="icon_name">format-text-bold-symbolic</property> + <signal name="toggled" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="btn_italic"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:Italic</property> + <property name="use_underline">True</property> + <property name="icon_name">format-text-italic-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="btn_underline"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:Underline</property> + <property name="use_underline">True</property> + <property name="icon_name">format-text-underline-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="btn_strikethrough"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:Strikeout</property> + <property name="use_underline">True</property> + <property name="icon_name">format-text-strikethrough-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separator7"> + <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> + <child> + <object class="GtkToggleToolButton" id="btn_superscript"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:SuperScript</property> + <property name="use_underline">True</property> + <property name="icon_name">go-up-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="btn_subscript"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:SubScript</property> + <property name="use_underline">True</property> + <property name="icon_name">go-down-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separator8"> + <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> + <child> + <object class="GtkToggleToolButton" id="btn_justifyleft"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:LeftPara</property> + <property name="use_underline">True</property> + <property name="icon_name">format-justify-left-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="btn_justifycenter"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:CenterPara</property> + <property name="use_underline">True</property> + <property name="icon_name">format-justify-center-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="btn_justifyright"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:RightPara</property> + <property name="use_underline">True</property> + <property name="icon_name">format-justify-right-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="btn_justifyfill"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:JustifyPara</property> + <property name="use_underline">True</property> + <property name="icon_name">format-justify-fill-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkSeparatorToolItem" id="separator9"> + <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> + <child> + <object class="GtkToolButton" id="btn_insertannotation"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:InsertAnnotation</property> + <property name="use_underline">True</property> + <property name="icon_name">changes-allow-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolButton" id="btn_removeannotation"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:DeleteComment</property> + <property name="use_underline">True</property> + <property name="icon_name">changes-prevent-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToggleToolButton" id="btn_trackchanges"> + <property name="visible">True</property> + <property name="label" translatable="yes">.uno:TrackChanges</property> + <property name="use_underline">True</property> + <property name="icon_name">media-record-symbolic</property> + <signal name="clicked" handler="btn_clicked" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolItem" id="addressbar_toolitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkEntry" id="addressbar_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="truncate-multiline">True</property> + <signal name="key-press-event" handler="signalAddressbar" swapped="no"/> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + <child> + <object class="GtkToolItem" id="formulabar_toolitem"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkEntry" id="formulabar_entry"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="truncate-multiline">True</property> + <signal name="key-press-event" handler="signalFormulabar" swapped="no"/> + </object> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="homogeneous">True</property> + </packing> + </child> + </object> + <object class="GtkPopover" id="calendar"> + <property name="can-focus">False</property> + <property name="position">bottom</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <property name="spacing">6</property> + <child> + <object class="GtkCalendar" id="date"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <signal name="day-selected-double-click" handler="changeDateContentControl" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/libreofficekit/qa/tilebench/tilebench.cxx b/libreofficekit/qa/tilebench/tilebench.cxx new file mode 100644 index 000000000..6b6dcf405 --- /dev/null +++ b/libreofficekit/qa/tilebench/tilebench.cxx @@ -0,0 +1,685 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <stdio.h> +#include <string.h> +#include <cmath> + +#include <vector> +#include <atomic> +#include <iostream> +#include <osl/time.h> + +#include <LibreOfficeKit/LibreOfficeKitEnums.h> +#include <LibreOfficeKit/LibreOfficeKitInit.h> +#include <LibreOfficeKit/LibreOfficeKit.hxx> + +#ifdef IOS +#include <vcl/svapp.hxx> +#endif + +#include <boost/property_tree/json_parser.hpp> + +using namespace lok; + +static int help( const char *error = nullptr ) +{ + if (error) + fprintf (stderr, "Error: %s\n\n", error); + fprintf( stderr, "Usage: tilebench <absolute-path-to-libreoffice-install> [path to document] [--preinit] <options>\n"); + fprintf( stderr, "\trenders a selection of small tiles from the document, checksums them and times the process based on options:\n" ); + fprintf( stderr, "\t--tile\t[max parts|-1] [max tiles|-1]\n" ); + fprintf( stderr, "\t--dialog\t<.uno:Command>\n" ); + fprintf( stderr, "\t--join\trun tile joining tests\n" ); + return 1; +} + +static double getTimeNow() +{ + TimeValue aValue; + osl_getSystemTime(&aValue); + return static_cast<double>(aValue.Seconds) + + static_cast<double>(aValue.Nanosec) / (1000*1000*1000); +} + +static double origin; + +namespace { + +struct TimeRecord { + const char *mpName; + double mfTime; + + TimeRecord() : mpName(nullptr), mfTime(getTimeNow()) { } + explicit TimeRecord(const char *pName) : + mpName(pName), mfTime(getTimeNow()) + { + fprintf(stderr, "%3.3fs - %s\n", (mfTime - origin), mpName); + } +}; + +} + +static std::vector< TimeRecord > aTimes; + +/// Dump an array (or sub-array) of RGBA or BGRA to an RGB PPM file. +static void dumpTile(const char *pNameStem, + const int nWidth, const int nHeight, + const int mode, const unsigned char* pBufferU, + const int nOffX = 0, const int nOffY = 0, + int nTotalWidth = -1) +{ + if (nTotalWidth < 0) + nTotalWidth = nWidth; + + auto pBuffer = reinterpret_cast<const char *>(pBufferU); + static int counter = 0; + std::string aName = "/tmp/dump_tile"; + aName += pNameStem; + aName += "_" + std::to_string(counter); + aName += ".ppm"; +#ifndef IOS + std::ofstream ofs(aName); +#else + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDirectory = [paths objectAtIndex:0]; + NSString *path = [NSString stringWithFormat:@"%@/dump_tile_%d.ppm", documentsDirectory, counter]; + std::ofstream ofs([path UTF8String]); + std::cerr << "---> Dumping tile\n"; +#endif + counter++; + ofs << "P6\n" + << nWidth << " " + << nHeight << "\n" + << 255 << "\n" ; + + const bool dumpText = false; + + if (dumpText) + fprintf(stderr, "Stream %s - %dx%d:\n", pNameStem, nWidth, nHeight); + + for (int y = 0; y < nHeight; ++y) + { + const char* row = pBuffer + (y + nOffY) * nTotalWidth * 4 + nOffX * 4; + for (int x = 0; x < nWidth; ++x) + { + const char* pixel = row + x * 4; + if (mode == LOK_TILEMODE_RGBA) + { + ofs.write(pixel, 3); // Skip alpha + } + else if (mode == LOK_TILEMODE_BGRA) + { + const int alpha = *(pixel + 3); + char buf[3]; + if (alpha == 0) + { + buf[0] = 0; + buf[1] = 0; + buf[2] = 0; + } + else + { + buf[0] = (*(pixel + 2) * 255 + alpha / 2) / alpha; + buf[1] = (*(pixel + 1) * 255 + alpha / 2) / alpha; + buf[2] = (*(pixel + 0) * 255 + alpha / 2) / alpha; + } + + ofs.write(buf, 3); + } + if (dumpText) + { + int lowResI = (pixel[0] + pixel[1] + pixel[2])/(3*16); + fprintf(stderr,"%1x", lowResI); + } + } + if (dumpText) + fprintf(stderr,"\n"); + } + ofs.close(); +} + +static void testTile( Document *pDocument, int max_parts, + int max_tiles, bool dump ) +{ + const int mode = pDocument->getTileMode(); + + aTimes.emplace_back("getparts"); + const int nOriginalPart = (pDocument->getDocumentType() == LOK_DOCTYPE_TEXT ? 1 : pDocument->getPart()); + // Writer really has 1 part (the full doc). + const int nTotalParts = (pDocument->getDocumentType() == LOK_DOCTYPE_TEXT ? 1 : pDocument->getParts()); + const int nParts = (max_parts < 0 ? nTotalParts : std::min(max_parts, nTotalParts)); + aTimes.emplace_back(); + + aTimes.emplace_back("get size of parts"); + long nWidth = 0; + long nHeight = 0; + for (int n = 0; n < nParts; ++n) + { + const int nPart = (nOriginalPart + n) % nTotalParts; + char* pName = pDocument->getPartName(nPart); + pDocument->setPart(nPart); + pDocument->getDocumentSize(&nWidth, &nHeight); + fprintf (stderr, " '%s' -> %ld, %ld\n", pName, nWidth, nHeight); + free (pName); + } + aTimes.emplace_back(); + + // Use realistic dimensions, similar to the Online client. + long const nTilePixelWidth = 512; + long const nTilePixelHeight = 512; + long const nTileTwipWidth = 3840; + long const nTileTwipHeight = 3840; + + // Estimate the maximum tiles based on the number of parts requested, if Writer. + if (pDocument->getDocumentType() == LOK_DOCTYPE_TEXT) + max_tiles = static_cast<int>(ceil(max_parts * 16128. / nTilePixelHeight) * ceil(static_cast<double>(nWidth) / nTilePixelWidth)); + fprintf(stderr, "Parts to render: %d, Total Parts: %d, Max parts: %d, Max tiles: %d\n", nParts, nTotalParts, max_parts, max_tiles); + + std::vector<unsigned char> vBuffer(nTilePixelWidth * nTilePixelHeight * 4); + unsigned char* pPixels = vBuffer.data(); + + for (int n = 0; n < nParts; ++n) + { + const int nPart = (nOriginalPart + n) % nTotalParts; + char* pName = pDocument->getPartName(nPart); + pDocument->setPart(nPart); + pDocument->getDocumentSize(&nWidth, &nHeight); + fprintf (stderr, "render '%s' -> %ld, %ld\n", pName, nWidth, nHeight); + free (pName); + + if (dump || pDocument->getDocumentType() != LOK_DOCTYPE_TEXT) + { + // whole part; meaningful only for non-writer documents. + aTimes.emplace_back("render whole part"); + pDocument->paintTile(pPixels, nTilePixelWidth, nTilePixelHeight, + nWidth/2, 2000, 1000, 1000); + aTimes.emplace_back(); + if (dump) + dumpTile("tile", nTilePixelWidth, nTilePixelHeight, mode, pPixels); + } + + { // 1:1 + aTimes.emplace_back("render sub-region at 1:1"); + // Estimate the maximum tiles based on the number of parts requested, if Writer. + int nMaxTiles = max_tiles; + int nTiles = 0; + for (long nY = 0; nY < nHeight - 1; nY += nTilePixelHeight) + { + for (long nX = 0; nX < nWidth - 1; nX += nTilePixelWidth) + { + if (nMaxTiles >= 0 && nTiles >= nMaxTiles) + { + nY = nHeight; + break; + } + pDocument->paintTile(pPixels, nTilePixelWidth, nTilePixelHeight, + nX, nY, nTilePixelWidth, nTilePixelHeight); + nTiles++; + fprintf (stderr, " rendered 1:1 tile %d at %ld, %ld\n", + nTiles, nX, nY); + } + } + aTimes.emplace_back(); + } + + { // scaled + aTimes.emplace_back("render sub-regions at scale"); + int nMaxTiles = max_tiles; + if (pDocument->getDocumentType() == LOK_DOCTYPE_TEXT) + nMaxTiles = static_cast<int>(ceil(max_parts * 16128. / nTileTwipHeight) * ceil(static_cast<double>(nWidth) / nTileTwipWidth)); + int nTiles = 0; + for (long nY = 0; nY < nHeight - 1; nY += nTileTwipHeight) + { + for (long nX = 0; nX < nWidth - 1; nX += nTileTwipWidth) + { + if (nMaxTiles >= 0 && nTiles >= nMaxTiles) + { + nY = nHeight; + break; + } + pDocument->paintTile(pPixels, nTilePixelWidth, nTilePixelHeight, + nX, nY, nTileTwipWidth, nTileTwipHeight); + nTiles++; + fprintf (stderr, " rendered scaled tile %d at %ld, %ld\n", + nTiles, nX, nY); + } + } + aTimes.emplace_back(); + } + } +} + +static uint32_t fade(uint32_t col) +{ + uint8_t a = (col >> 24) & 0xff; + uint8_t b = (col >> 16) & 0xff; + uint8_t g = (col >> 8) & 0xff; + uint8_t r = (col >> 0) & 0xff; + uint8_t grey = (r+g+b)/6; + return (a<<24) + (grey<<16) + (grey<<8) + grey; +} + +static bool sloppyEqual(uint32_t pixA, uint32_t pixB) +{ + uint8_t a[4], b[4]; + + a[0] = (pixA >> 24) & 0xff; + a[1] = (pixA >> 16) & 0xff; + a[2] = (pixA >> 8) & 0xff; + a[3] = (pixA >> 0) & 0xff; + + b[0] = (pixB >> 24) & 0xff; + b[1] = (pixB >> 16) & 0xff; + b[2] = (pixB >> 8) & 0xff; + b[3] = (pixB >> 0) & 0xff; + + for (int i = 0; i < 4; ++i) + { + int delta = a[i]; + delta -= b[i]; + // tolerate small differences + if (delta < -4 || delta > 4) + return false; + } + return true; +} + +// Count and build a picture of any differences into rDiff +static int diffTiles( const std::vector<unsigned char> &vBase, + long nBaseRowPixelWidth, + const std::vector<unsigned char> &vCompare, + long nCompareRowPixelWidth, + long nTilePixelHeight, + long nPosX, long nPosY, + std::vector<unsigned char> &rDiff ) +{ + int nDifferent = 0; + const uint32_t *pBase = reinterpret_cast<const uint32_t *>(vBase.data()); + const uint32_t *pCompare = reinterpret_cast<const uint32_t *>(vCompare.data()); + uint32_t *pDiff = reinterpret_cast<uint32_t *>(rDiff.data()); + long left = 0, mid = nCompareRowPixelWidth, right = nCompareRowPixelWidth*2; + for (long y = 0; y < nTilePixelHeight; ++y) + { + long nBaseOffset = nBaseRowPixelWidth * (y + nPosY) + nPosX * nCompareRowPixelWidth; + long nCompareOffset = nCompareRowPixelWidth * y; + long nDiffRowStart = nCompareOffset * 3; + for (long x = 0; x < nCompareRowPixelWidth; ++x) + { + pDiff[nDiffRowStart + left + x] = pBase[nBaseOffset + x]; + pDiff[nDiffRowStart + mid + x] = pCompare[nCompareOffset + x]; + pDiff[nDiffRowStart + right + x] = fade(pBase[nBaseOffset + x]); + if (!sloppyEqual(pBase[nBaseOffset + x], pCompare[nCompareOffset + x])) + { + pDiff[nDiffRowStart + right + x] = 0xffff00ff; + if (!nDifferent) + fprintf (stderr, "First mismatching pixel at %ld (pixels) into row %ld\n", x, y); + nDifferent++; + } + } + } + return nDifferent; +} + +static std::vector<unsigned char> paintTile( Document *pDocument, + long nX, long nY, + long const nTilePixelWidth, + long const nTilePixelHeight, + long const nTileTwipWidth, + long const nTileTwipHeight ) +{ +// long e = 0; // tweak if we suspect an overlap / visibility issue. +// pDocument->setClientVisibleArea( nX - e, nY - e, nTileTwipWidth + e, nTileTwipHeight + e ); + std::vector<unsigned char> vData( nTilePixelWidth * nTilePixelHeight * 4 ); + pDocument->paintTile( vData.data(), nTilePixelWidth, nTilePixelHeight, + nX, nY, nTileTwipWidth, nTileTwipHeight ); + return vData; +} + +static int testJoinsAt( Document *pDocument, long nX, long nY, + long const nTilePixelSize, + long const nTileTwipSize ) +{ + const int mode = pDocument->getTileMode(); + + long const nTilePixelWidth = nTilePixelSize; + long const nTilePixelHeight = nTilePixelSize; + long const nTileTwipWidth = nTileTwipSize; + long const nTileTwipHeight = nTileTwipSize; + + long initPosX = nX * nTileTwipWidth, initPosY = nY * nTileTwipHeight; + + // Calc has to do significant work on changing zoom ... + pDocument->setClientZoom( nTilePixelWidth, nTilePixelHeight, + nTileTwipWidth, nTileTwipHeight ); + + // Unfortunately without getting this nothing renders ... + std::stringstream aForceHeaders; + aForceHeaders << ".uno:ViewRowColumnHeaders?x=" << initPosX << "&y=" << initPosY << + "&width=" << (nTileTwipWidth * 2) << "&height=" << (nTileTwipHeight * 2); + std::string cmd = aForceHeaders.str(); + char* pJSON = pDocument->getCommandValues(cmd.c_str()); + fprintf(stderr, "command: '%s' values '%s'\n", cmd.c_str(), pJSON); + free(pJSON); + + // Get a base image 4x the size + std::vector<unsigned char> vBase( + paintTile(pDocument, initPosX, initPosY, + nTilePixelWidth * 2, nTilePixelHeight * 2, + nTileTwipWidth * 2, nTileTwipHeight * 2)); + + const struct { + long X; + long Y; + } aCompare[] = { + { 0, 0 }, + { 1, 0 }, + { 0, 1 }, + { 1, 1 } + }; + + int nDifferences = 0; + // Compare each of the 4x tiles with a sub-tile of the larger image + for( auto &rPos : aCompare ) + { + std::vector<unsigned char> vCompare( + paintTile(pDocument, + initPosX + rPos.X * nTileTwipWidth, + initPosY + rPos.Y * nTileTwipHeight, + nTilePixelWidth, nTilePixelHeight, + nTileTwipWidth, nTileTwipHeight)); + + std::vector<unsigned char> vDiff( nTilePixelWidth * 3 * nTilePixelHeight * 4 ); + int nDiffs = diffTiles( vBase, nTilePixelWidth * 2, + vCompare, nTilePixelWidth, + nTilePixelHeight, + rPos.X, rPos.Y * nTilePixelHeight, + vDiff ); + if ( nDiffs > 0 ) + { + fprintf( stderr, " %d differences in sub-tile pixel mismatch at %ld, %ld at offset %ld, %ld (twips) size %ld\n", + nDiffs, rPos.X, rPos.Y, initPosX, initPosY, + nTileTwipWidth); + dumpTile("_base", nTilePixelWidth * 2, nTilePixelHeight * 2, + mode, vBase.data()); +/* dumpTile("_sub", nTilePixelWidth, nTilePixelHeight, + mode, vBase.data(), + rPos.X*nTilePixelWidth, rPos.Y*nTilePixelHeight, + nTilePixelWidth * 2); + dumpTile("_compare", nTilePixelWidth, nTilePixelHeight, + mode, vCompare.data());*/ + dumpTile("_diff", nTilePixelWidth * 3, nTilePixelHeight, mode, vDiff.data()); + } + nDifferences += nDiffs; + } + + return nDifferences; +} + +// Check that our tiles join nicely ... +static int testJoin( Document *pDocument) +{ + // Ignore parts - just the first for now ... + long nWidth = 0, nHeight = 0; + pDocument->getDocumentSize(&nWidth, &nHeight); + fprintf (stderr, "Width is %ld, %ld (twips)\n", nWidth, nHeight); + + // Use realistic dimensions, similar to the Online client. + long const nTilePixelSize = 256; + long const nTileTwipSize = 3840; + double fZooms[] = { + 0.5, + 0.6, 0.7, 0.85, + 1.0, + 1.2, 1.5, 1.75, + 2.0 + }; + long nFails = 0; + std::stringstream results; + + for( auto z : fZooms ) + { + long nBad = 0; + long nDifferences = 0; + for( long y = 0; y < 8; ++y ) + { + for( long x = 0; x < 8; ++x ) + { + int nDiffs = testJoinsAt( pDocument, x, y, nTilePixelSize, nTileTwipSize * z ); + if (nDiffs) + nBad++; + nDifferences += nDiffs; + } + } + if (nBad > 0) + results << "\tZoom " << z << " bad tiles: " << nBad << " with " << nDifferences << " mismatching pixels\n"; + nFails += nBad; + } + + if (nFails > 0) + fprintf( stderr, "Failed %ld joins\n", nFails ); + else + fprintf( stderr, "All joins compared correctly\n" ); + + fprintf(stderr, "%s\n", results.str().c_str()); + + return nFails; +} + +static std::atomic<bool> bDialogRendered(false); +static std::atomic<int> nDialogId(-1); + +static void kitCallback(int nType, const char* pPayload, void* pData) +{ + Document *pDocument = static_cast<Document *>(pData); + + if (nType != LOK_CALLBACK_WINDOW) + return; + + std::stringstream aStream(pPayload); + boost::property_tree::ptree aRoot; + boost::property_tree::read_json(aStream, aRoot); + nDialogId = aRoot.get<unsigned>("id"); + const std::string aAction = aRoot.get<std::string>("action"); + + if (aAction != "created") + return; + + const std::string aType = aRoot.get<std::string>("type"); + const std::string aSize = aRoot.get<std::string>("size"); + int nWidth = atoi(aSize.c_str()); + int nHeight = 400; + const char *pComma = strstr(aSize.c_str(), ", "); + if (pComma) + nHeight = atoi(pComma + 2); + std::cerr << "Size " << aSize << " is " << nWidth << ", " << nHeight << "\n"; + + if (aType != "dialog") + return; + + aTimes.emplace_back(); // complete wait for dialog + + unsigned char *pBuffer = new unsigned char[nWidth * nHeight * 4]; + + aTimes.emplace_back("render dialog"); + pDocument->paintWindow(nDialogId, pBuffer, 0, 0, nWidth, nHeight); + dumpTile("dialog", nWidth, nHeight, pDocument->getTileMode(), pBuffer); + aTimes.emplace_back(); + + delete[] pBuffer; + + bDialogRendered = true; +} + +static void testDialog( Document *pDocument, const char *uno_cmd ) +{ + int view = pDocument->createView(); + pDocument->setView(view); + pDocument->registerCallback(kitCallback, pDocument); + + aTimes.emplace_back("open dialog"); + pDocument->postUnoCommand(uno_cmd, nullptr, true); + aTimes.emplace_back(); + + aTimes.emplace_back("wait for dialog"); + while (!bDialogRendered) + { + usleep (1000); + } + + aTimes.emplace_back("post close dialog"); + pDocument->postWindow(nDialogId, LOK_WINDOW_CLOSE); + aTimes.emplace_back(); + + pDocument->destroyView(view); +} + +static void documentCallback(const int type, const char* p, void*) +{ + std::cerr << "Document callback " << type << ": " << (p ? p : "(null)") << "\n"; +} + +// Avoid excessive dbgutil churn. +static void ignoreCallback(const int /*type*/, const char* /*p*/, void* /*data*/) +{ +} + +int main( int argc, char* argv[] ) +{ + int arg = 2; + origin = getTimeNow(); + +#ifndef IOS + // avoid X oddness etc. + unsetenv("DISPLAY"); + + if( argc < 4 || + ( argc > 1 && ( !strcmp( argv[1], "--help" ) || !strcmp( argv[1], "-h" ) ) ) ) + return help(); + + if ( argv[1][0] != '/' ) + { + fprintf(stderr, "Absolute path required to libreoffice install\n"); + return 1; + } + + const char *doc_url = argv[arg++]; + const char *mode = argv[arg++]; + + bool pre_init = false; + if (!strcmp(mode, "--preinit")) + { + pre_init = true; + mode = argv[arg++]; + } + + std::string user_url("file:///"); + user_url.append(argv[1]); + user_url.append("../user"); + + if (pre_init) + { + aTimes.emplace_back("pre-initialization"); + setenv("LOK_ALLOWLIST_LANGUAGES", "en_US", 0); + // coverity[tainted_string] - build time test tool + lok_preinit(argv[1], user_url.c_str()); + aTimes.emplace_back(); + } + const char *install_path = argv[1]; + const char *user_profile = user_url.c_str(); +#else + const char *install_path = nullptr; + const char *user_profile = nullptr; + const char *doc_url = strdup([[[[[NSBundle mainBundle] bundleURL] absoluteString] stringByAppendingString:@"/test.odt"] UTF8String]); + const char *mode = "--tile"; +#endif + + aTimes.emplace_back("initialization"); + // coverity[tainted_string] - build time test tool + std::unique_ptr<Office> pOffice( lok_cpp_init(install_path, user_profile) ); + if (pOffice == nullptr) + { + fprintf(stderr, "Failed to initialize Office from %s\n", argv[1]); + return 1; + } + aTimes.emplace_back(); + pOffice->registerCallback(ignoreCallback, nullptr); + + std::unique_ptr<Document> pDocument; + + pOffice->setOptionalFeatures(LOK_FEATURE_NO_TILED_ANNOTATIONS); + + aTimes.emplace_back("load document"); + if (doc_url != nullptr) + pDocument.reset(pOffice->documentLoad(doc_url)); + aTimes.emplace_back(); + + if (pDocument) + { + pDocument->initializeForRendering("{\".uno:Author\":{\"type\":\"string\",\"value\":\"Local Host #0\"}}"); + pDocument->registerCallback(documentCallback, nullptr); + if (!strcmp(mode, "--tile")) + { + const int max_parts = (argc > arg ? atoi(argv[arg++]) : -1); + int max_tiles = (argc > arg ? atoi(argv[arg++]) : -1); + const bool dump = true; + + // coverity[tainted_data] - we trust the contents of this variable + testTile (pDocument.get(), max_parts, max_tiles, dump); + } + else if (!strcmp(mode, "--join")) + { + return testJoin (pDocument.get()); + } + else if (!strcmp (mode, "--dialog")) + { + const char *uno_cmd = argc > arg ? argv[arg++] : nullptr; + if (!uno_cmd) + { + switch (pDocument->getDocumentType()) + { + case LOK_DOCTYPE_SPREADSHEET: + uno_cmd = ".uno:FormatCellDialog"; + break; + case LOK_DOCTYPE_TEXT: + case LOK_DOCTYPE_PRESENTATION: + case LOK_DOCTYPE_DRAWING: + case LOK_DOCTYPE_OTHER: + return help("missing argument to --dialog and no default"); + } + } + testDialog (pDocument.get(), uno_cmd); + } else + return help ("unknown parameter"); + } + +#ifdef IOS + Application::Quit(); +#endif + aTimes.emplace_back("destroy document"); + pDocument.reset(); + aTimes.emplace_back(); + + pOffice.reset(); + + double nTotal = 0.0; + fprintf (stderr, "profile run:\n"); + for (size_t i = 0; i < aTimes.size() - 1; i++) + { + const double nDelta = aTimes[i+1].mfTime - aTimes[i].mfTime; + fprintf (stderr, " %s - %2.4f(ms)\n", aTimes[i].mpName, nDelta * 1000.0); + if (aTimes[i+1].mpName == nullptr) + i++; // skip it. + nTotal += nDelta; + } + fprintf (stderr, "Total: %2.4f(s)\n", nTotal); + return 0; +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/unit/checkapi.cxx b/libreofficekit/qa/unit/checkapi.cxx new file mode 100644 index 000000000..ec4a71836 --- /dev/null +++ b/libreofficekit/qa/unit/checkapi.cxx @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#if defined LIBO_INTERNAL_ONLY +#error Build system problem; LIBO_INTERNAL_ONLY should not be defined here +#endif + +#include <sal/config.h> +#include <sal/types.h> + +#include <cppunit/plugin/TestPlugIn.h> + +CPPUNIT_PLUGIN_IMPLEMENT(); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/unit/compile_test.c b/libreofficekit/qa/unit/compile_test.c new file mode 100644 index 000000000..650718efa --- /dev/null +++ b/libreofficekit/qa/unit/compile_test.c @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#define LOK_USE_UNSTABLE_API +#include <LibreOfficeKit/LibreOfficeKit.h> +#include <LibreOfficeKit/LibreOfficeKitInit.h> + +// fake usage for loplugin:unreffun plugin +#include "test.h" + +// just make sure this stuff compiles from a plain C file +LibreOfficeKit* compile_test(void) { return lok_init("install/path"); } + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/unit/test.h b/libreofficekit/qa/unit/test.h new file mode 100644 index 000000000..f29db13bb --- /dev/null +++ b/libreofficekit/qa/unit/test.h @@ -0,0 +1,19 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef INCLUDED_LIBREOFFICEKIT_QA_TEST_H +#define INCLUDED_LIBREOFFICEKIT_QA_TEST_H + +#include <LibreOfficeKit/LibreOfficeKit.h> + +LibreOfficeKit* compile_test(void); + +#endif // INCLUDED_LIBREOFFICEKIT_QA_TEST_H + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/qa/unit/tiledrendering.cxx b/libreofficekit/qa/unit/tiledrendering.cxx new file mode 100644 index 000000000..56d789b61 --- /dev/null +++ b/libreofficekit/qa/unit/tiledrendering.cxx @@ -0,0 +1,456 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <memory> +#include <thread> +#include <boost/property_tree/json_parser.hpp> +#include <cppunit/TestFixture.h> +#include <cppunit/plugin/TestPlugIn.h> +#include <cppunit/extensions/HelperMacros.h> +#include <cstdlib> +#include <string> +#include <stdio.h> + +#include <osl/file.hxx> +#include <rtl/bootstrap.hxx> + +#include <com/sun/star/awt/Key.hpp> + +#if defined __clang__ && defined __linux__ +#include <cxxabi.h> +#include <config_options.h> +#if defined _LIBCPPABI_VERSION || !ENABLE_RUNTIME_OPTIMIZATIONS +#define LOK_LOADLIB_GLOBAL +#endif +#endif + +#include <LibreOfficeKit/LibreOfficeKitInit.h> +#include <LibreOfficeKit/LibreOfficeKit.hxx> +#include <LibreOfficeKit/LibreOfficeKitEnums.h> + +using namespace ::boost; +using namespace ::lok; +using namespace ::std; + +namespace { + +void processEventsToIdle() +{ + typedef void (ProcessEventsToIdleFn)(void); + static ProcessEventsToIdleFn *processFn = nullptr; + if (!processFn) + { + void *me = dlopen(nullptr, RTLD_NOW); + processFn = reinterpret_cast<ProcessEventsToIdleFn *>(dlsym(me, "unit_lok_process_events_to_idle")); + } + + CPPUNIT_ASSERT(processFn); + + (*processFn)(); +} + +void insertString(Document& rDocument, const std::string& s) +{ + for (const char c : s) + { + rDocument.postKeyEvent(LOK_KEYEVENT_KEYINPUT, c, 0); + rDocument.postKeyEvent(LOK_KEYEVENT_KEYUP, c, 0); + processEventsToIdle(); + } +} + +} + +static OUString getFileURLFromSystemPath(OUString const & path) +{ + OUString url; + osl::FileBase::RC e = osl::FileBase::getFileURLFromSystemPath(path, url); + CPPUNIT_ASSERT_EQUAL(osl::FileBase::E_None, e); + if (!url.endsWith("/")) + url += "/"; + return url; +} + +// We specifically don't use the usual BootStrapFixture, as LOK does +// all its own setup and bootstrapping, and should be usable in a +// raw C++ program. +class TiledRenderingTest : public ::CppUnit::TestFixture +{ +public: + const string m_sSrcRoot; + const string m_sInstDir; + const string m_sLOPath; + + std::unique_ptr<Document> loadDocument( Office *pOffice, const string &pName, + const char *pFilterOptions = nullptr ); + + TiledRenderingTest() + : m_sSrcRoot( getenv( "SRC_ROOT" ) ) + , m_sInstDir( getenv( "INSTDIR" ) ) + , m_sLOPath( m_sInstDir + "/program" ) + { + } + + // Currently it isn't possible to do multiple startup/shutdown + // cycle of LOK in a single process -- hence we run all our tests + // as one test, which simply carries out the individual test + // components on the one Office instance that we retrieve. + void runAllTests(); + + void testDocumentLoadFail( Office* pOffice ); + void testDocumentTypes( Office* pOffice ); + void testImpressSlideNames( Office* pOffice ); + void testCalcSheetNames( Office* pOffice ); + void testPaintPartTile( Office* pOffice ); + void testDocumentLoadLanguage(Office* pOffice); + void testMultiKeyInput(Office *pOffice); +#if 0 + void testOverlay( Office* pOffice ); +#endif + + CPPUNIT_TEST_SUITE(TiledRenderingTest); + CPPUNIT_TEST(runAllTests); + CPPUNIT_TEST_SUITE_END(); +}; + +void TiledRenderingTest::runAllTests() +{ + // set UserInstallation to user profile dir in test/user-template + const char* pWorkdirRoot = getenv("WORKDIR_FOR_BUILD"); + OUString aWorkdirRootPath = OUString::createFromAscii(pWorkdirRoot); + OUString aWorkdirRootURL = getFileURLFromSystemPath(aWorkdirRootPath); + OUString sUserInstallURL = aWorkdirRootURL + "/unittest"; + rtl::Bootstrap::set("UserInstallation", sUserInstallURL); + + std::unique_ptr< Office > pOffice( lok_cpp_init( + m_sLOPath.c_str() ) ); + CPPUNIT_ASSERT( pOffice ); + + testDocumentLoadFail( pOffice.get() ); + testDocumentTypes( pOffice.get() ); + testMultiKeyInput(pOffice.get()); + testImpressSlideNames( pOffice.get() ); + testCalcSheetNames( pOffice.get() ); + testPaintPartTile( pOffice.get() ); + testDocumentLoadLanguage(pOffice.get()); +#if 0 + testOverlay( pOffice.get() ); +#endif +} + +void TiledRenderingTest::testDocumentLoadFail( Office* pOffice ) +{ + const string sDocPath = m_sSrcRoot + "/libreofficekit/qa/data/IDONOTEXIST.odt"; + std::unique_ptr< Document> pDocument( pOffice->documentLoad( sDocPath.c_str() ) ); + CPPUNIT_ASSERT( !pDocument ); + // TODO: we probably want to have some way of returning what + // the cause of failure was. getError() will return + // something along the lines of: + // "Unsupported URL <file:///SRC_ROOT/libreofficekit/qa/data/IDONOTEXIST.odt>: "type detection failed"" +} + +// Our dumped .png files end up in +// workdir/CppunitTest/libreofficekit_tiledrendering.test.core + +static int getDocumentType( Office* pOffice, const string& rPath ) +{ + std::unique_ptr< Document> pDocument( pOffice->documentLoad( rPath.c_str() ) ); + CPPUNIT_ASSERT( pDocument ); + return pDocument->getDocumentType(); +} + +std::unique_ptr<Document> TiledRenderingTest::loadDocument( Office *pOffice, const string &pName, + const char *pFilterOptions ) +{ + const string sDocPath = m_sSrcRoot + "/libreofficekit/qa/data/" + pName; + const string sLockFile = m_sSrcRoot +"/libreofficekit/qa/data/.~lock." + pName + "#"; + + remove( sLockFile.c_str() ); + + return std::unique_ptr<Document>(pOffice->documentLoad( sDocPath.c_str(), pFilterOptions )); +} + +void TiledRenderingTest::testDocumentTypes( Office* pOffice ) +{ + std::unique_ptr<Document> pDocument(loadDocument(pOffice, "blank_text.odt")); + + CPPUNIT_ASSERT(pDocument); + CPPUNIT_ASSERT_EQUAL(LOK_DOCTYPE_TEXT, static_cast<LibreOfficeKitDocumentType>(pDocument->getDocumentType())); + // This crashed. + pDocument->postUnoCommand(".uno:Bold"); + processEventsToIdle(); + + const string sPresentationDocPath = m_sSrcRoot + "/libreofficekit/qa/data/blank_presentation.odp"; + const string sPresentationLockFile = m_sSrcRoot +"/libreofficekit/qa/data/.~lock.blank_presentation.odp#"; + + // FIXME: same comment as below wrt lockfile removal. + remove( sPresentationLockFile.c_str() ); + + CPPUNIT_ASSERT_EQUAL(LOK_DOCTYPE_PRESENTATION, static_cast<LibreOfficeKitDocumentType>(getDocumentType(pOffice, sPresentationDocPath))); + + // TODO: do this for all supported document types +} + +void TiledRenderingTest::testImpressSlideNames( Office* pOffice ) +{ + std::unique_ptr<Document> pDocument(loadDocument(pOffice, "impress_slidenames.odp")); + + CPPUNIT_ASSERT_EQUAL(3, pDocument->getParts()); + CPPUNIT_ASSERT_EQUAL(std::string("TestText1"), std::string(pDocument->getPartName(0))); + CPPUNIT_ASSERT_EQUAL(std::string("TestText2"), std::string(pDocument->getPartName(1))); + // The third slide hasn't had a name given to it (i.e. using the rename + // context menu in Impress), thus it should (as far as I can determine) + // have a localised version of "Slide 3". +} + +void TiledRenderingTest::testCalcSheetNames( Office* pOffice ) +{ + std::unique_ptr<Document> pDocument(loadDocument(pOffice, "calc_sheetnames.ods")); + + CPPUNIT_ASSERT_EQUAL(3, pDocument->getParts()); + CPPUNIT_ASSERT_EQUAL(std::string("TestText1"), std::string(pDocument->getPartName(0))); + CPPUNIT_ASSERT_EQUAL(std::string("TestText2"), std::string(pDocument->getPartName(1))); + CPPUNIT_ASSERT_EQUAL(std::string("Sheet3"), std::string(pDocument->getPartName(2))); +} + +void TiledRenderingTest::testPaintPartTile(Office* pOffice) +{ + std::unique_ptr<Document> pDocument(loadDocument(pOffice, "blank_text.odt")); + + CPPUNIT_ASSERT(pDocument); + CPPUNIT_ASSERT_EQUAL(LOK_DOCTYPE_TEXT, static_cast<LibreOfficeKitDocumentType>(pDocument->getDocumentType())); + + // Create two views. + pDocument->getView(); + pDocument->createView(); + + int nView2 = pDocument->getView(); + + // Destroy the current view + pDocument->destroyView(nView2); + + int nCanvasWidth = 256; + int nCanvasHeight = 256; + std::vector<unsigned char> aBuffer(nCanvasWidth * nCanvasHeight * 4); + + // And try to paintPartTile() - this used to crash when the current viewId + // was destroyed + pDocument->paintPartTile(aBuffer.data(), /*nPart=*/0, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, /*nTilePosY=*/0, /*nTileWidth=*/3840, /*nTileHeight=*/3840); +} + +void TiledRenderingTest::testDocumentLoadLanguage(Office* pOffice) +{ + std::unique_ptr<Document> pDocument(loadDocument(pOffice, "blank_text.odt", "Language=en-US")); + + // assert that '.' is the decimal separator + insertString(*pDocument, "1.5"); + + pDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, css::awt::Key::RIGHT); + pDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, css::awt::Key::RIGHT); + processEventsToIdle(); + + insertString(*pDocument, "=2*A1"); + + pDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, css::awt::Key::RETURN); + pDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, css::awt::Key::RETURN); + processEventsToIdle(); + pDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, css::awt::Key::UP); + pDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, css::awt::Key::UP); + processEventsToIdle(); + +#if 0 + // FIXME disabled, as occasionally fails + // we've got a meaningful result + OString aResult = pDocument->getTextSelection("text/plain;charset=utf-8"); + CPPUNIT_ASSERT_EQUAL(OString("3\n"), aResult); + + pDocument.reset(); + + // FIXME: LOK will fail when trying to open a locked file + remove(sLockFile.c_str()); + + // load the file again, now in another language + pDocument.reset(pOffice->documentLoad(sDocPath.c_str(), "Language=cs-CZ")); + + // with cs-CZ, the decimal separator is ',' instead, assert that + insertString(*pDocument, "1,5"); + + pDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, css::awt::Key::RIGHT); + pDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, css::awt::Key::RIGHT); + processEventsToIdle(); + + insertString(*pDocument, "=2*A1"); + + pDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, css::awt::Key::RETURN); + pDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, css::awt::Key::RETURN); + processEventsToIdle(); + pDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, css::awt::Key::UP); + pDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, css::awt::Key::UP); + processEventsToIdle(); + + // we've got a meaningful result + aResult = pDocument->getTextSelection("text/plain;charset=utf-8"); + CPPUNIT_ASSERT_EQUAL(OString("3\n"), aResult); +#endif +} + +#if 0 +static void dumpRGBABitmap( const OUString& rPath, const unsigned char* pBuffer, + const int nWidth, const int nHeight ) +{ + Bitmap aBitmap( Size( nWidth, nHeight ), 32 ); + BitmapScopedWriteAccess pWriteAccess( aBitmap ); + memcpy( pWriteAccess->GetBuffer(), pBuffer, 4*nWidth*nHeight ); + + BitmapEx aBitmapEx( aBitmap ); + vcl::PNGWriter aWriter( aBitmapEx ); + SvFileStream sOutput( rPath, StreamMode::WRITE ); + aWriter.Write( sOutput ); + sOutput.Close(); +} + +void TiledRenderingTest::testOverlay( Office* /*pOffice*/ ) +{ + const string sDocPath = m_sSrcRoot + "/odk/examples/java/DocumentHandling/test/test1.odt"; + const string sLockFile = m_sSrcRoot + "/odk/examples/java/DocumentHandling/test/.~lock.test1.odt#"; + + // FIXME: this is a temporary hack: LOK will fail when trying to open a + // locked file, and since we're reusing the file for a different unit + // test it's entirely possible that an unwanted lock file will remain. + // Hence forcefully remove it here. + remove( sLockFile.c_str() ); + std::unique_ptr< Office > pOffice( lok_cpp_init( + m_sLOPath.c_str() ) ); + assert( pOffice.get() ); + + std::unique_ptr< Document> pDocument( pOffice->documentLoad( + sDocPath.c_str() ) ); + + if ( !pDocument.get() ) + { + fprintf( stderr, "documentLoad failed: %s\n", pOffice->getError() ); + CPPUNIT_FAIL( "Document could not be loaded -- tiled rendering not possible." ); + } + + // We render one large tile, then subdivide it into 4 and render those parts, and finally + // iterate over each smaller tile and check whether their contents match the large + // tile. + const int nTotalWidthPix = 512; + const int nTotalHeightPix = 512; + int nRowStride; + + long nTotalWidthDoc; + long nTotalHeightDoc; + // pDocument->getDocumentSize( &nTotalWidthDoc, &nTotalHeightDoc ); + // TODO: make sure we select an actually interesting part of the document + // for this comparison, i.e. ideally an image and lots of text, in order + // to test as many edge cases as possible. + // Alternatively we could rewrite this to actually grab the document size + // and iterate over it (subdividing into an arbitrary number of tiles rather + // than our less sophisticated test of just 4 sub-tiles). + nTotalWidthDoc = 8000; + nTotalHeightDoc = 9000; + + std::unique_ptr< unsigned char []> pLarge( new unsigned char[ 4*nTotalWidthPix*nTotalHeightPix ] ); + pDocument->paintTile( pLarge.get(), nTotalWidthPix, nTotalHeightPix, &nRowStride, + 0, 0, + nTotalWidthDoc, nTotalHeightDoc ); + dumpRGBABitmap( "large.png", pLarge.get(), nTotalWidthPix, nTotalHeightPix ); + + std::unique_ptr< unsigned char []> pSmall[4]; + for ( int i = 0; i < 4; i++ ) + { + pSmall[i].reset( new unsigned char[ 4*(nTotalWidthPix/2)*(nTotalHeightPix/2) ] ); + pDocument->paintTile( pSmall[i].get(), nTotalWidthPix / 2, nTotalHeightPix / 2, &nRowStride, + // Tile 0/2: left. Tile 1/3: right. Tile 0/1: top. Tile 2/3: bottom + ((i%2 == 0) ? 0 : nTotalWidthDoc / 2), ((i < 2 ) ? 0 : nTotalHeightDoc / 2), + nTotalWidthDoc / 2, nTotalHeightDoc / 2); + dumpRGBABitmap( "small_" + OUString::number(i) + ".png", + pSmall[i].get(), nTotalWidthPix/2, nTotalHeightPix/2 ); + } + + // Iterate over each pixel of the sub-tile, and compare that pixel for every + // tile with the equivalent super-tile pixel. + for ( int i = 0; i < 4*nTotalWidthPix / 2 * nTotalHeightPix / 2; i++ ) + { + int xSmall = i % (4*nTotalWidthPix/2); + int ySmall = i / (4*nTotalWidthPix/2); + // Iterate over our array of tiles + // However for now we only bother with the top-left + // tile as the other ones don't match yet... + for ( int x = 0; x < 2; x++ ) + { + for ( int y = 0; y < 2; y++ ) + { + int xLarge = (x * (4 * nTotalWidthPix / 2)) + xSmall; + int yLarge = (y * (nTotalHeightPix / 2)) + ySmall; + CPPUNIT_ASSERT( pSmall[2*y+x][i] == pLarge[yLarge*4*nTotalWidthPix + xLarge] ); + } + } + } +} +#endif + +void TiledRenderingTest::testMultiKeyInput(Office *pOffice) +{ + std::unique_ptr<Document> pDocument(loadDocument(pOffice, "blank_text.odt")); + + CPPUNIT_ASSERT(pDocument); + CPPUNIT_ASSERT_EQUAL(LOK_DOCTYPE_TEXT, static_cast<LibreOfficeKitDocumentType>(pDocument->getDocumentType())); + + // Create two views. + int nViewA = pDocument->getView(); + pDocument->initializeForRendering("{\".uno:Author\":{\"type\":\"string\",\"value\":\"jill\"}}"); + + pDocument->createView(); + int nViewB = pDocument->getView(); + pDocument->initializeForRendering("{\".uno:Author\":{\"type\":\"string\",\"value\":\"jack\"}}"); + + // Enable change tracking + pDocument->postUnoCommand(".uno:TrackChanges"); + + // First a key-stroke from a + pDocument->setView(nViewA); + pDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 97, 0); // a + pDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, 512); // 'a + + // A space on 'a' - force commit + pDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 32, 0); // ' ' + pDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, 1284); // '' ' + + // Another 'a' + pDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 97, 0); // a + pDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, 512); // 'a + + // FIXME: Wait for writer input handler to commit that. + // without this we fall foul of edtwin's KeyInputFlushTimer + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + // Quickly a new key-stroke from b + pDocument->setView(nViewB); + pDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 98, 0); // b + pDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, 514); // 'b + + // A space on 'b' - force commit + pDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 32, 0); // ' ' + pDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, 1284); // '' ' + + // Wait for writer input handler to commit that. + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + // get track changes ? + char *values = pDocument->getCommandValues(".uno:AcceptTrackedChanges"); + std::cerr << "Values: '" << values << "'\n"; +} + +CPPUNIT_TEST_SUITE_REGISTRATION(TiledRenderingTest); + +CPPUNIT_PLUGIN_IMPLEMENT(); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/source/gtk/lokdocview.cxx b/libreofficekit/source/gtk/lokdocview.cxx new file mode 100644 index 000000000..39f2281b0 --- /dev/null +++ b/libreofficekit/source/gtk/lokdocview.cxx @@ -0,0 +1,4016 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <sal/types.h> +#include <math.h> +#include <string.h> +#include <memory> +#include <vector> +#include <string> +#include <sstream> +#include <mutex> +#include <boost/property_tree/json_parser.hpp> + +#include <com/sun/star/awt/Key.hpp> +#include <LibreOfficeKit/LibreOfficeKit.h> +#include <LibreOfficeKit/LibreOfficeKitInit.h> +#include <LibreOfficeKit/LibreOfficeKitEnums.h> +#include <LibreOfficeKit/LibreOfficeKitGtk.h> +#include <vcl/event.hxx> + +#include "tilebuffer.hxx" + +#if !GLIB_CHECK_VERSION(2,32,0) +#define G_SOURCE_REMOVE FALSE +#define G_SOURCE_CONTINUE TRUE +#endif +#if !GLIB_CHECK_VERSION(2,40,0) +#define g_info(...) g_log(G_LOG_DOMAIN, G_LOG_LEVEL_INFO, __VA_ARGS__) +#endif + +// Cursor bitmaps from the installation set. +#define CURSOR_HANDLE_DIR "/../share/libreofficekit/" +// Number of handles around a graphic selection. +#define GRAPHIC_HANDLE_COUNT 8 +// Maximum Zoom allowed +#define MAX_ZOOM 5.0f +// Minimum Zoom allowed +#define MIN_ZOOM 0.25f + +/// This is expected to be locked during setView(), doSomethingElse() LOK calls. +static std::mutex g_aLOKMutex; + +namespace { + +/// Same as a GdkRectangle, but also tracks in which part the rectangle is. +struct ViewRectangle +{ + int m_nPart; + GdkRectangle m_aRectangle; + + ViewRectangle(int nPart = 0, const GdkRectangle& rRectangle = GdkRectangle()) + : m_nPart(nPart), + m_aRectangle(rRectangle) + { + } +}; + +/// Same as a list of GdkRectangles, but also tracks in which part the rectangle is. +struct ViewRectangles +{ + int m_nPart; + std::vector<GdkRectangle> m_aRectangles; + + ViewRectangles(int nPart = 0, std::vector<GdkRectangle>&& rRectangles = std::vector<GdkRectangle>()) + : m_nPart(nPart), + m_aRectangles(std::move(rRectangles)) + { + } +}; + +/// Private struct used by this GObject type +struct LOKDocViewPrivateImpl +{ + std::string m_aLOPath; + std::string m_aUserProfileURL; + std::string m_aDocPath; + std::string m_aRenderingArguments; + gdouble m_nLoadProgress; + bool m_bIsLoading; + bool m_bInit; // initializeForRendering() has been called + bool m_bCanZoomIn; + bool m_bCanZoomOut; + bool m_bUnipoll; + LibreOfficeKit* m_pOffice; + LibreOfficeKitDocument* m_pDocument; + + std::unique_ptr<TileBuffer> m_pTileBuffer; + GThreadPool* lokThreadPool; + + gfloat m_fZoom; + glong m_nDocumentWidthTwips; + glong m_nDocumentHeightTwips; + /// View or edit mode. + bool m_bEdit; + /// LOK Features + guint64 m_nLOKFeatures; + /// Number of parts in currently loaded document + gint m_nParts; + /// Position and size of the visible cursor. + GdkRectangle m_aVisibleCursor; + /// Position and size of the view cursors. The current view can only see + /// them, can't modify them. Key is the view id. + std::map<int, ViewRectangle> m_aViewCursors; + /// Cursor overlay is visible or hidden (for blinking). + bool m_bCursorOverlayVisible; + /// Cursor is visible or hidden (e.g. for graphic selection). + bool m_bCursorVisible; + /// Visibility of view selections. The current view can only see / them, + /// can't modify them. Key is the view id. + std::map<int, bool> m_aViewCursorVisibilities; + /// Time of the last button press. + guint32 m_nLastButtonPressTime; + /// Time of the last button release. + guint32 m_nLastButtonReleaseTime; + /// Last pressed button (left, right, middle) + guint32 m_nLastButtonPressed; + /// Key modifier (ctrl, atl, shift) + guint32 m_nKeyModifier; + /// Rectangles of the current text selection. + std::vector<GdkRectangle> m_aTextSelectionRectangles; + /// Rectangles of the current content control. + std::vector<GdkRectangle> m_aContentControlRectangles; + /// Rectangles of view selections. The current view can only see + /// them, can't modify them. Key is the view id. + std::map<int, ViewRectangles> m_aTextViewSelectionRectangles; + /// Position and size of the selection start (as if there would be a cursor caret there). + GdkRectangle m_aTextSelectionStart; + /// Position and size of the selection end. + GdkRectangle m_aTextSelectionEnd; + GdkRectangle m_aGraphicSelection; + /// Position and size of the graphic view selections. The current view can only + /// see them, can't modify them. Key is the view id. + std::map<int, ViewRectangle> m_aGraphicViewSelections; + GdkRectangle m_aCellCursor; + /// Position and size of the cell view cursors. The current view can only + /// see them, can't modify them. Key is the view id. + std::map<int, ViewRectangle> m_aCellViewCursors; + bool m_bInDragGraphicSelection; + /// Position, size and color of the reference marks. The current view can only + /// see them, can't modify them. Key is the view id. + std::vector<std::pair<ViewRectangle, sal_uInt32>> m_aReferenceMarks; + + /// @name Start/middle/end handle. + ///@{ + /// Bitmap of the text selection start handle. + cairo_surface_t* m_pHandleStart; + /// Rectangle of the text selection start handle, to know if the user clicked on it or not + GdkRectangle m_aHandleStartRect; + /// If we are in the middle of a drag of the text selection end handle. + bool m_bInDragStartHandle; + /// Bitmap of the text selection middle handle. + cairo_surface_t* m_pHandleMiddle; + /// Rectangle of the text selection middle handle, to know if the user clicked on it or not + GdkRectangle m_aHandleMiddleRect; + /// If we are in the middle of a drag of the text selection middle handle. + bool m_bInDragMiddleHandle; + /// Bitmap of the text selection end handle. + cairo_surface_t* m_pHandleEnd; + /// Rectangle of the text selection end handle, to know if the user clicked on it or not + GdkRectangle m_aHandleEndRect; + /// If we are in the middle of a drag of the text selection end handle. + bool m_bInDragEndHandle; + ///@} + + /// @name Graphic handles. + ///@{ + /// Rectangle of a graphic selection handle, to know if the user clicked on it or not. + GdkRectangle m_aGraphicHandleRects[8]; + /// If we are in the middle of a drag of a graphic selection handle. + bool m_bInDragGraphicHandles[8]; + ///@} + + /// View ID, returned by createView() or 0 by default. + int m_nViewId; + + /// Cached part ID, returned by getPart(). + int m_nPartId; + + /// Cached document type, returned by getDocumentType(). + LibreOfficeKitDocumentType m_eDocumentType; + + /// Contains a freshly set zoom level: logic size of a tile. + /// It gets reset back to 0 when LOK was informed about this zoom change. + int m_nTileSizeTwips; + + GdkRectangle m_aVisibleArea; + bool m_bVisibleAreaSet; + + /// Event source ID for handleTimeout() of this widget. + guint m_nTimeoutId; + + /// Rectangles of view locks. The current view can only see + /// them, can't modify them. Key is the view id. + std::map<int, ViewRectangle> m_aViewLockRectangles; + + LOKDocViewPrivateImpl() + : m_nLoadProgress(0), + m_bIsLoading(false), + m_bInit(false), + m_bCanZoomIn(true), + m_bCanZoomOut(true), + m_bUnipoll(false), + m_pOffice(nullptr), + m_pDocument(nullptr), + lokThreadPool(nullptr), + m_fZoom(0), + m_nDocumentWidthTwips(0), + m_nDocumentHeightTwips(0), + m_bEdit(false), + m_nLOKFeatures(0), + m_nParts(0), + m_aVisibleCursor({0, 0, 0, 0}), + m_bCursorOverlayVisible(false), + m_bCursorVisible(true), + m_nLastButtonPressTime(0), + m_nLastButtonReleaseTime(0), + m_nLastButtonPressed(0), + m_nKeyModifier(0), + m_aTextSelectionStart({0, 0, 0, 0}), + m_aTextSelectionEnd({0, 0, 0, 0}), + m_aGraphicSelection({0, 0, 0, 0}), + m_aCellCursor({0, 0, 0, 0}), + m_bInDragGraphicSelection(false), + m_pHandleStart(nullptr), + m_aHandleStartRect({0, 0, 0, 0}), + m_bInDragStartHandle(false), + m_pHandleMiddle(nullptr), + m_aHandleMiddleRect({0, 0, 0, 0}), + m_bInDragMiddleHandle(false), + m_pHandleEnd(nullptr), + m_aHandleEndRect({0, 0, 0, 0}), + m_bInDragEndHandle(false), + m_nViewId(0), + m_nPartId(0), + m_eDocumentType(LOK_DOCTYPE_OTHER), + m_nTileSizeTwips(0), + m_aVisibleArea({0, 0, 0, 0}), + m_bVisibleAreaSet(false), + m_nTimeoutId(0) + { + memset(&m_aGraphicHandleRects, 0, sizeof(m_aGraphicHandleRects)); + memset(&m_bInDragGraphicHandles, 0, sizeof(m_bInDragGraphicHandles)); + } + + ~LOKDocViewPrivateImpl() + { + if (m_nTimeoutId) + g_source_remove(m_nTimeoutId); + } +}; + +} + +/// Wrapper around LOKDocViewPrivateImpl, managed by malloc/memset/free. +struct _LOKDocViewPrivate +{ + LOKDocViewPrivateImpl* m_pImpl; + + LOKDocViewPrivateImpl* operator->() + { + return m_pImpl; + } +}; + +enum +{ + LOAD_CHANGED, + EDIT_CHANGED, + COMMAND_CHANGED, + SEARCH_NOT_FOUND, + PART_CHANGED, + SIZE_CHANGED, + HYPERLINK_CLICKED, + CURSOR_CHANGED, + SEARCH_RESULT_COUNT, + COMMAND_RESULT, + ADDRESS_CHANGED, + FORMULA_CHANGED, + TEXT_SELECTION, + CONTENT_CONTROL, + PASSWORD_REQUIRED, + COMMENT, + RULER, + WINDOW, + INVALIDATE_HEADER, + + LAST_SIGNAL +}; + +enum +{ + PROP_0, + + PROP_LO_PATH, + PROP_LO_UNIPOLL, + PROP_LO_POINTER, + PROP_USER_PROFILE_URL, + PROP_DOC_PATH, + PROP_DOC_POINTER, + PROP_EDITABLE, + PROP_LOAD_PROGRESS, + PROP_ZOOM, + PROP_IS_LOADING, + PROP_IS_INITIALIZED, + PROP_DOC_WIDTH, + PROP_DOC_HEIGHT, + PROP_CAN_ZOOM_IN, + PROP_CAN_ZOOM_OUT, + PROP_DOC_PASSWORD, + PROP_DOC_PASSWORD_TO_MODIFY, + PROP_TILED_ANNOTATIONS, + + PROP_LAST +}; + +static guint doc_view_signals[LAST_SIGNAL] = { 0 }; +static GParamSpec *properties[PROP_LAST] = { nullptr }; + +static void lok_doc_view_initable_iface_init (GInitableIface *iface); +static void callbackWorker (int nType, const char* pPayload, void* pData); +static void updateClientZoom (LOKDocView *pDocView); + +SAL_DLLPUBLIC_EXPORT GType lok_doc_view_get_type(); +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#if defined __clang__ +#if __has_warning("-Wdeprecated-volatile") +#pragma clang diagnostic ignored "-Wdeprecated-volatile" +#endif +#endif +#endif +G_DEFINE_TYPE_WITH_CODE (LOKDocView, lok_doc_view, GTK_TYPE_DRAWING_AREA, + G_ADD_PRIVATE (LOKDocView) + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, lok_doc_view_initable_iface_init)); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + +static LOKDocViewPrivate& getPrivate(LOKDocView* pDocView) +{ + LOKDocViewPrivate* priv = static_cast<LOKDocViewPrivate*>(lok_doc_view_get_instance_private(pDocView)); + return *priv; +} + +namespace { + +/// Helper struct used to pass the data from soffice thread -> main thread. +struct CallbackData +{ + int m_nType; + std::string m_aPayload; + LOKDocView* m_pDocView; + + CallbackData(int nType, const std::string& rPayload, LOKDocView* pDocView) + : m_nType(nType), + m_aPayload(rPayload), + m_pDocView(pDocView) {} +}; + +} + +static void +LOKPostCommand (LOKDocView* pDocView, + const gchar* pCommand, + const gchar* pArguments, + bool bNotifyWhenFinished) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + LOEvent* pLOEvent = new LOEvent(LOK_POST_COMMAND); + GError* error = nullptr; + pLOEvent->m_pCommand = g_strdup(pCommand); + pLOEvent->m_pArguments = g_strdup(pArguments); + pLOEvent->m_bNotifyWhenFinished = bNotifyWhenFinished; + + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_POST_COMMAND: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); +} + +static void +doSearch(LOKDocView* pDocView, const char* pText, bool bBackwards, bool highlightAll) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + if (!priv->m_pDocument) + return; + + boost::property_tree::ptree aTree; + GtkWidget* drawingWidget = GTK_WIDGET(pDocView); + GdkWindow* drawingWindow = gtk_widget_get_window(drawingWidget); + if (!drawingWindow) + return; + std::shared_ptr<cairo_region_t> cairoVisRegion( gdk_window_get_visible_region(drawingWindow), + cairo_region_destroy); + cairo_rectangle_int_t cairoVisRect; + cairo_region_get_rectangle(cairoVisRegion.get(), 0, &cairoVisRect); + int x = pixelToTwip (cairoVisRect.x, priv->m_fZoom); + int y = pixelToTwip (cairoVisRect.y, priv->m_fZoom); + + aTree.put(boost::property_tree::ptree::path_type("SearchItem.SearchString/type", '/'), "string"); + aTree.put(boost::property_tree::ptree::path_type("SearchItem.SearchString/value", '/'), pText); + aTree.put(boost::property_tree::ptree::path_type("SearchItem.Backward/type", '/'), "boolean"); + aTree.put(boost::property_tree::ptree::path_type("SearchItem.Backward/value", '/'), bBackwards); + if (highlightAll) + { + aTree.put(boost::property_tree::ptree::path_type("SearchItem.Command/type", '/'), "unsigned short"); + // SvxSearchCmd::FIND_ALL + aTree.put(boost::property_tree::ptree::path_type("SearchItem.Command/value", '/'), "1"); + } + + aTree.put(boost::property_tree::ptree::path_type("SearchItem.SearchStartPointX/type", '/'), "long"); + aTree.put(boost::property_tree::ptree::path_type("SearchItem.SearchStartPointX/value", '/'), x); + aTree.put(boost::property_tree::ptree::path_type("SearchItem.SearchStartPointY/type", '/'), "long"); + aTree.put(boost::property_tree::ptree::path_type("SearchItem.SearchStartPointY/value", '/'), y); + + std::stringstream aStream; + boost::property_tree::write_json(aStream, aTree); + + LOKPostCommand (pDocView, ".uno:ExecuteSearch", aStream.str().c_str(), false); +} + +static bool +isEmptyRectangle(const GdkRectangle& rRectangle) +{ + return rRectangle.x == 0 && rRectangle.y == 0 && rRectangle.width == 0 && rRectangle.height == 0; +} + +/// if handled, returns TRUE else FALSE +static bool +handleTextSelectionOnButtonPress(GdkRectangle& aClick, LOKDocView* pDocView) { + LOKDocViewPrivate& priv = getPrivate(pDocView); + + if (gdk_rectangle_intersect(&aClick, &priv->m_aHandleStartRect, nullptr)) + { + g_info("LOKDocView_Impl::signalButton: start of drag start handle"); + priv->m_bInDragStartHandle = true; + return true; + } + else if (gdk_rectangle_intersect(&aClick, &priv->m_aHandleMiddleRect, nullptr)) + { + g_info("LOKDocView_Impl::signalButton: start of drag middle handle"); + priv->m_bInDragMiddleHandle = true; + return true; + } + else if (gdk_rectangle_intersect(&aClick, &priv->m_aHandleEndRect, nullptr)) + { + g_info("LOKDocView_Impl::signalButton: start of drag end handle"); + priv->m_bInDragEndHandle = true; + return true; + } + + return false; +} + +/// if handled, returns TRUE else FALSE +static bool +handleGraphicSelectionOnButtonPress(GdkRectangle& aClick, LOKDocView* pDocView) { + LOKDocViewPrivate& priv = getPrivate(pDocView); + GError* error = nullptr; + + for (int i = 0; i < GRAPHIC_HANDLE_COUNT; ++i) + { + if (gdk_rectangle_intersect(&aClick, &priv->m_aGraphicHandleRects[i], nullptr)) + { + g_info("LOKDocView_Impl::signalButton: start of drag graphic handle #%d", i); + priv->m_bInDragGraphicHandles[i] = true; + + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + LOEvent* pLOEvent = new LOEvent(LOK_SET_GRAPHIC_SELECTION); + pLOEvent->m_nSetGraphicSelectionType = LOK_SETGRAPHICSELECTION_START; + pLOEvent->m_nSetGraphicSelectionX = pixelToTwip(priv->m_aGraphicHandleRects[i].x + priv->m_aGraphicHandleRects[i].width / 2, priv->m_fZoom); + pLOEvent->m_nSetGraphicSelectionY = pixelToTwip(priv->m_aGraphicHandleRects[i].y + priv->m_aGraphicHandleRects[i].height / 2, priv->m_fZoom); + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_SET_GRAPHIC_SELECTION: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); + + return true; + } + } + + return false; +} + +/// if handled, returns TRUE else FALSE +static bool +handleTextSelectionOnButtonRelease(LOKDocView* pDocView) { + LOKDocViewPrivate& priv = getPrivate(pDocView); + + if (priv->m_bInDragStartHandle) + { + g_info("LOKDocView_Impl::signalButton: end of drag start handle"); + priv->m_bInDragStartHandle = false; + return true; + } + else if (priv->m_bInDragMiddleHandle) + { + g_info("LOKDocView_Impl::signalButton: end of drag middle handle"); + priv->m_bInDragMiddleHandle = false; + return true; + } + else if (priv->m_bInDragEndHandle) + { + g_info("LOKDocView_Impl::signalButton: end of drag end handle"); + priv->m_bInDragEndHandle = false; + return true; + } + + return false; +} + +/// if handled, returns TRUE else FALSE +static bool +handleGraphicSelectionOnButtonRelease(LOKDocView* pDocView, GdkEventButton* pEvent) { + LOKDocViewPrivate& priv = getPrivate(pDocView); + GError* error = nullptr; + + for (int i = 0; i < GRAPHIC_HANDLE_COUNT; ++i) + { + if (priv->m_bInDragGraphicHandles[i]) + { + g_info("LOKDocView_Impl::signalButton: end of drag graphic handle #%d", i); + priv->m_bInDragGraphicHandles[i] = false; + + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + LOEvent* pLOEvent = new LOEvent(LOK_SET_GRAPHIC_SELECTION); + pLOEvent->m_nSetGraphicSelectionType = LOK_SETGRAPHICSELECTION_END; + pLOEvent->m_nSetGraphicSelectionX = pixelToTwip(pEvent->x, priv->m_fZoom); + pLOEvent->m_nSetGraphicSelectionY = pixelToTwip(pEvent->y, priv->m_fZoom); + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_SET_GRAPHIC_SELECTION: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); + + return true; + } + } + + if (!priv->m_bInDragGraphicSelection) + return false; + + g_info("LOKDocView_Impl::signalButton: end of drag graphic selection"); + priv->m_bInDragGraphicSelection = false; + + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + LOEvent* pLOEvent = new LOEvent(LOK_SET_GRAPHIC_SELECTION); + pLOEvent->m_nSetGraphicSelectionType = LOK_SETGRAPHICSELECTION_END; + pLOEvent->m_nSetGraphicSelectionX = pixelToTwip(pEvent->x, priv->m_fZoom); + pLOEvent->m_nSetGraphicSelectionY = pixelToTwip(pEvent->y, priv->m_fZoom); + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_SET_GRAPHIC_SELECTION: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); + + return true; +} + +static void +postKeyEventInThread(gpointer data) +{ + GTask* task = G_TASK(data); + LOKDocView* pDocView = LOK_DOC_VIEW(g_task_get_source_object(task)); + LOKDocViewPrivate& priv = getPrivate(pDocView); + LOEvent* pLOEvent = static_cast<LOEvent*>(g_task_get_task_data(task)); + gint nScaleFactor = gtk_widget_get_scale_factor(GTK_WIDGET(pDocView)); + gint nTileSizePixelsScaled = nTileSizePixels * nScaleFactor; + + std::scoped_lock<std::mutex> aGuard(g_aLOKMutex); + std::stringstream ss; + ss << "lok::Document::setView(" << priv->m_nViewId << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId); + + if (priv->m_nTileSizeTwips) + { + ss.str(std::string()); + ss << "lok::Document::setClientZoom(" << nTileSizePixelsScaled << ", " << nTileSizePixelsScaled << ", " << priv->m_nTileSizeTwips << ", " << priv->m_nTileSizeTwips << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setClientZoom(priv->m_pDocument, + nTileSizePixelsScaled, + nTileSizePixelsScaled, + priv->m_nTileSizeTwips, + priv->m_nTileSizeTwips); + priv->m_nTileSizeTwips = 0; + } + if (priv->m_bVisibleAreaSet) + { + ss.str(std::string()); + ss << "lok::Document::setClientVisibleArea(" << priv->m_aVisibleArea.x << ", " << priv->m_aVisibleArea.y << ", "; + ss << priv->m_aVisibleArea.width << ", " << priv->m_aVisibleArea.height << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setClientVisibleArea(priv->m_pDocument, + priv->m_aVisibleArea.x, + priv->m_aVisibleArea.y, + priv->m_aVisibleArea.width, + priv->m_aVisibleArea.height); + priv->m_bVisibleAreaSet = false; + } + + ss.str(std::string()); + ss << "lok::Document::postKeyEvent(" << pLOEvent->m_nKeyEvent << ", " << pLOEvent->m_nCharCode << ", " << pLOEvent->m_nKeyCode << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->postKeyEvent(priv->m_pDocument, + pLOEvent->m_nKeyEvent, + pLOEvent->m_nCharCode, + pLOEvent->m_nKeyCode); +} + +static gboolean +signalKey (GtkWidget* pWidget, GdkEventKey* pEvent) +{ + LOKDocView* pDocView = LOK_DOC_VIEW(pWidget); + LOKDocViewPrivate& priv = getPrivate(pDocView); + int nCharCode = 0; + int nKeyCode = 0; + GError* error = nullptr; + + if (!priv->m_bEdit) + { + g_info("signalKey: not in edit mode, ignore"); + return FALSE; + } + + priv->m_nKeyModifier &= KEY_MOD2; + switch (pEvent->keyval) + { + case GDK_KEY_BackSpace: + nKeyCode = com::sun::star::awt::Key::BACKSPACE; + break; + case GDK_KEY_Delete: + nKeyCode = com::sun::star::awt::Key::DELETE; + break; + case GDK_KEY_Return: + case GDK_KEY_KP_Enter: + nKeyCode = com::sun::star::awt::Key::RETURN; + break; + case GDK_KEY_Escape: + nKeyCode = com::sun::star::awt::Key::ESCAPE; + break; + case GDK_KEY_Tab: + nKeyCode = com::sun::star::awt::Key::TAB; + break; + case GDK_KEY_Down: + nKeyCode = com::sun::star::awt::Key::DOWN; + break; + case GDK_KEY_Up: + nKeyCode = com::sun::star::awt::Key::UP; + break; + case GDK_KEY_Left: + nKeyCode = com::sun::star::awt::Key::LEFT; + break; + case GDK_KEY_Right: + nKeyCode = com::sun::star::awt::Key::RIGHT; + break; + case GDK_KEY_Page_Down: + nKeyCode = com::sun::star::awt::Key::PAGEDOWN; + break; + case GDK_KEY_Page_Up: + nKeyCode = com::sun::star::awt::Key::PAGEUP; + break; + case GDK_KEY_Insert: + nKeyCode = com::sun::star::awt::Key::INSERT; + break; + case GDK_KEY_Shift_L: + case GDK_KEY_Shift_R: + if (pEvent->type == GDK_KEY_PRESS) + priv->m_nKeyModifier |= KEY_SHIFT; + break; + case GDK_KEY_Control_L: + case GDK_KEY_Control_R: + if (pEvent->type == GDK_KEY_PRESS) + priv->m_nKeyModifier |= KEY_MOD1; + break; + case GDK_KEY_Alt_L: + case GDK_KEY_Alt_R: + if (pEvent->type == GDK_KEY_PRESS) + priv->m_nKeyModifier |= KEY_MOD2; + else + priv->m_nKeyModifier &= ~KEY_MOD2; + break; + default: + if (pEvent->keyval >= GDK_KEY_F1 && pEvent->keyval <= GDK_KEY_F26) + nKeyCode = com::sun::star::awt::Key::F1 + (pEvent->keyval - GDK_KEY_F1); + else + nCharCode = gdk_keyval_to_unicode(pEvent->keyval); + } + + // rsc is not public API, but should be good enough for debugging purposes. + // If this is needed for real, then probably a new param of type + // css::awt::KeyModifier is needed in postKeyEvent(). + if (pEvent->state & GDK_SHIFT_MASK) + nKeyCode |= KEY_SHIFT; + + if (pEvent->state & GDK_CONTROL_MASK) + nKeyCode |= KEY_MOD1; + + if (priv->m_nKeyModifier & KEY_MOD2) + nKeyCode |= KEY_MOD2; + + if (nKeyCode & (KEY_SHIFT | KEY_MOD1 | KEY_MOD2)) { + if (pEvent->keyval >= GDK_KEY_a && pEvent->keyval <= GDK_KEY_z) + { + nKeyCode |= 512 + (pEvent->keyval - GDK_KEY_a); + } + else if (pEvent->keyval >= GDK_KEY_A && pEvent->keyval <= GDK_KEY_Z) { + nKeyCode |= 512 + (pEvent->keyval - GDK_KEY_A); + } + else if (pEvent->keyval >= GDK_KEY_0 && pEvent->keyval <= GDK_KEY_9) { + nKeyCode |= 256 + (pEvent->keyval - GDK_KEY_0); + } + } + + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + LOEvent* pLOEvent = new LOEvent(LOK_POST_KEY); + pLOEvent->m_nKeyEvent = pEvent->type == GDK_KEY_RELEASE ? LOK_KEYEVENT_KEYUP : LOK_KEYEVENT_KEYINPUT; + pLOEvent->m_nCharCode = nCharCode; + pLOEvent->m_nKeyCode = nKeyCode; + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_POST_KEY: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); + + return FALSE; +} + +static gboolean +handleTimeout (gpointer pData) +{ + LOKDocView* pDocView = LOK_DOC_VIEW (pData); + LOKDocViewPrivate& priv = getPrivate(pDocView); + + if (priv->m_bEdit) + { + if (priv->m_bCursorOverlayVisible) + priv->m_bCursorOverlayVisible = false; + else + priv->m_bCursorOverlayVisible = true; + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); + } + + return G_SOURCE_CONTINUE; +} + +static void +commandChanged(LOKDocView* pDocView, const std::string& rString) +{ + g_signal_emit(pDocView, doc_view_signals[COMMAND_CHANGED], 0, rString.c_str()); +} + +static void +searchNotFound(LOKDocView* pDocView, const std::string& rString) +{ + g_signal_emit(pDocView, doc_view_signals[SEARCH_NOT_FOUND], 0, rString.c_str()); +} + +static void searchResultCount(LOKDocView* pDocView, const std::string& rString) +{ + g_signal_emit(pDocView, doc_view_signals[SEARCH_RESULT_COUNT], 0, rString.c_str()); +} + +static void commandResult(LOKDocView* pDocView, const std::string& rString) +{ + g_signal_emit(pDocView, doc_view_signals[COMMAND_RESULT], 0, rString.c_str()); +} + +static void addressChanged(LOKDocView* pDocView, const std::string& rString) +{ + g_signal_emit(pDocView, doc_view_signals[ADDRESS_CHANGED], 0, rString.c_str()); +} + +static void formulaChanged(LOKDocView* pDocView, const std::string& rString) +{ + g_signal_emit(pDocView, doc_view_signals[FORMULA_CHANGED], 0, rString.c_str()); +} + +static void reportError(LOKDocView* /*pDocView*/, const std::string& rString) +{ + GtkWidget *dialog = gtk_message_dialog_new(nullptr, + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + "%s", + rString.c_str()); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + +static void +setPart(LOKDocView* pDocView, const std::string& rString) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + priv->m_nPartId = std::stoi(rString); + g_signal_emit(pDocView, doc_view_signals[PART_CHANGED], 0, priv->m_nPartId); +} + +static void +hyperlinkClicked(LOKDocView* pDocView, const std::string& rString) +{ + g_signal_emit(pDocView, doc_view_signals[HYPERLINK_CLICKED], 0, rString.c_str()); +} + +/// Trigger a redraw, invoked on the main thread by other functions running in a thread. +static gboolean queueDraw(gpointer pData) +{ + GtkWidget* pWidget = static_cast<GtkWidget*>(pData); + + gtk_widget_queue_draw(pWidget); + + return G_SOURCE_REMOVE; +} + +/// Looks up the author string from initializeForRendering()'s rendering arguments. +static std::string getAuthorRenderingArgument(LOKDocViewPrivate& priv) +{ + std::stringstream aStream; + aStream << priv->m_aRenderingArguments; + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + std::string aRet; + for (const auto& rPair : aTree) + { + if (rPair.first == ".uno:Author") + { + aRet = rPair.second.get<std::string>("value"); + break; + } + } + return aRet; +} + +/// Author string <-> View ID map +static std::map<std::string, int> g_aAuthorViews; + +static void refreshSize(LOKDocView* pDocView) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + + priv->m_pDocument->pClass->getDocumentSize(priv->m_pDocument, &priv->m_nDocumentWidthTwips, &priv->m_nDocumentHeightTwips); + float zoom = priv->m_fZoom; + gint nScaleFactor = gtk_widget_get_scale_factor(GTK_WIDGET(pDocView)); + gint nTileSizePixelsScaled = nTileSizePixels * nScaleFactor; + long nDocumentWidthTwips = priv->m_nDocumentWidthTwips; + long nDocumentHeightTwips = priv->m_nDocumentHeightTwips; + long nDocumentWidthPixels = twipToPixel(nDocumentWidthTwips, zoom); + long nDocumentHeightPixels = twipToPixel(nDocumentHeightTwips, zoom); + + // Total number of columns in this document. + guint nColumns = ceil(static_cast<double>(nDocumentWidthPixels) / nTileSizePixelsScaled); + priv->m_pTileBuffer = std::make_unique<TileBuffer>(nColumns, nScaleFactor); + gtk_widget_set_size_request(GTK_WIDGET(pDocView), + nDocumentWidthPixels, + nDocumentHeightPixels); +} + +/// Set up LOKDocView after the document is loaded, invoked on the main thread by openDocumentInThread() running in a thread. +static gboolean postDocumentLoad(gpointer pData) +{ + LOKDocView* pLOKDocView = static_cast<LOKDocView*>(pData); + LOKDocViewPrivate& priv = getPrivate(pLOKDocView); + + std::unique_lock<std::mutex> aGuard(g_aLOKMutex); + priv->m_pDocument->pClass->initializeForRendering(priv->m_pDocument, priv->m_aRenderingArguments.c_str()); + // This returns the view id of the "current" view, but sadly if you load multiple documents that + // is apparently not a view showing the most recently loaded document. Not much we can do here, + // though. If that is fixed, this comment becomes incorrect. + priv->m_nViewId = priv->m_pDocument->pClass->getView(priv->m_pDocument); + g_aAuthorViews[getAuthorRenderingArgument(priv)] = priv->m_nViewId; + priv->m_pDocument->pClass->registerCallback(priv->m_pDocument, callbackWorker, pLOKDocView); + priv->m_nParts = priv->m_pDocument->pClass->getParts(priv->m_pDocument); + aGuard.unlock(); + priv->m_nTimeoutId = g_timeout_add(600, handleTimeout, pLOKDocView); + + refreshSize(pLOKDocView); + + gtk_widget_set_can_focus(GTK_WIDGET(pLOKDocView), true); + gtk_widget_grab_focus(GTK_WIDGET(pLOKDocView)); + lok_doc_view_set_zoom(pLOKDocView, 1.0); + + // we are completely loaded + priv->m_bInit = true; + g_object_notify_by_pspec(G_OBJECT(pLOKDocView), properties[PROP_IS_INITIALIZED]); + + return G_SOURCE_REMOVE; +} + +/// Implementation of the global callback handler, invoked by globalCallback(); +static gboolean +globalCallback (gpointer pData) +{ + CallbackData* pCallback = static_cast<CallbackData*>(pData); + LOKDocViewPrivate& priv = getPrivate(pCallback->m_pDocView); + bool bModify = false; + + switch (pCallback->m_nType) + { + case LOK_CALLBACK_STATUS_INDICATOR_START: + { + priv->m_nLoadProgress = 0.0; + g_signal_emit (pCallback->m_pDocView, doc_view_signals[LOAD_CHANGED], 0, 0.0); + } + break; + case LOK_CALLBACK_STATUS_INDICATOR_SET_VALUE: + { + priv->m_nLoadProgress = static_cast<gdouble>(std::stoi(pCallback->m_aPayload)/100.0); + g_signal_emit (pCallback->m_pDocView, doc_view_signals[LOAD_CHANGED], 0, priv->m_nLoadProgress); + } + break; + case LOK_CALLBACK_STATUS_INDICATOR_FINISH: + { + priv->m_nLoadProgress = 1.0; + g_signal_emit (pCallback->m_pDocView, doc_view_signals[LOAD_CHANGED], 0, 1.0); + } + break; + case LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY: + bModify = true; + [[fallthrough]]; + case LOK_CALLBACK_DOCUMENT_PASSWORD: + { + char const*const pURL(pCallback->m_aPayload.c_str()); + g_signal_emit (pCallback->m_pDocView, doc_view_signals[PASSWORD_REQUIRED], 0, pURL, bModify); + } + break; + case LOK_CALLBACK_ERROR: + { + reportError(pCallback->m_pDocView, pCallback->m_aPayload); + } + break; + case LOK_CALLBACK_SIGNATURE_STATUS: + { + // TODO + } + break; + default: + g_assert(false); + break; + } + delete pCallback; + + return G_SOURCE_REMOVE; +} + +static void +globalCallbackWorker(int nType, const char* pPayload, void* pData) +{ + LOKDocView* pDocView = LOK_DOC_VIEW (pData); + + CallbackData* pCallback = new CallbackData(nType, pPayload ? pPayload : "(nil)", pDocView); + g_info("LOKDocView_Impl::globalCallbackWorkerImpl: %s, '%s'", lokCallbackTypeToString(nType), pPayload); + gdk_threads_add_idle(globalCallback, pCallback); +} + +static GdkRectangle +payloadToRectangle (LOKDocView* pDocView, const char* pPayload) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + GdkRectangle aRet; + // x, y, width, height, part number. + gchar** ppCoordinates = g_strsplit(pPayload, ", ", 5); + gchar** ppCoordinate = ppCoordinates; + + aRet.width = aRet.height = aRet.x = aRet.y = 0; + + if (!*ppCoordinate) + { + g_strfreev(ppCoordinates); + return aRet; + } + aRet.x = atoi(*ppCoordinate); + if (aRet.x < 0) + aRet.x = 0; + ++ppCoordinate; + if (!*ppCoordinate) + { + g_strfreev(ppCoordinates); + return aRet; + } + aRet.y = atoi(*ppCoordinate); + if (aRet.y < 0) + aRet.y = 0; + ++ppCoordinate; + if (!*ppCoordinate) + { + g_strfreev(ppCoordinates); + return aRet; + } + long l = atol(*ppCoordinate); + if (l > std::numeric_limits<int>::max()) + aRet.width = std::numeric_limits<int>::max(); + else + aRet.width = l; + if (aRet.x + aRet.width > priv->m_nDocumentWidthTwips) + aRet.width = priv->m_nDocumentWidthTwips - aRet.x; + ++ppCoordinate; + if (!*ppCoordinate) + { + g_strfreev(ppCoordinates); + return aRet; + } + l = atol(*ppCoordinate); + if (l > std::numeric_limits<int>::max()) + aRet.height = std::numeric_limits<int>::max(); + else + aRet.height = l; + if (aRet.y + aRet.height > priv->m_nDocumentHeightTwips) + aRet.height = priv->m_nDocumentHeightTwips - aRet.y; + + g_strfreev(ppCoordinates); + return aRet; +} + +static std::vector<GdkRectangle> +payloadToRectangles(LOKDocView* pDocView, const char* pPayload) +{ + std::vector<GdkRectangle> aRet; + + if (g_strcmp0(pPayload, "EMPTY") == 0) + return aRet; + + gchar** ppRectangles = g_strsplit(pPayload, "; ", 0); + for (gchar** ppRectangle = ppRectangles; *ppRectangle; ++ppRectangle) + aRet.push_back(payloadToRectangle(pDocView, *ppRectangle)); + g_strfreev(ppRectangles); + + return aRet; +} + + +static void +setTilesInvalid (LOKDocView* pDocView, const GdkRectangle& rRectangle) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + GdkRectangle aRectanglePixels; + GdkPoint aStart, aEnd; + gint nScaleFactor = gtk_widget_get_scale_factor(GTK_WIDGET(pDocView)); + gint nTileSizePixelsScaled = nTileSizePixels * nScaleFactor; + + aRectanglePixels.x = twipToPixel(rRectangle.x, priv->m_fZoom) * nScaleFactor; + aRectanglePixels.y = twipToPixel(rRectangle.y, priv->m_fZoom) * nScaleFactor; + aRectanglePixels.width = twipToPixel(rRectangle.width, priv->m_fZoom) * nScaleFactor; + aRectanglePixels.height = twipToPixel(rRectangle.height, priv->m_fZoom) * nScaleFactor; + + aStart.x = aRectanglePixels.y / nTileSizePixelsScaled; + aStart.y = aRectanglePixels.x / nTileSizePixelsScaled; + aEnd.x = (aRectanglePixels.y + aRectanglePixels.height + nTileSizePixelsScaled) / nTileSizePixelsScaled; + aEnd.y = (aRectanglePixels.x + aRectanglePixels.width + nTileSizePixelsScaled) / nTileSizePixelsScaled; + for (int i = aStart.x; i < aEnd.x; i++) + { + for (int j = aStart.y; j < aEnd.y; j++) + { + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + priv->m_pTileBuffer->setInvalid(i, j, priv->m_fZoom, task, priv->lokThreadPool); + g_object_unref(task); + } + } +} + +static gboolean +callback (gpointer pData) +{ + CallbackData* pCallback = static_cast<CallbackData*>(pData); + LOKDocView* pDocView = LOK_DOC_VIEW (pCallback->m_pDocView); + LOKDocViewPrivate& priv = getPrivate(pDocView); + + //callback registered before the widget was destroyed. + //Use existence of lokThreadPool as flag it was torn down + if (!priv->lokThreadPool) + { + delete pCallback; + return G_SOURCE_REMOVE; + } + + switch (static_cast<LibreOfficeKitCallbackType>(pCallback->m_nType)) + { + case LOK_CALLBACK_INVALIDATE_TILES: + { + if (pCallback->m_aPayload.compare(0, 5, "EMPTY") != 0) // payload doesn't start with "EMPTY" + { + GdkRectangle aRectangle = payloadToRectangle(pDocView, pCallback->m_aPayload.c_str()); + setTilesInvalid(pDocView, aRectangle); + } + else + priv->m_pTileBuffer->resetAllTiles(); + + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); + } + break; + case LOK_CALLBACK_INVALIDATE_VISIBLE_CURSOR: + { + + std::stringstream aStream(pCallback->m_aPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + const std::string& rRectangle = aTree.get<std::string>("rectangle"); + int nViewId = aTree.get<int>("viewId"); + + priv->m_aVisibleCursor = payloadToRectangle(pDocView, rRectangle.c_str()); + priv->m_bCursorOverlayVisible = true; + if(nViewId == priv->m_nViewId) + { + g_signal_emit(pDocView, doc_view_signals[CURSOR_CHANGED], 0, + priv->m_aVisibleCursor.x, + priv->m_aVisibleCursor.y, + priv->m_aVisibleCursor.width, + priv->m_aVisibleCursor.height); + } + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); + } + break; + case LOK_CALLBACK_TEXT_SELECTION: + { + priv->m_aTextSelectionRectangles = payloadToRectangles(pDocView, pCallback->m_aPayload.c_str()); + bool bIsTextSelected = !priv->m_aTextSelectionRectangles.empty(); + // In case the selection is empty, then we get no LOK_CALLBACK_TEXT_SELECTION_START/END events. + if (!bIsTextSelected) + { + memset(&priv->m_aTextSelectionStart, 0, sizeof(priv->m_aTextSelectionStart)); + memset(&priv->m_aHandleStartRect, 0, sizeof(priv->m_aHandleStartRect)); + memset(&priv->m_aTextSelectionEnd, 0, sizeof(priv->m_aTextSelectionEnd)); + memset(&priv->m_aHandleEndRect, 0, sizeof(priv->m_aHandleEndRect)); + } + else + memset(&priv->m_aHandleMiddleRect, 0, sizeof(priv->m_aHandleMiddleRect)); + + g_signal_emit(pDocView, doc_view_signals[TEXT_SELECTION], 0, bIsTextSelected); + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); + } + break; + case LOK_CALLBACK_TEXT_SELECTION_START: + { + priv->m_aTextSelectionStart = payloadToRectangle(pDocView, pCallback->m_aPayload.c_str()); + } + break; + case LOK_CALLBACK_TEXT_SELECTION_END: + { + priv->m_aTextSelectionEnd = payloadToRectangle(pDocView, pCallback->m_aPayload.c_str()); + } + break; + case LOK_CALLBACK_CURSOR_VISIBLE: + { + priv->m_bCursorVisible = pCallback->m_aPayload == "true"; + } + break; + case LOK_CALLBACK_MOUSE_POINTER: + { + // We do not want the cursor to get changed in view-only mode + if (priv->m_bEdit) + { + // The gtk docs claim that most css cursors should be supported, however + // on my system at least this is not true and many cursors are unsupported. + // In this case pCursor = null, which results in the default cursor + // being set. + GdkCursor* pCursor = gdk_cursor_new_from_name(gtk_widget_get_display(GTK_WIDGET(pDocView)), + pCallback->m_aPayload.c_str()); + gdk_window_set_cursor(gtk_widget_get_window(GTK_WIDGET(pDocView)), pCursor); + } + } + break; + case LOK_CALLBACK_GRAPHIC_SELECTION: + { + if (pCallback->m_aPayload != "EMPTY") + priv->m_aGraphicSelection = payloadToRectangle(pDocView, pCallback->m_aPayload.c_str()); + else + memset(&priv->m_aGraphicSelection, 0, sizeof(priv->m_aGraphicSelection)); + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); + } + break; + case LOK_CALLBACK_GRAPHIC_VIEW_SELECTION: + { + std::stringstream aStream(pCallback->m_aPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + int nViewId = aTree.get<int>("viewId"); + int nPart = aTree.get<int>("part"); + const std::string& rRectangle = aTree.get<std::string>("selection"); + if (rRectangle != "EMPTY") + priv->m_aGraphicViewSelections[nViewId] = ViewRectangle(nPart, payloadToRectangle(pDocView, rRectangle.c_str())); + else + { + auto it = priv->m_aGraphicViewSelections.find(nViewId); + if (it != priv->m_aGraphicViewSelections.end()) + priv->m_aGraphicViewSelections.erase(it); + } + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); + break; + } + break; + case LOK_CALLBACK_CELL_CURSOR: + { + if (pCallback->m_aPayload != "EMPTY") + priv->m_aCellCursor = payloadToRectangle(pDocView, pCallback->m_aPayload.c_str()); + else + memset(&priv->m_aCellCursor, 0, sizeof(priv->m_aCellCursor)); + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); + } + break; + case LOK_CALLBACK_HYPERLINK_CLICKED: + { + hyperlinkClicked(pDocView, pCallback->m_aPayload); + } + break; + case LOK_CALLBACK_STATE_CHANGED: + { + commandChanged(pDocView, pCallback->m_aPayload); + } + break; + case LOK_CALLBACK_SEARCH_NOT_FOUND: + { + searchNotFound(pDocView, pCallback->m_aPayload); + } + break; + case LOK_CALLBACK_DOCUMENT_SIZE_CHANGED: + { + refreshSize(pDocView); + g_signal_emit(pDocView, doc_view_signals[SIZE_CHANGED], 0, nullptr); + } + break; + case LOK_CALLBACK_SET_PART: + { + setPart(pDocView, pCallback->m_aPayload); + } + break; + case LOK_CALLBACK_SEARCH_RESULT_SELECTION: + { + boost::property_tree::ptree aTree; + std::stringstream aStream(pCallback->m_aPayload); + boost::property_tree::read_json(aStream, aTree); + int nCount = aTree.get_child("searchResultSelection").size(); + searchResultCount(pDocView, std::to_string(nCount)); + } + break; + case LOK_CALLBACK_UNO_COMMAND_RESULT: + { + commandResult(pDocView, pCallback->m_aPayload); + } + break; + case LOK_CALLBACK_CELL_ADDRESS: + { + addressChanged(pDocView, pCallback->m_aPayload); + } + break; + case LOK_CALLBACK_CELL_FORMULA: + { + formulaChanged(pDocView, pCallback->m_aPayload); + } + break; + case LOK_CALLBACK_ERROR: + { + reportError(pDocView, pCallback->m_aPayload); + } + break; + case LOK_CALLBACK_INVALIDATE_VIEW_CURSOR: + { + std::stringstream aStream(pCallback->m_aPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + int nViewId = aTree.get<int>("viewId"); + int nPart = aTree.get<int>("part"); + const std::string& rRectangle = aTree.get<std::string>("rectangle"); + priv->m_aViewCursors[nViewId] = ViewRectangle(nPart, payloadToRectangle(pDocView, rRectangle.c_str())); + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); + break; + } + case LOK_CALLBACK_TEXT_VIEW_SELECTION: + { + std::stringstream aStream(pCallback->m_aPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + int nViewId = aTree.get<int>("viewId"); + int nPart = aTree.get<int>("part"); + const std::string& rSelection = aTree.get<std::string>("selection"); + priv->m_aTextViewSelectionRectangles[nViewId] = ViewRectangles(nPart, payloadToRectangles(pDocView, rSelection.c_str())); + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); + break; + } + case LOK_CALLBACK_VIEW_CURSOR_VISIBLE: + { + std::stringstream aStream(pCallback->m_aPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + int nViewId = aTree.get<int>("viewId"); + const std::string& rVisible = aTree.get<std::string>("visible"); + priv->m_aViewCursorVisibilities[nViewId] = rVisible == "true"; + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); + break; + } + break; + case LOK_CALLBACK_CELL_VIEW_CURSOR: + { + std::stringstream aStream(pCallback->m_aPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + int nViewId = aTree.get<int>("viewId"); + int nPart = aTree.get<int>("part"); + const std::string& rRectangle = aTree.get<std::string>("rectangle"); + if (rRectangle != "EMPTY") + priv->m_aCellViewCursors[nViewId] = ViewRectangle(nPart, payloadToRectangle(pDocView, rRectangle.c_str())); + else + { + auto it = priv->m_aCellViewCursors.find(nViewId); + if (it != priv->m_aCellViewCursors.end()) + priv->m_aCellViewCursors.erase(it); + } + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); + break; + } + case LOK_CALLBACK_VIEW_LOCK: + { + std::stringstream aStream(pCallback->m_aPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + int nViewId = aTree.get<int>("viewId"); + int nPart = aTree.get<int>("part"); + const std::string& rRectangle = aTree.get<std::string>("rectangle"); + if (rRectangle != "EMPTY") + priv->m_aViewLockRectangles[nViewId] = ViewRectangle(nPart, payloadToRectangle(pDocView, rRectangle.c_str())); + else + { + auto it = priv->m_aViewLockRectangles.find(nViewId); + if (it != priv->m_aViewLockRectangles.end()) + priv->m_aViewLockRectangles.erase(it); + } + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); + break; + } + case LOK_CALLBACK_REDLINE_TABLE_SIZE_CHANGED: + { + break; + } + case LOK_CALLBACK_REDLINE_TABLE_ENTRY_MODIFIED: + { + break; + } + case LOK_CALLBACK_COMMENT: + g_signal_emit(pCallback->m_pDocView, doc_view_signals[COMMENT], 0, pCallback->m_aPayload.c_str()); + break; + case LOK_CALLBACK_RULER_UPDATE: + g_signal_emit(pCallback->m_pDocView, doc_view_signals[RULER], 0, pCallback->m_aPayload.c_str()); + break; + case LOK_CALLBACK_WINDOW: + g_signal_emit(pCallback->m_pDocView, doc_view_signals[WINDOW], 0, pCallback->m_aPayload.c_str()); + break; + case LOK_CALLBACK_INVALIDATE_HEADER: + g_signal_emit(pCallback->m_pDocView, doc_view_signals[INVALIDATE_HEADER], 0, pCallback->m_aPayload.c_str()); + break; + case LOK_CALLBACK_REFERENCE_MARKS: + { + std::stringstream aStream(pCallback->m_aPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + + priv->m_aReferenceMarks.clear(); + + for(const auto& rMark : aTree.get_child("marks")) + { + sal_uInt32 nColor = std::stoi(rMark.second.get<std::string>("color"), nullptr, 16); + std::string sRect = rMark.second.get<std::string>("rectangle"); + sal_uInt32 nPart = std::stoi(rMark.second.get<std::string>("part")); + + GdkRectangle aRect = payloadToRectangle(pDocView, sRect.c_str()); + priv->m_aReferenceMarks.push_back(std::pair<ViewRectangle, sal_uInt32>(ViewRectangle(nPart, aRect), nColor)); + } + + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); + break; + } + + case LOK_CALLBACK_CONTENT_CONTROL: + { + std::stringstream aPayloadStream(pCallback->m_aPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aPayloadStream, aTree); + auto aAction = aTree.get<std::string>("action"); + if (aAction == "show") + { + auto aRectangles = aTree.get<std::string>("rectangles"); + priv->m_aContentControlRectangles = payloadToRectangles(pDocView, aRectangles.c_str()); + } + else if (aAction == "hide") + { + priv->m_aContentControlRectangles.clear(); + } + else if (aAction == "change-picture") + { + GtkWidget* pDialog = gtk_file_chooser_dialog_new( + "Open File", GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(pDocView))), + GTK_FILE_CHOOSER_ACTION_OPEN, "Cancel", GTK_RESPONSE_CANCEL, "Open", + GTK_RESPONSE_ACCEPT, nullptr); + gint nRet = gtk_dialog_run(GTK_DIALOG(pDialog)); + if (nRet == GTK_RESPONSE_ACCEPT) + { + GtkFileChooser* pChooser = GTK_FILE_CHOOSER(pDialog); + char* pFilename = gtk_file_chooser_get_uri(pChooser); + boost::property_tree::ptree aValues; + aValues.put("type", "picture"); + aValues.put("changed", pFilename); + std::stringstream aStream; + boost::property_tree::write_json(aStream, aValues); + std::string aJson = aStream.str(); + lok_doc_view_send_content_control_event(pDocView, aJson.c_str()); + + g_free(pFilename); + } + gtk_widget_destroy(pDialog); + } + 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; + + case LOK_CALLBACK_STATUS_INDICATOR_START: + case LOK_CALLBACK_STATUS_INDICATOR_SET_VALUE: + case LOK_CALLBACK_STATUS_INDICATOR_FINISH: + case LOK_CALLBACK_DOCUMENT_PASSWORD: + case LOK_CALLBACK_DOCUMENT_PASSWORD_TO_MODIFY: + case LOK_CALLBACK_VALIDITY_LIST_BUTTON: + case LOK_CALLBACK_VALIDITY_INPUT_HELP: + case LOK_CALLBACK_SIGNATURE_STATUS: + case LOK_CALLBACK_CONTEXT_MENU: + case LOK_CALLBACK_PROFILE_FRAME: + case LOK_CALLBACK_CLIPBOARD_CHANGED: + case LOK_CALLBACK_CONTEXT_CHANGED: + case LOK_CALLBACK_CELL_SELECTION_AREA: + case LOK_CALLBACK_CELL_AUTO_FILL_AREA: + case LOK_CALLBACK_TABLE_SELECTED: + case LOK_CALLBACK_JSDIALOG: + case LOK_CALLBACK_CALC_FUNCTION_LIST: + case LOK_CALLBACK_TAB_STOP_LIST: + case LOK_CALLBACK_FORM_FIELD_BUTTON: + case LOK_CALLBACK_INVALIDATE_SHEET_GEOMETRY: + case LOK_CALLBACK_DOCUMENT_BACKGROUND_COLOR: + case LOK_COMMAND_BLOCKED: + case LOK_CALLBACK_SC_FOLLOW_JUMP: + case LOK_CALLBACK_PRINT_RANGES: + { + // TODO: Implement me + break; + } + } + delete pCallback; + + return G_SOURCE_REMOVE; +} + +static void callbackWorker (int nType, const char* pPayload, void* pData) +{ + LOKDocView* pDocView = LOK_DOC_VIEW (pData); + + CallbackData* pCallback = new CallbackData(nType, pPayload ? pPayload : "(nil)", pDocView); + LOKDocViewPrivate& priv = getPrivate(pDocView); + std::stringstream ss; + ss << "callbackWorker, view #" << priv->m_nViewId << ": " << lokCallbackTypeToString(nType) << ", '" << (pPayload ? pPayload : "(nil)") << "'"; + g_info("%s", ss.str().c_str()); + gdk_threads_add_idle(callback, pCallback); +} + +static void +renderHandle(LOKDocView* pDocView, + cairo_t* pCairo, + const GdkRectangle& rCursor, + cairo_surface_t* pHandle, + GdkRectangle& rRectangle) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + gint nScaleFactor = gtk_widget_get_scale_factor(GTK_WIDGET(pDocView)); + GdkPoint aCursorBottom; + int nHandleWidth, nHandleHeight; + double fHandleScale; + + nHandleWidth = cairo_image_surface_get_width(pHandle); + nHandleHeight = cairo_image_surface_get_height(pHandle); + // We want to scale down the handle, so that its height is the same as the cursor caret. + fHandleScale = twipToPixel(rCursor.height, priv->m_fZoom) / nHandleHeight; + // We want the top center of the handle bitmap to be at the bottom center of the cursor rectangle. + aCursorBottom.x = twipToPixel(rCursor.x, priv->m_fZoom) + twipToPixel(rCursor.width, priv->m_fZoom) / 2 - (nHandleWidth * fHandleScale) / 2; + aCursorBottom.y = twipToPixel(rCursor.y, priv->m_fZoom) + twipToPixel(rCursor.height, priv->m_fZoom); + + cairo_save (pCairo); + cairo_scale(pCairo, 1.0 / nScaleFactor, 1.0 / nScaleFactor); + cairo_translate(pCairo, aCursorBottom.x * nScaleFactor, aCursorBottom.y * nScaleFactor); + cairo_scale(pCairo, fHandleScale * nScaleFactor, fHandleScale * nScaleFactor); + cairo_set_source_surface(pCairo, pHandle, 0, 0); + cairo_paint(pCairo); + cairo_restore (pCairo); + + rRectangle.x = aCursorBottom.x; + rRectangle.y = aCursorBottom.y; + rRectangle.width = nHandleWidth * fHandleScale; + rRectangle.height = nHandleHeight * fHandleScale; +} + +/// Renders handles around an rSelection rectangle on pCairo. +static void +renderGraphicHandle(LOKDocView* pDocView, + cairo_t* pCairo, + const GdkRectangle& rSelection, + const GdkRGBA& rColor) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + int nHandleWidth = 9, nHandleHeight = 9; + GdkRectangle aSelection; + + aSelection.x = twipToPixel(rSelection.x, priv->m_fZoom); + aSelection.y = twipToPixel(rSelection.y, priv->m_fZoom); + aSelection.width = twipToPixel(rSelection.width, priv->m_fZoom); + aSelection.height = twipToPixel(rSelection.height, priv->m_fZoom); + + for (int i = 0; i < GRAPHIC_HANDLE_COUNT; ++i) + { + int x = aSelection.x, y = aSelection.y; + + switch (i) + { + case 0: // top-left + break; + case 1: // top-middle + x += aSelection.width / 2; + break; + case 2: // top-right + x += aSelection.width; + break; + case 3: // middle-left + y += aSelection.height / 2; + break; + case 4: // middle-right + x += aSelection.width; + y += aSelection.height / 2; + break; + case 5: // bottom-left + y += aSelection.height; + break; + case 6: // bottom-middle + x += aSelection.width / 2; + y += aSelection.height; + break; + case 7: // bottom-right + x += aSelection.width; + y += aSelection.height; + break; + } + + // Center the handle. + x -= nHandleWidth / 2; + y -= nHandleHeight / 2; + + priv->m_aGraphicHandleRects[i].x = x; + priv->m_aGraphicHandleRects[i].y = y; + priv->m_aGraphicHandleRects[i].width = nHandleWidth; + priv->m_aGraphicHandleRects[i].height = nHandleHeight; + + cairo_set_source_rgb(pCairo, rColor.red, rColor.green, rColor.blue); + cairo_rectangle(pCairo, x, y, nHandleWidth, nHandleHeight); + cairo_fill(pCairo); + } +} + +/// Finishes the paint tile operation and returns the result, if any +static gpointer +paintTileFinish(LOKDocView* pDocView, GAsyncResult* res, GError **error) +{ + GTask* task = G_TASK(res); + + g_return_val_if_fail(LOK_IS_DOC_VIEW(pDocView), nullptr); + g_return_val_if_fail(g_task_is_valid(res, pDocView), nullptr); + g_return_val_if_fail(error == nullptr || *error == nullptr, nullptr); + + return g_task_propagate_pointer(task, error); +} + +/// Callback called in the main UI thread when paintTileInThread in LOK thread has finished +static void +paintTileCallback(GObject* sourceObject, GAsyncResult* res, gpointer userData) +{ + LOKDocView* pDocView = LOK_DOC_VIEW(sourceObject); + LOKDocViewPrivate& priv = getPrivate(pDocView); + LOEvent* pLOEvent = static_cast<LOEvent*>(userData); + std::unique_ptr<TileBuffer>& buffer = priv->m_pTileBuffer; + GError* error; + + error = nullptr; + cairo_surface_t* pSurface = static_cast<cairo_surface_t*>(paintTileFinish(pDocView, res, &error)); + if (error != nullptr) + { + if (error->domain == LOK_TILEBUFFER_ERROR && + error->code == LOK_TILEBUFFER_CHANGED) + g_info("Skipping paint tile request because corresponding" + "tile buffer has been destroyed"); + else + g_warning("Unable to get painted GdkPixbuf: %s", error->message); + g_error_free(error); + return; + } + + buffer->setTile(pLOEvent->m_nPaintTileX, pLOEvent->m_nPaintTileY, pSurface); + gdk_threads_add_idle(queueDraw, GTK_WIDGET(pDocView)); + + cairo_surface_destroy(pSurface); +} + + +static bool +renderDocument(LOKDocView* pDocView, cairo_t* pCairo) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + GdkRectangle aVisibleArea; + gint nScaleFactor = gtk_widget_get_scale_factor(GTK_WIDGET(pDocView)); + gint nTileSizePixelsScaled = nTileSizePixels * nScaleFactor; + long nDocumentWidthPixels = twipToPixel(priv->m_nDocumentWidthTwips, priv->m_fZoom) * nScaleFactor; + long nDocumentHeightPixels = twipToPixel(priv->m_nDocumentHeightTwips, priv->m_fZoom) * nScaleFactor; + // Total number of rows / columns in this document. + guint nRows = ceil(static_cast<double>(nDocumentHeightPixels) / nTileSizePixelsScaled); + guint nColumns = ceil(static_cast<double>(nDocumentWidthPixels) / nTileSizePixelsScaled); + + cairo_save (pCairo); + cairo_scale (pCairo, 1.0/nScaleFactor, 1.0/nScaleFactor); + gdk_cairo_get_clip_rectangle (pCairo, &aVisibleArea); + aVisibleArea.x = pixelToTwip (aVisibleArea.x, priv->m_fZoom); + aVisibleArea.y = pixelToTwip (aVisibleArea.y, priv->m_fZoom); + aVisibleArea.width = pixelToTwip (aVisibleArea.width, priv->m_fZoom); + aVisibleArea.height = pixelToTwip (aVisibleArea.height, priv->m_fZoom); + + // Render the tiles. + for (guint nRow = 0; nRow < nRows; ++nRow) + { + for (guint nColumn = 0; nColumn < nColumns; ++nColumn) + { + GdkRectangle aTileRectangleTwips, aTileRectanglePixels; + bool bPaint = true; + + // Determine size of the tile: the rightmost/bottommost tiles may + // be smaller, and we need the size to decide if we need to repaint. + if (nColumn == nColumns - 1) + aTileRectanglePixels.width = nDocumentWidthPixels - nColumn * nTileSizePixelsScaled; + else + aTileRectanglePixels.width = nTileSizePixelsScaled; + if (nRow == nRows - 1) + aTileRectanglePixels.height = nDocumentHeightPixels - nRow * nTileSizePixelsScaled; + else + aTileRectanglePixels.height = nTileSizePixelsScaled; + + // Determine size and position of the tile in document coordinates, + // so we can decide if we can skip painting for partial rendering. + aTileRectangleTwips.x = pixelToTwip(nTileSizePixelsScaled, priv->m_fZoom) * nColumn; + aTileRectangleTwips.y = pixelToTwip(nTileSizePixelsScaled, priv->m_fZoom) * nRow; + aTileRectangleTwips.width = pixelToTwip(aTileRectanglePixels.width, priv->m_fZoom); + aTileRectangleTwips.height = pixelToTwip(aTileRectanglePixels.height, priv->m_fZoom); + + if (!gdk_rectangle_intersect(&aVisibleArea, &aTileRectangleTwips, nullptr)) + bPaint = false; + + if (bPaint) + { + LOEvent* pLOEvent = new LOEvent(LOK_PAINT_TILE); + pLOEvent->m_nPaintTileX = nRow; + pLOEvent->m_nPaintTileY = nColumn; + pLOEvent->m_fPaintTileZoom = priv->m_fZoom; + pLOEvent->m_pTileBuffer = &*priv->m_pTileBuffer; + GTask* task = g_task_new(pDocView, nullptr, paintTileCallback, pLOEvent); + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + + Tile& currentTile = priv->m_pTileBuffer->getTile(nRow, nColumn, task, priv->lokThreadPool); + cairo_surface_t* pSurface = currentTile.getBuffer(); + cairo_set_source_surface(pCairo, pSurface, + twipToPixel(aTileRectangleTwips.x, priv->m_fZoom), + twipToPixel(aTileRectangleTwips.y, priv->m_fZoom)); + cairo_paint(pCairo); + g_object_unref(task); + } + } + } + + cairo_restore (pCairo); + return false; +} + +static const GdkRGBA& getDarkColor(int nViewId, LOKDocViewPrivate& priv) +{ + static std::map<int, GdkRGBA> aColorMap; + auto it = aColorMap.find(nViewId); + if (it != aColorMap.end()) + return it->second; + + if (priv->m_eDocumentType == LOK_DOCTYPE_TEXT) + { + char* pValues = priv->m_pDocument->pClass->getCommandValues(priv->m_pDocument, ".uno:TrackedChangeAuthors"); + std::stringstream aInfo; + aInfo << "lok::Document::getCommandValues('.uno:TrackedChangeAuthors') returned '" << pValues << "'" << std::endl; + g_info("%s", aInfo.str().c_str()); + + std::stringstream aStream(pValues); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + for (const auto& rValue : aTree.get_child("authors")) + { + const std::string& rName = rValue.second.get<std::string>("name"); + guint32 nColor = rValue.second.get<guint32>("color"); + GdkRGBA aColor{static_cast<double>(static_cast<guint8>(nColor>>16))/255, static_cast<double>(static_cast<guint8>(static_cast<guint16>(nColor) >> 8))/255, static_cast<double>(static_cast<guint8>(nColor))/255, 0}; + auto itAuthorViews = g_aAuthorViews.find(rName); + if (itAuthorViews != g_aAuthorViews.end()) + aColorMap[itAuthorViews->second] = aColor; + } + } + else + { + // Based on tools/color.hxx, COL_AUTHOR1_DARK..COL_AUTHOR9_DARK. + static std::vector<GdkRGBA> aColors = + { + {(double(198))/255, (double(146))/255, (double(0))/255, 0}, + {(double(6))/255, (double(70))/255, (double(162))/255, 0}, + {(double(87))/255, (double(157))/255, (double(28))/255, 0}, + {(double(105))/255, (double(43))/255, (double(157))/255, 0}, + {(double(197))/255, (double(0))/255, (double(11))/255, 0}, + {(double(0))/255, (double(128))/255, (double(128))/255, 0}, + {(double(140))/255, (double(132))/255, (double(0))/255, 0}, + {(double(43))/255, (double(85))/255, (double(107))/255, 0}, + {(double(209))/255, (double(118))/255, (double(0))/255, 0}, + }; + static int nColorCounter = 0; + GdkRGBA aColor = aColors[nColorCounter++ % aColors.size()]; + aColorMap[nViewId] = aColor; + } + assert(aColorMap.find(nViewId) != aColorMap.end()); + return aColorMap[nViewId]; +} + +static bool +renderOverlay(LOKDocView* pDocView, cairo_t* pCairo) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + + if (priv->m_bEdit && priv->m_bCursorVisible && priv->m_bCursorOverlayVisible && !isEmptyRectangle(priv->m_aVisibleCursor)) + { + if (priv->m_aVisibleCursor.width < 30) + // Set a minimal width if it would be 0. + priv->m_aVisibleCursor.width = 30; + + cairo_set_source_rgb(pCairo, 0, 0, 0); + cairo_rectangle(pCairo, + twipToPixel(priv->m_aVisibleCursor.x, priv->m_fZoom), + twipToPixel(priv->m_aVisibleCursor.y, priv->m_fZoom), + twipToPixel(priv->m_aVisibleCursor.width, priv->m_fZoom), + twipToPixel(priv->m_aVisibleCursor.height, priv->m_fZoom)); + cairo_fill(pCairo); + } + + // View cursors: they do not blink and are colored. + if (priv->m_bEdit && !priv->m_aViewCursors.empty()) + { + for (auto& rPair : priv->m_aViewCursors) + { + auto itVisibility = priv->m_aViewCursorVisibilities.find(rPair.first); + if (itVisibility != priv->m_aViewCursorVisibilities.end() && !itVisibility->second) + continue; + + // Show view cursors when in Writer or when the part matches. + if (rPair.second.m_nPart != priv->m_nPartId && priv->m_eDocumentType != LOK_DOCTYPE_TEXT) + continue; + + GdkRectangle& rCursor = rPair.second.m_aRectangle; + if (rCursor.width < 30) + // Set a minimal width if it would be 0. + rCursor.width = 30; + + const GdkRGBA& rDark = getDarkColor(rPair.first, priv); + cairo_set_source_rgb(pCairo, rDark.red, rDark.green, rDark.blue); + cairo_rectangle(pCairo, + twipToPixel(rCursor.x, priv->m_fZoom), + twipToPixel(rCursor.y, priv->m_fZoom), + twipToPixel(rCursor.width, priv->m_fZoom), + twipToPixel(rCursor.height, priv->m_fZoom)); + cairo_fill(pCairo); + } + } + + if (priv->m_bEdit && priv->m_bCursorVisible && !isEmptyRectangle(priv->m_aVisibleCursor) && priv->m_aTextSelectionRectangles.empty()) + { + // Have a cursor, but no selection: we need the middle handle. + gchar* handleMiddlePath = g_strconcat (priv->m_aLOPath.c_str(), CURSOR_HANDLE_DIR, "handle_image_middle.png", nullptr); + if (!priv->m_pHandleMiddle) + { + priv->m_pHandleMiddle = cairo_image_surface_create_from_png(handleMiddlePath); + assert(cairo_surface_status(priv->m_pHandleMiddle) == CAIRO_STATUS_SUCCESS); + } + g_free (handleMiddlePath); + renderHandle(pDocView, pCairo, priv->m_aVisibleCursor, priv->m_pHandleMiddle, priv->m_aHandleMiddleRect); + } + + if (!priv->m_aTextSelectionRectangles.empty()) + { + for (const GdkRectangle& rRectangle : priv->m_aTextSelectionRectangles) + { + // Blue with 75% transparency. + cairo_set_source_rgba(pCairo, (double(0x43))/255, (double(0xac))/255, (double(0xe8))/255, 0.25); + cairo_rectangle(pCairo, + twipToPixel(rRectangle.x, priv->m_fZoom), + twipToPixel(rRectangle.y, priv->m_fZoom), + twipToPixel(rRectangle.width, priv->m_fZoom), + twipToPixel(rRectangle.height, priv->m_fZoom)); + cairo_fill(pCairo); + } + + // Handles + if (!isEmptyRectangle(priv->m_aTextSelectionStart)) + { + // Have a start position: we need a start handle. + gchar* handleStartPath = g_strconcat (priv->m_aLOPath.c_str(), CURSOR_HANDLE_DIR, "handle_image_start.png", nullptr); + if (!priv->m_pHandleStart) + { + priv->m_pHandleStart = cairo_image_surface_create_from_png(handleStartPath); + assert(cairo_surface_status(priv->m_pHandleStart) == CAIRO_STATUS_SUCCESS); + } + renderHandle(pDocView, pCairo, priv->m_aTextSelectionStart, priv->m_pHandleStart, priv->m_aHandleStartRect); + g_free (handleStartPath); + } + if (!isEmptyRectangle(priv->m_aTextSelectionEnd)) + { + // Have a start position: we need an end handle. + gchar* handleEndPath = g_strconcat (priv->m_aLOPath.c_str(), CURSOR_HANDLE_DIR, "handle_image_end.png", nullptr); + if (!priv->m_pHandleEnd) + { + priv->m_pHandleEnd = cairo_image_surface_create_from_png(handleEndPath); + assert(cairo_surface_status(priv->m_pHandleEnd) == CAIRO_STATUS_SUCCESS); + } + renderHandle(pDocView, pCairo, priv->m_aTextSelectionEnd, priv->m_pHandleEnd, priv->m_aHandleEndRect); + g_free (handleEndPath); + } + } + + if (!priv->m_aContentControlRectangles.empty()) + { + for (const GdkRectangle& rRectangle : priv->m_aContentControlRectangles) + { + // Black with 75% transparency. + cairo_set_source_rgba(pCairo, (double(0x7f))/255, (double(0x7f))/255, (double(0x7f))/255, 0.25); + cairo_rectangle(pCairo, + twipToPixel(rRectangle.x, priv->m_fZoom), + twipToPixel(rRectangle.y, priv->m_fZoom), + twipToPixel(rRectangle.width, priv->m_fZoom), + twipToPixel(rRectangle.height, priv->m_fZoom)); + cairo_fill(pCairo); + } + } + + // Selections of other views. + for (const auto& rPair : priv->m_aTextViewSelectionRectangles) + { + if (rPair.second.m_nPart != priv->m_nPartId && priv->m_eDocumentType != LOK_DOCTYPE_TEXT) + continue; + + for (const GdkRectangle& rRectangle : rPair.second.m_aRectangles) + { + const GdkRGBA& rDark = getDarkColor(rPair.first, priv); + // 75% transparency. + cairo_set_source_rgba(pCairo, rDark.red, rDark.green, rDark.blue, 0.25); + cairo_rectangle(pCairo, + twipToPixel(rRectangle.x, priv->m_fZoom), + twipToPixel(rRectangle.y, priv->m_fZoom), + twipToPixel(rRectangle.width, priv->m_fZoom), + twipToPixel(rRectangle.height, priv->m_fZoom)); + cairo_fill(pCairo); + } + } + + if (!isEmptyRectangle(priv->m_aGraphicSelection)) + { + GdkRGBA const aBlack{0, 0, 0, 0}; + renderGraphicHandle(pDocView, pCairo, priv->m_aGraphicSelection, aBlack); + } + + // Graphic selections of other views. + for (const auto& rPair : priv->m_aGraphicViewSelections) + { + const ViewRectangle& rRectangle = rPair.second; + if (rRectangle.m_nPart != priv->m_nPartId && priv->m_eDocumentType != LOK_DOCTYPE_TEXT) + continue; + + const GdkRGBA& rDark = getDarkColor(rPair.first, priv); + renderGraphicHandle(pDocView, pCairo, rRectangle.m_aRectangle, rDark); + } + + // Draw the cell cursor. + if (!isEmptyRectangle(priv->m_aCellCursor)) + { + cairo_set_source_rgb(pCairo, 0, 0, 0); + cairo_rectangle(pCairo, + twipToPixel(priv->m_aCellCursor.x, priv->m_fZoom), + twipToPixel(priv->m_aCellCursor.y, priv->m_fZoom), + twipToPixel(priv->m_aCellCursor.width, priv->m_fZoom), + twipToPixel(priv->m_aCellCursor.height, priv->m_fZoom)); + cairo_set_line_width(pCairo, 2.0); + cairo_stroke(pCairo); + } + + // Cell view cursors: they are colored. + for (const auto& rPair : priv->m_aCellViewCursors) + { + const ViewRectangle& rCursor = rPair.second; + if (rCursor.m_nPart != priv->m_nPartId) + continue; + + const GdkRGBA& rDark = getDarkColor(rPair.first, priv); + cairo_set_source_rgb(pCairo, rDark.red, rDark.green, rDark.blue); + cairo_rectangle(pCairo, + twipToPixel(rCursor.m_aRectangle.x, priv->m_fZoom), + twipToPixel(rCursor.m_aRectangle.y, priv->m_fZoom), + twipToPixel(rCursor.m_aRectangle.width, priv->m_fZoom), + twipToPixel(rCursor.m_aRectangle.height, priv->m_fZoom)); + cairo_set_line_width(pCairo, 2.0); + cairo_stroke(pCairo); + } + + // Draw reference marks. + for (const auto& rPair : priv->m_aReferenceMarks) + { + const ViewRectangle& rMark = rPair.first; + if (rMark.m_nPart != priv->m_nPartId) + continue; + + sal_uInt32 nColor = rPair.second; + sal_uInt8 nRed = (nColor >> 16) & 0xff; + sal_uInt8 nGreen = (nColor >> 8) & 0xff; + sal_uInt8 nBlue = nColor & 0xff; + cairo_set_source_rgb(pCairo, nRed, nGreen, nBlue); + cairo_rectangle(pCairo, + twipToPixel(rMark.m_aRectangle.x, priv->m_fZoom), + twipToPixel(rMark.m_aRectangle.y, priv->m_fZoom), + twipToPixel(rMark.m_aRectangle.width, priv->m_fZoom), + twipToPixel(rMark.m_aRectangle.height, priv->m_fZoom)); + cairo_set_line_width(pCairo, 2.0); + cairo_stroke(pCairo); + } + + // View locks: they are colored. + for (const auto& rPair : priv->m_aViewLockRectangles) + { + const ViewRectangle& rRectangle = rPair.second; + if (rRectangle.m_nPart != priv->m_nPartId) + continue; + + // Draw a rectangle. + const GdkRGBA& rDark = getDarkColor(rPair.first, priv); + cairo_set_source_rgb(pCairo, rDark.red, rDark.green, rDark.blue); + cairo_rectangle(pCairo, + twipToPixel(rRectangle.m_aRectangle.x, priv->m_fZoom), + twipToPixel(rRectangle.m_aRectangle.y, priv->m_fZoom), + twipToPixel(rRectangle.m_aRectangle.width, priv->m_fZoom), + twipToPixel(rRectangle.m_aRectangle.height, priv->m_fZoom)); + cairo_set_line_width(pCairo, 2.0); + cairo_stroke(pCairo); + + // And a lock. + cairo_rectangle(pCairo, + twipToPixel(rRectangle.m_aRectangle.x + rRectangle.m_aRectangle.width, priv->m_fZoom) - 25, + twipToPixel(rRectangle.m_aRectangle.y + rRectangle.m_aRectangle.height, priv->m_fZoom) - 15, + 20, + 10); + cairo_fill(pCairo); + cairo_arc(pCairo, + twipToPixel(rRectangle.m_aRectangle.x + rRectangle.m_aRectangle.width, priv->m_fZoom) - 15, + twipToPixel(rRectangle.m_aRectangle.y + rRectangle.m_aRectangle.height, priv->m_fZoom) - 15, + 5, + M_PI, + 2 * M_PI); + cairo_stroke(pCairo); + } + + return false; +} + +static gboolean +lok_doc_view_signal_button(GtkWidget* pWidget, GdkEventButton* pEvent) +{ + LOKDocView* pDocView = LOK_DOC_VIEW (pWidget); + LOKDocViewPrivate& priv = getPrivate(pDocView); + GError* error = nullptr; + + g_info("LOKDocView_Impl::signalButton: %d, %d (in twips: %d, %d)", + static_cast<int>(pEvent->x), static_cast<int>(pEvent->y), + static_cast<int>(pixelToTwip(pEvent->x, priv->m_fZoom)), + static_cast<int>(pixelToTwip(pEvent->y, priv->m_fZoom))); + gtk_widget_grab_focus(GTK_WIDGET(pDocView)); + + switch (pEvent->type) + { + case GDK_BUTTON_PRESS: + { + GdkRectangle aClick; + aClick.x = pEvent->x; + aClick.y = pEvent->y; + aClick.width = 1; + aClick.height = 1; + + if (handleTextSelectionOnButtonPress(aClick, pDocView)) + return FALSE; + if (handleGraphicSelectionOnButtonPress(aClick, pDocView)) + return FALSE; + + int nCount = 1; + if ((pEvent->time - priv->m_nLastButtonPressTime) < 250) + nCount++; + priv->m_nLastButtonPressTime = pEvent->time; + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + LOEvent* pLOEvent = new LOEvent(LOK_POST_MOUSE_EVENT); + pLOEvent->m_nPostMouseEventType = LOK_MOUSEEVENT_MOUSEBUTTONDOWN; + pLOEvent->m_nPostMouseEventX = pixelToTwip(pEvent->x, priv->m_fZoom); + pLOEvent->m_nPostMouseEventY = pixelToTwip(pEvent->y, priv->m_fZoom); + pLOEvent->m_nPostMouseEventCount = nCount; + switch (pEvent->button) + { + case 1: + pLOEvent->m_nPostMouseEventButton = MOUSE_LEFT; + break; + case 2: + pLOEvent->m_nPostMouseEventButton = MOUSE_MIDDLE; + break; + case 3: + pLOEvent->m_nPostMouseEventButton = MOUSE_RIGHT; + break; + } + pLOEvent->m_nPostMouseEventModifier = priv->m_nKeyModifier; + priv->m_nLastButtonPressed = pLOEvent->m_nPostMouseEventButton; + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_POST_MOUSE_EVENT: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); + break; + } + case GDK_BUTTON_RELEASE: + { + if (handleTextSelectionOnButtonRelease(pDocView)) + return FALSE; + if (handleGraphicSelectionOnButtonRelease(pDocView, pEvent)) + return FALSE; + + int nCount = 1; + if ((pEvent->time - priv->m_nLastButtonReleaseTime) < 250) + nCount++; + priv->m_nLastButtonReleaseTime = pEvent->time; + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + LOEvent* pLOEvent = new LOEvent(LOK_POST_MOUSE_EVENT); + pLOEvent->m_nPostMouseEventType = LOK_MOUSEEVENT_MOUSEBUTTONUP; + pLOEvent->m_nPostMouseEventX = pixelToTwip(pEvent->x, priv->m_fZoom); + pLOEvent->m_nPostMouseEventY = pixelToTwip(pEvent->y, priv->m_fZoom); + pLOEvent->m_nPostMouseEventCount = nCount; + switch (pEvent->button) + { + case 1: + pLOEvent->m_nPostMouseEventButton = MOUSE_LEFT; + break; + case 2: + pLOEvent->m_nPostMouseEventButton = MOUSE_MIDDLE; + break; + case 3: + pLOEvent->m_nPostMouseEventButton = MOUSE_RIGHT; + break; + } + pLOEvent->m_nPostMouseEventModifier = priv->m_nKeyModifier; + priv->m_nLastButtonPressed = pLOEvent->m_nPostMouseEventButton; + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_POST_MOUSE_EVENT: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); + break; + } + default: + break; + } + return FALSE; +} + +static void +getDragPoint(GdkRectangle* pHandle, + GdkEventMotion* pEvent, + GdkPoint* pPoint) +{ + GdkPoint aCursor, aHandle; + + // Center of the cursor rectangle: we know that it's above the handle. + aCursor.x = pHandle->x + pHandle->width / 2; + aCursor.y = pHandle->y - pHandle->height / 2; + // Center of the handle rectangle. + aHandle.x = pHandle->x + pHandle->width / 2; + aHandle.y = pHandle->y + pHandle->height / 2; + // Our target is the original cursor position + the dragged offset. + pPoint->x = aCursor.x + (pEvent->x - aHandle.x); + pPoint->y = aCursor.y + (pEvent->y - aHandle.y); +} + +static gboolean +lok_doc_view_signal_motion (GtkWidget* pWidget, GdkEventMotion* pEvent) +{ + LOKDocView* pDocView = LOK_DOC_VIEW (pWidget); + LOKDocViewPrivate& priv = getPrivate(pDocView); + GdkPoint aPoint; + GError* error = nullptr; + + std::unique_lock<std::mutex> aGuard(g_aLOKMutex); + std::stringstream ss; + ss << "lok::Document::setView(" << priv->m_nViewId << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId); + if (priv->m_bInDragMiddleHandle) + { + g_info("lcl_signalMotion: dragging the middle handle"); + getDragPoint(&priv->m_aHandleMiddleRect, pEvent, &aPoint); + priv->m_pDocument->pClass->setTextSelection(priv->m_pDocument, LOK_SETTEXTSELECTION_RESET, pixelToTwip(aPoint.x, priv->m_fZoom), pixelToTwip(aPoint.y, priv->m_fZoom)); + return FALSE; + } + if (priv->m_bInDragStartHandle) + { + g_info("lcl_signalMotion: dragging the start handle"); + getDragPoint(&priv->m_aHandleStartRect, pEvent, &aPoint); + priv->m_pDocument->pClass->setTextSelection(priv->m_pDocument, LOK_SETTEXTSELECTION_START, pixelToTwip(aPoint.x, priv->m_fZoom), pixelToTwip(aPoint.y, priv->m_fZoom)); + return FALSE; + } + if (priv->m_bInDragEndHandle) + { + g_info("lcl_signalMotion: dragging the end handle"); + getDragPoint(&priv->m_aHandleEndRect, pEvent, &aPoint); + priv->m_pDocument->pClass->setTextSelection(priv->m_pDocument, LOK_SETTEXTSELECTION_END, pixelToTwip(aPoint.x, priv->m_fZoom), pixelToTwip(aPoint.y, priv->m_fZoom)); + return FALSE; + } + aGuard.unlock(); + for (int i = 0; i < GRAPHIC_HANDLE_COUNT; ++i) + { + if (priv->m_bInDragGraphicHandles[i]) + { + g_info("lcl_signalMotion: dragging the graphic handle #%d", i); + return FALSE; + } + } + if (priv->m_bInDragGraphicSelection) + { + g_info("lcl_signalMotion: dragging the graphic selection"); + return FALSE; + } + + GdkRectangle aMotionInTwipsInTwips; + aMotionInTwipsInTwips.x = pixelToTwip(pEvent->x, priv->m_fZoom); + aMotionInTwipsInTwips.y = pixelToTwip(pEvent->y, priv->m_fZoom); + aMotionInTwipsInTwips.width = 1; + aMotionInTwipsInTwips.height = 1; + if (gdk_rectangle_intersect(&aMotionInTwipsInTwips, &priv->m_aGraphicSelection, nullptr)) + { + g_info("lcl_signalMotion: start of drag graphic selection"); + priv->m_bInDragGraphicSelection = true; + + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + LOEvent* pLOEvent = new LOEvent(LOK_SET_GRAPHIC_SELECTION); + pLOEvent->m_nSetGraphicSelectionType = LOK_SETGRAPHICSELECTION_START; + pLOEvent->m_nSetGraphicSelectionX = pixelToTwip(pEvent->x, priv->m_fZoom); + pLOEvent->m_nSetGraphicSelectionY = pixelToTwip(pEvent->y, priv->m_fZoom); + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_SET_GRAPHIC_SELECTION: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); + + return FALSE; + } + + // Otherwise a mouse move, as on the desktop. + + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + LOEvent* pLOEvent = new LOEvent(LOK_POST_MOUSE_EVENT); + pLOEvent->m_nPostMouseEventType = LOK_MOUSEEVENT_MOUSEMOVE; + pLOEvent->m_nPostMouseEventX = pixelToTwip(pEvent->x, priv->m_fZoom); + pLOEvent->m_nPostMouseEventY = pixelToTwip(pEvent->y, priv->m_fZoom); + pLOEvent->m_nPostMouseEventCount = 1; + pLOEvent->m_nPostMouseEventButton = priv->m_nLastButtonPressed; + pLOEvent->m_nPostMouseEventModifier = priv->m_nKeyModifier; + + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_MOUSEEVENT_MOUSEMOVE: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); + + return FALSE; +} + +static void +setGraphicSelectionInThread(gpointer data) +{ + GTask* task = G_TASK(data); + LOKDocView* pDocView = LOK_DOC_VIEW(g_task_get_source_object(task)); + LOKDocViewPrivate& priv = getPrivate(pDocView); + LOEvent* pLOEvent = static_cast<LOEvent*>(g_task_get_task_data(task)); + + std::scoped_lock<std::mutex> aGuard(g_aLOKMutex); + std::stringstream ss; + ss << "lok::Document::setView(" << priv->m_nViewId << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId); + ss.str(std::string()); + ss << "lok::Document::setGraphicSelection(" << pLOEvent->m_nSetGraphicSelectionType; + ss << ", " << pLOEvent->m_nSetGraphicSelectionX; + ss << ", " << pLOEvent->m_nSetGraphicSelectionY << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setGraphicSelection(priv->m_pDocument, + pLOEvent->m_nSetGraphicSelectionType, + pLOEvent->m_nSetGraphicSelectionX, + pLOEvent->m_nSetGraphicSelectionY); +} + +static void +setClientZoomInThread(gpointer data) +{ + GTask* task = G_TASK(data); + LOKDocView* pDocView = LOK_DOC_VIEW(g_task_get_source_object(task)); + LOKDocViewPrivate& priv = getPrivate(pDocView); + LOEvent* pLOEvent = static_cast<LOEvent*>(g_task_get_task_data(task)); + + std::scoped_lock<std::mutex> aGuard(g_aLOKMutex); + priv->m_pDocument->pClass->setClientZoom(priv->m_pDocument, + pLOEvent->m_nTilePixelWidth, + pLOEvent->m_nTilePixelHeight, + pLOEvent->m_nTileTwipWidth, + pLOEvent->m_nTileTwipHeight); +} + +static void +postMouseEventInThread(gpointer data) +{ + GTask* task = G_TASK(data); + LOKDocView* pDocView = LOK_DOC_VIEW(g_task_get_source_object(task)); + LOKDocViewPrivate& priv = getPrivate(pDocView); + LOEvent* pLOEvent = static_cast<LOEvent*>(g_task_get_task_data(task)); + + std::scoped_lock<std::mutex> aGuard(g_aLOKMutex); + std::stringstream ss; + ss << "lok::Document::setView(" << priv->m_nViewId << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId); + ss.str(std::string()); + ss << "lok::Document::postMouseEvent(" << pLOEvent->m_nPostMouseEventType; + ss << ", " << pLOEvent->m_nPostMouseEventX; + ss << ", " << pLOEvent->m_nPostMouseEventY; + ss << ", " << pLOEvent->m_nPostMouseEventCount; + ss << ", " << pLOEvent->m_nPostMouseEventButton; + ss << ", " << pLOEvent->m_nPostMouseEventModifier << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->postMouseEvent(priv->m_pDocument, + pLOEvent->m_nPostMouseEventType, + pLOEvent->m_nPostMouseEventX, + pLOEvent->m_nPostMouseEventY, + pLOEvent->m_nPostMouseEventCount, + pLOEvent->m_nPostMouseEventButton, + pLOEvent->m_nPostMouseEventModifier); +} + +static void +openDocumentInThread (gpointer data) +{ + GTask* task = G_TASK(data); + LOKDocView* pDocView = LOK_DOC_VIEW(g_task_get_source_object(task)); + LOKDocViewPrivate& priv = getPrivate(pDocView); + + std::scoped_lock<std::mutex> aGuard(g_aLOKMutex); + if ( priv->m_pDocument ) + { + priv->m_pDocument->pClass->destroy( priv->m_pDocument ); + priv->m_pDocument = nullptr; + } + + priv->m_pOffice->pClass->registerCallback(priv->m_pOffice, globalCallbackWorker, pDocView); + priv->m_pDocument = priv->m_pOffice->pClass->documentLoadWithOptions( priv->m_pOffice, priv->m_aDocPath.c_str(), "en-US" ); + if ( !priv->m_pDocument ) + { + char *pError = priv->m_pOffice->pClass->getError( priv->m_pOffice ); + g_task_return_new_error(task, g_quark_from_static_string ("LOK error"), 0, "%s", pError); + } + else + { + priv->m_eDocumentType = static_cast<LibreOfficeKitDocumentType>(priv->m_pDocument->pClass->getDocumentType(priv->m_pDocument)); + gdk_threads_add_idle(postDocumentLoad, pDocView); + g_task_return_boolean (task, true); + } +} + +static void +setPartInThread(gpointer data) +{ + GTask* task = G_TASK(data); + LOKDocView* pDocView = LOK_DOC_VIEW(g_task_get_source_object(task)); + LOKDocViewPrivate& priv = getPrivate(pDocView); + LOEvent* pLOEvent = static_cast<LOEvent*>(g_task_get_task_data(task)); + int nPart = pLOEvent->m_nPart; + + std::unique_lock<std::mutex> aGuard(g_aLOKMutex); + std::stringstream ss; + ss << "lok::Document::setView(" << priv->m_nViewId << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId); + priv->m_pDocument->pClass->setPart( priv->m_pDocument, nPart ); + aGuard.unlock(); + + lok_doc_view_reset_view(pDocView); +} + +static void +setPartmodeInThread(gpointer data) +{ + GTask* task = G_TASK(data); + LOKDocView* pDocView = LOK_DOC_VIEW(g_task_get_source_object(task)); + LOKDocViewPrivate& priv = getPrivate(pDocView); + LOEvent* pLOEvent = static_cast<LOEvent*>(g_task_get_task_data(task)); + int nPartMode = pLOEvent->m_nPartMode; + + std::scoped_lock<std::mutex> aGuard(g_aLOKMutex); + std::stringstream ss; + ss << "lok::Document::setView(" << priv->m_nViewId << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId); + priv->m_pDocument->pClass->setPartMode( priv->m_pDocument, nPartMode ); +} + +static void +setEditInThread(gpointer data) +{ + GTask* task = G_TASK(data); + LOKDocView* pDocView = LOK_DOC_VIEW(g_task_get_source_object(task)); + LOKDocViewPrivate& priv = getPrivate(pDocView); + LOEvent* pLOEvent = static_cast<LOEvent*>(g_task_get_task_data(task)); + bool bWasEdit = priv->m_bEdit; + bool bEdit = pLOEvent->m_bEdit; + + if (!priv->m_bEdit && bEdit) + g_info("lok_doc_view_set_edit: entering edit mode"); + else if (priv->m_bEdit && !bEdit) + { + g_info("lok_doc_view_set_edit: leaving edit mode"); + std::scoped_lock<std::mutex> aGuard(g_aLOKMutex); + std::stringstream ss; + ss << "lok::Document::setView(" << priv->m_nViewId << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId); + priv->m_pDocument->pClass->resetSelection(priv->m_pDocument); + } + priv->m_bEdit = bEdit; + g_signal_emit(pDocView, doc_view_signals[EDIT_CHANGED], 0, bWasEdit); + gdk_threads_add_idle(queueDraw, GTK_WIDGET(pDocView)); +} + +static void +postCommandInThread (gpointer data) +{ + GTask* task = G_TASK(data); + LOKDocView* pDocView = LOK_DOC_VIEW(g_task_get_source_object(task)); + LOEvent* pLOEvent = static_cast<LOEvent*>(g_task_get_task_data(task)); + LOKDocViewPrivate& priv = getPrivate(pDocView); + + std::scoped_lock<std::mutex> aGuard(g_aLOKMutex); + std::stringstream ss; + ss << "lok::Document::setView(" << priv->m_nViewId << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId); + ss.str(std::string()); + ss << "lok::Document::postUnoCommand(" << pLOEvent->m_pCommand << ", " << pLOEvent->m_pArguments << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->postUnoCommand(priv->m_pDocument, pLOEvent->m_pCommand, pLOEvent->m_pArguments, pLOEvent->m_bNotifyWhenFinished); +} + +static void +paintTileInThread (gpointer data) +{ + GTask* task = G_TASK(data); + LOKDocView* pDocView = LOK_DOC_VIEW(g_task_get_source_object(task)); + LOKDocViewPrivate& priv = getPrivate(pDocView); + LOEvent* pLOEvent = static_cast<LOEvent*>(g_task_get_task_data(task)); + gint nScaleFactor = gtk_widget_get_scale_factor(GTK_WIDGET(pDocView)); + gint nTileSizePixelsScaled = nTileSizePixels * nScaleFactor; + + // check if "source" tile buffer is different from "current" tile buffer + if (pLOEvent->m_pTileBuffer != &*priv->m_pTileBuffer) + { + pLOEvent->m_pTileBuffer = nullptr; + g_task_return_new_error(task, + LOK_TILEBUFFER_ERROR, + LOK_TILEBUFFER_CHANGED, + "TileBuffer has changed"); + return; + } + std::unique_ptr<TileBuffer>& buffer = priv->m_pTileBuffer; + if (buffer->hasValidTile(pLOEvent->m_nPaintTileX, pLOEvent->m_nPaintTileY)) + return; + + cairo_surface_t *pSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, nTileSizePixelsScaled, nTileSizePixelsScaled); + if (cairo_surface_status(pSurface) != CAIRO_STATUS_SUCCESS) + { + cairo_surface_destroy(pSurface); + g_task_return_new_error(task, + LOK_TILEBUFFER_ERROR, + LOK_TILEBUFFER_MEMORY, + "Error allocating Surface"); + return; + } + + unsigned char* pBuffer = cairo_image_surface_get_data(pSurface); + GdkRectangle aTileRectangle; + aTileRectangle.x = pixelToTwip(nTileSizePixelsScaled, pLOEvent->m_fPaintTileZoom * nScaleFactor) * pLOEvent->m_nPaintTileY; + aTileRectangle.y = pixelToTwip(nTileSizePixelsScaled, pLOEvent->m_fPaintTileZoom * nScaleFactor) * pLOEvent->m_nPaintTileX; + + std::unique_lock<std::mutex> aGuard(g_aLOKMutex); + std::stringstream ss; + ss << "lok::Document::setView(" << priv->m_nViewId << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId); + ss.str(std::string()); + GTimer* aTimer = g_timer_new(); + gulong nElapsedMs; + ss << "lok::Document::paintTile(" << static_cast<void*>(pBuffer) << ", " + << nTileSizePixelsScaled << ", " << nTileSizePixelsScaled << ", " + << aTileRectangle.x << ", " << aTileRectangle.y << ", " + << pixelToTwip(nTileSizePixelsScaled, pLOEvent->m_fPaintTileZoom * nScaleFactor) << ", " + << pixelToTwip(nTileSizePixelsScaled, pLOEvent->m_fPaintTileZoom * nScaleFactor) << ")"; + + priv->m_pDocument->pClass->paintTile(priv->m_pDocument, + pBuffer, + nTileSizePixelsScaled, nTileSizePixelsScaled, + aTileRectangle.x, aTileRectangle.y, + pixelToTwip(nTileSizePixelsScaled, pLOEvent->m_fPaintTileZoom * nScaleFactor), + pixelToTwip(nTileSizePixelsScaled, pLOEvent->m_fPaintTileZoom * nScaleFactor)); + aGuard.unlock(); + + g_timer_elapsed(aTimer, &nElapsedMs); + ss << " rendered in " << (nElapsedMs / 1000.) << " milliseconds"; + g_info("%s", ss.str().c_str()); + g_timer_destroy(aTimer); + + cairo_surface_mark_dirty(pSurface); + + // Its likely that while the tilebuffer has changed, one of the paint tile + // requests has passed the previous check at start of this function, and has + // rendered the tile already. We want to stop such rendered tiles from being + // stored in new tile buffer. + if (pLOEvent->m_pTileBuffer != &*priv->m_pTileBuffer) + { + pLOEvent->m_pTileBuffer = nullptr; + g_task_return_new_error(task, + LOK_TILEBUFFER_ERROR, + LOK_TILEBUFFER_CHANGED, + "TileBuffer has changed"); + return; + } + + g_task_return_pointer(task, pSurface, reinterpret_cast<GDestroyNotify>(cairo_surface_destroy)); +} + + +static void +lokThreadFunc(gpointer data, gpointer /*user_data*/) +{ + GTask* task = G_TASK(data); + LOEvent* pLOEvent = static_cast<LOEvent*>(g_task_get_task_data(task)); + LOKDocView* pDocView = LOK_DOC_VIEW(g_task_get_source_object(task)); + LOKDocViewPrivate& priv = getPrivate(pDocView); + + switch (pLOEvent->m_nType) + { + case LOK_LOAD_DOC: + openDocumentInThread(task); + break; + case LOK_POST_COMMAND: + postCommandInThread(task); + break; + case LOK_SET_EDIT: + setEditInThread(task); + break; + case LOK_SET_PART: + setPartInThread(task); + break; + case LOK_SET_PARTMODE: + setPartmodeInThread(task); + break; + case LOK_POST_KEY: + // view-only/editable mode already checked during signal key signal emission + postKeyEventInThread(task); + break; + case LOK_PAINT_TILE: + paintTileInThread(task); + break; + case LOK_POST_MOUSE_EVENT: + postMouseEventInThread(task); + break; + case LOK_SET_GRAPHIC_SELECTION: + if (priv->m_bEdit) + setGraphicSelectionInThread(task); + else + g_info ("LOK_SET_GRAPHIC_SELECTION: skipping graphical operation in view-only mode"); + break; + case LOK_SET_CLIENT_ZOOM: + setClientZoomInThread(task); + break; + } + + g_object_unref(task); +} + +static void +onStyleContextChanged (LOKDocView* pDocView) +{ + // The scale factor might have changed + updateClientZoom (pDocView); + gtk_widget_queue_draw (GTK_WIDGET (pDocView)); +} + +static void lok_doc_view_init (LOKDocView* pDocView) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + priv.m_pImpl = new LOKDocViewPrivateImpl(); + + gtk_widget_add_events(GTK_WIDGET(pDocView), + GDK_BUTTON_PRESS_MASK + |GDK_BUTTON_RELEASE_MASK + |GDK_BUTTON_MOTION_MASK + |GDK_KEY_PRESS_MASK + |GDK_KEY_RELEASE_MASK); + + priv->lokThreadPool = g_thread_pool_new(lokThreadFunc, + nullptr, + 1, + FALSE, + nullptr); + + g_signal_connect (pDocView, "style-updated", G_CALLBACK(onStyleContextChanged), nullptr); +} + +static void lok_doc_view_set_property (GObject* object, guint propId, const GValue *value, GParamSpec *pspec) +{ + LOKDocView* pDocView = LOK_DOC_VIEW (object); + LOKDocViewPrivate& priv = getPrivate(pDocView); + bool bDocPasswordEnabled = priv->m_nLOKFeatures & LOK_FEATURE_DOCUMENT_PASSWORD; + bool bDocPasswordToModifyEnabled = priv->m_nLOKFeatures & LOK_FEATURE_DOCUMENT_PASSWORD_TO_MODIFY; + bool bTiledAnnotationsEnabled = !(priv->m_nLOKFeatures & LOK_FEATURE_NO_TILED_ANNOTATIONS); + + switch (propId) + { + case PROP_LO_PATH: + priv->m_aLOPath = g_value_get_string (value); + break; + case PROP_LO_UNIPOLL: + priv->m_bUnipoll = g_value_get_boolean (value); + break; + case PROP_LO_POINTER: + priv->m_pOffice = static_cast<LibreOfficeKit*>(g_value_get_pointer(value)); + break; + case PROP_USER_PROFILE_URL: + if (const gchar* pUserProfile = g_value_get_string(value)) + priv->m_aUserProfileURL = pUserProfile; + break; + case PROP_DOC_PATH: + priv->m_aDocPath = g_value_get_string (value); + break; + case PROP_DOC_POINTER: + priv->m_pDocument = static_cast<LibreOfficeKitDocument*>(g_value_get_pointer(value)); + priv->m_eDocumentType = static_cast<LibreOfficeKitDocumentType>(priv->m_pDocument->pClass->getDocumentType(priv->m_pDocument)); + break; + case PROP_EDITABLE: + lok_doc_view_set_edit (pDocView, g_value_get_boolean (value)); + break; + case PROP_ZOOM: + lok_doc_view_set_zoom (pDocView, g_value_get_float (value)); + break; + case PROP_DOC_WIDTH: + priv->m_nDocumentWidthTwips = g_value_get_long (value); + break; + case PROP_DOC_HEIGHT: + priv->m_nDocumentHeightTwips = g_value_get_long (value); + break; + case PROP_DOC_PASSWORD: + if (bool(g_value_get_boolean (value)) != bDocPasswordEnabled) + { + priv->m_nLOKFeatures = priv->m_nLOKFeatures ^ LOK_FEATURE_DOCUMENT_PASSWORD; + priv->m_pOffice->pClass->setOptionalFeatures(priv->m_pOffice, priv->m_nLOKFeatures); + } + break; + case PROP_DOC_PASSWORD_TO_MODIFY: + if ( bool(g_value_get_boolean (value)) != bDocPasswordToModifyEnabled) + { + priv->m_nLOKFeatures = priv->m_nLOKFeatures ^ LOK_FEATURE_DOCUMENT_PASSWORD_TO_MODIFY; + priv->m_pOffice->pClass->setOptionalFeatures(priv->m_pOffice, priv->m_nLOKFeatures); + } + break; + case PROP_TILED_ANNOTATIONS: + if ( bool(g_value_get_boolean (value)) != bTiledAnnotationsEnabled) + { + priv->m_nLOKFeatures = priv->m_nLOKFeatures ^ LOK_FEATURE_NO_TILED_ANNOTATIONS; + priv->m_pOffice->pClass->setOptionalFeatures(priv->m_pOffice, priv->m_nLOKFeatures); + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propId, pspec); + } +} + +static void lok_doc_view_get_property (GObject* object, guint propId, GValue *value, GParamSpec *pspec) +{ + LOKDocView* pDocView = LOK_DOC_VIEW (object); + LOKDocViewPrivate& priv = getPrivate(pDocView); + + switch (propId) + { + case PROP_LO_PATH: + g_value_set_string (value, priv->m_aLOPath.c_str()); + break; + case PROP_LO_UNIPOLL: + g_value_set_boolean (value, priv->m_bUnipoll); + break; + case PROP_LO_POINTER: + g_value_set_pointer(value, priv->m_pOffice); + break; + case PROP_USER_PROFILE_URL: + g_value_set_string(value, priv->m_aUserProfileURL.c_str()); + break; + case PROP_DOC_PATH: + g_value_set_string (value, priv->m_aDocPath.c_str()); + break; + case PROP_DOC_POINTER: + g_value_set_pointer(value, priv->m_pDocument); + break; + case PROP_EDITABLE: + g_value_set_boolean (value, priv->m_bEdit); + break; + case PROP_LOAD_PROGRESS: + g_value_set_double (value, priv->m_nLoadProgress); + break; + case PROP_ZOOM: + g_value_set_float (value, priv->m_fZoom); + break; + case PROP_IS_LOADING: + g_value_set_boolean (value, priv->m_bIsLoading); + break; + case PROP_IS_INITIALIZED: + g_value_set_boolean (value, priv->m_bInit); + break; + case PROP_DOC_WIDTH: + g_value_set_long (value, priv->m_nDocumentWidthTwips); + break; + case PROP_DOC_HEIGHT: + g_value_set_long (value, priv->m_nDocumentHeightTwips); + break; + case PROP_CAN_ZOOM_IN: + g_value_set_boolean (value, priv->m_bCanZoomIn); + break; + case PROP_CAN_ZOOM_OUT: + g_value_set_boolean (value, priv->m_bCanZoomOut); + break; + case PROP_DOC_PASSWORD: + g_value_set_boolean (value, (priv->m_nLOKFeatures & LOK_FEATURE_DOCUMENT_PASSWORD) != 0); + break; + case PROP_DOC_PASSWORD_TO_MODIFY: + g_value_set_boolean (value, (priv->m_nLOKFeatures & LOK_FEATURE_DOCUMENT_PASSWORD_TO_MODIFY) != 0); + break; + case PROP_TILED_ANNOTATIONS: + g_value_set_boolean (value, !(priv->m_nLOKFeatures & LOK_FEATURE_NO_TILED_ANNOTATIONS)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, propId, pspec); + } +} + +static gboolean lok_doc_view_draw (GtkWidget* pWidget, cairo_t* pCairo) +{ + LOKDocView *pDocView = LOK_DOC_VIEW (pWidget); + + renderDocument (pDocView, pCairo); + renderOverlay (pDocView, pCairo); + + return FALSE; +} + +//rhbz#1444437 finalize may not occur immediately when this widget is destroyed +//it may happen during GC of javascript, e.g. in gnome-documents but "destroy" +//will be called promptly, so close documents in destroy, not finalize +static void lok_doc_view_destroy (GtkWidget* widget) +{ + LOKDocView* pDocView = LOK_DOC_VIEW (widget); + LOKDocViewPrivate& priv = getPrivate(pDocView); + + // Ignore notifications sent to this view on shutdown. + std::unique_lock<std::mutex> aGuard(g_aLOKMutex); + std::stringstream ss; + ss << "lok::Document::setView(" << priv->m_nViewId << ")"; + g_info("%s", ss.str().c_str()); + if (priv->m_pDocument) + { + priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId); + priv->m_pDocument->pClass->registerCallback(priv->m_pDocument, nullptr, nullptr); + } + + if (priv->lokThreadPool) + { + g_thread_pool_free(priv->lokThreadPool, true, true); + priv->lokThreadPool = nullptr; + } + + aGuard.unlock(); + + if (priv->m_pDocument) + { + if (priv->m_pDocument->pClass->getViewsCount(priv->m_pDocument) > 1) + { + priv->m_pDocument->pClass->destroyView(priv->m_pDocument, priv->m_nViewId); + } + else + { + if (priv->m_pDocument) + { + priv->m_pDocument->pClass->destroy (priv->m_pDocument); + priv->m_pDocument = nullptr; + } + if (priv->m_pOffice) + { + priv->m_pOffice->pClass->destroy (priv->m_pOffice); + priv->m_pOffice = nullptr; + } + } + } + + GTK_WIDGET_CLASS (lok_doc_view_parent_class)->destroy (widget); +} + +static void lok_doc_view_finalize (GObject* object) +{ + LOKDocView* pDocView = LOK_DOC_VIEW (object); + LOKDocViewPrivate& priv = getPrivate(pDocView); + + delete priv.m_pImpl; + priv.m_pImpl = nullptr; + + G_OBJECT_CLASS (lok_doc_view_parent_class)->finalize (object); +} + +// kicks the mainloop awake +static gboolean timeout_wakeup(void *) +{ + return FALSE; +} + +// integrate our mainloop with LOK's +static int lok_poll_callback(void*, int timeoutUs) +{ + bool bWasEvent(false); + if (timeoutUs > 0) + { + guint timeout = g_timeout_add(timeoutUs / 1000, timeout_wakeup, nullptr); + bWasEvent = g_main_context_iteration(nullptr, true); + g_source_remove(timeout); + } + else + bWasEvent = g_main_context_iteration(nullptr, timeoutUs < 0); + + return bWasEvent ? 1 : 0; +} + +// thread-safe wakeup of our mainloop +static void lok_wake_callback(void *) +{ + g_main_context_wakeup(nullptr); +} + +static gboolean spin_lok_loop(void *pData) +{ + LOKDocView *pDocView = LOK_DOC_VIEW (pData); + LOKDocViewPrivate& priv = getPrivate(pDocView); + priv->m_pOffice->pClass->runLoop(priv->m_pOffice, lok_poll_callback, lok_wake_callback, nullptr); + return FALSE; +} + +// Update the client's view size +static void updateClientZoom(LOKDocView *pDocView) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + gint nScaleFactor = gtk_widget_get_scale_factor(GTK_WIDGET(pDocView)); + gint nTileSizePixelsScaled = nTileSizePixels * nScaleFactor; + GError* error = nullptr; + + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + LOEvent* pLOEvent = new LOEvent(LOK_SET_CLIENT_ZOOM); + pLOEvent->m_nTilePixelWidth = nTileSizePixelsScaled; + pLOEvent->m_nTilePixelHeight = nTileSizePixelsScaled; + pLOEvent->m_nTileTwipWidth = pixelToTwip(nTileSizePixelsScaled, priv->m_fZoom * nScaleFactor); + pLOEvent->m_nTileTwipHeight = pixelToTwip(nTileSizePixelsScaled, priv->m_fZoom * nScaleFactor); + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_SET_CLIENT_ZOOM: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); + + priv->m_nTileSizeTwips = pixelToTwip(nTileSizePixelsScaled, priv->m_fZoom * nScaleFactor); +} + +static gboolean lok_doc_view_initable_init (GInitable *initable, GCancellable* /*cancellable*/, GError **error) +{ + LOKDocView *pDocView = LOK_DOC_VIEW (initable); + LOKDocViewPrivate& priv = getPrivate(pDocView); + + if (priv->m_pOffice != nullptr) + return true; + + if (priv->m_bUnipoll) + (void)g_setenv("SAL_LOK_OPTIONS", "unipoll", FALSE); + + static const char testingLangs[] = "de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru"; + (void)g_setenv("LOK_ALLOWLIST_LANGUAGES", testingLangs, FALSE); + + priv->m_pOffice = lok_init_2(priv->m_aLOPath.c_str(), priv->m_aUserProfileURL.empty() ? nullptr : priv->m_aUserProfileURL.c_str()); + + if (priv->m_pOffice == nullptr) + { + g_set_error (error, + g_quark_from_static_string ("LOK initialization error"), 0, + "Failed to get LibreOfficeKit context. Make sure path (%s) is correct", + priv->m_aLOPath.c_str()); + return FALSE; + } + priv->m_nLOKFeatures |= LOK_FEATURE_PART_IN_INVALIDATION_CALLBACK; + priv->m_nLOKFeatures |= LOK_FEATURE_VIEWID_IN_VISCURSOR_INVALIDATION_CALLBACK; + priv->m_pOffice->pClass->setOptionalFeatures(priv->m_pOffice, priv->m_nLOKFeatures); + + if (priv->m_bUnipoll) + g_idle_add(spin_lok_loop, pDocView); + + return true; +} + +static void lok_doc_view_initable_iface_init (GInitableIface *iface) +{ + iface->init = lok_doc_view_initable_init; +} + +static void lok_doc_view_class_init (LOKDocViewClass* pClass) +{ + GObjectClass *pGObjectClass = G_OBJECT_CLASS(pClass); + GtkWidgetClass *pWidgetClass = GTK_WIDGET_CLASS(pClass); + + pGObjectClass->get_property = lok_doc_view_get_property; + pGObjectClass->set_property = lok_doc_view_set_property; + pGObjectClass->finalize = lok_doc_view_finalize; + + pWidgetClass->draw = lok_doc_view_draw; + pWidgetClass->button_press_event = lok_doc_view_signal_button; + pWidgetClass->button_release_event = lok_doc_view_signal_button; + pWidgetClass->key_press_event = signalKey; + pWidgetClass->key_release_event = signalKey; + pWidgetClass->motion_notify_event = lok_doc_view_signal_motion; + pWidgetClass->destroy = lok_doc_view_destroy; + + /** + * LOKDocView:lopath: + * + * The absolute path of the LibreOffice install. + */ + properties[PROP_LO_PATH] = + g_param_spec_string("lopath", + "LO Path", + "LibreOffice Install Path", + nullptr, + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:unipoll: + * + * Whether we use our own unified polling mainloop in place of glib's + */ + properties[PROP_LO_UNIPOLL] = + g_param_spec_boolean("unipoll", + "Unified Polling", + "Whether we use a custom unified polling loop", + FALSE, + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + /** + * LOKDocView:lopointer: + * + * A LibreOfficeKit* in case lok_init() is already called + * previously. + */ + properties[PROP_LO_POINTER] = + g_param_spec_pointer("lopointer", + "LO Pointer", + "A LibreOfficeKit* from lok_init()", + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:userprofileurl: + * + * The absolute path of the LibreOffice user profile. + */ + properties[PROP_USER_PROFILE_URL] = + g_param_spec_string("userprofileurl", + "User profile path", + "LibreOffice user profile path", + nullptr, + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:docpath: + * + * The path of the document that is currently being viewed. + */ + properties[PROP_DOC_PATH] = + g_param_spec_string("docpath", + "Document Path", + "The URI of the document to open", + nullptr, + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:docpointer: + * + * A LibreOfficeKitDocument* in case documentLoad() is already called + * previously. + */ + properties[PROP_DOC_POINTER] = + g_param_spec_pointer("docpointer", + "Document Pointer", + "A LibreOfficeKitDocument* from documentLoad()", + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:editable: + * + * Whether the document loaded inside of #LOKDocView is editable or not. + */ + properties[PROP_EDITABLE] = + g_param_spec_boolean("editable", + "Editable", + "Whether the content is in edit mode or not", + FALSE, + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:load-progress: + * + * The percent completion of the current loading operation of the + * document. This can be used for progress bars. Note that this is not a + * very accurate progress indicator, and its value might reset it couple of + * times to 0 and start again. You should not rely on its numbers. + */ + properties[PROP_LOAD_PROGRESS] = + g_param_spec_double("load-progress", + "Estimated Load Progress", + "Shows the progress of the document load operation", + 0.0, 1.0, 0.0, + static_cast<GParamFlags>(G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:zoom-level: + * + * The current zoom level of the document loaded inside #LOKDocView. The + * default value is 1.0. + */ + properties[PROP_ZOOM] = + g_param_spec_float("zoom-level", + "Zoom Level", + "The current zoom level of the content", + 0, 5.0, 1.0, + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:is-loading: + * + * Whether the requested document is being loaded or not. %TRUE if it is + * being loaded, otherwise %FALSE. + */ + properties[PROP_IS_LOADING] = + g_param_spec_boolean("is-loading", + "Is Loading", + "Whether the view is loading a document", + FALSE, + static_cast<GParamFlags>(G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:is-initialized: + * + * Whether the requested document has completely loaded or not. + */ + properties[PROP_IS_INITIALIZED] = + g_param_spec_boolean("is-initialized", + "Has initialized", + "Whether the view has completely initialized", + FALSE, + static_cast<GParamFlags>(G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:doc-width: + * + * The width of the currently loaded document in #LOKDocView in twips. + */ + properties[PROP_DOC_WIDTH] = + g_param_spec_long("doc-width", + "Document Width", + "Width of the document in twips", + 0, G_MAXLONG, 0, + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:doc-height: + * + * The height of the currently loaded document in #LOKDocView in twips. + */ + properties[PROP_DOC_HEIGHT] = + g_param_spec_long("doc-height", + "Document Height", + "Height of the document in twips", + 0, G_MAXLONG, 0, + static_cast<GParamFlags>(G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:can-zoom-in: + * + * It tells whether the view can further be zoomed in or not. + */ + properties[PROP_CAN_ZOOM_IN] = + g_param_spec_boolean("can-zoom-in", + "Can Zoom In", + "Whether the view can be zoomed in further", + true, + static_cast<GParamFlags>(G_PARAM_READABLE + | G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:can-zoom-out: + * + * It tells whether the view can further be zoomed out or not. + */ + properties[PROP_CAN_ZOOM_OUT] = + g_param_spec_boolean("can-zoom-out", + "Can Zoom Out", + "Whether the view can be zoomed out further", + true, + static_cast<GParamFlags>(G_PARAM_READABLE + | G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:doc-password: + * + * Set it to true if client supports providing password for viewing + * password protected documents + */ + properties[PROP_DOC_PASSWORD] = + g_param_spec_boolean("doc-password", + "Document password capability", + "Whether client supports providing document passwords", + FALSE, + static_cast<GParamFlags>(G_PARAM_READWRITE + | G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:doc-password-to-modify: + * + * Set it to true if client supports providing password for edit-protected documents + */ + properties[PROP_DOC_PASSWORD_TO_MODIFY] = + g_param_spec_boolean("doc-password-to-modify", + "Edit document password capability", + "Whether the client supports providing passwords to edit documents", + FALSE, + static_cast<GParamFlags>(G_PARAM_READWRITE + | G_PARAM_STATIC_STRINGS)); + + /** + * LOKDocView:tiled-annotations-rendering: + * + * Set it to false if client does not want LO to render comments in tiles and + * instead interested in using comments API to access comments + */ + properties[PROP_TILED_ANNOTATIONS] = + g_param_spec_boolean("tiled-annotations", + "Render comments in tiles", + "Whether the client wants in tile comment rendering", + true, + static_cast<GParamFlags>(G_PARAM_READWRITE + | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties(pGObjectClass, PROP_LAST, properties); + + /** + * LOKDocView::load-changed: + * @pDocView: the #LOKDocView on which the signal is emitted + * @fLoadProgress: the new progress value + */ + doc_view_signals[LOAD_CHANGED] = + g_signal_new("load-changed", + G_TYPE_FROM_CLASS (pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__DOUBLE, + G_TYPE_NONE, 1, + G_TYPE_DOUBLE); + + /** + * LOKDocView::edit-changed: + * @pDocView: the #LOKDocView on which the signal is emitted + * @bEdit: the new edit value of the view + */ + doc_view_signals[EDIT_CHANGED] = + g_signal_new("edit-changed", + G_TYPE_FROM_CLASS (pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__BOOLEAN, + G_TYPE_NONE, 1, + G_TYPE_BOOLEAN); + + /** + * LOKDocView::command-changed: + * @pDocView: the #LOKDocView on which the signal is emitted + * @aCommand: the command that was changed + */ + doc_view_signals[COMMAND_CHANGED] = + g_signal_new("command-changed", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + /** + * LOKDocView::search-not-found: + * @pDocView: the #LOKDocView on which the signal is emitted + * @aCommand: the string for which the search was not found. + */ + doc_view_signals[SEARCH_NOT_FOUND] = + g_signal_new("search-not-found", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + /** + * LOKDocView::part-changed: + * @pDocView: the #LOKDocView on which the signal is emitted + * @aCommand: the part number which the view changed to + */ + doc_view_signals[PART_CHANGED] = + g_signal_new("part-changed", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__INT, + G_TYPE_NONE, 1, + G_TYPE_INT); + + /** + * LOKDocView::size-changed: + * @pDocView: the #LOKDocView on which the signal is emitted + * @aCommand: NULL, we just notify that want to notify the UI elements that are interested. + */ + doc_view_signals[SIZE_CHANGED] = + g_signal_new("size-changed", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 1, + G_TYPE_INT); + + /** + * LOKDocView::hyperlinked-clicked: + * @pDocView: the #LOKDocView on which the signal is emitted + * @aHyperlink: the URI which the application should handle + */ + doc_view_signals[HYPERLINK_CLICKED] = + g_signal_new("hyperlink-clicked", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + /** + * LOKDocView::cursor-changed: + * @pDocView: the #LOKDocView on which the signal is emitted + * @nX: The new cursor position (X coordinate) in pixels + * @nY: The new cursor position (Y coordinate) in pixels + * @nWidth: The width of new cursor + * @nHeight: The height of new cursor + */ + doc_view_signals[CURSOR_CHANGED] = + g_signal_new("cursor-changed", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_generic, + G_TYPE_NONE, 4, + G_TYPE_INT, G_TYPE_INT, + G_TYPE_INT, G_TYPE_INT); + + /** + * LOKDocView::search-result-count: + * @pDocView: the #LOKDocView on which the signal is emitted + * @aCommand: number of matches. + */ + doc_view_signals[SEARCH_RESULT_COUNT] = + g_signal_new("search-result-count", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + /** + * LOKDocView::command-result: + * @pDocView: the #LOKDocView on which the signal is emitted + * @aCommand: JSON containing the info about the command that finished, + * and its success status. + */ + doc_view_signals[COMMAND_RESULT] = + g_signal_new("command-result", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + /** + * LOKDocView::address-changed: + * @pDocView: the #LOKDocView on which the signal is emitted + * @aCommand: formula text content + */ + doc_view_signals[ADDRESS_CHANGED] = + g_signal_new("address-changed", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + /** + * LOKDocView::formula-changed: + * @pDocView: the #LOKDocView on which the signal is emitted + * @aCommand: formula text content + */ + doc_view_signals[FORMULA_CHANGED] = + g_signal_new("formula-changed", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__STRING, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + /** + * LOKDocView::text-selection: + * @pDocView: the #LOKDocView on which the signal is emitted + * @bIsTextSelected: whether text selected is non-null + */ + doc_view_signals[TEXT_SELECTION] = + g_signal_new("text-selection", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_VOID__BOOLEAN, + G_TYPE_NONE, 1, + G_TYPE_BOOLEAN); + + /** + * LOKDocView::content-control: + * @pDocView: the #LOKDocView on which the signal is emitted + * @pPayload: the JSON string containing the information about ruler properties + */ + doc_view_signals[CONTENT_CONTROL] = + g_signal_new("content-control", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_generic, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + /** + * LOKDocView::password-required: + * @pDocView: the #LOKDocView on which the signal is emitted + * @pUrl: URL of the document for which password is required + * @bModify: whether password id required to modify the document + * This is true when password is required to edit the document, + * while it can still be viewed without password. In such cases, provide a NULL + * password for read-only access to the document. + * If false, password is required for opening the document, and document + * cannot be opened without providing a valid password. + * + * Password must be provided by calling lok_doc_view_set_document_password + * function with pUrl as provided by the callback. + * + * Upon entering an invalid password, another `password-required` signal is + * emitted. + * Upon entering a valid password, document starts to load. + * Upon entering a NULL password: if bModify is %TRUE, document starts to + * open in view-only mode, else loading of document is aborted. + */ + doc_view_signals[PASSWORD_REQUIRED] = + g_signal_new("password-required", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_generic, + G_TYPE_NONE, 2, + G_TYPE_STRING, + G_TYPE_BOOLEAN); + + /** + * LOKDocView::comment: + * @pDocView: the #LOKDocView on which the signal is emitted + * @pComment: the JSON string containing comment notification + * The has following structure containing the information telling whether + * the comment has been added, deleted or modified. + * The example: + * { + * "comment": { + * "action": "Add", + * "id": "11", + * "parent": "4", + * "author": "Unknown Author", + * "text": "This is a comment", + * "dateTime": "2016-08-18T13:13:00", + * "anchorPos": "4529, 3906", + * "textRange": "1418, 3906, 3111, 919" + * } + * } + * 'action' can be 'Add', 'Remove' or 'Modify' depending on whether + * comment has been added, removed or modified. + * 'parent' is a non-zero comment id if this comment is a reply comment, + * otherwise it's a root comment. + */ + doc_view_signals[COMMENT] = + g_signal_new("comment", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_generic, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + /** + * LOKDocView::ruler: + * @pDocView: the #LOKDocView on which the signal is emitted + * @pPayload: the JSON string containing the information about ruler properties + * + * The payload format is: + * + * { + * "margin1": "...", + * "margin2": "...", + * "leftOffset": "...", + * "pageOffset": "...", + * "pageWidth": "...", + * "unit": "..." + * } + */ + doc_view_signals[RULER] = + g_signal_new("ruler", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_generic, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + /** + * LOKDocView::window:: + * @pDocView: the #LOKDocView on which the signal is emitted + * @pPayload: the JSON string containing the information about the window + * + * This signal emits information about external windows like dialogs, autopopups for now. + * + * The payload format of pPayload is: + * + * { + * "id": "unique integer id of the dialog", + * "action": "<see below>", + * "type": "<see below>" + * "rectangle": "x, y, width, height" + * } + * + * "type" tells the type of the window the action is associated with + * - "dialog" - window is a dialog + * - "child" - window is a floating window (combo boxes, etc.) + * + * "action" can take following values: + * - "created" - window is created in the backend, client can render it now + * - "title_changed" - window's title is changed + * - "size_changed" - window's size is changed + * - "invalidate" - the area as described by "rectangle" is invalidated + * Clients must request the new area + * - "cursor_invalidate" - cursor is invalidated. New position is in "rectangle" + * - "cursor_visible" - cursor visible status is changed. Status is available + * in "visible" field + * - "close" - window is closed + */ + doc_view_signals[WINDOW] = + g_signal_new("window", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_generic, + G_TYPE_NONE, 1, + G_TYPE_STRING); + + /** + * LOKDocView::invalidate-header:: + * @pDocView: the #LOKDocView on which the signal is emitted + * @pPayload: can be either "row", "column", or "all". + * + * The column/row header is no more valid because of a column/row insertion + * or a similar event. Clients must query a new column/row header set. + * + * The payload says if we are invalidating a row or column header + */ + doc_view_signals[INVALIDATE_HEADER] = + g_signal_new("invalidate-header", + G_TYPE_FROM_CLASS(pGObjectClass), + G_SIGNAL_RUN_FIRST, + 0, + nullptr, nullptr, + g_cclosure_marshal_generic, + G_TYPE_NONE, 1, + G_TYPE_STRING); +} + +SAL_DLLPUBLIC_EXPORT GtkWidget* +lok_doc_view_new (const gchar* pPath, GCancellable *cancellable, GError **error) +{ + return GTK_WIDGET (g_initable_new (LOK_TYPE_DOC_VIEW, cancellable, error, + "lopath", pPath == nullptr ? LOK_PATH : pPath, + "halign", GTK_ALIGN_CENTER, + "valign", GTK_ALIGN_CENTER, + nullptr)); +} + +SAL_DLLPUBLIC_EXPORT GtkWidget* +lok_doc_view_new_from_user_profile (const gchar* pPath, const gchar* pUserProfile, GCancellable *cancellable, GError **error) +{ + return GTK_WIDGET(g_initable_new(LOK_TYPE_DOC_VIEW, cancellable, error, + "lopath", pPath == nullptr ? LOK_PATH : pPath, + "userprofileurl", pUserProfile, + "halign", GTK_ALIGN_CENTER, + "valign", GTK_ALIGN_CENTER, + nullptr)); +} + +SAL_DLLPUBLIC_EXPORT GtkWidget* lok_doc_view_new_from_widget(LOKDocView* pOldLOKDocView, + const gchar* pRenderingArguments) +{ + LOKDocViewPrivate& pOldPriv = getPrivate(pOldLOKDocView); + GtkWidget* pNewDocView = GTK_WIDGET(g_initable_new(LOK_TYPE_DOC_VIEW, /*cancellable=*/nullptr, /*error=*/nullptr, + "lopath", pOldPriv->m_aLOPath.c_str(), + "userprofileurl", pOldPriv->m_aUserProfileURL.c_str(), + "lopointer", pOldPriv->m_pOffice, + "docpointer", pOldPriv->m_pDocument, + "halign", GTK_ALIGN_CENTER, + "valign", GTK_ALIGN_CENTER, + nullptr)); + + // No documentLoad(), just a createView(). + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(LOK_DOC_VIEW(pNewDocView)); + LOKDocViewPrivate& pNewPriv = getPrivate(LOK_DOC_VIEW(pNewDocView)); + // Store the view id only later in postDocumentLoad(), as + // initializeForRendering() changes the id in Impress. + pDocument->pClass->createView(pDocument); + pNewPriv->m_aRenderingArguments = pRenderingArguments; + + postDocumentLoad(pNewDocView); + return pNewDocView; +} + +SAL_DLLPUBLIC_EXPORT gboolean +lok_doc_view_open_document_finish (LOKDocView* pDocView, GAsyncResult* res, GError** error) +{ + GTask* task = G_TASK(res); + + g_return_val_if_fail(g_task_is_valid(res, pDocView), false); + g_return_val_if_fail(g_task_get_source_tag(task) == lok_doc_view_open_document, false); + g_return_val_if_fail(error == nullptr || *error == nullptr, false); + + return g_task_propagate_boolean(task, error); +} + +SAL_DLLPUBLIC_EXPORT void +lok_doc_view_open_document (LOKDocView* pDocView, + const gchar* pPath, + const gchar* pRenderingArguments, + GCancellable* cancellable, + GAsyncReadyCallback callback, + gpointer userdata) +{ + GTask* task = g_task_new(pDocView, cancellable, callback, userdata); + LOKDocViewPrivate& priv = getPrivate(pDocView); + GError* error = nullptr; + + LOEvent* pLOEvent = new LOEvent(LOK_LOAD_DOC); + + g_object_set(G_OBJECT(pDocView), "docpath", pPath, nullptr); + if (pRenderingArguments) + priv->m_aRenderingArguments = pRenderingArguments; + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + g_task_set_source_tag(task, reinterpret_cast<gpointer>(lok_doc_view_open_document)); + + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_LOAD_DOC: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); +} + +SAL_DLLPUBLIC_EXPORT LibreOfficeKitDocument* +lok_doc_view_get_document (LOKDocView* pDocView) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + return priv->m_pDocument; +} + +SAL_DLLPUBLIC_EXPORT void +lok_doc_view_set_visible_area (LOKDocView* pDocView, GdkRectangle* pVisibleArea) +{ + if (!pVisibleArea) + return; + + LOKDocViewPrivate& priv = getPrivate(pDocView); + priv->m_aVisibleArea = *pVisibleArea; + priv->m_bVisibleAreaSet = true; +} + +namespace { +// This used to be rtl::math::approxEqual() but since that isn't inline anymore +// in rtl/math.hxx and was moved into libuno_sal as rtl_math_approxEqual() to +// cater for representable integer cases and we don't want to link against +// libuno_sal, we'll have to have an own implementation. The special large +// integer cases seems not be needed here. +bool lok_approxEqual(double a, double b) +{ + static const double e48 = 1.0 / (16777216.0 * 16777216.0); + if (a == b) + return true; + if (a == 0.0 || b == 0.0) + return false; + const double d = fabs(a - b); + return (d < fabs(a) * e48 && d < fabs(b) * e48); +} +} + +SAL_DLLPUBLIC_EXPORT void +lok_doc_view_set_zoom (LOKDocView* pDocView, float fZoom) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + + if (!priv->m_pDocument) + return; + + // Clamp the input value in [MIN_ZOOM, MAX_ZOOM] + fZoom = fZoom < MIN_ZOOM ? MIN_ZOOM : fZoom; + fZoom = std::min(fZoom, MAX_ZOOM); + + if (lok_approxEqual(fZoom, priv->m_fZoom)) + return; + + gint nScaleFactor = gtk_widget_get_scale_factor(GTK_WIDGET(pDocView)); + gint nTileSizePixelsScaled = nTileSizePixels * nScaleFactor; + priv->m_fZoom = fZoom; + long nDocumentWidthPixels = twipToPixel(priv->m_nDocumentWidthTwips, fZoom * nScaleFactor); + long nDocumentHeightPixels = twipToPixel(priv->m_nDocumentHeightTwips, fZoom * nScaleFactor); + // Total number of columns in this document. + guint nColumns = ceil(static_cast<double>(nDocumentWidthPixels) / nTileSizePixelsScaled); + priv->m_pTileBuffer = std::make_unique<TileBuffer>(nColumns, nScaleFactor); + gtk_widget_set_size_request(GTK_WIDGET(pDocView), + nDocumentWidthPixels / nScaleFactor, + nDocumentHeightPixels / nScaleFactor); + + g_object_notify_by_pspec(G_OBJECT(pDocView), properties[PROP_ZOOM]); + + // set properties to indicate if view can be further zoomed in/out + bool bCanZoomIn = priv->m_fZoom < MAX_ZOOM; + bool bCanZoomOut = priv->m_fZoom > MIN_ZOOM; + if (bCanZoomIn != bool(priv->m_bCanZoomIn)) + { + priv->m_bCanZoomIn = bCanZoomIn; + g_object_notify_by_pspec(G_OBJECT(pDocView), properties[PROP_CAN_ZOOM_IN]); + } + if (bCanZoomOut != bool(priv->m_bCanZoomOut)) + { + priv->m_bCanZoomOut = bCanZoomOut; + g_object_notify_by_pspec(G_OBJECT(pDocView), properties[PROP_CAN_ZOOM_OUT]); + } + + updateClientZoom(pDocView); +} + +SAL_DLLPUBLIC_EXPORT gfloat +lok_doc_view_get_zoom (LOKDocView* pDocView) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + return priv->m_fZoom; +} + +SAL_DLLPUBLIC_EXPORT gint +lok_doc_view_get_parts (LOKDocView* pDocView) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + if (!priv->m_pDocument) + return -1; + + std::scoped_lock<std::mutex> aGuard(g_aLOKMutex); + std::stringstream ss; + ss << "lok::Document::setView(" << priv->m_nViewId << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId); + return priv->m_pDocument->pClass->getParts( priv->m_pDocument ); +} + +SAL_DLLPUBLIC_EXPORT gint +lok_doc_view_get_part (LOKDocView* pDocView) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + if (!priv->m_pDocument) + return -1; + + std::scoped_lock<std::mutex> aGuard(g_aLOKMutex); + std::stringstream ss; + ss << "lok::Document::setView(" << priv->m_nViewId << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId); + return priv->m_pDocument->pClass->getPart( priv->m_pDocument ); +} + +SAL_DLLPUBLIC_EXPORT void +lok_doc_view_set_part (LOKDocView* pDocView, int nPart) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + if (!priv->m_pDocument) + return; + + if (nPart < 0 || nPart >= priv->m_nParts) + { + g_warning("Invalid part request : %d", nPart); + return; + } + + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + LOEvent* pLOEvent = new LOEvent(LOK_SET_PART); + GError* error = nullptr; + + pLOEvent->m_nPart = nPart; + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_SET_PART: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); + 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) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + if (!priv->m_pDocument) + return nullptr; + + std::scoped_lock<std::mutex> aGuard(g_aLOKMutex); + std::stringstream ss; + ss << "lok::Document::setView(" << priv->m_nViewId << ")"; + g_info("%s", ss.str().c_str()); + priv->m_pDocument->pClass->setView(priv->m_pDocument, priv->m_nViewId); + return priv->m_pDocument->pClass->getPartName( priv->m_pDocument, nPart ); +} + +SAL_DLLPUBLIC_EXPORT void +lok_doc_view_set_partmode(LOKDocView* pDocView, + int nPartMode) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + if (!priv->m_pDocument) + return; + + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + LOEvent* pLOEvent = new LOEvent(LOK_SET_PARTMODE); + GError* error = nullptr; + + pLOEvent->m_nPartMode = nPartMode; + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_SET_PARTMODE: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); +} + +SAL_DLLPUBLIC_EXPORT void +lok_doc_view_reset_view(LOKDocView* pDocView) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + + if (priv->m_pTileBuffer != nullptr) + priv->m_pTileBuffer->resetAllTiles(); + priv->m_nLoadProgress = 0.0; + + memset(&priv->m_aVisibleCursor, 0, sizeof(priv->m_aVisibleCursor)); + priv->m_bCursorOverlayVisible = false; + priv->m_bCursorVisible = false; + + priv->m_nLastButtonPressTime = 0; + priv->m_nLastButtonReleaseTime = 0; + priv->m_aTextSelectionRectangles.clear(); + priv->m_aContentControlRectangles.clear(); + + memset(&priv->m_aTextSelectionStart, 0, sizeof(priv->m_aTextSelectionStart)); + memset(&priv->m_aTextSelectionEnd, 0, sizeof(priv->m_aTextSelectionEnd)); + memset(&priv->m_aGraphicSelection, 0, sizeof(priv->m_aGraphicSelection)); + priv->m_bInDragGraphicSelection = false; + memset(&priv->m_aCellCursor, 0, sizeof(priv->m_aCellCursor)); + + cairo_surface_destroy(priv->m_pHandleStart); + priv->m_pHandleStart = nullptr; + memset(&priv->m_aHandleStartRect, 0, sizeof(priv->m_aHandleStartRect)); + priv->m_bInDragStartHandle = false; + + cairo_surface_destroy(priv->m_pHandleMiddle); + priv->m_pHandleMiddle = nullptr; + memset(&priv->m_aHandleMiddleRect, 0, sizeof(priv->m_aHandleMiddleRect)); + priv->m_bInDragMiddleHandle = false; + + cairo_surface_destroy(priv->m_pHandleEnd); + priv->m_pHandleEnd = nullptr; + memset(&priv->m_aHandleEndRect, 0, sizeof(priv->m_aHandleEndRect)); + priv->m_bInDragEndHandle = false; + + memset(&priv->m_aGraphicHandleRects, 0, sizeof(priv->m_aGraphicHandleRects)); + memset(&priv->m_bInDragGraphicHandles, 0, sizeof(priv->m_bInDragGraphicHandles)); + + gtk_widget_queue_draw(GTK_WIDGET(pDocView)); +} + +SAL_DLLPUBLIC_EXPORT void +lok_doc_view_set_edit(LOKDocView* pDocView, + gboolean bEdit) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + if (!priv->m_pDocument) + return; + + GTask* task = g_task_new(pDocView, nullptr, nullptr, nullptr); + LOEvent* pLOEvent = new LOEvent(LOK_SET_EDIT); + GError* error = nullptr; + + pLOEvent->m_bEdit = bEdit; + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + + g_thread_pool_push(priv->lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_SET_EDIT: %s", error->message); + g_clear_error(&error); + } + g_object_unref(task); +} + +SAL_DLLPUBLIC_EXPORT gboolean +lok_doc_view_get_edit (LOKDocView* pDocView) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + return priv->m_bEdit; +} + +SAL_DLLPUBLIC_EXPORT void +lok_doc_view_post_command (LOKDocView* pDocView, + const gchar* pCommand, + const gchar* pArguments, + gboolean bNotifyWhenFinished) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + if (!priv->m_pDocument) + return; + + if (priv->m_bEdit) + LOKPostCommand(pDocView, pCommand, pArguments, bNotifyWhenFinished); + else + g_info ("LOK_POST_COMMAND: ignoring commands in view-only mode"); +} + +SAL_DLLPUBLIC_EXPORT gchar * +lok_doc_view_get_command_values (LOKDocView* pDocView, + const gchar* pCommand) +{ + g_return_val_if_fail (LOK_IS_DOC_VIEW (pDocView), nullptr); + g_return_val_if_fail (pCommand != nullptr, nullptr); + + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(pDocView); + if (!pDocument) + return nullptr; + + return pDocument->pClass->getCommandValues(pDocument, pCommand); +} + +SAL_DLLPUBLIC_EXPORT void +lok_doc_view_find_prev (LOKDocView* pDocView, + const gchar* pText, + gboolean bHighlightAll) +{ + doSearch(pDocView, pText, true, bHighlightAll); +} + +SAL_DLLPUBLIC_EXPORT void +lok_doc_view_find_next (LOKDocView* pDocView, + const gchar* pText, + gboolean bHighlightAll) +{ + doSearch(pDocView, pText, false, bHighlightAll); +} + +SAL_DLLPUBLIC_EXPORT void +lok_doc_view_highlight_all (LOKDocView* pDocView, + const gchar* pText) +{ + doSearch(pDocView, pText, false, true); +} + +SAL_DLLPUBLIC_EXPORT gchar* +lok_doc_view_copy_selection (LOKDocView* pDocView, + const gchar* pMimeType, + gchar** pUsedMimeType) +{ + LibreOfficeKitDocument* pDocument = lok_doc_view_get_document(pDocView); + if (!pDocument) + return nullptr; + + std::stringstream ss; + ss << "lok::Document::getTextSelection('" << pMimeType << "')"; + g_info("%s", ss.str().c_str()); + return pDocument->pClass->getTextSelection(pDocument, pMimeType, pUsedMimeType); +} + +SAL_DLLPUBLIC_EXPORT gboolean +lok_doc_view_paste (LOKDocView* pDocView, + const gchar* pMimeType, + const gchar* pData, + gsize nSize) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + LibreOfficeKitDocument* pDocument = priv->m_pDocument; + bool ret = false; + + if (!pDocument) + return false; + + if (!priv->m_bEdit) + { + g_info ("ignoring paste in view-only mode"); + return ret; + } + + if (pData) + { + std::stringstream ss; + ss << "lok::Document::paste('" << pMimeType << "', '" << std::string(pData, nSize) << ", "<<nSize<<"')"; + g_info("%s", ss.str().c_str()); + ret = pDocument->pClass->paste(pDocument, pMimeType, pData, nSize); + } + + return ret; +} + +SAL_DLLPUBLIC_EXPORT void +lok_doc_view_set_document_password (LOKDocView* pDocView, + const gchar* pURL, + const gchar* pPassword) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + + priv->m_pOffice->pClass->setDocumentPassword(priv->m_pOffice, pURL, pPassword); +} + +SAL_DLLPUBLIC_EXPORT gchar* +lok_doc_view_get_version_info (LOKDocView* pDocView) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + + return priv->m_pOffice->pClass->getVersionInfo(priv->m_pOffice); +} + + +SAL_DLLPUBLIC_EXPORT gfloat +lok_doc_view_pixel_to_twip (LOKDocView* pDocView, float fInput) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + return pixelToTwip(fInput, priv->m_fZoom); +} + +SAL_DLLPUBLIC_EXPORT gfloat +lok_doc_view_twip_to_pixel (LOKDocView* pDocView, float fInput) +{ + LOKDocViewPrivate& priv = getPrivate(pDocView); + return twipToPixel(fInput, priv->m_fZoom); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/source/gtk/tilebuffer.cxx b/libreofficekit/source/gtk/tilebuffer.cxx new file mode 100644 index 000000000..3c73c9ddd --- /dev/null +++ b/libreofficekit/source/gtk/tilebuffer.cxx @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "tilebuffer.hxx" + +#include <o3tl/unit_conversion.hxx> + + +/* ------------------ + Utility functions + ------------------ +*/ +// We know that VirtualDevices use a DPI of 96. +float pixelToTwip(float fInput, float zoom) +{ + return o3tl::toTwips(fInput / zoom, o3tl::Length::px); +} + +float twipToPixel(float fInput, float zoom) +{ + return o3tl::convert(fInput * zoom, o3tl::Length::twip, o3tl::Length::px); +} + +/* ---------------------------- + Tile class member functions + ---------------------------- +*/ +cairo_surface_t* Tile::getBuffer() +{ + return m_pBuffer; +} + +void Tile::setSurface(cairo_surface_t *buffer) +{ + if (m_pBuffer == buffer) + return; + if (m_pBuffer) + cairo_surface_destroy(m_pBuffer); + if (buffer != nullptr) + cairo_surface_reference(buffer); + m_pBuffer = buffer; +} + +/* ---------------------------------- + TileBuffer class member functions + ---------------------------------- +*/ +void TileBuffer::resetAllTiles() +{ + for (auto & tile : m_mTiles) + { + tile.second.valid = false; + } +} + +void TileBuffer::setInvalid(int x, int y, float fZoom, GTask* task, + GThreadPool* lokThreadPool) +{ + int index = x * m_nWidth + y; + GError* error = nullptr; + if (m_mTiles.find(index) == m_mTiles.end()) + return; + + m_mTiles[index].valid = false; + + LOEvent* pLOEvent = new LOEvent(LOK_PAINT_TILE); + pLOEvent->m_nPaintTileX = x; + pLOEvent->m_nPaintTileY = y; + pLOEvent->m_fPaintTileZoom = fZoom; + g_task_set_task_data(task, pLOEvent, LOEvent::destroy); + g_thread_pool_push(lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_PAINT_TILE: %s", error->message); + g_clear_error(&error); + } +} + +Tile& TileBuffer::getTile(int x, int y, GTask* task, + GThreadPool* lokThreadPool) +{ + int index = x * m_nWidth + y; + GError* error = nullptr; + + if (m_mTiles.find(index) != m_mTiles.end() && !m_mTiles[index].valid) + { + g_thread_pool_push(lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_PAINT_TILE: %s", error->message); + g_clear_error(&error); + } + return m_mTiles[index]; + } + else if(m_mTiles.find(index) == m_mTiles.end()) + { + g_thread_pool_push(lokThreadPool, g_object_ref(task), &error); + if (error != nullptr) + { + g_warning("Unable to call LOK_PAINT_TILE: %s", error->message); + g_clear_error(&error); + } + return m_DummyTile; + } + + return m_mTiles[index]; +} + +void TileBuffer::setTile(int x, int y, cairo_surface_t *surface) +{ + int index = x * m_nWidth + y; + + m_mTiles[index].setSurface(surface); + m_mTiles[index].valid = true; +} + +bool TileBuffer::hasValidTile(int x, int y) +{ + int index = x * m_nWidth + y; + auto it = m_mTiles.find(index); + return (it != m_mTiles.end()) && it->second.valid; +} + +void LOEvent::destroy(void* pMemory) +{ + LOEvent* pLOEvent = static_cast<LOEvent*>(pMemory); + delete pLOEvent; +} + +GQuark +LOKTileBufferErrorQuark() +{ + return g_quark_from_static_string("lok-tilebuffer-error"); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/libreofficekit/source/gtk/tilebuffer.hxx b/libreofficekit/source/gtk/tilebuffer.hxx new file mode 100644 index 000000000..239482e34 --- /dev/null +++ b/libreofficekit/source/gtk/tilebuffer.hxx @@ -0,0 +1,281 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#ifndef INCLUDED_TILEBUFFER_HXX +#define INCLUDED_TILEBUFFER_HXX + +#include <cairo.h> +#include <gio/gio.h> +#include <glib.h> + +#include <map> + +#define LOK_TILEBUFFER_ERROR (LOKTileBufferErrorQuark()) + +// Lets use a square of side 256 pixels for each tile. +const int nTileSizePixels = 256; + +/** + Converts the pixel value to zoom independent twip value. + + @param fInput value to convert + @param zoom the current zoom level + + @return the pixels value corresponding to given twip value +*/ +float pixelToTwip(float fInput, float zoom); + +/** + Converts the zoom independent twip value pixel value. + + @param fInput value to convert + @param zoom the current zoom level + + @return the twip value corresponding to given pixel value +*/ +float twipToPixel(float fInput, float zoom); + +/** + Gets GQuark identifying this tile buffer errors +*/ +GQuark LOKTileBufferErrorQuark(void); + +/** + This class represents a single tile in the tile buffer. + It encloses a reference to GdkPixBuf containing the pixel data of the tile. +*/ +class Tile +{ +public: + Tile() + : valid(false) + , m_pBuffer(nullptr) + { + } + ~Tile() + { + if (m_pBuffer) + cairo_surface_destroy(m_pBuffer); + } + + /** + Tells if this tile is valid or not. Initialised to 0 (invalid) during + object creation. + */ + bool valid; + + /// Function to get the pointer to enclosing cairo_surface_t + cairo_surface_t* getBuffer(); + /// Used to set the pixel buffer of this object + void setSurface(cairo_surface_t*); + +private: + /// Pixel buffer data for this tile + cairo_surface_t* m_pBuffer; +}; + +/** + This class represents the tile buffer which is responsible for managing, + reusing and caching all the already rendered tiles. If the given tile is not + present in the buffer, call to LOK Document's (m_pLOKDocument) paintTile + method is made which fetches the rendered tile from LO core and store it in + buffer for future reuse. +*/ +class TileBuffer +{ +public: + TileBuffer(int columns = 0, int scale = 1) + : m_nWidth(columns) + { + cairo_surface_t* pSurface = cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, nTileSizePixels * scale, nTileSizePixels * scale); + m_DummyTile.setSurface(pSurface); + cairo_surface_destroy(pSurface); + } + + /** + Gets the underlying Tile object for given position. The position (0, 0) + points to the left top most tile of the buffer. + + If the tile is not cached by the tile buffer, it makes a paintTile call + to LO core asking to render the given tile. It then stores the tile for + future reuse. + + @param x the tile along the x-axis of the buffer + @param y the tile along the y-axis of the buffer + @param task GTask object containing the necessary data + @param pool GThreadPool managed by the widget instance used for all the + LOK calls made by widget. It is needed here because getTile invokes one + of the LOK call : paintTile. + + @return the tile at the mentioned position (x, y) + */ + Tile& getTile(int x, int y, GTask* task, GThreadPool* pool); + + /* + Takes ownership of the surface and sets it on a tile at a given location + */ + void setTile(int x, int y, cairo_surface_t* surface); + + /// Returns true if a valid tile exists at this location + bool hasValidTile(int x, int y); + + /// Destroys all the tiles in the tile buffer; also frees the memory allocated + /// for all the Tile objects. + void resetAllTiles(); + /** + Marks the tile as invalid. The tile (0, 0) is the left topmost tile in + the tile buffer. + + @param x the position of tile along x-axis + @param y the position of tile along y-axis + @param zoom zoom factor of the document + @param task GTask object containing the necessary data + @param pool GThreadPool managed by the widget instance used for all the + LOK calls made by widget. It is needed here because setInvalid() invokes one + of the LOK call : paintTile. + */ + void setInvalid(int x, int y, float zoom, GTask* task, GThreadPool*); + +private: + /// Stores all the tiles cached by this tile buffer. + std::map<int, Tile> m_mTiles; + /// Width of the current tile buffer (number of columns) + int m_nWidth; + /// Dummy tile + Tile m_DummyTile; +}; + +enum +{ + LOK_LOAD_DOC, + LOK_POST_COMMAND, + LOK_SET_EDIT, + LOK_SET_PARTMODE, + LOK_SET_PART, + LOK_POST_KEY, + LOK_PAINT_TILE, + LOK_POST_MOUSE_EVENT, + LOK_SET_GRAPHIC_SELECTION, + LOK_SET_CLIENT_ZOOM +}; + +enum +{ + LOK_TILEBUFFER_CHANGED, + LOK_TILEBUFFER_MEMORY +}; + +/** + A struct that we use to store the data about the LOK call. + + Object of this type is passed with all the LOK calls, + so that they can be identified. Additionally, it also contains + the data that LOK call needs. +*/ +struct LOEvent +{ + /// To identify the type of LOK call + int m_nType; + + /// @name post_command parameters + ///@{ + const gchar* m_pCommand; + gchar* m_pArguments; + gboolean m_bNotifyWhenFinished; + ///@} + + /// set_edit parameter + gboolean m_bEdit; + + /// set_partmode parameter + int m_nPartMode; + + /// set_part parameter + int m_nPart; + + /// @name postKeyEvent parameters + ///@{ + int m_nKeyEvent; + int m_nCharCode; + int m_nKeyCode; + ///@} + + /// @name paintTile parameters + ///@{ + int m_nPaintTileX; + int m_nPaintTileY; + float m_fPaintTileZoom; + TileBuffer* m_pTileBuffer; + ///@} + + /// @name postMouseEvent parameters + ///@{ + int m_nPostMouseEventType; + int m_nPostMouseEventX; + int m_nPostMouseEventY; + int m_nPostMouseEventCount; + int m_nPostMouseEventButton; + int m_nPostMouseEventModifier; + ///@} + + /// @name setGraphicSelection parameters + ///@{ + int m_nSetGraphicSelectionType; + int m_nSetGraphicSelectionX; + int m_nSetGraphicSelectionY; + ///@} + + /// @name setClientView parameters + ///@{ + int m_nTilePixelWidth; + int m_nTilePixelHeight; + int m_nTileTwipWidth; + int m_nTileTwipHeight; + ///@} + + /// Constructor to instantiate an object of type `type`. + explicit LOEvent(int type) + : m_nType(type) + , m_pCommand(nullptr) + , m_pArguments(nullptr) + , m_bNotifyWhenFinished(false) + , m_bEdit(false) + , m_nPartMode(0) + , m_nPart(0) + , m_nKeyEvent(0) + , m_nCharCode(0) + , m_nKeyCode(0) + , m_nPaintTileX(0) + , m_nPaintTileY(0) + , m_fPaintTileZoom(0) + , m_pTileBuffer(nullptr) + , m_nPostMouseEventType(0) + , m_nPostMouseEventX(0) + , m_nPostMouseEventY(0) + , m_nPostMouseEventCount(0) + , m_nPostMouseEventButton(0) + , m_nPostMouseEventModifier(0) + , m_nSetGraphicSelectionType(0) + , m_nSetGraphicSelectionX(0) + , m_nSetGraphicSelectionY(0) + , m_nTilePixelWidth(0) + , m_nTilePixelHeight(0) + , m_nTileTwipWidth(0) + , m_nTileTwipHeight(0) + { + } + + /// Wrapper around delete to help GLib. + static void destroy(void* pMemory); +}; + +#endif // INCLUDED_TILEBUFFER_HXX + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |