Source code for pympress.pointer

# -*- 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.pointer` -- Manage when and where to draw a software-emulated laser pointer on screen
----------------------------------------------------------------------------------------------------
"""

import logging
logger = logging.getLogger(__name__)

import enum

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

from pympress import util, extras


[docs] class PointerMode(enum.Enum): """ Possible values for the pointer. """ #: Pointer switched on continuously CONTINUOUS = 2 #: Pointer switched on only manual MANUAL = 1 #: Pointer never switched on DISABLED = 0
[docs] class Pointer(object): """ Manage and draw the software “laser pointer” to point at the slide. Displays a pointer of chosen color on the current slide (in both windows), either on all the time or only when clicking while ctrl pressed. Args: config (:class:`~pympress.config.Config`): A config object containing preferences builder (:class:`~pympress.builder.Builder`): A builder from which to load widgets """ #: :class:`~GdkPixbuf.Pixbuf` to read XML descriptions of GUIs and load them. pointer = GdkPixbuf.Pixbuf() #: `(float, float)` of position relative to slide, where the pointer should appear pointer_pos = (.5, .5) #: `bool` indicating whether we should show the pointer show_pointer = False #: :class:`~pympress.pointer.PointerMode` indicating the pointer mode pointer_mode = PointerMode.MANUAL #: The :class:`~pympress.pointer.PointerMode` to which we toggle back old_pointer_mode = PointerMode.CONTINUOUS #: A reference to the UI's :class:`~pympress.config.Config`, to update the pointer preference config = None #: :class:`~Gtk.DrawingArea` Slide in the Presenter window, used to reliably set cursors. p_da_cur = None #: :class:`~Gtk.DrawingArea` Slide in the Contents window, used to reliably set cursors. c_da = None #: :class:`~Gtk.AspectFrame` Frame of the Contents window, used to reliably set cursors. c_frame = None #: a `dict` of the :class:`~Gtk.RadioMenuItem` selecting the pointer mode pointermode_radios = {} #: callback, to be connected to :func:`~pympress.ui.UI.redraw_current_slide` redraw_current_slide = lambda *args: None #: callback, to be connected to :meth:`~pympress.app.Pympress.set_action_state` set_action_state = None def __init__(self, config, builder): super(Pointer, self).__init__() self.config = config builder.load_widgets(self) self.redraw_current_slide = builder.get_callback_handler('redraw_current_slide') self.set_action_state = builder.get_callback_handler('app.set_action_state') default_mode = config.get('presenter', 'pointer_mode') default_color = config.get('presenter', 'pointer') try: default_mode = PointerMode[default_mode.upper()] except KeyError: default_mode = PointerMode.MANUAL self.activate_pointermode(default_mode) self.load_pointer(default_color) self.action_map = builder.setup_actions({ 'pointer-color': dict(activate=self.change_pointercolor, state=default_color, parameter_type=str), 'pointer-mode': dict(activate=self.change_pointermode, state=default_mode.name.lower(), parameter_type=str), })
[docs] def load_pointer(self, name): """ Perform the change of pointer using its color name. Args: name (`str`): Name of the pointer to load """ if name not in ['red', 'green', 'blue']: raise ValueError('Wrong color name') path = util.get_icon_path('pointer_' + name + '.png') try: self.pointer = GdkPixbuf.Pixbuf.new_from_file(path) except Exception: logger.exception(_('Failed loading pixbuf for pointer "{}" from: {}'.format(name, path)))
[docs] def change_pointercolor(self, action, target): """ Callback for a radio item selection as pointer mode (continuous, manual, none). Args: action (:class:`~Gio.Action`): The action activatd target (:class:`~GLib.Variant`): The selected mode """ color = target.get_string() self.load_pointer(color) self.config.set('presenter', 'pointer', color) action.change_state(target)
[docs] def activate_pointermode(self, mode=None): """ Activate the pointer as given by mode. Depending on the given mode, shows or hides the laser pointer and the normal mouse pointer. Args: mode (:class:`~pympress.pointer.PointerMode`): The mode to activate """ # Set internal variables, unless called without mode (from ui, after windows have been mapped) if mode == self.pointer_mode: return elif mode is not None: self.old_pointer_mode, self.pointer_mode = self.pointer_mode, mode self.config.set('presenter', 'pointer_mode', self.pointer_mode.name.lower()) # Set mouse pointer and cursors on/off, if windows are already mapped self.show_pointer = False for slide_widget in [self.p_da_cur, self.c_da]: ww, wh = slide_widget.get_allocated_width(), slide_widget.get_allocated_height() if max(ww, wh) == 1: continue pointer_x, pointer_y = -1, -1 window = slide_widget.get_window() if window is not None: pointer_coords = window.get_pointer() pointer_x, pointer_y = pointer_coords.x, pointer_coords.y if 0 < pointer_x < ww and 0 < pointer_y < wh \ and self.pointer_mode == PointerMode.CONTINUOUS: # Laser activated right away self.pointer_pos = (pointer_x / ww, pointer_y / wh) self.show_pointer = True extras.Cursor.set_cursor(slide_widget, 'invisible') else: extras.Cursor.set_cursor(slide_widget, 'parent') self.redraw_current_slide()
[docs] def change_pointermode(self, action, target): """ Callback for a radio item selection as pointer mode (continuous, manual, none). Args: action (:class:`~Gio.Action`): The action activatd target (:class:`~GLib.Variant`): The selected mode """ if target is None or target.get_string() == 'toggle': mode = self.old_pointer_mode if self.pointer_mode == PointerMode.CONTINUOUS else PointerMode.CONTINUOUS else: mode = PointerMode[target.get_string().upper()] self.activate_pointermode(mode) action.change_state(GLib.Variant.new_string(mode.name.lower()))
[docs] def render_pointer(self, cairo_context, ww, wh): """ Draw the laser pointer on screen. Args: cairo_context (:class:`~cairo.Context`): The canvas on which to render the pointer ww (`int`): The widget width wh (`int`): The widget height """ if self.show_pointer: x = ww * self.pointer_pos[0] - self.pointer.get_width() / 2 y = wh * self.pointer_pos[1] - self.pointer.get_height() / 2 Gdk.cairo_set_source_pixbuf(cairo_context, self.pointer, x, y) cairo_context.paint()
[docs] def track_pointer(self, widget, event): """ Move the laser pointer at the mouse location. 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.show_pointer: ww, wh = widget.get_allocated_width(), widget.get_allocated_height() ex, ey = event.get_coords() self.pointer_pos = (ex / ww, ey / wh) self.redraw_current_slide() return True else: return False
[docs] def track_enter_leave(self, widget, event): """ Switches laser off/on in continuous mode on leave/enter slides. In continuous mode, the laser pointer is switched off when the mouse leaves the slide (otherwise the laser pointer "sticks" to the edge of the slide). It is switched on again when the mouse reenters the slide. 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 """ # Only handle enter/leave events on one of the current slides if self.pointer_mode != PointerMode.CONTINUOUS or widget not in [self.c_da, self.p_da_cur]: return False if event.type == Gdk.EventType.ENTER_NOTIFY: self.show_pointer = True extras.Cursor.set_cursor(widget, 'invisible') elif event.type == Gdk.EventType.LEAVE_NOTIFY: self.show_pointer = False extras.Cursor.set_cursor(widget, 'parent') self.redraw_current_slide() return True
[docs] def toggle_pointer(self, widget, event): """ Track events defining when the laser is pointing. 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.pointer_mode in {PointerMode.DISABLED, PointerMode.CONTINUOUS}: return False ctrl_pressed = event.get_state() & Gdk.ModifierType.CONTROL_MASK if ctrl_pressed and event.type == Gdk.EventType.BUTTON_PRESS: self.show_pointer = True extras.Cursor.set_cursor(widget, 'invisible') # Immediately place & draw the pointer return self.track_pointer(widget, event) elif self.show_pointer and event.type == Gdk.EventType.BUTTON_RELEASE: self.show_pointer = False extras.Cursor.set_cursor(widget, 'parent') self.redraw_current_slide() return True else: return False