"""
pygame helpers
The module pygame is not imported in this module but sent
to every function as a parameter to avoid importing
the module if not needed.
:githublink:`%|py|9`
"""
import math
MOUSE = "mouse"
KEY = "key"
[docs]def wait_event(pygame):
"""
The function waits for an event, a
:param pygame: module pygame
:githublink:`%|py|21`
"""
while True:
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONUP:
return (MOUSE, event.button, event.pos[0], event.pos[1])
elif event.type == pygame.KEYUP:
if event.key == 27:
return None
else:
return (KEY, event.key)
elif event.type == pygame.QUIT:
return None
[docs]def empty_main_loop(pygame):
"""
Removes all events in the main loop,
a mouse click make the program halt,
another click makes it start again.
:param pygame: module pygame
:return: event ``pygame.QUIT``?
:githublink:`%|py|43`
"""
for event in pygame.event.get():
if event.type == pygame.QUIT:
return False
if event.type == pygame.MOUSEBUTTONUP:
wait_event(pygame)
return True
[docs]def get_pygame_screen_font(h, size, flags=0):
"""
Creates a surface with :epkg:`pygame`, initialize the module,
creates font.
:param h: size of the main font
:param size: screen size
:param flags: see `pygame.display.set_mode <https://www.pygame.org/docs/ref/display.html#pygame.display.set_mode>`_
:return: pygame, screen, dictionary of fonts
The dictionary of fonts contains three fonts of size *h*,
*3h/4*, *5h/6*.
This function leaves file still opened and generates warnings.
Parameter *flag* can be useful if you run the function just
to test that it is working and the result does not need to be seen.
:githublink:`%|py|68`
"""
import pygame
pygame.init()
font = pygame.font.Font("freesansbold.ttf", h)
font_small = pygame.font.Font("freesansbold.ttf", 3 * h // 4)
try:
screen = pygame.display.set_mode(size, flags)
except pygame.error as e:
raise Exception(
"Unable to create a screen, flags={0}".format(flags)) from e
font = pygame.font.Font("freesansbold.ttf", h)
font_small = pygame.font.Font("freesansbold.ttf", 3 * h // 4)
font_half = pygame.font.Font("freesansbold.ttf", 5 * h // 6)
return pygame, screen, dict(font=font, font_half=font_half, font_small=font_small)
[docs]def build_diff_image(pygame, screen, h, maxw, seq1=None, seq2=None, diff=None, fonts=None,
bars=None, colors=None, progress=None, prev_bars=None):
"""
Builds an image to show a difference between two lists,
we assume these list contain distinct items.
:param pygame: module pygame
:param screen: screen (pygame surface)
:param h: height of a line
:param maxw: width of the screen
:param seq1: list 1 (can be None)
:param seq2: list 2 (cannot be None)
:param diff: difference (object `SequenceMatcher <https://docs.python.org/3.5/library/difflib.html#sequencematcher-objects>`_)
:param fonts: dictionary of fonts with keys ``'font'``, ``'font_small'``, ``'font_half'``
:param bars: each item of sequence 2 can be associated to a width (in [0, 1])
:param colors: dictionary of colors (see below)
:param progress: draws the progress between two list
:param prev_bars: previous width
Colors:
* black: no change
* blue: new
* red: deleted
* green: vert
* yellow: bars
When *progress* is not None, the picture is a kind of average
between the previous position and the new one. When a suggestion moves
from *p1* to *p2*, it defines a circle.
The result looks like this.
.. raw:: html
<video autoplay=" controls="" loop="" height="250">
<source src="http://www.xavierdupre.fr/enseignement/complements/diff.mp4" type="video/mp4" />
</video>
:githublink:`%|py|122`
"""
font = fonts.get('font', None)
font_small = fonts.get('font_small', None)
font_half = fonts.get('font_half', None)
if font is None:
raise ValueError("font cannot be None")
if font_small is None:
raise ValueError("font_small cannot be None")
if font_half is None:
raise ValueError("font_half cannot be None")
if seq2 is None:
raise ValueError("seq2 cannot be None")
if colors is None:
colors = {}
set_seq1 = {} if seq1 is None else set(seq1)
set_seq2 = set(seq2)
width = h // 3
color_bar = colors.get('yellow', (240, 240, 0))
pos = 0
if diff is not None:
if progress is None:
# just the diff
opcodes = []
for opcode in diff.get_opcodes():
if opcode[0] in {'delete', 'equal', 'insert'}:
opcodes.append(opcode)
elif opcode[0] == "replace":
opcodes.append(
('delete', opcode[1], opcode[2], None, None))
opcodes.append(
('insert', None, None, opcode[3], opcode[4]))
else:
raise ValueError("unexpected: {0}".format(opcode))
for opcode in opcodes:
if opcode[0] == "delete":
for i in range(opcode[1], opcode[2]):
text = seq1[i]
if text not in set_seq2:
color = colors.get('red', (200, 0, 0))
text = font_small.render(text, True, color)
screen.blit(text, (10, h * pos + h // 6))
pos += 1
else:
# we skip, it is going to be display by the other
# part of the loop
pass
elif opcode[0] == "equal":
color = colors.get('black', (0, 0, 0))
for i in range(opcode[3], opcode[4]):
if bars is not None:
y = h * pos + (h - width) // 2 + width
pygame.draw.line(
screen, color_bar, (0, y), (int(bars[i] * maxw), y), width)
text = seq2[i]
text = font.render(text, True, color)
screen.blit(text, (10, h * pos))
pos += 1
else:
for i in range(opcode[3], opcode[4]):
if bars is not None:
y = h * pos + (h - width) // 2 + width
pygame.draw.line(
screen, color_bar, (0, y), (int(bars[i] * maxw), y), width)
text = seq2[i]
if text in set_seq1:
color = colors.get('green', (0, 120, 0))
text = font.render(text, True, color)
screen.blit(text, (10, h * pos))
pos += 1
else:
color = colors.get("blue", (0, 120, 120))
text = font.render(text, True, color)
screen.blit(text, (10, h * pos))
pos += 1
else:
# animation
positions = []
opcodes = []
for opcode in diff.get_opcodes():
if opcode[0] in {'delete', 'equal', 'insert'}:
opcodes.append(opcode)
elif opcode[0] == "replace":
opcodes.append(
('delete', opcode[1], opcode[2], None, None))
opcodes.append(
('insert', None, None, opcode[3], opcode[4]))
else:
raise ValueError("unexpected: {0}".format(opcode))
for opcode in opcodes:
if opcode[0] == "delete":
for i in range(opcode[1], opcode[2]):
row = (seq1[i], i, seq2.index(seq1[i]) if seq1[i] in seq2 else None,
prev_bars[i] if prev_bars is not None else None)
positions.append(row)
elif opcode[0] == "equal":
for i in range(opcode[3], opcode[4]):
row = (seq2[i], seq1.index(seq2[i]), i,
bars[i] if bars is not None else None)
positions.append(row)
else:
for i in range(opcode[3], opcode[4]):
row = (seq2[i], seq1.index(seq2[i]) if seq2[i] in seq1 else None, i,
bars[i] if bars is not None else None)
positions.append(row)
for text, p1, p2, bar_ in positions:
if p1 is None:
# new
x = maxw * (1 - progress)
y = p2 * h
color = colors.get('blue', (0, 120, 120))
elif p2 is None:
# deleted
x = maxw * progress
y = p1 * h
color = colors.get('green', (0, 120, 0))
else:
# moved or equal
if p1 == p2:
x = 0.0
y = p1 * h
else:
x = math.sin(progress * math.pi) * maxw / 2 * \
abs(p2 - p1) / len(seq2)
y = (p1 + p2) * h / 2 - (p2 - p1) * \
h * math.cos(progress * math.pi) / 2
color = colors.get('black', (0, 0, 0))
x = int(x)
y = int(y)
if bar_ is not None:
y2 = y + (h - width) // 2 + width
pygame.draw.line(screen, color_bar, (x, y2),
(x + int(bar_ * maxw), y2), width)
text = font.render(text, True, color)
screen.blit(text, (x, y))
else:
color = colors.get('black', (0, 0, 0))
for i in range(0, len(seq2)):
text = seq2[i]
text = font.render(text, True, color)
screen.blit(text, (10, h * pos))
pos += 1