diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:51:28 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 16:51:28 +0000 |
commit | 940b4d1848e8c70ab7642901a68594e8016caffc (patch) | |
tree | eb72f344ee6c3d9b80a7ecc079ea79e9fba8676d /libreofficekit/source/gtk | |
parent | Initial commit. (diff) | |
download | libreoffice-940b4d1848e8c70ab7642901a68594e8016caffc.tar.xz libreoffice-940b4d1848e8c70ab7642901a68594e8016caffc.zip |
Adding upstream version 1:7.0.4.upstream/1%7.0.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'libreofficekit/source/gtk')
-rw-r--r-- | libreofficekit/source/gtk/lokdocview.cxx | 3901 | ||||
-rw-r--r-- | libreofficekit/source/gtk/tilebuffer.cxx | 139 | ||||
-rw-r--r-- | libreofficekit/source/gtk/tilebuffer.hxx | 278 |
3 files changed, 4318 insertions, 0 deletions
diff --git a/libreofficekit/source/gtk/lokdocview.cxx b/libreofficekit/source/gtk/lokdocview.cxx new file mode 100644 index 000000000..75deee7a3 --- /dev/null +++ b/libreofficekit/source/gtk/lokdocview.cxx @@ -0,0 +1,3901 @@ +/* -*- 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 <sal/types.h> +#include <math.h> +#include <string.h> +#include <memory> +#include <vector> +#include <string> +#include <sstream> +#include <iostream> +#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, const std::vector<GdkRectangle>& rRectangles = std::vector<GdkRectangle>()) + : m_nPart(nPart), + m_aRectangles(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 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, + 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) + { + 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; + } + + return false; +} + +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()); + 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) + return aRet; + aRet.x = atoi(*ppCoordinate); + if (aRet.x < 0) + aRet.x = 0; + ++ppCoordinate; + if (!*ppCoordinate) + return aRet; + aRet.y = atoi(*ppCoordinate); + if (aRet.y < 0) + aRet.y = 0; + ++ppCoordinate; + if (!*ppCoordinate) + 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) + 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; + std::cerr << nViewId; + std::cerr << priv->m_nViewId; + 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_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_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: + { + // 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); + } + } + + // 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, + 180.0 * (M_PI/180.0), + 360.0 * (M_PI/180.0)); + 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) +{ + if (timeoutUs) + { + guint timeout = g_timeout_add(timeoutUs / 1000, timeout_wakeup, nullptr); + g_main_context_iteration(nullptr, true); + g_source_remove(timeout); + } + else + g_main_context_iteration(nullptr, FALSE); + + return 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) + g_setenv("SAL_LOK_OPTIONS", "unipoll", 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::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 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(); + + 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..82f0253d9 --- /dev/null +++ b/libreofficekit/source/gtk/tilebuffer.cxx @@ -0,0 +1,139 @@ +/* -*- 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" + + +/* ------------------ + Utility functions + ------------------ +*/ +float pixelToTwip(float fInput, float zoom) +{ + return (fInput / DPI / zoom) * 1440.0f; +} + +float twipToPixel(float fInput, float zoom) +{ + return fInput / 1440.0f * DPI * zoom; +} + +/* ---------------------------- + 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()) + { + 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..9e7203b3b --- /dev/null +++ b/libreofficekit/source/gtk/tilebuffer.hxx @@ -0,0 +1,278 @@ +/* -*- 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()) + +// We know that VirtualDevices use a DPI of 96. +const int DPI = 96; +// 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: */ |