# Copyright (C) 2007, 2008 One Laptop per Child
# Copyright (C) 2013, Ignacio Rodriguez
#
# Muriel de Souza Godoi - muriel@laptop.org
#
# 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.
import logging
import cairo
from gi.repository import GLib
from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import Gtk
from gi.repository import Pango
from gi.repository import PangoCairo
from sugar3.util import LRU
from sugar3.graphics import style
import face
import model
BORDER_WIDTH = style.zoom(10)
class Card(Gtk.EventBox):
# Default properties
default_props = {}
default_props['back'] = {'fill_color': style.Color('#666666'),
'stroke_color': style.Color('#666666')}
default_props['back_text'] = {'text_color': style.Color('#c7c8cc')}
default_props['front'] = {'fill_color': style.Color('#4b4d4a'),
'stroke_color': style.Color('#111111')}
default_props['front_text'] = {'text_color': '#ffffff'}
def __init__(self, identifier, pprops, image_path, size,
bg_color='#000000', font_name=model.DEFAULT_FONT,
show_robot=True):
Gtk.EventBox.__init__(self)
logging.debug('Card image_path %s', image_path)
self.bg_color = bg_color
self.flipped = False
self.id = identifier
self._image_path = image_path
self.jpeg = None
self.size = size
# animation data
self._steps_scales = [0.66, 0.33, 0.1, 0.33, 0.66]
self._animation_steps = len(self._steps_scales)
self._on_animation = False
self._animation_step = 0
self.show_robot = show_robot
self.text_layouts = [None, None]
self.font_name = font_name
self._highlighted = False
self.modify_bg(Gtk.StateType.NORMAL, Gdk.color_parse(bg_color))
self.set_size_request(size, size)
# Views properties
views = ['back', 'back_text', 'front', 'front_text']
self.pprops = pprops
self.props = {}
for view in views:
self.props[view] = {}
self.props[view].update(self.default_props[view])
self.props[view].update(pprops.get(view, {}))
self._cached_surface = {True: None, False: None}
self.draw = Gtk.DrawingArea()
self.draw.modify_bg(Gtk.StateType.NORMAL, Gdk.color_parse(bg_color))
self.draw.set_events(Gdk.EventMask.ALL_EVENTS_MASK)
self.draw.connect('draw', self.__draw_cb)
self.draw.show_all()
self.workspace = Gtk.VBox()
self.workspace.add(self.draw)
self.add(self.workspace)
self.show_all()
def resize(self, new_size):
self.size = new_size
self.set_size_request(self.size, self.size)
self._cached_surface = {True: None, False: None}
self.jpeg = None
self.text_layouts = [None, None]
def __draw_cb(self, widget, context):
flipped = self.flipped
highlighted = self._highlighted
if self._on_animation:
if self._animation_step > self._animation_steps // 2:
flipped = not self.flipped
if not self._cached_surface[flipped]:
self._prepare_cached_surface(context, flipped)
if self._on_animation:
scale = self._steps_scales[self._animation_step]
context.translate(0, self.size * (1 - scale) // 2)
context.scale(1.0, scale)
self._animation_step += 1
highlighted = False
context.set_source_surface(self._cached_surface[flipped])
context.paint()
if highlighted:
radio = self.size // 3
self.draw_round_rect(context, 0, 0, self.size, self.size, radio)
context.set_source_rgb(1., 1., 1.)
context.set_line_width(BORDER_WIDTH)
context.stroke()
return False
def _prepare_cached_surface(self, context, flipped):
self._cached_surface[flipped] = \
context.get_target().create_similar(cairo.CONTENT_COLOR_ALPHA,
self.size, self.size)
cache_context = cairo.Context(self._cached_surface[flipped])
if flipped:
icon_data = self.props['front']
else:
icon_data = self.props['back']
cache_context.save()
radio = self.size // 3
self.draw_round_rect(cache_context, 0, 0, self.size, self.size,
radio)
r, g, b, a = icon_data['fill_color'].get_rgba()
cache_context.set_source_rgb(r, g, b)
cache_context.fill_preserve()
r, g, b, a = icon_data['stroke_color'].get_rgba()
cache_context.set_source_rgb(r, g, b)
cache_context.set_line_width(BORDER_WIDTH)
cache_context.stroke()
cache_context.restore()
text_props = self.props[flipped and 'front_text' or 'back_text']
if self._image_path is not None:
if self.jpeg is None:
image_size = self.size - style.DEFAULT_SPACING * 2
self.jpeg = GdkPixbuf.Pixbuf.new_from_file_at_size(
self._image_path, image_size, image_size)
if self.jpeg is not None and flipped:
Gdk.cairo_set_source_pixbuf(
cache_context, self.jpeg,
style.DEFAULT_SPACING, style.DEFAULT_SPACING)
cache_context.paint()
elif text_props['card_text']:
cache_context.save()
layout = self.text_layouts[flipped]
if not layout:
layout = self.text_layouts[flipped] = \
self.create_text_layout(text_props['card_text'])
width, height = layout.get_pixel_size()
y = (self.size - height) // 2
x = (self.size - width) // 2
cache_context.set_source_rgb(1, 1, 1)
cache_context.translate(x, y)
PangoCairo.update_layout(cache_context, layout)
PangoCairo.show_layout(cache_context, layout)
cache_context.fill()
cache_context.restore()
def set_border(self, stroke_color, fill_color, full_animation=False):
"""
style_color, fill_color: str with format #RRGGBB
"""
self.props['front'].update({'fill_color': style.Color(fill_color),
'stroke_color': style.Color(stroke_color)})
self._cached_surface[True] = None
if full_animation:
if self.get_speak():
# If we displayed the robot face, displayed the text
self.jpeg = None
self.props['front_text']['speak'] = False
if not self.is_flipped():
self.flip(full_animation)
else:
self.queue_draw()
def set_image_path(self, image_path):
self._image_path = image_path
self.jpeg = None
self._cached_surface[True] = None
self.queue_draw()
def get_image_path(self):
return self._image_path
def set_highlight(self, status, mouse=False):
if self.flipped and mouse:
return
self._highlighted = status
self.queue_draw()
def flip(self, full_animation=False):
if self.flipped:
return
if self.jpeg is None:
if self.jpeg is not None:
image_size = self.size - style.DEFAULT_SPACING * 2
self.jpeg = GdkPixbuf.Pixbuf.new_from_file_at_size(
self._image_path, image_size, image_size)
if full_animation:
if self.id != -1 and self.get_speak():
speaking_face = face.acquire()
if speaking_face:
image_size = self.size - style.DEFAULT_SPACING * 2
if self.show_robot:
self.jpeg = GdkPixbuf.Pixbuf.new_from_file_at_size(
'icons/speak.svg', image_size, image_size)
speaking_face.face.say(self.get_text())
self._animation_step = 0
self._on_animation = True
self._animate_flip()
else:
self._finish_flip()
def _animate_flip(self):
if self._animation_step < self._animation_steps - 1:
self.queue_draw()
GLib.timeout_add(100, self._animate_flip)
else:
self._finish_flip()
return False
def _finish_flip(self):
self._on_animation = False
self.flipped = True
self.queue_draw()
def cement(self):
if not self.get_speak():
return
def flop(self):
self._animation_step = 0
self._on_animation = True
self._animate_flop()
def _animate_flop(self):
if self._animation_step < self._animation_steps - 1:
self.queue_draw()
GLib.timeout_add(100, self._animate_flop)
else:
self._finish_flop()
return False
def _finish_flop(self):
self._on_animation = False
self.flipped = False
self.queue_draw()
def is_flipped(self):
return self.flipped or self._on_animation
def get_id(self):
return self.id
def reset(self):
if self.flipped:
self.flop()
def create_text_layout(self, text):
key = (self.size, text)
if key in _text_layout_cache:
return _text_layout_cache[key]
max_lines_count = len([i for i in text.split(' ') if i])
for size in list(range(80, 66, -8)) + list(range(66, 44, -6)) + \
list(range(44, 24, -4)) + list(range(24, 15, -2)) + list(range(15, 7, -1)):
card_size = self.size - BORDER_WIDTH * 2
layout = self.create_pango_layout(text)
layout.set_width(PIXELS_PANGO(card_size))
layout.set_wrap(Pango.WrapMode.WORD)
desc = Pango.FontDescription(self.font_name + " " + str(size))
layout.set_font_description(desc)
if layout.get_line_count() <= max_lines_count and \
layout.get_pixel_size()[0] <= card_size and \
layout.get_pixel_size()[1] <= card_size:
break
if layout.get_line_count() > 1:
# XXX for single line ALIGN_CENTER wrongly affects on text position
# and also in some cases for multilined text
layout.set_alignment(Pango.Alignment.CENTER)
_text_layout_cache[key] = layout
return layout
def change_font(self, font_name):
# remove from local cache
self.text_layouts[self.flipped] = False
text = self.props['front_text']['card_text']
key = (self.size, text)
if key in _text_layout_cache:
del _text_layout_cache[key]
self.font_name = font_name
self._cached_surface[True] = None
self.queue_draw()
def set_background(self, color):
self.bg_color = color
self.draw.modify_bg(Gtk.StateType.NORMAL,
Gdk.color_parse(self.bg_color))
def change_text(self, newtext):
self.text_layouts[self.flipped] = None
self.props['front_text']['card_text'] = newtext
self._cached_surface[True] = None
self.queue_draw()
def get_text(self):
return self.props['front_text'].get('card_text', '')
def change_speak(self, value):
self.props['front_text']['speak'] = value
def get_speak(self):
return self.props['front_text'].get('speak') or False
def draw_round_rect(self, context, x, y, w, h, r):
context.move_to(x + r, y)
context.line_to(x + w - r, y)
context.curve_to(x + w, y, x + w, y, x + w, y + r)
context.line_to(x + w, y + h - r)
context.curve_to(x + w, y + h, x + w, y + h, x + w - r, y + h)
context.line_to(x + r, y + h)
context.curve_to(x, y + h, x, y + h, x, y + h - r)
context.line_to(x, y + r)
context.curve_to(x, y, x, y, x + r, y)
def PIXELS_PANGO(x):
return x * 1000
_text_layout_cache = LRU(50)