summaryrefslogtreecommitdiffstats
path: root/src/ui/cursor-utils.cpp
blob: 5887974288eea8c9d721e20a13e417b1245198fd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Cursor utilities
 *
 * Copyright (C) 2020 Tavmjong Bah
 *
 * The contents of this file may be used under the GNU General Public License Version 2 or later.
 *
 */

#include <iomanip>
#include <sstream>
#include <unordered_map>
#include <boost/functional/hash.hpp>

#include "cursor-utils.h"

#include "document.h"
#include "preferences.h"

#include "display/cairo-utils.h"

#include "helper/pixbuf-ops.h"

#include "io/file.h"
#include "io/resource.h"

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

#include "util/units.h"

using Inkscape::IO::Resource::SYSTEM;
using Inkscape::IO::Resource::ICONS;

namespace Inkscape {

// SVG cursor unique ID/key
typedef std::tuple<std::string, std::string, std::string, guint32, guint32, double, double, bool, int> Key;

struct KeyHasher {
    std::size_t operator () (const Key& k) const { return boost::hash_value(k); }
};

/**
 * Loads an SVG cursor from the specified file name.
 *
 * Returns pointer to cursor (or null cursor if we could not load a cursor).
 */
Glib::RefPtr<Gdk::Cursor>
load_svg_cursor(Glib::RefPtr<Gdk::Display> display,
                Glib::RefPtr<Gdk::Window> window,
                std::string const &file_name,
                guint32 fill,
                guint32 stroke,
                double fill_opacity,
                double stroke_opacity)
{
    // GTK puts cursors in a "cursors" subdirectory of icon themes. We'll do the same... but
    // note that we cannot use the normal GTK method for loading cursors as GTK knows nothing
    // about scalable SVG cursors. We must locate and load the files ourselves. (Even if
    // GTK could handle scalable cursors, we would need to load the files ourselves inorder
    // to modify CSS 'fill' and 'stroke' properties.)

    Glib::RefPtr<Gdk::Cursor> cursor;

    // Make list of icon themes, highest priority first.
    std::vector<std::string> theme_names;

    // Set in preferences
    Inkscape::Preferences *prefs = Inkscape::Preferences::get();
    Glib::ustring theme_name = prefs->getString("/theme/iconTheme", prefs->getString("/theme/defaultIconTheme", ""));
    if (!theme_name.empty()) {
        theme_names.push_back(theme_name);
    }

    // System
    theme_name = Gtk::Settings::get_default()->property_gtk_icon_theme_name();
    theme_names.push_back(theme_name);

    // Our default
    theme_names.emplace_back("hicolor");

    // quantize opacity to limit number of cursor variations we generate
    fill_opacity   = std::floor(std::clamp(fill_opacity,   0.0, 1.0) * 100) / 100;
    stroke_opacity = std::floor(std::clamp(stroke_opacity, 0.0, 1.0) * 100) / 100;

    const auto enable_drop_shadow = prefs->getBool("/options/cursor-drop-shadow", true);

    // Find the rendered size of the icon.
    int scale = 1;
    bool cursor_scaling = false;
#ifndef GDK_WINDOWING_QUARTZ
    // Default cursor size (get_default_cursor_size()) fixed to 32 on Quartz. Cursor scaling handled elsewhere.

    cursor_scaling = prefs->getBool("/options/cursorscaling"); // Fractional scaling is broken but we can't detect it.
    if (cursor_scaling) {
        scale = window->get_scale_factor(); // Adjust for HiDPI screens.
    }
#endif
    static std::unordered_map<Key, Glib::RefPtr<Gdk::Cursor>, KeyHasher> cursor_cache;
    Key cursor_key;

    const auto cache_enabled = prefs->getBool("/options/cache_svg_cursors", true);
    if (cache_enabled) {
        // construct a key
        cursor_key = std::make_tuple(std::string(theme_names[0]), std::string(theme_names[1]), file_name, fill, stroke, fill_opacity, stroke_opacity, enable_drop_shadow, scale);
        if (auto cursor = cursor_cache[cursor_key]) {
            return cursor;
        }
    }

    // Find theme paths.
    auto screen = display->get_default_screen();
    auto icon_theme = Gtk::IconTheme::get_for_screen(screen);
    auto theme_paths = icon_theme->get_search_path();

    // Loop over theme names and paths, looking for file.
    Glib::RefPtr<Gio::File> file;
    std::string full_file_path;
    for (auto theme_name : theme_names) {
        for (auto theme_path : theme_paths) {
            full_file_path = Glib::build_filename(theme_path, theme_name, "cursors", file_name);
            // std::cout << "Checking: " << full_file_path << std::endl;
            file = Gio::File::create_for_path(full_file_path);
            if (file->query_exists()) break;
        }
        if (file->query_exists()) break;
    }

    if (!file->query_exists()) {
        std::cerr << "load_svg_cursor: Cannot locate cursor file: " << file_name << std::endl;
        return cursor;
    }

    bool cancelled = false;
    std::unique_ptr<SPDocument> document;
    document.reset(ink_file_open(file, &cancelled));

    if (!document) {
        std::cerr << "load_svg_cursor: Could not open document: " << full_file_path << std::endl;
        return cursor;
    }

    SPRoot *root = document->getRoot();
    if (!root) {
        std::cerr << "load_svg_cursor: Could not find SVG element: " << full_file_path << std::endl;
        return cursor;
    }

    // Set the CSS 'fill' and 'stroke' properties on the SVG element (for cascading).
    SPCSSAttr *css = sp_repr_css_attr(root->getRepr(), "style");

    std::stringstream fill_stream;
    fill_stream << "#"
                << std::setfill ('0') << std::setw(6)
                << std::hex << (fill >> 8);
    std::stringstream stroke_stream;
    stroke_stream << "#"
                  << std::setfill ('0') << std::setw(6)
                  << std::hex << (stroke >> 8);

    sp_repr_css_set_property(css, "fill",   fill_stream.str().c_str());
    sp_repr_css_set_property(css, "stroke", stroke_stream.str().c_str());
    sp_repr_css_set_property_double(css, "fill-opacity",   fill_opacity);
    sp_repr_css_set_property_double(css, "stroke-opacity", stroke_opacity);
    root->changeCSS(css, "style");
    sp_repr_css_attr_unref(css);

    if (!enable_drop_shadow) {
        // turn off drop shadow, if any
        Glib::ustring shadow("drop-shadow");
        auto objects = document->getObjectsByClass(shadow);
        for (auto&& el : objects) {
            if (auto val = el->getAttribute("class")) {
                Glib::ustring cls = val;
                auto pos = cls.find(shadow);
                if (pos != Glib::ustring::npos) {
                    cls.erase(pos, shadow.length());
                }
                el->setAttribute("class", cls);
            }
        }
    }

    // Check for maximum size
    // int mwidth = 0;
    // int mheight = 0;
    // display->get_maximal_cursor_size(mwidth, mheight);
    // int normal_size = display->get_default_cursor_size();

    auto w = document->getWidth().value("px");
    auto h = document->getHeight().value("px");
    // Calculate the hotspot.
    int hotspot_x = root->getIntAttribute("inkscape:hotspot_x", 0); // Do not include window scale factor!
    int hotspot_y = root->getIntAttribute("inkscape:hotspot_y", 0);

    Geom::Rect area(0, 0, cursor_scaling ? w * scale : w, cursor_scaling ? h * scale : h);
    int dpi = 96 * scale;
    // render document into internal bitmap; returns null on failure
    if (auto ink_pixbuf = std::unique_ptr<Inkscape::Pixbuf>(sp_generate_internal_bitmap(document.get(), area, dpi))) {
       if (cursor_scaling) {
            // creating cursor from Cairo surface rather than pixbuf gives us opportunity to set device scaling;
            // what that means in practice is we can prepare high-res image and it will be used as-is on
            // a high-res display; cursors created from pixbuf are up-scaled to device pixels (blurry)
            auto surface = ink_pixbuf->getSurface();
            if (surface && surface->cobj()) {
                cairo_surface_set_device_scale(surface->cobj(), scale, scale);
                cursor = Gdk::Cursor::create(display, surface, hotspot_x, hotspot_y);
            }
            else {
                std::cerr << "load_svg_cursor: failed to get surface for: " << full_file_path << std::endl;
            }
        }
        else {
            // original code path when cursor scaling is turned off in preferences
            auto pixbuf = Glib::wrap(ink_pixbuf->getPixbufRaw(), true);

            if (pixbuf) {
                cursor = Gdk::Cursor::create(display, pixbuf, hotspot_x, hotspot_y);
            }
        }
    } else {
        std::cerr << "load_svg_cursor: failed to create pixbuf for: " << full_file_path << std::endl;
    }

    // Explicit delete required for SPDocument to be freed
    // see https://gitlab.com/inkscape/inkscape/-/issues/2723
    delete document.release();

    if (cache_enabled) {
        cursor_cache[cursor_key] = cursor;
    }

    return cursor;
}

} // Namespace

/*
  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 :