# -*- coding: utf-8 -*-
#
# talk_time.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.talk_time` -- Manages the clock of elapsed talk time
-------------------------------------------------------------------
"""
import logging
logger = logging.getLogger(__name__)
import time
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib
[docs]
class TimeLabelColorer(object):
""" Manage the colors of a label with a set of colors between which to fade, based on how much time remains.
Times are given in seconds (<0 has run out of time). In between timestamps the color will interpolated linearly,
outside of the intervals the closest color will be used.
Args:
label_time (:class:`Gtk.Label`): the label where the talk time is displayed
"""
#: The :class:`Gtk.Label` whose colors need updating
label_time = None
#: :class:`~Gdk.RGBA` The default color of the info labels
label_color_default = None
#: :class:`~Gtk.CssProvider` affecting the style context of the labels
color_override = None
#: `list` of tuples (`int`, :class:`~Gdk.RGBA`), which are the desired colors at the corresponding timestamps.
#: Sorted on the timestamps.
color_map = []
def __init__(self, label_time):
self.label_time = label_time
style_context = self.label_time.get_style_context()
self.color_override = Gtk.CssProvider()
style_context.add_provider(self.color_override, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + 1)
self.label_color_default = self.load_color_from_css(style_context)
label_color_ett_reached = self.load_color_from_css(style_context, "ett-reached")
label_color_ett_info = self.load_color_from_css(style_context, "ett-info")
label_color_ett_warn = self.load_color_from_css(style_context, "ett-warn")
self.color_map = [
( 300, self.label_color_default),
( 0, label_color_ett_reached),
(-150, label_color_ett_info),
(-300, label_color_ett_warn)
]
[docs]
def load_color_from_css(self, style_context, class_name = None):
""" Add class class_name to the time label and return its color.
Args:
label_time (:class:`Gtk.Label`): the label where the talk time is displayed
style_context (:class:`~Gtk.StyleContext`): the CSS context managing the color of the label
class_name (`str` or `None`): The name of the class, if any
Returns:
:class:`~Gdk.RGBA`: The color of the label with class "class_name"
"""
if class_name:
style_context.add_class(class_name)
self.label_time.show()
color = style_context.get_color(Gtk.StateType.NORMAL)
if class_name:
style_context.remove_class(class_name)
return color
[docs]
def default_color(self):
""" Forces to reset the default colors on the label.
"""
self.color_override.load_from_data(''.encode('ascii'))
[docs]
def update_time_color(self, remaining):
""" Update the color of the time label based on how much time is remaining.
Args:
remaining (`int`): Remaining time until estimated talk time is reached, in seconds.
"""
if (remaining <= 0 and remaining > -5) or (remaining <= -300 and remaining > -310):
self.label_time.get_style_context().add_class("time-warn")
else:
self.label_time.get_style_context().remove_class("time-warn")
prev_time, prev_color = None, None
for timestamp, color in self.color_map:
if remaining >= timestamp:
break
prev_time, prev_color = (timestamp, color)
else:
# if remaining < all timestamps, use only last color
prev_color = None
if prev_color:
position = (remaining - prev_time) / (timestamp - prev_time)
color_spec = '* {{color: mix({}, {}, {})}}'.format(prev_color.to_string(), color.to_string(), position)
else:
color_spec = '* {{color: {}}}'.format(color.to_string())
self.color_override.load_from_data(color_spec.encode('ascii'))
[docs]
class TimeCounter(object):
""" A double counter, that displays the time elapsed in the talk and a clock.
Args:
builder (builder.Builder): The builder from which to load widgets.
ett (`int`): the estimated time for the talk, in seconds.
timing_tracker: (:class:`~pympress.extras.TimingReport`): to inform when the slides change
autoplay: (:class:`~pympress.dialog.AutoPlay`): to adjust the timer display if we’re auto-playing/looping slides
"""
#: Elapsed time :class:`~Gtk.Label`
label_time = None
#: Clock :class:`~Gtk.Label`
label_clock = None
#: Time at which the counter was started, `int` in seconds as returned by :func:`~time.time()`
restart_time = 0
#: Time elapsed since the beginning of the presentation, `int` in seconds
elapsed_time = 0
#: Timer paused status, `bool`
paused = True
#: :class:`~TimeLabelColorer` that handles setting the colors of :attr:`label_time`
label_colorer = None
#: :class:`~pympress.editable_label.EstimatedTalkTime` that handles changing the ett
ett = None
#: The pause-timer :class:`~Gio.Action`
pause_action = None
#: The :class:`~pympress.extras.TimingReport`, needs to know when the slides change
timing_tracker = None
#: The :class:`~pympress.dialog.AutoPlay`, to adjust the timer display if we’re auto-playing/looping slides
autoplay = None
def __init__(self, builder, ett, timing_tracker, autoplay):
super(TimeCounter, self).__init__()
self.label_colorer = TimeLabelColorer(builder.get_object('label_time'))
self.ett = ett
self.timing_tracker = timing_tracker
self.autoplay = autoplay
builder.load_widgets(self)
builder.setup_actions({
'pause-timer': dict(activate=self.switch_pause, state=self.paused),
'reset-timer': dict(activate=self.reset_timer),
})
self.pause_action = builder.get_application().lookup_action('pause-timer')
# Setup timer for clocks
GLib.timeout_add(250, self.update_time)
[docs]
def switch_pause(self, gaction, param=None):
""" Switch the timer between paused mode and running (normal) mode.
Returns:
`bool`: whether the clock's pause was toggled.
"""
if self.paused:
self.unpause()
else:
self.pause()
return None
[docs]
def pause(self):
""" Pause the timer if it is not paused, otherwise do nothing.
Returns:
`bool`: whether the clock's pause was toggled.
"""
if self.paused:
return False
self.paused = True
self.pause_action.change_state(GLib.Variant.new_boolean(self.paused))
self.elapsed_time += time.time() - self.restart_time
self.timing_tracker.end_time = self.elapsed_time
if self.autoplay.is_looping():
self.autoplay.pause()
self.update_time()
return True
[docs]
def unpause(self):
""" Unpause the timer if it is paused, otherwise do nothing.
Returns:
`bool`: whether the clock's pause was toggled.
"""
if not self.paused:
return False
self.restart_time = time.time()
self.paused = False
self.pause_action.change_state(GLib.Variant.new_boolean(self.paused))
if self.autoplay.is_looping():
self.autoplay.unpause()
self.update_time()
return True
[docs]
def reset_timer(self, *args):
""" Reset the timer.
"""
self.timing_tracker.reset(self.current_time())
self.restart_time = time.time()
self.elapsed_time = 0
if self.autoplay.is_looping():
self.autoplay.start_looping()
self.update_time()
[docs]
def current_time(self):
""" Returns the time elapsed in the presentation.
Returns:
`int`: the time since the presentation started in seconds.
"""
# Time elapsed since the beginning of the presentation
if self.paused:
return self.elapsed_time
else:
return self.elapsed_time + (time.time() - self.restart_time)
[docs]
def update_time(self):
""" Update the timer and clock labels.
Returns:
`bool`: `True` (to prevent the timer from stopping)
"""
# Current time
clock = time.strftime('%X') # '%H:%M:%S'
elapsed = self.current_time()
# Time elapsed since the beginning of the presentation
if self.autoplay.is_looping():
first, stop, loop, delay = self.autoplay.get_page_range()
display_time = '{} {}-{} / {:.1f}s'.format(_('Loop') if loop else _('Auto'), first + 1, stop, delay / 1000)
else:
display_time = '{:02}:{:02}'.format(*divmod(int(elapsed), 60))
if self.paused:
display_time += ' ' + _('(paused)')
self.label_time.set_text(display_time)
self.label_clock.set_text(clock)
if not self.paused:
self.timing_tracker.end_time = elapsed
if self.ett.est_time:
self.label_colorer.update_time_color(self.ett.est_time - elapsed)
else:
self.label_colorer.default_color()
return True