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
---------------------------------------------------------------------
"""

from __future__ import print_function, unicode_literals

import logging
logger = logging.getLogger(__name__)

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

from pympress import builder, surfacecache, document, extras


[docs]class Scribbler(builder.Builder): #: 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`, and a `list` of points. scribble_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 is replaces normal panes when scribbling is toggled, 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 #: A :class:`~Gtk.OffscreenWindow` where we render the scribbling interface when it's not shown off_render = None #: :class:`~Gtk.Box` in the Presenter window, where we insert scribbling. p_central = None #: :class:`~Gtk.CheckMenuItem` that shows whether the scribbling is toggled pres_highlight = 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: None #: callback, to be connected to :func:`~pympress.ui.UI.on_draw` on_draw = lambda: None #: callback, to be connected to :func:`~pympress.ui.UI.track_motions` track_motions = lambda: None #: callback, to be connected to :func:`~pympress.ui.UI.track_clicks` track_clicks = lambda: None #: callback, to be connected to :func:`~pympress.ui.UI.swap_layout` swap_layout = lambda: None #: callback, to be connected to :func:`~pympress.ui.UI.redraw_current_slide` redraw_current_slide = lambda: None #: callback, to be connected to :func:`~pympress.extras.Zoom.get_slide_point` get_slide_point = lambda: None #: callback, to be connected to :func:`~pympress.extras.Zoom.start_zooming` start_zooming = lambda: None #: callback, to be connected to :func:`~pympress.extras.Zoom.stop_zooming` stop_zooming = lambda: None def __init__(self, config, builder, notes_mode): """ Setup all the necessary for scribbling 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 """ super(Scribbler, self).__init__() self.load_ui('highlight') builder.load_widgets(self) 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.swap_layout = builder.get_callback_handler('swap_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.scribble_color = Gdk.RGBA() self.scribble_color.parse(config.get('scribble', 'color')) self.scribble_width = config.getint('scribble', 'width') self.config = config # Presenter-size setup self.get_object("scribble_color").set_rgba(self.scribble_color) self.get_object("scribble_width").set_value(self.scribble_width)
[docs] def nav_scribble(self, name, ctrl_pressed): """ Handles an key press event: undo or disable scribbling. Args: name (`str`): The name of the key pressed ctrl_pressed (`bool`): whether the ctrl modifier key was pressed Returns: `bool`: whether the event was consumed """ if not self.scribbling_mode: return False elif name.upper() == 'Z' and ctrl_pressed: self.pop_scribble() elif name == 'Escape': self.disable_scribbling() else: return False return True
[docs] 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 """ if self.scribble_drawing: self.scribble_list[-1][2].append(self.get_slide_point(widget, event)) self.redraw_current_slide() return True else: return False
[docs] 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: 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 return True return False
[docs] 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 """ ww, wh = widget.get_allocated_width(), widget.get_allocated_height() cairo_context.set_line_cap(cairo.LINE_CAP_ROUND) for color, width, points in self.scribble_list: points = [(p[0] * ww, p[1] * wh) for p in points] cairo_context.set_source_rgba(*color) cairo_context.set_line_width(width) cairo_context.move_to(*points[0]) for p in points[1:]: cairo_context.line_to(*p) cairo_context.stroke()
[docs] 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.config.set('scribble', 'color', self.scribble_color.to_string())
[docs] 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 = int(value) self.config.set('scribble', 'width', str(self.scribble_width))
[docs] def clear_scribble(self, *args): """ Callback for the scribble clear button, to remove all scribbles """ del self.scribble_list[:] self.redraw_current_slide()
[docs] def pop_scribble(self, *args): """ Callback for the scribble undo button, to undo the last scribble """ if self.scribble_list: self.scribble_list.pop() self.redraw_current_slide()
[docs] 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)
[docs] def switch_scribbling(self, widget, event = None, name = None): """ Starts the mode where one can read on top of the screen Args: widget (:class:`~Gtk.Widget`): the widget which has received the event. event (:class:`~Gdk.Event` or None): the GTK event., None when called through a menu item name (`str`): The name of the key pressed Returns: `bool`: whether the event was consumed """ if issubclass(type(widget), Gtk.CheckMenuItem) and widget.get_active() == self.scribbling_mode: # Checking the checkbox conforming to current situation: do nothing return False elif issubclass(type(widget), Gtk.Actionable): # A button or menu item, etc. directly connected to this action pass elif event.type == Gdk.EventType.KEY_PRESS: if name is None: name = Gdk.keyval_name(event.keyval) if name.upper() != 'H': return False else: return False # Perform the state toggle if self.scribbling_mode: return self.disable_scribbling() else: return self.enable_scribbling()
[docs] 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.off_render.remove(self.scribble_overlay) self.swap_layout(None, 'highlight') p_layout = self.p_central.get_children()[0] self.p_central.queue_draw() self.scribble_overlay.queue_draw() self.scribbling_mode = True self.pres_highlight.set_active(self.scribbling_mode) return True
[docs] 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.swap_layout('highlight', None) p_layout = self.p_central.get_children()[0] self.off_render.add(self.scribble_overlay) self.scribbling_mode = False self.pres_highlight.set_active(self.scribbling_mode) self.p_central.queue_draw() extras.Cursor.set_cursor(self.p_central) return True