# Source code for pympress.scribble

    # -*- coding: utf-8 -*-
    #
    #       pointer.py
    #
    #       Copyright 2017 Cimbali <me@cimba.li>
    #
    #       This program is free software; you can redistribute it and/or modify
    #       it under the terms of the GNU General Public License as published by
    #       the Free Software Foundation; either version 2 of the License, or
    #       (at your option) any later version.
    #
    #       This program is distributed in the hope that it will be useful,
    #       but WITHOUT ANY WARRANTY; without even the implied warranty of
    #       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    #       GNU General Public License for more details.
    #
    #       You should have received a copy of the GNU General Public License
    #       along with this program; if not, write to the Free Software
    #       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
    #       MA 02110-1301, USA.
    """
    :mod:`pympress.scribble` -- Manage user drawings on the current slide
    ---------------------------------------------------------------------
    """

    import logging
    logger = logging.getLogger(__name__)

    import math

    import gi
    import cairo
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk, Gdk, GLib

    from pympress import builder, extras, util

    class Scribbler(builder.Builder):
        """ UI that allows to draw free-hand on top of the current slide.

        Args:
            config (:class:`~pympress.config.Config`): A config object containing preferences
            builder (:class:`~pympress.builder.Builder`): A builder from which to load widgets
            notes_mode (`bool`): The current notes mode, i.e. whether we display the notes on second slide
        """
        #: Whether we are displaying the interface to scribble on screen and the overlays containing said scribbles
        scribbling_mode = False
        #: `list` of scribbles to be drawn, as tuples of color :class:`~Gdk.RGBA`, width `int`, a `list` of points,
        #: and a `list` of pressure values.
        scribble_list = []
        #: `list` of undone scribbles to possibly redo
        scribble_redo_list = []
        #: Whether the current mouse movements are drawing strokes or should be ignored
        scribble_drawing = False
        #: :class:`~Gdk.RGBA` current color of the scribbling tool
        scribble_color = Gdk.RGBA()
        #: `int` current stroke width of the scribbling tool
        scribble_width = 1

        #: :class:`~Gtk.HBox` that replaces normal panes when scribbling is on, contains buttons and scribble drawing area.
        scribble_overlay = None
        #: :class:`~Gtk.DrawingArea` for the scribbles in the Presenter window. Actually redraws the slide.
        scribble_p_da = None
        #: :class:`~Gtk.EventBox` for the scribbling in the Content window, captures freehand drawing
        scribble_c_eb = None
        #: :class:`~Gtk.EventBox` for the scribbling in the Presenter window, captures freehand drawing
        scribble_p_eb = None
        #: :class:`~Gtk.AspectFrame` for the slide in the Presenter's highlight mode
        scribble_p_frame = None
        #: The :class:`~Gtk.DrawingArea` in the content window
        c_da = None

        #: The :class:`~Gtk.ColorButton` selecting the color of the pen
        scribble_color_selector = None
        #: The :class:`~Gtk.Scale` selecting the size of the pen
        scribble_width_selector = None
        #: The `list` containing the radio buttons :class:`~Gtk.ModelButton`
        scribble_preset_buttons = []

        #: The position of the mouse on the slide as `tuple` of `float`
        mouse_pos = None
        #: A :class:`~cairo.Surface` to hold drawn highlights
        scribble_cache = None
        #: The next scribble to render (i.e. that is not rendered in cache)
        next_render = 0

        #: :class:`~Gtk.Button` for removing the last drawn scribble
        scribble_undo = None
        #: :class:`~Gtk.Button` for drawing the last removed scribble
        scribble_redo = None
        #: :class:`~Gtk.Button` for removing all drawn scribbles
        scribble_clear = None

        #: A :class:`~Gtk.OffscreenWindow` where we render the scribbling interface when it's not shown
        scribble_off_render = None
        #: :class:`~Gtk.Box` in the Presenter window, where we insert scribbling.
        p_central = None

        #: :class:`~Gtk.Button` that is clicked to stop zooming, unsensitive when there is no zooming
        zoom_stop_button = None

        #: callback, to be connected to :func:`~pympress.surfacecache.SurfaceCache.resize_widget`
        resize_cache = lambda *args: None
        #: callback, to be connected to :func:`~pympress.ui.UI.on_draw`
        on_draw = lambda *args: None
        #: callback, to be connected to :func:`~pympress.ui.UI.track_motions`
        track_motions = lambda *args: None
        #: callback, to be connected to :func:`~pympress.ui.UI.track_clicks`
        track_clicks = lambda *args: None

        #: callback, to be connected to :func:`~pympress.ui.UI.load_layout`
        load_layout = lambda *args: None
        #: callback, to be connected to :func:`~pympress.ui.UI.redraw_current_slide`
        redraw_current_slide = lambda *args: None

        #: callback, to be connected to :func:`~pympress.extras.Zoom.get_slide_point`
        get_slide_point = lambda *args: None
        #: callback, to be connected to :func:`~pympress.extras.Zoom.start_zooming`
        start_zooming = lambda *args: None
        #: callback, to be connected to :func:`~pympress.extras.Zoom.stop_zooming`
        stop_zooming = lambda *args: None

        #: `int` that is the currently selected element
        active_preset = -1
        #: `int` to remember the previously selected element, before holding “eraser”
        previous_preset = -1
        #: `list` that contains the modifiers which, when held on scribble start, toggle the eraser
        toggle_erase_modifiers = []
        #: `list` that contains the non-modifier shortcuts which, when held on scribble start, toggle the eraser
        toggle_erase_shortcuts = []
        #: `str` or `None` that indicates whether a modifier + click or a held shortcut is toggling the eraser
        toggle_erase_source = None

        #: The :class:`~Gio.Action` that contains the currently selected pen
        pen_action = None

        #: `str` which is the mode for scribbling, one of 3 possile values:
        # global means one set of scribbles for the whole document
        # single-page means we manage a single page of scribbles, and clear everything on page change (historical behaviour)
        # per-page means we manage a set of scribbles per document page, and clear or restore them on page change
        # per-label means we manage a set of scribbles per document page, but defined by label and not page number
        highlight_mode = 'single-page'
        #: `bool` indicating whether we exit highlighting mode on page change
        page_change_exits = True

        #: `dict` of scribbles per page
        remembered_scribbles = {}
        #: `tuple` of (`int`, `str`) indicating the current page number and label
        current_page = (None, None)

        #: `str` indicating the current layout of the highlight toolbar
        tools_orientation = 'vertical'
        #: :class:`~Gtk.Box` containing the presets
        preset_toolbar = None
        #: :class:`~Gtk.Box` containing the scribble buttons
        scribble_toolbar = None
        #: :class:`~Gtk.Box` containing the scribble color and width selectors
        scribble_color_toolbox = None

        def __init__(self, config, builder, notes_mode):
            super(Scribbler, self).__init__()

            self.load_ui('highlight')
            builder.load_widgets(self)
            self.get_application().add_window(self.scribble_off_render)

            self.on_draw = builder.get_callback_handler('on_draw')
            self.track_motions = builder.get_callback_handler('track_motions')
            self.track_clicks = builder.get_callback_handler('track_clicks')
            self.load_layout = builder.get_callback_handler('load_layout')
            self.redraw_current_slide = builder.get_callback_handler('redraw_current_slide')
            self.resize_cache = builder.get_callback_handler('cache.resize_widget')
            self.get_slide_point = builder.get_callback_handler('zoom.get_slide_point')
            self.start_zooming = builder.get_callback_handler('zoom.start_zooming')
            self.stop_zooming = builder.get_callback_handler('zoom.stop_zooming')

            self.connect_signals(self)
            self.config = config

            # Prepare cairo surfaces for markers, with 3 different marker sizes, and for eraser
            ms = [1, 2, 3]
            icons = [cairo.ImageSurface.create_from_png(util.get_icon_path('marker_{}.png'.format(n))) for n in ms]
            masks = [cairo.ImageSurface.create_from_png(util.get_icon_path('marker_fill_{}.png'.format(n))) for n in ms]

            self.marker_surfaces = list(zip(icons, masks))
            self.eraser_surface = cairo.ImageSurface.create_from_png(str(util.get_icon_path('eraser.png')))

            # Load color and active pen preferences. Pen 0 is the eraser.
            self.color_width = [(Gdk.RGBA(0, 0, 0, 0), config.getfloat('highlight', 'width_eraser'))] + list(zip(
                [self.parse_color(config.get('highlight', 'color_{}'.format(pen))) for pen in range(1, 10)],
                [config.getfloat('highlight', 'width_{}'.format(pen)) for pen in range(1, 10)],
            ))

            self.scribble_preset_buttons = [
                self.get_object('pen_preset_{}'.format(pen) if pen else 'eraser') for pen in range(10)
            ]

            self.tools_orientation = self.config.get('layout', 'highlight_tools')
            self.adjust_tools_orientation()

            active_pen = config.get('highlight', 'active_pen')
            self.page_change_exits = config.getboolean('highlight', 'page_change_exits')
            self.setup_actions({
                'highlight':         dict(activate=self.switch_scribbling, state=False),
                'highlight-use-pen': dict(activate=self.load_preset, state=active_pen, parameter_type=str, enabled=False),
                'highlight-clear':   dict(activate=self.clear_scribble),
                'highlight-redo':    dict(activate=self.redo_scribble),
                'highlight-undo':    dict(activate=self.pop_scribble),
                'highlight-mode':    dict(activate=self.set_mode, state=self.highlight_mode, parameter_type=str),
                'highlight-page-exit': dict(activate=self.page_change_action, state=self.page_change_exits),
                'highlight-tools-orientation': dict(activate=self.set_tools_orientation, state=self.tools_orientation,
                                                    parameter_type=str),
            })

            hold_erase = [Gtk.accelerator_parse(keys) for keys in config.shortcuts.get('highlight-hold-to-erase', [])]
            self.toggle_erase_modifiers = [mod for keycode, mod in hold_erase if not keycode]
            self.toggle_erase_shortcuts = [(keycode, mod) for keycode, mod in hold_erase if keycode]

            self.pen_action = self.get_application().lookup_action('highlight-use-pen')
            self.load_preset(self.pen_action, int(active_pen) if active_pen.isnumeric() else 0)
            self.set_mode(None, GLib.Variant.new_string(config.get('highlight', 'mode')))

        def page_change_action(self, gaction, param):
            """ Change whether we exit or stay in highlighting mode on page changes

            Args:
                gaction (:class:`~Gio.Action`): the action triggering the call
                param (:class:`~GLib.Variant`): the new mode as a string wrapped in a GLib.Variant
            """
            self.page_change_exits = not gaction.get_state().get_boolean()
            self.config.set('highlight', 'page_change_exits', 'on' if self.page_change_exits else 'off')
            gaction.change_state(GLib.Variant.new_boolean(self.page_change_exits))

            return True

        def set_mode(self, gaction, param):
            """ Change the mode of clearing and restoring highlights

            Args:
                gaction (:class:`~Gio.Action`): the action triggering the call
                param (:class:`~GLib.Variant`): the new mode as a string wrapped in a GLib.Variant
            """
            new_mode = param.get_string()
            if new_mode not in {'single-page', 'global', 'per-page', 'per-label'}:
                return False

            self.get_application().lookup_action('highlight-mode').change_state(GLib.Variant.new_string(new_mode))
            self.highlight_mode = new_mode
            self.config.set('highlight', 'mode', self.highlight_mode)
            self.remembered_scribbles.clear()

            return True

        def try_cancel(self):
            """ Cancel scribbling, if it is enabled.

            Returns:
                `bool`: `True` if scribbling got cancelled, `False` if it was already disabled.
            """
            if not self.scribbling_mode:
                return False

            self.disable_scribbling()
            return True

        @staticmethod
        def parse_color(text):
            """ Transform a string to a Gdk object in a single function call

            Args:
                text (`str`): A string describing a color

            Returns:
                :class:`~Gdk.RGBA`: A new color object parsed from the string
            """
            color = Gdk.RGBA()
            color.parse(text)
            return color

        def points_to_curves(self, points):
            """ Transform a list of points from scribbles to bezier curves

            Returns:
                `list`: control points of a bezier curves to draw
            """
            curves = []

            if len(points) <= 2:
                return curves

            for (ax, ay), (bx, by), (cx, cy), (dx, dy) in zip(
                    [points[0], *points[:-2]], points[:-1], points[1:], [*points[2:], points[-1]]
            ):
                curves.append((bx, by,
                               bx + (cx - ax) / 4, by + (cy - ay) / 4,
                               cx + (bx - dx) / 4, cy + (by - dy) / 4,
                               cx, cy))

            return curves

        def track_scribble(self, widget, event):
            """ Draw the scribble following the mouse's moves.

            Args:
                widget (:class:`~Gtk.Widget`):  the widget which has received the event.
                event (:class:`~Gdk.Event`):  the GTK event.

            Returns:
                `bool`: whether the event was consumed
            """
            pos = self.get_slide_point(widget, event) + ()

            if self.scribble_drawing:
                self.scribble_list[-1][-2].append(pos)
                pressure = event.get_axis(Gdk.AxisUse.PRESSURE)
                self.scribble_list[-1][-1].append(1. if pressure is None else pressure)
                self.scribble_redo_list.clear()

                self.adjust_buttons()

            self.mouse_pos = pos
            self.redraw_current_slide()
            return self.scribble_drawing

        def key_event(self, widget, event):
            """ Handle key events to activate the eraser while the shortcut is held

            Args:
                widget (:class:`~Gtk.Widget`):  the widget which has received the event.
                event (:class:`~Gdk.Event`):  the GTK event.

            Returns:
                `bool`: whether the event was consumed
            """
            if not self.scribbling_mode:
                return False
            elif event.type != Gdk.EventType.KEY_PRESS and event.type != Gdk.EventType.KEY_RELEASE:
                return False
            elif not (*event.get_keyval()[1:], event.get_state()) in self.toggle_erase_shortcuts:
                return False

            if event.type == Gdk.EventType.KEY_PRESS and self.active_preset and self.toggle_erase_source is None:
                self.previous_preset = self.active_preset
                self.toggle_erase_source = 'shortcut'
                self.load_preset(target=0)
            elif event.type == Gdk.EventType.KEY_RELEASE and self.toggle_erase_source == 'shortcut' \
                    and self.previous_preset and not self.active_preset:
                self.load_preset(target=self.previous_preset)
                self.previous_preset = 0
                self.toggle_erase_source = None
            else:
                return False
            return True

        def toggle_scribble(self, widget, event):
            """ Start/stop drawing scribbles.

            Args:
                widget (:class:`~Gtk.Widget`):  the widget which has received the event.
                event (:class:`~Gdk.Event`):  the GTK event.

            Returns:
                `bool`: whether the event was consumed
            """
            if not self.scribbling_mode:
                return False

            if event.get_event_type() == Gdk.EventType.BUTTON_PRESS:
                eraser_button = event.get_source_device().get_source() == Gdk.InputSource.ERASER
                eraser_modifier = any(mod & event.get_state() == mod for mod in self.toggle_erase_modifiers)
                if (eraser_button or eraser_modifier) and self.active_preset and self.toggle_erase_source is None:
                    self.previous_preset = self.active_preset
                    self.toggle_erase_source = 'modifier'
                    self.load_preset(target=0)

                self.scribble_list.append((self.scribble_color, self.scribble_width, [], []))
                self.scribble_drawing = True

                return self.track_scribble(widget, event)
            elif event.get_event_type() == Gdk.EventType.BUTTON_RELEASE:
                self.scribble_drawing = False
                self.prerender()

                if not self.active_preset and self.previous_preset and self.toggle_erase_source == 'modifier':
                    self.load_preset(target=self.previous_preset)
                    self.previous_preset = 0
                    self.toggle_erase_source = None

                return True

            return False

        def reset_scribble_cache(self):
            """ Clear the cached scribbles.
            """
            window = self.c_da.get_window()

            if window is None:
                logger.error('Cannot initialize scribble cache without drawing area window')
                return

            scale = window.get_scale_factor()
            ww, wh = self.c_da.get_allocated_width() * scale, self.c_da.get_allocated_height() * scale
            try:
                self.scribble_cache = window.create_similar_image_surface(cairo.Format.ARGB32, ww, wh, scale)
            except ValueError as e:
                if e.args == ('invalid enum value: 5',):
                    # Just try again as this error seems to always happen at the first call but not later on
                    # This is the gi introspection of cairo not accepting the value Format.RGB30 (???)
                    try:
                        self.scribble_cache = window.create_similar_image_surface(cairo.Format.ARGB32, ww, wh, scale)
                    except ValueError:
                        logger.exception('Error creating highlight cache')
                    else:
                        self.next_render = 0
                else:
                    logger.exception('Error creating highlight cache')
            except cairo.Error:
                logger.warning('Failed creating an ARGB32 surface sized {}x{} scale {} for highlight cache'
                               .format(ww, wh, scale), exc_info=True)
            else:
                self.next_render = 0

        def prerender(self):
            """ Commit scribbles to cache so they are faster to draw on the slide
            """
            if self.scribble_cache is None:
                self.reset_scribble_cache()

            if self.scribble_cache is None:
                self.next_render = 0
                return

            scale = self.c_da.get_window().get_scale_factor()
            ww, wh = self.scribble_cache.get_width() / scale, self.scribble_cache.get_height() / scale
            pen_scale_factor = max(ww / 900, wh / 900)  # or sqrt of product

            cairo_context = cairo.Context(self.scribble_cache)
            cairo_context.set_line_cap(cairo.LINE_CAP_ROUND)

            draw = slice(self.next_render, -1 if self.scribble_drawing else None)

            for color, width, points, pressure in self.scribble_list[draw]:
                self.render_scribble(cairo_context, color, width * pen_scale_factor, [(x * ww, y * wh) for x, y in points],
                                     pressure)
            del cairo_context

            self.next_render = draw.indices(len(self.scribble_list))[1]

        def render_scribble(self, cairo_context, color, width, points, pressures):
            """ Draw a single scribble, i.e. a bezier curve, on the cairo context

            Args:
                cairo_context (:class:`~cairo.Context`): The canvas on which to render the drawings
                color (:class:`~Gdk.RGBA`): The color of the scribble
                width (`float`): The width of the curve
                points (`list`): The control points of the curve, scaled to the surface.
                pressures (`list`): The relative line width at each point as `float` values in 0..1
            """
            if not points:
                return

            # Draw every stroke on a separate surface, then merge them all into the scribble cache
            # Erasers do not have their own group as they are meant to interfere with strokes below
            if color.alpha:
                cairo_context.push_group()
                cairo_context.set_operator(cairo.OPERATOR_SOURCE)
            else:
                # alpha == 0 -> Eraser mode
                cairo_context.set_operator(cairo.OPERATOR_CLEAR)
            cairo_context.set_source_rgba(*color)

            curves = self.points_to_curves(points)
            curve_widths = [(a + b) / 2 for a, b in zip(pressures[:-1], pressures[1:])]
            for curve, relwidth in zip(curves, curve_widths):
                cairo_context.move_to(*curve[:2])
                cairo_context.set_line_width(width * relwidth)
                cairo_context.curve_to(*curve[2:])
                cairo_context.stroke()

            # Draw from last uneven-indexed point to last point
            cairo_context.move_to(*points[-2 if len(points) % 2 and len(points) > 1 else -1])
            cairo_context.set_line_width(width * (curve_widths[-1] if curve_widths else pressures[-1]))
            cairo_context.line_to(*points[-1])
            cairo_context.stroke()

            if color.alpha:
                cairo_context.pop_group_to_source()
                cairo_context.paint()

        def draw_scribble(self, widget, cairo_context):
            """ Perform the drawings by user.

            Args:
                widget (:class:`~Gtk.DrawingArea`): The widget where to draw the scribbles.
                cairo_context (:class:`~cairo.Context`): The canvas on which to render the drawings
            """
            scale = widget.get_window().get_scale_factor()
            ww, wh = widget.get_allocated_width(), widget.get_allocated_height()
            cw, ch = self.scribble_cache.get_width(), self.scribble_cache.get_height()

            cairo_context.push_group()

            cairo_context.save()

            cairo_context.scale(ww * scale / cw, wh * scale / ch)
            cairo_context.set_source_surface(self.scribble_cache)
            cairo_context.paint()

            cairo_context.restore()

            pen_scale_factor = max(ww / 900, wh / 900)  # or sqrt of product
            if self.scribble_drawing:
                cairo_context.set_line_cap(cairo.LINE_CAP_ROUND)
                color, width, points, pressure = self.scribble_list[-1]
                self.render_scribble(cairo_context, color, width * pen_scale_factor, [(x * ww, y * wh) for x, y in points],
                                     pressure)

            cairo_context.pop_group_to_source()
            cairo_context.paint()

            if widget.get_name() == 'scribble_p_da' and self.mouse_pos is not None:
                cairo_context.set_source_rgba(0, 0, 0, 1)
                cairo_context.set_line_width(1)

                mx, my = self.mouse_pos
                cairo_context.arc(mx * ww, my * wh, self.scribble_width * pen_scale_factor / 2, 0, 2 * math.pi)

                cairo_context.stroke_preserve()

                cairo_context.set_source_rgba(*list(self.scribble_color)[:3], self.scribble_color.alpha * .5)
                cairo_context.close_path()
                cairo_context.fill()

        def update_color(self, widget):
            """ Callback for the color chooser button, to set scribbling color.

            Args:
                widget (:class:`~Gtk.ColorButton`):  the clicked button to trigger this event, if any
            """
            self.scribble_color = widget.get_rgba()
            self.update_active_color_width()

        def update_width(self, widget, event, value):
            """ Callback for the width chooser slider, to set scribbling width.

            Args:
                widget (:class:`~Gtk.Scale`): The slider control used to select the scribble width
                event (:class:`~Gdk.Event`):  the GTK event triggering this update.
                value (`int`): the width of the scribbles to be drawn
            """
            self.scribble_width = max(1, min(100, 10 ** value if value < 1 else 10 + (value - 1) * 90))
            self.update_active_color_width()

        def update_active_color_width(self):
            """ Update modifications to the active scribble color and width, on the pen button and config object
            """
            self.color_width[self.active_preset] = self.scribble_color, self.scribble_width
            if self.active_preset:
                self.scribble_preset_buttons[self.active_preset].queue_draw()

            pen = self.active_preset if self.active_preset else 'eraser'
            if self.active_preset:
                self.config.set('highlight', 'color_{}'.format(pen), self.scribble_color.to_string())
            self.config.set('highlight', 'width_{}'.format(pen), str(self.scribble_width))

        def adjust_buttons(self):
            """ Properly enable and disable buttons based on scribblings lists.
            """
            self.scribble_undo.set_sensitive(bool(self.scribble_list))
            self.scribble_clear.set_sensitive(bool(self.scribble_list))
            self.scribble_redo.set_sensitive(bool(self.scribble_redo_list))

        def clear_scribble(self, *args):
            """ Callback for the scribble clear button, to remove all scribbles.
            """
            self.scribble_list.clear()

            self.reset_scribble_cache()
            self.redraw_current_slide()
            self.adjust_buttons()

        def page_change(self, page_number, page_label):
            """ Called when we change pages, to clear or restore scribbles

            Args:
                page_number (`int`): The number of the new page
                page_label (`str`): The label of the new page
            """
            if self.highlight_mode == 'per-page':
                current_page = self.current_page[0]
                new_page = page_number
            elif self.highlight_mode == 'per-label':
                current_page = self.current_page[1]
                new_page = page_label

            # Remember whatever the current mode, to facilitate switching modes
            self.current_page = (page_number, page_label)

            if self.highlight_mode == 'global':
                return
            elif self.highlight_mode == 'single-page':
                return self.clear_scribble()
            else:
                # Now optionally save the current scribbles
                if current_page is not None and self.scribble_list:
                    self.remembered_scribbles[current_page] = self.scribble_list.copy()

                self.scribble_list = self.remembered_scribbles.pop(new_page, [])

                self.reset_scribble_cache()
                self.adjust_buttons()
                self.prerender()
                self.redraw_current_slide()

        def pop_scribble(self, *args):
            """ Callback for the scribble undo button, to undo the last scribble.
            """
            if self.scribble_list:
                self.scribble_redo_list.append(self.scribble_list.pop())

            self.adjust_buttons()
            self.reset_scribble_cache()
            self.prerender()
            self.redraw_current_slide()

        def redo_scribble(self, *args):
            """ Callback for the scribble undo button, to undo the last scribble.
            """
            if self.scribble_redo_list:
                self.scribble_list.append(self.scribble_redo_list.pop())

            self.adjust_buttons()
            self.prerender()
            self.redraw_current_slide()

        def on_configure_da(self, widget, event):
            """ Transfer configure resize to the cache.

            Args:
                widget (:class:`~Gtk.Widget`):  the widget which has been resized
                event (:class:`~Gdk.Event`):  the GTK event, which contains the new dimensions of the widget
            """
            # Don't trust those
            if not event.send_event:
                return

            self.resize_cache(widget.get_name(), event.width, event.height)

        def set_tools_orientation(self, gaction, target):
            """ Changes the orientation of the highlighting tool box.

            Args:
                gaction (:class:`~Gio.Action`): the action triggering the call
                target (:class:`~GLib.Variant`): the new orientation to set, as a string wrapped in a GLib.Variant

            Returns:
                `bool`: whether the preset was loaded
            """
            orientation = target.get_string()
            if orientation == self.tools_orientation:
                return False
            elif orientation not in ['horizontal', 'vertical']:
                logger.error('Unexpected highlight-tools orientation {}'.format(orientation))
                return False

            self.tools_orientation = orientation
            self.adjust_tools_orientation()

            gaction.change_state(target)
            self.config.set('layout', 'highlight_tools', self.tools_orientation)

        def adjust_tools_orientation(self):
            """ Actually change the highlight tool elements orientations according to self.tools_orientation
            """
            orientation = Gtk.Orientation.VERTICAL if self.tools_orientation == 'vertical' else Gtk.Orientation.HORIZONTAL
            self.preset_toolbar.set_orientation(orientation)
            self.scribble_toolbar.set_orientation(orientation)
            self.scribble_color_toolbox.set_orientation(orientation)
            self.scribble_width_selector.set_orientation(orientation)

            w, h = sorted(self.scribble_width_selector.get_size_request(), reverse=self.tools_orientation != 'vertical')
            self.scribble_width_selector.set_size_request(w, h)

            # NB the parent container is layed out perpendicularly to its contents
            self.scribble_overlay.set_orientation(Gtk.Orientation.HORIZONTAL if self.tools_orientation == 'vertical' else
                                                  Gtk.Orientation.VERTICAL)

        def switch_scribbling(self, gaction, target=None):
            """ Starts the mode where one can read on top of the screen.

            Args:

            Returns:
                `bool`: whether the event was consumed
            """
            if target is not None and target == self.scribbling_mode:
                return False

            # Perform the state toggle
            if self.scribbling_mode:
                return self.disable_scribbling()
            else:
                return self.enable_scribbling()

        def enable_scribbling(self):
            """ Enable the scribbling mode.

            Returns:
                `bool`: whether it was possible to enable (thus if it was not enabled already)
            """
            if self.scribbling_mode:
                return False

            self.scribble_off_render.remove(self.scribble_overlay)
            self.load_layout('highlight')

            self.p_central.queue_draw()
            self.scribble_overlay.queue_draw()

            # Get frequent events for smooth drawing
            self.p_central.get_window().set_event_compression(False)

            self.scribbling_mode = True
            self.get_application().lookup_action('highlight').change_state(GLib.Variant.new_boolean(self.scribbling_mode))
            self.pen_action.set_enabled(self.scribbling_mode)

            self.p_central.queue_draw()
            extras.Cursor.set_cursor(self.scribble_p_da, 'invisible')
            return True

        def disable_scribbling(self):
            """ Disable the scribbling mode.

            Returns:
                `bool`: whether it was possible to disable (thus if it was not disabled already)
            """
            if not self.scribbling_mode:
                return False

            self.scribbling_mode = False

            extras.Cursor.set_cursor(self.scribble_p_da, 'default')
            self.load_layout(None)
            self.scribble_off_render.add(self.scribble_overlay)
            window = self.p_central.get_window()
            if window:
                window.set_event_compression(True)

            self.get_application().lookup_action('highlight').change_state(GLib.Variant.new_boolean(self.scribbling_mode))
            self.pen_action.set_enabled(self.scribbling_mode)

            self.p_central.queue_draw()
            extras.Cursor.set_cursor(self.p_central)
            self.mouse_pos = None

            return True

        def load_preset(self, gaction=None, target=None):
            """ Loads the preset color of a given number or designed by a given widget, as an event handler.

            Args:
                gaction (:class:`~Gio.Action`): the action triggering the call
                target (:class:`~GLib.Variant`): the new preset to load, as a string wrapped in a GLib.Variant

            Returns:
                `bool`: whether the preset was loaded
            """
            if isinstance(target, int):
                self.active_preset = target
            else:
                self.active_preset = int(target.get_string()) if target.get_string() != 'eraser' else 0

            target = str(self.active_preset) if self.active_preset else 'eraser'

            self.config.set('highlight', 'active_pen', target)
            self.pen_action.change_state(GLib.Variant.new_string(target))
            self.scribble_color, self.scribble_width = self.color_width[self.active_preset]

            # Presenter-side setup
            self.scribble_color_selector.set_rgba(self.scribble_color)
            self.scribble_width_selector.set_value(math.log10(self.scribble_width) if self.scribble_width < 10
                                                   else 1 + (self.scribble_width - 10) / 90)
            self.scribble_color_selector.set_sensitive(target != 'eraser')

            # Re-draw the eraser
            self.scribble_p_da.queue_draw()
            self.c_da.queue_draw()

            return True

        def on_eraser_button_draw(self, widget, cairo_context):
            """ Handle drawing the eraser button.

            Args:
                widget (:class:`~Gtk.Widget`):  the widget to update
                cairo_context (:class:`~cairo.Context`):  the Cairo context (or `None` if called directly)
            """
            cairo_context.push_group()
            scale = widget.get_allocated_height() / self.eraser_surface.get_height()
            cairo_context.scale(scale, scale)

            cairo_context.set_source_surface(self.eraser_surface)
            cairo_context.paint()

            cairo_context.pop_group_to_source()
            cairo_context.paint()

        def on_preset_button_draw(self, widget, cairo_context):
            """ Handle drawing the marker/pencil buttons, with appropriate thickness and color.

            Args:
                widget (:class:`~Gtk.Widget`):  the widget to update
                cairo_context (:class:`~cairo.Context`):  the Cairo context (or `None` if called directly)
            """
            button_number = int(widget.get_name().split('_')[-1])
            color, width = self.color_width[button_number]
            icon, mask = self.marker_surfaces[int(width >= 2) + int(width >= 40)]

            ww, wh = widget.get_allocated_width(), widget.get_allocated_height()
            scale = wh / icon.get_height()

            dw, dh = self.scribble_p_da.get_allocated_width(), self.scribble_p_da.get_allocated_height()
            pen_scale_factor = max(dw / 900, dh / 900)  # or sqrt of product
            width *= pen_scale_factor

            cairo_context.push_group()

            # A line demonstrating the scribble style
            cairo_context.set_source_rgba(*color)
            cairo_context.set_line_width(width)
            cairo_context.move_to(0, wh - width / 2)
            cairo_context.line_to(ww, wh - width / 2)
            cairo_context.stroke()

            cairo_context.set_operator(cairo.OPERATOR_DEST_OUT)

            # Clip the line to the lower triangle
            cairo_context.set_source_rgba(0, 0, 0, 1)
            cairo_context.set_line_width(0)
            cairo_context.move_to(0, 0)
            cairo_context.line_to(0, wh)
            cairo_context.line_to(ww, 0)
            cairo_context.close_path()
            cairo_context.fill()

            # Also clip the colored part of the marker
            cairo_context.scale(scale, scale)
            cairo_context.set_source_surface(mask)
            cairo_context.paint()

            cairo_context.pop_group_to_source()
            cairo_context.paint()

            cairo_context.push_group()

            # Fill with desired color
            cairo_context.set_source_rgba(*color)
            cairo_context.rectangle(0, 0, ww, wh)
            cairo_context.fill()

            # Transform for surfaces
            cairo_context.scale(scale, scale)

            # Clip color to the mask
            cairo_context.set_operator(cairo.OPERATOR_DEST_IN)
            cairo_context.set_source_surface(mask)
            cairo_context.paint()

            # Add the rest of the marker
            cairo_context.set_operator(cairo.OPERATOR_OVER)
            cairo_context.set_source_surface(icon)
            cairo_context.paint()

            cairo_context.pop_group_to_source()
            cairo_context.paint()