// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Editable view implementation
 *
 * Authors:
 *   Lauris Kaplinski <lauris@kaplinski.com>
 *   MenTaLguY <mental@rydia.net>
 *   bulia byak <buliabyak@users.sf.net>
 *   Ralf Stephan <ralf@ark.in-berlin.de>
 *   John Bintz <jcoswell@coswellproductions.org>
 *   Johan Engelen <j.b.c.engelen@ewi.utwente.nl>
 *   Jon A. Cruz <jon@joncruz.org>
 *   Abhishek Sharma
 *
 * Copyright (C) 2007 Jon A. Cruz
 * Copyright (C) 2006-2008 Johan Engelen
 * Copyright (C) 2006 John Bintz
 * Copyright (C) 2004 MenTaLguY
 * Copyright (C) 1999-2002 Lauris Kaplinski
 * Copyright (C) 2000-2001 Ximian, Inc.
 *
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */

#include <glibmm/i18n.h>
#include <2geom/transforms.h>
#include <2geom/rect.h>
#include <memory>

#include "desktop.h"

#include "color.h"
#include "desktop-events.h"
#include "desktop-style.h"
#include "device-manager.h"
#include "document-undo.h"
#include "event-log.h"
#include "inkscape-window.h"
#include "layer-fns.h"
#include "layer-manager.h"
#include "message-context.h"
#include "message-stack.h"

#include "display/canvas-arena.h"
#include "display/canvas-debug.h"
#include "display/canvas-grid.h"
#include "display/canvas-rotate.h"
#include "display/canvas-temporary-item-list.h"
#include "display/drawing-group.h"
#include "display/gnome-canvas-acetate.h"
#include "display/snap-indicator.h"
#include "display/sodipodi-ctrlrect.h"
#include "display/sp-canvas-group.h"
#include "display/sp-canvas-util.h"

#include "helper/action-context.h"
#include "helper/action.h" //sp_action_perform

#include "io/resource-manager.h"

#include "object/sp-namedview.h"
#include "object/sp-root.h"

#include "ui/desktop/menubar.h"
#include "ui/dialog/dialog-manager.h"
#include "ui/interface.h" // Only for getLayoutPrefPath
#include "ui/tool-factory.h"
#include "ui/tools/tool-base.h"
#include "ui/tools/box3d-tool.h"
#include "ui/tools/select-tool.h"

#include "widgets/desktop-widget.h"

// TODO those includes are only for node tool quick zoom. Remove them after fixing it.
#include "ui/tools/node-tool.h"
#include "ui/tool/control-point-selection.h"

namespace Inkscape { namespace XML { class Node; }}

// Callback declarations
static void _onSelectionChanged (Inkscape::Selection *selection, SPDesktop *desktop);
static gint _arena_handler (SPCanvasArena *arena, Inkscape::DrawingItem *ai, GdkEvent *event, SPDesktop *desktop);
static void _layer_activated(SPObject *layer, SPDesktop *desktop);
static void _layer_deactivated(SPObject *layer, SPDesktop *desktop);
static void _layer_hierarchy_changed(SPObject *top, SPObject *bottom, SPDesktop *desktop);
static void _reconstruction_start(SPDesktop * desktop);
static void _reconstruction_finish(SPDesktop * desktop);
static void _namedview_modified (SPObject *obj, guint flags, SPDesktop *desktop);

static gdouble _pinch_begin_zoom = 1.;

static void _pinch_begin_handler(GtkGesture *gesture, GdkEventSequence *sequence, SPDesktop *desktop)
{
    _pinch_begin_zoom = desktop->current_zoom();
}

static void _pinch_scale_changed_handler(GtkGesture *gesture, gdouble delta, SPDesktop *desktop)
{
    GdkEventSequence *sequence = gtk_gesture_get_last_updated_sequence(gesture);
    const GdkEvent *event = gtk_gesture_get_last_event(gesture, sequence);

    Geom::Point button_window(event->button.x, event->button.y);
    Geom::Point button_world = sp_canvas_window_to_world(desktop->canvas, button_window);
    Geom::Point button_dt(desktop->w2d(button_world));

    desktop->zoom_absolute_keep_point(button_dt, _pinch_begin_zoom * delta);
}

SPDesktop::SPDesktop()
    : _dlg_mgr(nullptr)
    , namedview(nullptr)
    , canvas(nullptr)
    , layers(nullptr)
    , selection(nullptr)
    , event_context(nullptr)
    , layer_manager(nullptr)
    , event_log(nullptr)
    , temporary_item_list(nullptr)
    , snapindicator(nullptr)
    , acetate(nullptr)
    , main(nullptr)
    , gridgroup(nullptr)
    , guides(nullptr)
    , drawing(nullptr)
    , sketch(nullptr)
    , controls(nullptr)
    , tempgroup(nullptr)
    , page(nullptr)
    , page_border(nullptr)
    , current(nullptr)
    , _focusMode(false)
    , dkey(0)
    , number(0)
    , window_state(0)
    , interaction_disabled_counter(0)
    , waiting_cursor(false)
    , showing_dialogs(false)
    , guides_active(false)
    , gr_item(nullptr)
    , gr_point_type(POINT_LG_BEGIN)
    , gr_point_i(0)
    , gr_fill_or_stroke(Inkscape::FOR_FILL)
    , _reconstruction_old_layer_id()
    , // an id attribute is not allowed to be the empty string
    _display_mode(Inkscape::RENDERMODE_NORMAL)
    , _display_color_mode(Inkscape::COLORMODE_NORMAL)
    , _split_canvas(false)
    , _xray(false)
    , _widget(nullptr)
    , _guides_message_context(nullptr)
    , _active(false)
    , _image_render_observer(this, "/options/rendering/imageinoutlinemode")
    , grids_visible(false)
{
    layers = new Inkscape::LayerModel();
    layers->_layer_activated_signal.connect(sigc::bind(sigc::ptr_fun(_layer_activated), this));
    layers->_layer_deactivated_signal.connect(sigc::bind(sigc::ptr_fun(_layer_deactivated), this));
    layers->_layer_changed_signal.connect(sigc::bind(sigc::ptr_fun(_layer_hierarchy_changed), this));
    selection = Inkscape::GC::release( new Inkscape::Selection(layers, this) );
}

void
SPDesktop::init (SPNamedView *nv, SPCanvas *aCanvas, Inkscape::UI::View::EditWidgetInterface *widget)
{
    _widget = widget;

    // Temporary workaround for link order issues:
    Inkscape::DeviceManager::getManager().getDevices();
    Inkscape::ResourceManager::getManager();
    Inkscape::Preferences *prefs = Inkscape::Preferences::get();

    _guides_message_context = std::unique_ptr<Inkscape::MessageContext>(new Inkscape::MessageContext(messageStack()));

    current = prefs->getStyle("/desktop/style");

    namedview = nv;
    canvas = aCanvas;

    SPDocument *document = namedview->document;
    /* XXX:
     * ensureUpToDate() sends a 'modified' signal to the root element.
     * This is reportedly required to prevent flickering after the document
     * loads. However, many SPObjects write to their repr in response
     * to this signal. This is apparently done to support live path effects,
     * which rewrite their result paths after each modification of the base object.
     * This causes the generation of an incomplete undo transaction,
     * which causes problems down the line, including crashes in the
     * Undo History dialog.
     *
     * For now, this is handled by disabling undo tracking during this call.
     * A proper fix would involve modifying the way ensureUpToDate() works,
     * so that the LPE results are not rewritten.
     */
    Inkscape::DocumentUndo::setUndoSensitive(document, false);
    document->ensureUpToDate();
    Inkscape::DocumentUndo::setUndoSensitive(document, true);

    /* Setup Dialog Manager */
    _dlg_mgr = &Inkscape::UI::Dialog::DialogManager::getInstance();

    dkey = SPItem::display_key_new(1);

    /* Connect display key to layer model */
    layers->setDisplayKey(dkey);

    /* Connect document */
    setDocument (document);

    number = namedview->getViewCount();


    /* Setup Canvas */
    g_object_set_data (G_OBJECT (canvas), "SPDesktop", this);

    SPCanvasGroup *root = canvas->getRoot();

    /* Setup administrative layers */
    acetate = sp_canvas_item_new (root, GNOME_TYPE_CANVAS_ACETATE, nullptr);
    g_signal_connect (G_OBJECT (acetate), "event", G_CALLBACK (sp_desktop_root_handler), this);
    main = (SPCanvasGroup *) sp_canvas_item_new (root, SP_TYPE_CANVAS_GROUP, nullptr);
    g_signal_connect (G_OBJECT (main), "event", G_CALLBACK (sp_desktop_root_handler), this);

    /* This is the background the page sits on. */
    canvas->setBackgroundColor(0xffffff00);

    page = sp_canvas_item_new (main, SP_TYPE_CTRLRECT, nullptr);
    ((CtrlRect *) page)->setColor(0x00000000, FALSE, 0x00000000);
    page_border = sp_canvas_item_new (main, SP_TYPE_CTRLRECT, nullptr);

    drawing = sp_canvas_item_new (main, SP_TYPE_CANVAS_ARENA, nullptr);
    g_signal_connect (G_OBJECT (drawing), "arena_event", G_CALLBACK (_arena_handler), this);

    // pinch zoom
    zoomgesture = gtk_gesture_zoom_new(GTK_WIDGET(getCanvas()));
    g_signal_connect(zoomgesture, "begin", G_CALLBACK(_pinch_begin_handler), this);
    g_signal_connect(zoomgesture, "scale-changed", G_CALLBACK(_pinch_scale_changed_handler), this);

    SP_CANVAS_ARENA (drawing)->drawing.delta = prefs->getDouble("/options/cursortolerance/value", 1.0); // default is 1 px

    if (prefs->getBool("/options/startmode/outline")) {
        // Start in outline mode
        setDisplayModeOutline();
    } else {
        // Start in normal mode, default
        setDisplayModeNormal();
    }

    // The order in which these canvas items are added determines the z-order. It's therefore
    // important to add the tempgroup (which will contain the snapindicator) before adding the
    // controls. Only this way one will be able to quickly (before the snap indicator has
    // disappeared) reselect a node after snapping it. If the z-order is wrong however, this
    // will not work (the snap indicator is on top of the node handler; is the snapindicator
    // being selected? or does it intercept some of the events that should have gone to the
    // node handler? see bug https://bugs.launchpad.net/inkscape/+bug/414142)
    gridgroup = (SPCanvasGroup *) sp_canvas_item_new (main, SP_TYPE_CANVAS_GROUP, nullptr);
    guides    = (SPCanvasGroup *) sp_canvas_item_new (main, SP_TYPE_CANVAS_GROUP, nullptr);
    sketch    = (SPCanvasGroup *) sp_canvas_item_new (main, SP_TYPE_CANVAS_GROUP, nullptr);
    tempgroup = (SPCanvasGroup *) sp_canvas_item_new (main, SP_TYPE_CANVAS_GROUP, nullptr);
    controls  = (SPCanvasGroup *) sp_canvas_item_new (main, SP_TYPE_CANVAS_GROUP, nullptr);

    // Set the select tool as the active tool.
    setEventContext("/tools/select");

    // display rect and zoom are now handled in sp_desktop_widget_realize()

    Geom::Rect const d(Geom::Point(0.0, 0.0),
                       Geom::Point(document->getWidth().value("px"), document->getHeight().value("px")));

    SP_CTRLRECT(page)->setRectangle(d);
    SP_CTRLRECT(page_border)->setRectangle(d);

    /* the following sets the page shadow on the canvas
       It was originally set to 5, which is really cheesy!
       It now is an attribute in the document's namedview. If a value of
       0 is used, then the constructor for a shadow is not initialized.
    */

    if ( namedview->pageshadow != 0 && namedview->showpageshadow ) {
        SP_CTRLRECT(page_border)->setShadow(namedview->pageshadow, 0x3f3f3fff);
    }


    /* Connect event for page resize */
    sp_canvas_item_affine_absolute (SP_CANVAS_ITEM (drawing), doc2dt());

    _modified_connection =
        namedview->connectModified(sigc::bind<2>(sigc::ptr_fun(&_namedview_modified), this));

    Inkscape::DrawingItem *ai = document->getRoot()->invoke_show(
            SP_CANVAS_ARENA (drawing)->drawing,
            dkey,
            SP_ITEM_SHOW_DISPLAY);
    if (ai) {
        SP_CANVAS_ARENA (drawing)->drawing.root()->prependChild(ai);
    }

    namedview->show(this);
    /* Ugly hack */
    activate_guides (true);
    /* Ugly hack */
    _namedview_modified (namedview, SP_OBJECT_MODIFIED_FLAG, this);

/* Set up notification of rebuilding the document, this allows
       for saving object related settings in the document. */
    _reconstruction_start_connection =
        document->connectReconstructionStart(sigc::bind(sigc::ptr_fun(_reconstruction_start), this));
    _reconstruction_finish_connection =
        document->connectReconstructionFinish(sigc::bind(sigc::ptr_fun(_reconstruction_finish), this));
    _reconstruction_old_layer_id.clear();

    // ?
    // sp_active_desktop_set (desktop);

    _activate_connection = _activate_signal.connect(
        sigc::bind(
            sigc::ptr_fun(_onActivate),
            this
        )
    );
     _deactivate_connection = _deactivate_signal.connect(
        sigc::bind(
            sigc::ptr_fun(_onDeactivate),
            this
        )
    );

    _sel_modified_connection = selection->connectModified(
        sigc::bind(
            sigc::ptr_fun(&_onSelectionModified),
            this
        )
    );
    _sel_changed_connection = selection->connectChanged(
        sigc::bind(
            sigc::ptr_fun(&_onSelectionChanged),
            this
        )
    );


    /* setup LayerManager */
    //   (Setting up after the connections are all in place, as it may use some of them)
    layer_manager = new Inkscape::LayerManager( this );

    showGrids(namedview->grids_visible, false);

    temporary_item_list = new Inkscape::Display::TemporaryItemList( this );
    snapindicator = new Inkscape::Display::SnapIndicator ( this );

    canvas_rotate = sp_canvas_item_new (root, SP_TYPE_CANVAS_ROTATE, nullptr);
    sp_canvas_item_hide( canvas_rotate );
    // canvas_debug = sp_canvas_item_new (main, SP_TYPE_CANVAS_DEBUG, NULL);
}

void SPDesktop::destroy()
{
    _destroy_signal.emit(this);

    if (snapindicator) {
        delete snapindicator;
        snapindicator = nullptr;
    }

    if (temporary_item_list) {
        delete temporary_item_list;
        temporary_item_list = nullptr;
    }

    if (selection) {
        delete selection;
        selection = nullptr;
    }

    namedview->hide(this);

    _activate_connection.disconnect();
    _deactivate_connection.disconnect();
    _sel_modified_connection.disconnect();
    _sel_changed_connection.disconnect();
    _modified_connection.disconnect();
    _commit_connection.disconnect();
    _reconstruction_start_connection.disconnect();
    _reconstruction_finish_connection.disconnect();

    g_signal_handlers_disconnect_by_func(G_OBJECT (acetate), (gpointer) G_CALLBACK(sp_desktop_root_handler), this);
    g_signal_handlers_disconnect_by_func(G_OBJECT (main), (gpointer) G_CALLBACK(sp_desktop_root_handler), this);
    g_signal_handlers_disconnect_by_func(G_OBJECT (drawing), (gpointer) G_CALLBACK(_arena_handler), this);

    if (zoomgesture) {
        g_signal_handlers_disconnect_by_data(zoomgesture, this);
        g_clear_object(&zoomgesture);
    }

    delete layers;

    if (layer_manager) {
        delete layer_manager;
        layer_manager = nullptr;
    }

    if (drawing) {
        doc()->getRoot()->invoke_hide(dkey);
        g_object_unref(drawing);
        drawing = nullptr;
    }

    _guides_message_context = nullptr;
}

SPDesktop::~SPDesktop()
= default;


Inkscape::UI::Tools::ToolBase* SPDesktop::getEventContext() const {
	return event_context;
}

Inkscape::Selection* SPDesktop::getSelection() const {
	return selection;
}

SPDocument* SPDesktop::getDocument() const {
	return doc();
}

SPCanvas* SPDesktop::getCanvas() const {
	return SP_CANVAS_ITEM(main)->canvas;
}

SPCanvasItem* SPDesktop::getAcetate() const {
	return acetate;
}

SPCanvasGroup* SPDesktop::getMain() const {
	return main;
}

SPCanvasGroup* SPDesktop::getGridGroup() const {
	return gridgroup;
}

SPCanvasGroup* SPDesktop::getGuides() const {
	return guides;
}

SPCanvasItem* SPDesktop::getDrawing() const {
	return drawing;
}

SPCanvasGroup* SPDesktop::getSketch() const {
	return sketch;
}

SPCanvasGroup* SPDesktop::getControls() const {
	return controls;
}

SPCanvasGroup* SPDesktop::getTempGroup() const {
	return tempgroup;
}

Inkscape::MessageStack* SPDesktop::getMessageStack() const {
	return messageStack().get();
}

SPNamedView* SPDesktop::getNamedView() const {
	return namedview;
}



//--------------------------------------------------------------------
/* Public methods */


/* These methods help for temporarily showing things on-canvas.
 * The *only* valid use of the TemporaryItem* that you get from add_temporary_canvasitem
 * is when you want to prematurely remove the item from the canvas, by calling
 * desktop->remove_temporary_canvasitem(tempitem).
 */
/** Note that lifetime is measured in milliseconds
 * One should *not* keep a reference to the SPCanvasItem, the temporary item code will
 * delete the object for you and the reference will become invalid without you knowing it.
 * It is perfectly safe to ignore the returned pointer: the object is deleted by itself, so don't delete it elsewhere!
 * The *only* valid use of the returned TemporaryItem* is as argument for SPDesktop::remove_temporary_canvasitem,
 * because the object might be deleted already without you knowing it.
 * move_to_bottom = true by default so the item does not interfere with handling of other items on the canvas like nodes.
 */
Inkscape::Display::TemporaryItem *
SPDesktop::add_temporary_canvasitem (SPCanvasItem *item, guint lifetime, bool move_to_bottom)
{
    if (move_to_bottom) {
        sp_canvas_item_move_to_z(item, 0);
    }

    return temporary_item_list->add_item(item, lifetime);
}

/** It is perfectly safe to call this function while the object has already been deleted due to a timeout.
*/
void
SPDesktop::remove_temporary_canvasitem (Inkscape::Display::TemporaryItem * tempitem)
{
    // check for non-null temporary_item_list, because during destruction of desktop, some destructor might try to access this list!
    if (tempitem && temporary_item_list) {
        temporary_item_list->delete_item(tempitem);
    }
}

void SPDesktop::redrawDesktop() {
    sp_canvas_item_affine_absolute (SP_CANVAS_ITEM (main), _current_affine.d2w()); // redraw
}

void SPDesktop::_setDisplayMode(Inkscape::RenderMode mode) {
    SP_CANVAS_ARENA (drawing)->drawing.setRenderMode(mode);
    canvas->_rendermode = mode;
    _display_mode = mode;
    if (_display_mode == Inkscape::RENDERMODE_OUTLINE) {
        if (_split_canvas) {
            toggleSplitMode();
        }
        if (_xray) {
            toggleXRay();
        }
    }
    redrawDesktop();
    _widget->setTitle( this->getDocument()->getDocumentName() );
}

void SPDesktop::_setDisplayColorMode(Inkscape::ColorMode mode) {
    // reload grayscale matrix from prefs
    if (mode == Inkscape::COLORMODE_GRAYSCALE) {
        Inkscape::Preferences *prefs = Inkscape::Preferences::get();
        gdouble r = prefs->getDoubleLimited("/options/rendering/grayscale/red-factor",0.21,0.,1.);
        gdouble g = prefs->getDoubleLimited("/options/rendering/grayscale/green-factor",0.72,0.,1.);
        gdouble b = prefs->getDoubleLimited("/options/rendering/grayscale/blue-factor",0.072,0.,1.);
        gdouble grayscale_value_matrix[20] = { r, g, b, 0, 0,
                                               r, g, b, 0, 0,
                                               r, g, b, 0, 0,
                                               0, 0, 0, 1, 0 };
        // g_message("%g",grayscale_value_matrix[0]);
        SP_CANVAS_ARENA (drawing)->drawing.setGrayscaleMatrix(grayscale_value_matrix);
    }

    SP_CANVAS_ARENA (drawing)->drawing.setColorMode(mode);
    canvas->_colorrendermode = mode;
    _display_color_mode = mode;
    redrawDesktop();
    _widget->setTitle( this->getDocument()->getDocumentName() );
}

bool SPDesktop::displayModeToggle()
{
    Inkscape::Verb *verb = Inkscape::Verb::get(SP_VERB_VIEW_MODE_NORMAL);
    switch (_display_mode) {
    case Inkscape::RENDERMODE_NORMAL:
        _setDisplayMode(Inkscape::RENDERMODE_NO_FILTERS);
        verb = Inkscape::Verb::get(SP_VERB_VIEW_MODE_NO_FILTERS);
        if (verb) {
            _menu_update.emit(verb->get_code(), setDisplayModeNoFilters());
        }
        break;
    case Inkscape::RENDERMODE_NO_FILTERS:
        _setDisplayMode(Inkscape::RENDERMODE_OUTLINE);
        verb = Inkscape::Verb::get(SP_VERB_VIEW_MODE_OUTLINE);
        if (verb) {
            _menu_update.emit(verb->get_code(), setDisplayModeOutline());
        }

        break;
    case Inkscape::RENDERMODE_OUTLINE:
        _setDisplayMode(Inkscape::RENDERMODE_VISIBLE_HAIRLINES);
        verb = Inkscape::Verb::get(SP_VERB_VIEW_MODE_VISIBLE_HAIRLINES);
        if (verb) {
            _menu_update.emit(verb->get_code(), setDisplayModeVisibleHairlines());
        }
        break;
    case Inkscape::RENDERMODE_VISIBLE_HAIRLINES:
        _setDisplayMode(Inkscape::RENDERMODE_NORMAL);
        verb = Inkscape::Verb::get(SP_VERB_VIEW_MODE_NORMAL);
        if (verb) {
            _menu_update.emit(verb->get_code(), setDisplayModeNormal());
        }
        break;
    default:
        _setDisplayMode(Inkscape::RENDERMODE_NORMAL);
        verb = Inkscape::Verb::get(SP_VERB_VIEW_MODE_NORMAL);
        if (verb) {
            _menu_update.emit(verb->get_code(), setDisplayModeNormal());
        }
    }
    return true;
}
bool SPDesktop::displayColorModeToggle()
{
    Inkscape::Verb *verb = Inkscape::Verb::get(SP_VERB_VIEW_COLOR_MODE_NORMAL);
    switch (_display_color_mode) {
    case Inkscape::COLORMODE_NORMAL:
        _setDisplayColorMode(Inkscape::COLORMODE_GRAYSCALE);
        verb = Inkscape::Verb::get(SP_VERB_VIEW_COLOR_MODE_GRAYSCALE);
        if (verb) {
            _menu_update.emit(verb->get_code(), setDisplayColorModeGrayscale());
        }
        break;
    case Inkscape::COLORMODE_GRAYSCALE:
        _setDisplayColorMode(Inkscape::COLORMODE_NORMAL);
        if (verb) {
            _menu_update.emit(verb->get_code(), setDisplayColorModeNormal());
        }
        break;
//    case Inkscape::COLORMODE_PRINT_COLORS_PREVIEW:
    default:
        _setDisplayColorMode(Inkscape::COLORMODE_NORMAL);
        if (verb) {
            _menu_update.emit(verb->get_code(), setDisplayColorModeNormal());
        }
    }
    return true;
}

// Pass-through LayerModel functions
SPObject *SPDesktop::currentRoot() const
{
    return layers->currentRoot();
}

SPObject *SPDesktop::currentLayer() const
{
    return layers->currentLayer();
}

void SPDesktop::setCurrentLayer(SPObject *object)
{
    layers->setCurrentLayer(object);
}

void SPDesktop::toggleLayerSolo(SPObject *object)
{
    layers->toggleLayerSolo(object);
}

void SPDesktop::toggleHideAllLayers(bool hide)
{
    layers->toggleHideAllLayers(hide);
}

void SPDesktop::toggleLockAllLayers(bool lock)
{
    layers->toggleLockAllLayers(lock);
}

void SPDesktop::toggleLockOtherLayers(SPObject *object)
{
    layers->toggleLockOtherLayers(object);
}

bool SPDesktop::isLayer(SPObject *object) const
{
    return layers->isLayer(object);
}

/**
 * True if desktop viewport intersects \a item's bbox.
 */
bool SPDesktop::isWithinViewport (SPItem *item) const
{
    Geom::Rect const viewport = get_display_area();
    Geom::OptRect const bbox = item->desktopVisualBounds();
    if (bbox) {
        return viewport.intersects(*bbox);
    } else {
        return false;
    }
}

///
bool SPDesktop::itemIsHidden(SPItem const *item) const {
    return item->isHidden(this->dkey);
}

/**
 * Set activate property of desktop; emit signal if changed.
 */
void
SPDesktop::set_active (bool new_active)
{
    if (new_active != _active) {
        _active = new_active;
        if (new_active) {
            _activate_signal.emit();
        } else {
            _deactivate_signal.emit();
        }
    }
}

/**
 * Set activate status of current desktop's named view.
 */
void
SPDesktop::activate_guides(bool activate)
{
    guides_active = activate;
    namedview->activateGuides(this, activate);
}

/**
 * Make desktop switch documents.
 */
void
SPDesktop::change_document (SPDocument *theDocument)
{
    g_return_if_fail (theDocument != nullptr);

    /* unselect everything before switching documents */
    selection->clear();

    setDocument (theDocument);

    /* update the rulers, connect the desktop widget's signal to the new namedview etc.
       (this can probably be done in a better way) */
    InkscapeWindow *parent = this->getInkscapeWindow();
    g_assert(parent != nullptr);
    parent->change_document(theDocument);
    SPDesktopWidget *dtw = parent->get_desktop_widget();
    if (dtw) {
        dtw->desktop = this;
        dtw->updateNamedview();
    } else {
        std::cerr << "SPDesktop::change_document: failed to get desktop widget!" << std::endl;
    }

    _namedview_modified (namedview, SP_OBJECT_MODIFIED_FLAG, this);
    _document_replaced_signal.emit (this, theDocument);
}

/**
 * Replaces the currently active tool with a new one. Pass the empty string to
 * unset and free the current tool.
 */
void SPDesktop::setEventContext(const std::string& toolName)
{
    // Tool should be able to be replaced with itself. See commit 29df5ca05d
    if (event_context) {
        event_context->finish();
        delete event_context;
    }

    if (toolName.empty()) {
        event_context = nullptr;
    } else {
        event_context = ToolFactory::createObject(toolName);
        event_context->desktop = this;
        event_context->message_context = std::unique_ptr<Inkscape::MessageContext>(new Inkscape::MessageContext(this->messageStack()));
        event_context->setup();

        // Make sure no delayed snapping events are carried over after switching tools
        // (this is only an additional safety measure against sloppy coding, because each
        // tool should take care of this by itself)
        sp_event_context_discard_delayed_snap_event(event_context);
    }

    _event_context_changed_signal.emit(this, event_context);
}

/**
 * Sets the coordinate status to a given point
 */
void
SPDesktop::set_coordinate_status (Geom::Point p) {
    _widget->setCoordinateStatus(p);
}

Inkscape::UI::Widget::Dock* SPDesktop::getDock() {
	return _widget->getDock();
}

/**
 * \see SPDocument::getItemFromListAtPointBottom()
 */
SPItem *SPDesktop::getItemFromListAtPointBottom(const std::vector<SPItem*> &list, Geom::Point const &p) const
{
    g_return_val_if_fail (doc() != nullptr, NULL);
    return SPDocument::getItemFromListAtPointBottom(dkey, doc()->getRoot(), list, p);
}

/**
 * \see SPDocument::getItemAtPoint()
 */
SPItem *SPDesktop::getItemAtPoint(Geom::Point const &p, bool into_groups, SPItem *upto) const
{
    g_return_val_if_fail (doc() != nullptr, NULL);
    return doc()->getItemAtPoint( dkey, p, into_groups, upto);
}

/**
 * \see SPDocument::getGroupAtPoint()
 */
SPItem *SPDesktop::getGroupAtPoint(Geom::Point const &p) const
{
    g_return_val_if_fail (doc() != nullptr, NULL);
    return doc()->getGroupAtPoint(dkey, p);
}

/**
 * Returns the mouse point in document coordinates; if mouse is
 * outside the canvas, returns the center of canvas viewpoint.
 */
Geom::Point SPDesktop::point(bool outside_canvas) const
{
    Geom::Point p = _widget->getPointer();
    Geom::Point pw = sp_canvas_window_to_world (canvas, p);
    Geom::Rect const r = canvas->getViewbox();

    if (r.interiorContains(pw) || outside_canvas) {
        p = w2d(pw);
        return p;
    }
    Geom::Point r0 = w2d(r.min());
    Geom::Point r1 = w2d(r.max());
    return (r0 + r1) / 2.0;
}


/**
 * Revert back to previous transform if possible. Note: current transform is
 * always at front of stack.
 */
void
SPDesktop::prev_transform()
{
    if (transforms_past.empty()) {
        std::cerr << "SPDesktop::prev_transform: current transform missing!" << std::endl;
        return;
    }

    if (transforms_past.size() == 1) {
        messageStack()->flash(Inkscape::WARNING_MESSAGE, _("No previous transform."));
        return;
    }

    // Push current transform into future transforms list.
    transforms_future.push_front( _current_affine );

    // Remove the current transform from the past transforms list.
    transforms_past.pop_front();

    // restore previous transform
    _current_affine = transforms_past.front();
    set_display_area (false);

}


/**
 * Set transform to next in list.
 */
void SPDesktop::next_transform()
{
    if (transforms_future.empty()) {
        this->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("No next transform."));
        return;
    }

    // restore next transform
    _current_affine = transforms_future.front();
    set_display_area (false);

    // remove the just-used transform from the future transforms list
    transforms_future.pop_front();

    // push current transform into past transforms list
    transforms_past.push_front( _current_affine );
}


/**
 * Clear transform lists.
 */
void
SPDesktop::clear_transform_history()
{
    transforms_past.clear();
    transforms_future.clear();
}


/**
 * Does all the dirty work in setting the display area.
 * _current_affine must already be full updated (including offset).
 * log: if true, save transform in transform stack for reuse.
 */
void
SPDesktop::set_display_area (bool log)
{
    // Save the transform
    if (log) {
        transforms_past.push_front( _current_affine );
        // if we do a logged transform, our transform-forward list is invalidated, so delete it
        transforms_future.clear();
    }

    redrawDesktop();

    // Scroll
    Geom::Point offset = _current_affine.getOffset();
    canvas->scrollTo(offset, true);
    // To do: if transform unchanged call with 'false' (redraw only newly exposed areas).

    /* Update perspective lines if we are in the 3D box tool (so that infinite ones are shown
     * correctly) */
    if (SP_IS_BOX3D_CONTEXT(event_context)) {
    	SP_BOX3D_CONTEXT(event_context)->_vpdrag->updateLines();
    }

    _widget->updateRulers();
    _widget->updateScrollbars(_current_affine.getZoom());
    _widget->updateZoom();
    _widget->updateRotation();

    signal_zoom_changed.emit(_current_affine.getZoom());
}


/**
 * Map the drawing to the window so that 'c' lies at 'w' where where 'c'
 * is a point on the canvas and 'w' is position in window in screen pixels.
 */
void
SPDesktop::set_display_area (Geom::Point const &c, Geom::Point const &w, bool log)
{
    // The relative offset needed to keep c at w.
    Geom::Point offset = d2w(c) - w;
    _current_affine.addOffset( offset );
    set_display_area( log );
}


/**
 * Map the center of rectangle 'r' (which specifies a non-rotated region of the
 * drawing) to lie at the center of the window. The zoom factor is calculated such that
 * the edges of 'r' closest to 'w' are 'border' length inside of the window (if
 * there is no rotation). 'r' is in document pixel units, 'border' is in screen pixels.
 */
void
SPDesktop::set_display_area( Geom::Rect const &r, double border, bool log)
{
    // Create a rectangle the size of the window aligned with origin.
    Geom::Rect w( Geom::Point(), canvas->getViewbox().dimensions() ); // Not the SVG 'viewBox'.

    // Shrink window to account for border padding.
    w.expandBy( -border );

    double zoom = 1.0;
    // Determine which direction limits scale:
    //   if (r.width/w.width > r.height/w.height) then zoom using width.
    //   Avoiding division in test:
    if ( r.width()*w.height() > r.height()*w.width() ) {
        zoom = w.width() / r.width();
    } else {
        zoom = w.height() / r.height();
    }
    zoom = CLAMP(zoom, SP_DESKTOP_ZOOM_MIN, SP_DESKTOP_ZOOM_MAX);
    _current_affine.setScale( Geom::Scale(zoom, yaxisdir() * zoom) );
    // Zero offset, actual offset calculated later.
    _current_affine.setOffset( Geom::Point( 0, 0 ) );

    set_display_area( r.midpoint(), w.midpoint(), log );
}


/**
 * Return canvas viewbox bounding box in desktop coordinates
 *
 * @fixme Will be improved for non-90° rotations in https://gitlab.com/inkscape/inkscape/-/merge_requests/1399
 */
Geom::Rect SPDesktop::get_display_area(bool use_integer_viewbox) const
{
    // viewbox in world coordinates
    Geom::Rect const viewbox = use_integer_viewbox ? canvas->getViewboxIntegers() : canvas->getViewbox();

    // display area in desktop coordinates
    return viewbox * w2d();
}

/**
 * Zoom keeping the point 'c' fixed in the desktop window.
 */
void
SPDesktop::zoom_absolute_keep_point (Geom::Point const &c, double zoom)
{
    zoom = CLAMP (zoom, SP_DESKTOP_ZOOM_MIN, SP_DESKTOP_ZOOM_MAX);
    Geom::Point w = d2w( c ); // Must be before zoom changed.
    _current_affine.setScale( Geom::Scale(zoom, yaxisdir() * zoom) );
    set_display_area( c, w );
}


void
SPDesktop::zoom_relative_keep_point (Geom::Point const &c, double zoom)
{
    double new_zoom = _current_affine.getZoom() * zoom;
    zoom_absolute_keep_point( c, new_zoom );
}


/**
 * Zoom aligning the point 'c' to the center of desktop window.
 */
void
SPDesktop::zoom_absolute_center_point (Geom::Point const &c, double zoom)
{
    zoom = CLAMP (zoom, SP_DESKTOP_ZOOM_MIN, SP_DESKTOP_ZOOM_MAX);
    _current_affine.setScale( Geom::Scale(zoom, yaxisdir() * zoom) );
    Geom::Rect viewbox = canvas->getViewbox();
    set_display_area( c, viewbox.midpoint() );
}


void
SPDesktop::zoom_relative_center_point (Geom::Point const &c, double zoom)
{
    double new_zoom = _current_affine.getZoom() * zoom;
    zoom_absolute_center_point( c, new_zoom );
}


/**
 * Set display area to origin and current document dimensions.
 */
void
SPDesktop::zoom_page()
{
    Geom::Rect d(Geom::Point(0, 0),
                 Geom::Point(doc()->getWidth().value("px"), doc()->getHeight().value("px")));

    if (d.minExtent() < 1.0) {
        return;
    }

    set_display_area(d, 10);
}

/**
 * Set display area to current document width.
 */
void
SPDesktop::zoom_page_width()
{
    Geom::Rect const a = get_display_area();

    if (doc()->getWidth().value("px") < 1.0) {
        return;
    }

    Geom::Rect d(Geom::Point(0, a.midpoint()[Geom::Y]),
                 Geom::Point(doc()->getWidth().value("px"), a.midpoint()[Geom::Y]));

    set_display_area(d, 10);
}


/**
 * Zoom to whole drawing.
 */
void
SPDesktop::zoom_drawing()
{
    g_return_if_fail (doc() != nullptr);
    SPItem *docitem = doc()->getRoot();
    g_return_if_fail (docitem != nullptr);

    docitem->bbox_valid = FALSE;
    Geom::OptRect d = docitem->desktopVisualBounds();

    /* Note that the second condition here indicates that
    ** there are no items in the drawing.
    */
    if ( !d || d->minExtent() < 0.1 ) {
        return;
    }

    set_display_area(*d, 10);
}


/**
 * Zoom to selection.
 */
void
SPDesktop::zoom_selection()
{
    Geom::OptRect const d = selection->visualBounds();

    if ( !d || d->minExtent() < 0.1 ) {
        return;
    }

    set_display_area(*d, 10);
}

/**
 * Centre Page in window, without zooming
 */
void SPDesktop::zoom_center_page()
{
    zoom_absolute_center_point(Geom::Point(doc()->getWidth().value("px")/2, doc()->getHeight().value("px")/2), this->current_zoom());
}


/**
 * Performs a quick zoom into what the user is working on.
 *
 * @param  enable  Whether we're going in or out of quick zoom.
 */
void SPDesktop::zoom_quick(bool enable)
{
    if (enable == _quick_zoom_enabled) {
        return;
    }

    if (enable) {
        _quick_zoom_affine = _current_affine;
        bool zoomed = false;

        // TODO This needs to migrate into the node tool, but currently the design
        // of this method is sufficiently wrong to prevent this.
        if (!zoomed && INK_IS_NODE_TOOL(event_context)) {
            Inkscape::UI::Tools::NodeTool *nt = static_cast<Inkscape::UI::Tools::NodeTool*>(event_context);
            if (!nt->_selected_nodes->empty()) {
                Geom::Rect nodes = *nt->_selected_nodes->bounds();
                double area = nodes.area();
                // do not zoom if a single cusp node is selected aand the bounds
                // have zero area.
                if (!Geom::are_near(area, 0)) {
                    set_display_area(nodes, true);
                    zoomed = true;
                }
            }
        }

        if (!zoomed) {
            Geom::OptRect const d = selection->visualBounds();
            if (d) {
                set_display_area(*d, true);
                zoomed = true;
            }
        }

        if (!zoomed) {
            Geom::Rect const d_canvas = canvas->getViewbox(); // Not SVG 'viewBox'
            Geom::Point midpoint = w2d(d_canvas.midpoint()); // Midpoint of drawing on canvas.
            zoom_relative_center_point(midpoint, 2.0);
        }
    } else {
        _current_affine = _quick_zoom_affine;
        set_display_area( false );
    }

    _quick_zoom_enabled = enable;
    return;
}


/**
 * Tell widget to let zoom widget grab keyboard focus.
 */
void
SPDesktop::zoom_grab_focus()
{
    _widget->letZoomGrabFocus();
}


/**
 * Set new rotation, keeping the point 'c' fixed in the desktop window.
 *
 * @param c Point in desktop coordinates
 * @param rotate Angle in clockwise direction
 */
void
SPDesktop::rotate_absolute_keep_point (Geom::Point const &c, double rotate)
{
    Geom::Point w = d2w( c ); // Must be before rotate changed.
    _current_affine.setRotate( rotate );
    set_display_area( c, w );
}


/**
 * Rotate keeping the point 'c' fixed in the desktop window.
 *
 * @param c Point in desktop coordinates
 * @param rotate Angle in clockwise direction
 */
void
SPDesktop::rotate_relative_keep_point (Geom::Point const &c, double rotate)
{
    Geom::Point w = d2w( c ); // Must be before rotate changed.
    _current_affine.addRotate( rotate );
    set_display_area( c, w );
}


/**
 * Set new rotation, aligning the point 'c' to the center of desktop window.
 *
 * @param c Point in desktop coordinates
 * @param rotate Angle in clockwise direction
 */
void
SPDesktop::rotate_absolute_center_point (Geom::Point const &c, double rotate)
{
    _current_affine.setRotate( rotate );
    Geom::Rect viewbox = canvas->getViewbox();
    set_display_area(c, viewbox.midpoint());
}


/**
 * Rotate aligning the point 'c' to the center of desktop window.
 *
 * @param c Point in desktop coordinates
 * @param rotate Angle in clockwise direction
 */
void
SPDesktop::rotate_relative_center_point (Geom::Point const &c, double rotate)
{
    _current_affine.addRotate( rotate );
    Geom::Rect viewbox = canvas->getViewbox();
    set_display_area(c, viewbox.midpoint());
}


/**
 * Set new flip direction, keeping the point 'c' fixed in the desktop window.
 *
 * @param c Point in desktop coordinates
 * @param flip Direction the canvas will be set as.
 */
void
SPDesktop::flip_absolute_keep_point (Geom::Point const &c, CanvasFlip flip)
{
    Geom::Point w = d2w(c); // Must be before flip.
    _current_affine.setFlip(flip);
    set_display_area(c, w);
}


/**
 * Flip direction, keeping the point 'c' fixed in the desktop window.
 *
 * @param c Point in desktop coordinates
 * @param flip Direction to flip canvas
 */
void
SPDesktop::flip_relative_keep_point (Geom::Point const &c, CanvasFlip flip)
{
    Geom::Point w = d2w(c); // Must be before flip.
    _current_affine.addFlip(flip);
    set_display_area(c, w);
}


/**
 * Set new flip direction, aligning the point 'c' to the center of desktop window.
 *
 * @param c Point in desktop coordinates
 * @param flip Direction the canvas will be set as.
 */
void
SPDesktop::flip_absolute_center_point (Geom::Point const &c, CanvasFlip flip)
{
    _current_affine.setFlip(flip);
    Geom::Rect viewbox = canvas->getViewbox();
    set_display_area(c, viewbox.midpoint());
}


/**
 * Flip direction, aligning the point 'c' to the center of desktop window.
 *
 * @param c Point in desktop coordinates
 * @param flip Direction to flip canvas
 */
void
SPDesktop::flip_relative_center_point (Geom::Point const &c, CanvasFlip flip)
{
    _current_affine.addFlip(flip);
    Geom::Rect viewbox = canvas->getViewbox();
    set_display_area(c, viewbox.midpoint());
}

bool
SPDesktop::is_flipped (CanvasFlip flip)
{
    return _current_affine.isFlipped(flip);
}


/**
 * Scroll canvas by to a particular point (window coordinates).
 */
void
SPDesktop::scroll_absolute (Geom::Point const &point, bool is_scrolling)
{
    canvas->scrollTo(point, FALSE, is_scrolling);
    _current_affine.setOffset( point );

    /*  update perspective lines if we are in the 3D box tool (so that infinite ones are shown correctly) */
    //sp_box3d_context_update_lines(event_context);
    if (SP_IS_BOX3D_CONTEXT(event_context)) {
		SP_BOX3D_CONTEXT(event_context)->_vpdrag->updateLines();
	}

    _widget->updateRulers();
    _widget->updateScrollbars(_current_affine.getZoom());
}


/**
 * Scroll canvas by specific coordinate amount (window coordinates).
 */
void
SPDesktop::scroll_relative (Geom::Point const &delta, bool is_scrolling)
{
    Geom::Rect const viewbox = canvas->getViewbox();
    scroll_absolute( viewbox.min() - delta, is_scrolling );
}


/**
 * Scroll canvas by specific coordinate amount in svg coordinates.
 */
void
SPDesktop::scroll_relative_in_svg_coords (double dx, double dy, bool is_scrolling)
{
    double scale = _current_affine.getZoom();
    scroll_relative(Geom::Point(dx*scale, dy*scale), is_scrolling);
}


/**
 * Scroll screen so as to keep point 'p' visible in window.
 * (Used, for example, when a node is being dragged.)
 * 'p': The point in desktop coordinates.
 * 'autoscrollspeed': The scroll speed (or zero to use preferences' value).
 */
bool
SPDesktop::scroll_to_point (Geom::Point const &p, gdouble autoscrollspeed)
{
    Inkscape::Preferences *prefs = Inkscape::Preferences::get();

    // autoscrolldistance is in screen pixels.
    gdouble autoscrolldistance = (gdouble) prefs->getIntLimited("/options/autoscrolldistance/value", 0, -1000, 10000);

    Geom::Rect w = canvas->getViewbox(); // Window in screen coordinates.
    w.expandBy(-autoscrolldistance);  // Shrink window

    Geom::Point c = d2w(p);  // Point 'p' in screen coordinates.
    if (!w.contains(c)) {

        Geom::Point c2 = w.clamp(c); // Constrain c to window.

        if (autoscrollspeed == 0)
            autoscrollspeed = prefs->getDoubleLimited("/options/autoscrollspeed/value", 1, 0, 10);

        if (autoscrollspeed != 0)
            scroll_relative (autoscrollspeed * (c2 - c) );

        return true;
    }
    return false;
}

bool
SPDesktop::is_iconified()
{
    return 0!=(window_state & GDK_WINDOW_STATE_ICONIFIED);
}

void
SPDesktop::iconify()
{
    _widget->setIconified();
}

bool SPDesktop::is_darktheme() { return getToplevel()->get_style_context()->has_class("dark"); }

bool
SPDesktop::is_maximized()
{
    return 0!=(window_state & GDK_WINDOW_STATE_MAXIMIZED);
}

void
SPDesktop::maximize()
{
    _widget->setMaximized();
}

bool
SPDesktop::is_fullscreen()
{
    return 0!=(window_state & GDK_WINDOW_STATE_FULLSCREEN);
}

void
SPDesktop::fullscreen()
{
    _widget->setFullscreen();
}

/**
 * Checks to see if the user is working in focused mode.
 *
 * @return  the value of \c _focusMode.
 */
bool SPDesktop::is_focusMode()
{
    return _focusMode;
}

/**
 * Changes whether the user is in focus mode or not.
 *
 * @param  mode  Which mode the view should be in.
 */
void SPDesktop::focusMode(bool mode)
{
    if (mode == _focusMode) { return; }

    _focusMode = mode;

    layoutWidget();
    //sp_desktop_widget_layout(SPDesktopWidget);

    return;
}

void
SPDesktop::getWindowGeometry (gint &x, gint &y, gint &w, gint &h)
{
    _widget->getGeometry (x, y, w, h);
}

void
SPDesktop::setWindowPosition (Geom::Point p)
{
    _widget->setPosition (p);
}

void
SPDesktop::setWindowSize (gint w, gint h)
{
    _widget->setSize (w, h);
}

void
SPDesktop::setWindowTransient (void *p, int transient_policy)
{
    _widget->setTransient (p, transient_policy);
}

Gtk::Window*
SPDesktop::getToplevel( )
{
    return _widget->getWindow();
}

InkscapeWindow*
SPDesktop::getInkscapeWindow( )
{
    InkscapeWindow* window = dynamic_cast<InkscapeWindow*>(_widget->getWindow());
    if (!window) {
        std::cerr << "SPDesktop::getInkscapeWindow: Failed to get window." << std::endl;
    }
    return window;
}

void
SPDesktop::presentWindow()
{
    _widget->present();
}

bool SPDesktop::showInfoDialog( Glib::ustring const & message )
{
    return _widget->showInfoDialog( message );
}

bool
SPDesktop::warnDialog (Glib::ustring const &text)
{
    return _widget->warnDialog (text);
}

void
SPDesktop::toggleRulers()
{
    _widget->toggleRulers();
    Inkscape::Verb *verb = Inkscape::Verb::get(SP_VERB_TOGGLE_RULERS);
    if (verb) {
        _menu_update.emit(verb->get_code(), getStateFromPref(this, "rulers"));
    }
}

void
SPDesktop::toggleScrollbars()
{
    _widget->toggleScrollbars();
    Inkscape::Verb *verb = Inkscape::Verb::get(SP_VERB_TOGGLE_SCROLLBARS);
    if (verb) {
        _menu_update.emit(verb->get_code(), getStateFromPref(this, "scrollbars"));
    }
}


void SPDesktop::toggleToolbar(gchar const *toolbar_name, unsigned int verbenum)
{
    Glib::ustring pref_path = getLayoutPrefPath(this) + toolbar_name + "/state";
    Inkscape::Preferences *prefs = Inkscape::Preferences::get();
    gboolean visible = prefs->getBool(pref_path, true);
    prefs->setBool(pref_path, !visible);
    Inkscape::Verb *verb = Inkscape::Verb::get(verbenum);
    if (verb) {
        _menu_update.emit(verb->get_code(), getStateFromPref(this, toolbar_name));
    }
    layoutWidget();
}

void
SPDesktop::layoutWidget()
{
    _widget->layout();
}

void
SPDesktop::destroyWidget()
{
    _widget->destroy();
}

bool
SPDesktop::shutdown()
{
    return _widget->shutdown();
}

bool SPDesktop::onDeleteUI (GdkEventAny*)
{
    if(shutdown())
        return true;

    destroyWidget();
    return false;
}

/**
 *  onWindowStateEvent
 *
 *  Called when the window changes its maximize/fullscreen/iconify/pinned state.
 *  Since GTK doesn't have a way to query this state information directly, we
 *  record it for the desktop here, and also possibly trigger a layout.
 */
bool
SPDesktop::onWindowStateEvent (GdkEventWindowState* event)
{
    // Record the desktop window's state
    window_state = event->new_window_state;

    // Layout may differ depending on full-screen mode or not
    GdkWindowState changed = event->changed_mask;
    if (changed & (GDK_WINDOW_STATE_FULLSCREEN|GDK_WINDOW_STATE_MAXIMIZED)) {
        layoutWidget();
    }

    return false;
}


/**
  * Apply the desktop's current style or the tool style to the object.
  */
void SPDesktop::applyCurrentOrToolStyle(SPObject *obj, Glib::ustring const &tool_path, bool with_text)
{
    SPCSSAttr *css_current = sp_desktop_get_style(this, with_text);
    Inkscape::Preferences *prefs = Inkscape::Preferences::get();

    if (prefs->getBool(tool_path + "/usecurrent") && css_current) {
        obj->setCSS(css_current,"style");
    } else {
        SPCSSAttr *css = prefs->getInheritedStyle(tool_path + "/style");
        obj->setCSS(css,"style");
        sp_repr_css_attr_unref(css);
    }
    if (css_current) {
        sp_repr_css_attr_unref(css_current);
    }
}


void
SPDesktop::setToolboxFocusTo (gchar const *label)
{
    _widget->setToolboxFocusTo (label);
}

void
SPDesktop::setToolboxAdjustmentValue (gchar const* id, double val)
{
    _widget->setToolboxAdjustmentValue (id, val);
}

Gtk::Toolbar*
SPDesktop::get_toolbar_by_name(const Glib::ustring& name)
{
    return _widget->get_toolbar_by_name(name);
}

bool
SPDesktop::isToolboxButtonActive (gchar const *id)
{
    return _widget->isToolboxButtonActive (id);
}

void
SPDesktop::emitToolSubselectionChanged(gpointer data)
{
    _tool_subselection_changed.emit(data);
    INKSCAPE.subselection_changed (this);
}

void SPDesktop::updateNow()
{
    canvas->updateNow();
}

void
SPDesktop::enableInteraction()
{
  _widget->enableInteraction();
}

void SPDesktop::disableInteraction()
{
  _widget->disableInteraction();
}

void SPDesktop::setWaitingCursor()
{
    Glib::RefPtr<Gdk::Display> display = Gdk::Display::get_default();
    Glib::RefPtr<Gdk::Cursor> waiting = Gdk::Cursor::create(display, Gdk::WATCH);
    Glib::wrap(GTK_WIDGET(getCanvas()))->get_window()->set_cursor(waiting);
    // GDK needs the flush for the cursor change to take effect
    display->flush();
    waiting_cursor = true;
}

void SPDesktop::clearWaitingCursor() {
  if (waiting_cursor) {
      this->event_context->sp_event_context_update_cursor();
  }
}

void SPDesktop::toggleColorProfAdjust()
{
    _widget->toggleColorProfAdjust();
    Inkscape::Verb *verb = Inkscape::Verb::get(SP_VERB_VIEW_CMS_TOGGLE);
    if (verb) {
        _menu_update.emit(verb->get_code(), colorProfAdjustEnabled());
    }
}

void SPDesktop::toggleGuidesLock()
{
    sp_namedview_guides_toggle_lock(this->getDocument(), namedview);
    Inkscape::Verb *verb = Inkscape::Verb::get(SP_VERB_EDIT_GUIDES_TOGGLE_LOCK);
    if (verb) {
        _menu_update.emit(verb->get_code(), namedview->lockguides);
    }
}

bool SPDesktop::colorProfAdjustEnabled()
{
    return _widget->colorProfAdjustEnabled();
}

void SPDesktop::toggleGrids()
{
    if (! namedview->grids.empty()) {
        if(gridgroup) {
            showGrids(!grids_visible);
        }
    } else {
        //there is no grid present at the moment. add a rectangular grid and make it visible
        namedview->writeNewGrid(this->getDocument(), Inkscape::GRID_RECTANGULAR);
        showGrids(true);
    }
    Inkscape::Verb *verb = Inkscape::Verb::get(SP_VERB_TOGGLE_GRID);
    if (verb) {
        _menu_update.emit(verb->get_code(), gridsEnabled());
    }
}

void SPDesktop::toggleSplitMode()
{
    if (this->getToplevel()) {
        _split_canvas = !_split_canvas;
        if (_split_canvas && _xray) {
            toggleXRay();
        }
        SPCanvas *canvas = getCanvas();
        canvas->requestFullRedraw();
        Inkscape::Verb *verb = Inkscape::Verb::get(SP_VERB_VIEW_TOGGLE_SPLIT);
        if (verb) {
            _menu_update.emit(verb->get_code(), splitMode());
        }
        if (_display_mode == Inkscape::RENDERMODE_OUTLINE) {
            if (_split_canvas) {
                toggleSplitMode();
            }
        }
    }
}

void SPDesktop::toggleXRay()
{
    if (this->getToplevel()) {
        _xray = !_xray;
        if (_split_canvas && _xray) {
            toggleSplitMode();
        }
        SPCanvas *canvas = getCanvas();
        canvas->requestFullRedraw();
        Inkscape::Verb *verb = Inkscape::Verb::get(SP_VERB_VIEW_TOGGLE_XRAY);
        if (verb) {
            _menu_update.emit(verb->get_code(), xrayMode());
        }
        if (_display_mode == Inkscape::RENDERMODE_OUTLINE) {
            if (_xray) {
                toggleXRay();
            }
        }
    }
}

void SPDesktop::showGrids(bool show, bool dirty_document)
{
    grids_visible = show;
    sp_namedview_show_grids(namedview, grids_visible, dirty_document);
    if (show) {
        sp_canvas_item_show(SP_CANVAS_ITEM(gridgroup));
    } else {
        sp_canvas_item_hide(SP_CANVAS_ITEM(gridgroup));
    }
}

void SPDesktop::toggleSnapGlobal()
{
    bool v = namedview->getSnapGlobal();
    namedview->setSnapGlobal(!v);
}

//----------------------------------------------------------------------
// Callback implementations. The virtual ones are connected by the view.

void
SPDesktop::onResized (double /*x*/, double /*y*/)
{
   // Nothing called here
}

/**
 * Redraw callback; queues Gtk redraw; connected by View.
 */
void SPDesktop::storeDesktopPosition()
{
    if (main) {
        _widget->storeDesktopPosition();
    }
}

/**
 * Redraw callback; queues Gtk redraw; connected by View.
 */
void
SPDesktop::onRedrawRequested ()
{
    if (main) {
        _widget->requestCanvasUpdate();
    }
}

void
SPDesktop::updateCanvasNow()
{
  _widget->requestCanvasUpdateAndWait();
}

/**
 * Associate document with desktop.
 */
void
SPDesktop::setDocument (SPDocument *doc)
{
    if (!doc) return;

    if (this->doc()) {
        namedview->hide(this);
        this->doc()->getRoot()->invoke_hide(dkey);
    }

    layers->setDocument(doc);
    selection->setDocument(doc);

    if (event_log) {
        // Remove it from the replaced document. This prevents Inkscape from
        // crashing since we access it in the replaced document's destructor
        // which results in an undefined behavior. (See also: bug #1670688)
        if (this->doc()) {
            this->doc()->removeUndoObserver(*event_log);
        }
        delete event_log;
        event_log = nullptr;
    }

    /* setup EventLog */
    event_log = new Inkscape::EventLog(doc);
    doc->addUndoObserver(*event_log);

    _commit_connection.disconnect();
    _commit_connection = doc->connectCommit(sigc::mem_fun(*this, &SPDesktop::updateNow));

    /// \todo fixme: This condition exists to make sure the code
    /// inside is NOT called on initialization, only on replacement. But there
    /// are surely more safe methods to accomplish this.
    // TODO since the comment had reversed logic, check the intent of this block of code:
    if (drawing) {
        Inkscape::DrawingItem *ai = nullptr;

        namedview = sp_document_namedview (doc, nullptr);
        _modified_connection = namedview->connectModified(sigc::bind<2>(sigc::ptr_fun(&_namedview_modified), this));
        number = namedview->getViewCount();

        ai = doc->getRoot()->invoke_show(
                SP_CANVAS_ARENA (drawing)->drawing,
                dkey,
                SP_ITEM_SHOW_DISPLAY);
        if (ai) {
            SP_CANVAS_ARENA (drawing)->drawing.root()->prependChild(ai);
        }
        namedview->show(this);
        /* Ugly hack */
        activate_guides (true);
        /* Ugly hack */
        _namedview_modified (namedview, SP_OBJECT_MODIFIED_FLAG, this);
    }

    _document_replaced_signal.emit (this, doc);

    View::setDocument (doc);
}

void
SPDesktop::onStatusMessage
(Inkscape::MessageType type, gchar const *message)
{
    if (_widget) {
        _widget->setMessage(type, message);
    }
}

void
SPDesktop::onDocumentURISet (gchar const* uri)
{
    _widget->setTitle(uri);
}

/**
 * Resized callback.
 */
void
SPDesktop::onDocumentResized (gdouble width, gdouble height)
{
    sp_canvas_item_affine_absolute (SP_CANVAS_ITEM (drawing), doc2dt());
    Geom::Rect const a(Geom::Point(0, 0), Geom::Point(width, height));
    SP_CTRLRECT(page)->setRectangle(a);
    SP_CTRLRECT(page_border)->setRectangle(a);
}


void
SPDesktop::_onActivate (SPDesktop* dt)
{
    if (!dt->_widget) return;
    dt->_widget->activateDesktop();
}

void
SPDesktop::_onDeactivate (SPDesktop* dt)
{
    if (!dt->_widget) return;
    dt->_widget->deactivateDesktop();
}

void
SPDesktop::_onSelectionModified
(Inkscape::Selection */*selection*/, guint /*flags*/, SPDesktop *dt)
{
    if (!dt->_widget) return;
    dt->_widget->updateScrollbars (dt->_current_affine.getZoom());
}

static void
_onSelectionChanged
(Inkscape::Selection *selection, SPDesktop *desktop)
{
    /** \todo
     * only change the layer for single selections, or what?
     * This seems reasonable -- for multiple selections there can be many
     * different layers involved.
     */
    SPItem *item=selection->singleItem();
    if (item) {
        SPObject *layer=desktop->layers->layerForObject(item);
        if ( layer && layer != desktop->currentLayer() ) {
            desktop->layers->setCurrentLayer(layer);
        }
    }
}

/**
 * Calls event handler of current event context.
 * \param arena Unused
 * \todo fixme
 */
static gint
_arena_handler (SPCanvasArena */*arena*/, Inkscape::DrawingItem *ai, GdkEvent *event, SPDesktop *desktop)
{
    if (event->type == GDK_KEY_PRESS && 
        Inkscape::UI::Tools::get_latin_keyval(&event->key) == GDK_KEY_space &&
        desktop->event_context->space_panning) 
    {
        return true;
    }
    if (ai) {
        SPItem *spi = ai->getItem();
        return sp_event_context_item_handler (desktop->event_context, spi, event);
    } else {
        return sp_event_context_root_handler (desktop->event_context, event);
    }
}

static void
_layer_activated(SPObject *layer, SPDesktop *desktop) {
    g_return_if_fail(SP_IS_GROUP(layer));
    SP_GROUP(layer)->setLayerDisplayMode(desktop->dkey, SPGroup::LAYER);
}

/// Callback
static void
_layer_deactivated(SPObject *layer, SPDesktop *desktop) {
    g_return_if_fail(SP_IS_GROUP(layer));
    SP_GROUP(layer)->setLayerDisplayMode(desktop->dkey, SPGroup::GROUP);
}

/// Callback
static void
_layer_hierarchy_changed(SPObject */*top*/, SPObject *bottom,
                                         SPDesktop *desktop)
{
    desktop->_layer_changed_signal.emit (bottom);
}

/// Called when document is starting to be rebuilt.
static void _reconstruction_start(SPDesktop * desktop)
{
    desktop->_reconstruction_old_layer_id = desktop->currentLayer()->getId() ? desktop->currentLayer()->getId() : "";
    desktop->layers->reset();

    desktop->selection->clear();
}

/// Called when document rebuild is finished.
static void _reconstruction_finish(SPDesktop * desktop)
{
    g_debug("Desktop, finishing reconstruction\n");
    if ( !desktop->_reconstruction_old_layer_id.empty() ) {
        SPObject * newLayer = desktop->namedview->document->getObjectById(desktop->_reconstruction_old_layer_id);
        if (newLayer != nullptr) {
            desktop->layers->setCurrentLayer(newLayer);
        }

        desktop->_reconstruction_old_layer_id.clear();
    }
    g_debug("Desktop, finishing reconstruction end\n");
}

/**
 * Namedview_modified callback.
 */
static void _namedview_modified (SPObject *obj, guint flags, SPDesktop *desktop)
{
    SPNamedView *nv=SP_NAMEDVIEW(obj);

    if (flags & SP_OBJECT_MODIFIED_FLAG) {
        if (nv->pagecheckerboard) {
            desktop->canvas->setBackgroundCheckerboard(nv->pagecolor);
        } else {
            desktop->canvas->setBackgroundColor(nv->pagecolor);
        }

        /* Show/hide page border */
        if (nv->showborder) {
            // show
            sp_canvas_item_show (desktop->page_border);
            // set color and shadow
            ((CtrlRect *) desktop->page_border)->setColor(nv->bordercolor, false, 0x00000000);
            if (nv->pageshadow) {
                ((CtrlRect *) desktop->page_border)->setShadow(nv->pageshadow, nv->bordercolor);
            }
            // place in the z-order stack
            if (nv->borderlayer == SP_BORDER_LAYER_BOTTOM) {
                 sp_canvas_item_move_to_z (desktop->page_border, 1);
            } else {
                int order = sp_canvas_item_order (desktop->page_border);
                int morder = sp_canvas_item_order (desktop->drawing);
                if (morder > order) sp_canvas_item_raise (desktop->page_border,
                    morder - order);
            }
        } else {
                sp_canvas_item_hide (desktop->page_border);
                if (nv->pageshadow) {
                    ((CtrlRect *) desktop->page)->setShadow(0, 0x00000000);
                }
        }

        /* Show/hide page shadow */
        if (nv->showpageshadow && nv->pageshadow) {
            ((CtrlRect *) desktop->page_border)->setShadow(nv->pageshadow, nv->bordercolor);
        } else {
            ((CtrlRect *) desktop->page_border)->setShadow(0, 0x00000000);
        }

        Inkscape::Preferences *prefs = Inkscape::Preferences::get();
        if (SP_RGBA32_R_U(nv->pagecolor) +
            SP_RGBA32_G_U(nv->pagecolor) +
            SP_RGBA32_B_U(nv->pagecolor) >= 384) {
            // the background color is light, use black outline
            SP_CANVAS_ARENA (desktop->drawing)->drawing.outlinecolor = prefs->getInt("/options/wireframecolors/onlight", 0xff);
        } else { // use white outline
            SP_CANVAS_ARENA (desktop->drawing)->drawing.outlinecolor = prefs->getInt("/options/wireframecolors/ondark", 0xffffffff);
        }
    }
}

Geom::Affine SPDesktop::w2d() const
{
    return _current_affine.w2d();
}

Geom::Point SPDesktop::w2d(Geom::Point const &p) const
{
    return p * _current_affine.w2d();
}

Geom::Point SPDesktop::d2w(Geom::Point const &p) const
{
    return p * _current_affine.d2w();
}

const Geom::Affine &SPDesktop::doc2dt() const
{
    g_assert(doc() != nullptr);
    return doc()->doc2dt();
}

Geom::Affine SPDesktop::dt2doc() const
{
    g_assert(doc() != nullptr);
    return doc()->dt2doc();
}

Geom::Point SPDesktop::doc2dt(Geom::Point const &p) const
{
    return p * doc2dt();
}

Geom::Point SPDesktop::dt2doc(Geom::Point const &p) const
{
    return p * dt2doc();
}

void
SPDesktop::show_dialogs()
{

    Inkscape::Preferences *prefs = Inkscape::Preferences::get();
    if (prefs == nullptr) {
        return;
    }

    int active = prefs->getInt("/options/savedialogposition/value", 1);
    if (active == 0) {
        // User has turned off this feature in preferences
        return;
    }

    if (showing_dialogs) {
        return;
    }

    showing_dialogs = TRUE;


    /*
     * Get each dialogs previous state from preferences and reopen on startup if needed, without grabbing focus (canvas retains focus).
     * Map dialog manager's dialog IDs to dialog last visible state preference. FIXME: store this correspondence in dialogs themselves!
     */
    std::map<Glib::ustring, Glib::ustring> mapVerbPreference;
    mapVerbPreference.insert(std::make_pair ("LayersPanel", "/dialogs/layers") );
    mapVerbPreference.insert(std::make_pair ("FillAndStroke", "/dialogs/fillstroke") );
    mapVerbPreference.insert(std::make_pair ("ExtensionEditor", "/dialogs/extensioneditor") );
    mapVerbPreference.insert(std::make_pair ("AlignAndDistribute", "/dialogs/align") );
    mapVerbPreference.insert(std::make_pair ("DocumentMetadata", "/dialogs/documentmetadata") );
    mapVerbPreference.insert(std::make_pair ("DocumentProperties", "/dialogs/documentoptions") );
    mapVerbPreference.insert(std::make_pair ("FilterEffectsDialog", "/dialogs/filtereffects") );
    mapVerbPreference.insert(std::make_pair ("Find", "/dialogs/find") );
    mapVerbPreference.insert(std::make_pair ("Glyphs", "/dialogs/glyphs") );
    mapVerbPreference.insert(std::make_pair ("Messages", "/dialogs/messages") );
    mapVerbPreference.insert(std::make_pair ("Memory", "/dialogs/memory") );
    mapVerbPreference.insert(std::make_pair ("LivePathEffect", "/dialogs/livepatheffect") );
    mapVerbPreference.insert(std::make_pair ("UndoHistory", "/dialogs/undo-history") );
    mapVerbPreference.insert(std::make_pair ("Transformation", "/dialogs/transformation") );
    mapVerbPreference.insert(std::make_pair ("Swatches", "/dialogs/swatches") );
    mapVerbPreference.insert(std::make_pair ("IconPreviewPanel", "/dialogs/iconpreview") );
    mapVerbPreference.insert(std::make_pair ("SvgFontsDialog", "/dialogs/svgfonts") );
    mapVerbPreference.insert(std::make_pair ("InputDevices", "/dialogs/inputdevices") );
    mapVerbPreference.insert(std::make_pair ("InkscapePreferences", "/dialogs/preferences") );
    mapVerbPreference.insert(std::make_pair ("TileDialog", "/dialogs/gridtiler") );
    mapVerbPreference.insert(std::make_pair ("Trace", "/dialogs/trace") );
    mapVerbPreference.insert(std::make_pair ("TextFont", "/dialogs/textandfont") );
    mapVerbPreference.insert(std::make_pair ("Export", "/dialogs/export") );
    mapVerbPreference.insert(std::make_pair ("XmlTree", "/dialogs/xml") );
    mapVerbPreference.insert(std::make_pair ("Selectors", "/dialogs/selectors") );
    mapVerbPreference.insert(std::make_pair ("CloneTiler", "/dialogs/clonetiler") );
    mapVerbPreference.insert(std::make_pair ("ObjectProperties", "/dialogs/object") );
    mapVerbPreference.insert(std::make_pair ("SpellCheck", "/dialogs/spellcheck") );
    mapVerbPreference.insert(std::make_pair ("Symbols", "/dialogs/symbols") );
    mapVerbPreference.insert(std::make_pair ("PaintServers", "/dialogs/paint") );
    mapVerbPreference.insert(std::make_pair ("ObjectsPanel", "/dialogs/objects") );
    mapVerbPreference.insert(std::make_pair ("TagsPanel", "/dialogs/tags") );
    mapVerbPreference.insert(std::make_pair ("Prototype", "/dialogs/prototype") );


    for (std::map<Glib::ustring, Glib::ustring>::const_iterator iter = mapVerbPreference.begin(); iter != mapVerbPreference.end(); ++iter) {
        Glib::ustring pref = iter->second;

        int visible = prefs->getInt(pref + "/visible", 0);
        if (visible) {

#ifdef GDK_WINDOWING_WAYLAND
            // Hack to prevent crash with Wayland. See: https://gitlab.com/inkscape/inkscape/issues/454
            if (iter->first == "InkscapePreferences") {
                Glib::ustring session_type  = Glib::getenv("XDG_SESSION_TYPE");  // Window session
                Glib::ustring session_type2 = Glib::getenv("GDK_BACKEND");       // Possible override
                if (session_type == "wayland" && session_type2 != "x11") {
                    std::cerr << "SPDesktop::show_dialog: Cannot restore InkscapePreferences dialog due to GTK Wayland bug." << std::endl;
                    continue;
                }
            }
#endif

            // Try to ensure that the panel is created attached to the correct desktop (bug 1720096).
            // There must be a better way of handling this problem!
            INKSCAPE.activate_desktop(this);

            _dlg_mgr->showDialog(iter->first.c_str(), false); // without grabbing focus, we need focus to remain on the canvas
        }
    }
}
/*
 * Pop event context from desktop's context stack. Never used.
 */
// void
// SPDesktop::pop_event_context (unsigned int key)
// {
//    ToolBase *ec = NULL;
//
//    if (event_context && event_context->key == key) {
//        g_return_if_fail (event_context);
//        g_return_if_fail (event_context->next);
//        ec = event_context;
//        sp_event_context_deactivate (ec);
//        event_context = ec->next;
//        sp_event_context_activate (event_context);
//        _event_context_changed_signal.emit (this, ec);
//    }
//
//    ToolBase *ref = event_context;
//    while (ref && ref->next && ref->next->key != key)
//        ref = ref->next;
//
//    if (ref && ref->next) {
//        ec = ref->next;
//        ref->next = ec->next;
//    }
//
//    if (ec) {
//        sp_event_context_finish (ec);
//        g_object_unref (G_OBJECT (ec));
//    }
// }

/*
  Local Variables:
  mode:c++
  c-file-style:"stroustrup"
  c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
  indent-tabs-mode:nil
  fill-column:99
  End:
*/
// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
