Last active
December 22, 2015 19:29
-
-
Save clayote/b32ae6256d91fa7d492a to your computer and use it in GitHub Desktop.
Test for deck builder layout
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This is meant to be a deck builder for trading card games like Magic the Gathering. | |
# You drag cards from your collection on the right to your deck on the left. | |
# The order may be significant. | |
# A gap will open where your card will drop, if you release it. | |
# When you drop it, it will snap into place. | |
# | |
# Problem: | |
# If you drag a card from one stack to the front of the other, and then press Reset, | |
# the card goes back to its original position like it should, but it leaves behind | |
# a non-interactive afterimage. | |
from kivy.clock import Clock | |
from kivy.lang import Builder | |
from kivy.logger import Logger | |
from kivy.properties import ( | |
AliasProperty, | |
BooleanProperty, | |
DictProperty, | |
ListProperty, | |
NumericProperty, | |
ObjectProperty, | |
OptionProperty, | |
ReferenceListProperty, | |
StringProperty, | |
BoundedNumericProperty | |
) | |
from kivy.uix.boxlayout import BoxLayout | |
from kivy.uix.floatlayout import FloatLayout | |
from kivy.uix.layout import Layout | |
from kivy.uix.stencilview import StencilView | |
from kivy.uix.widget import Widget | |
"""Widget that looks like a trading card, and a layout within which it | |
can be dragged and dropped to some particular position within stacks | |
of other cards. | |
""" | |
dbg = Logger.debug | |
class Card(FloatLayout): | |
"""A trading card, similar to the kind you use to play games like | |
_Magic: the Gathering_. | |
""" | |
deck = NumericProperty() | |
idx = NumericProperty() | |
collide_x = NumericProperty() | |
collide_y = NumericProperty() | |
collide_pos = ReferenceListProperty(collide_x, collide_y) | |
foreground_color = ListProperty([1, 1, 1, 1]) | |
text = StringProperty('') | |
text_color = ListProperty([0, 0, 0, 1]) | |
markup = BooleanProperty(True) | |
font_name = StringProperty('Roboto-Regular') | |
font_size = NumericProperty(12) | |
class DeckBuilderLayout(Layout): | |
"""Sizes and positions :class:`Card` objects based on their order | |
within ``decks``, a list of lists where each sublist is a deck of | |
cards. | |
""" | |
decks = ListProperty([[]]) # list of lists of cards | |
"""Put a list of lists of :class:`Card` objects here and I'll position | |
them appropriately. Please don't use ``add_widget``. | |
""" | |
insertion_deck = BoundedNumericProperty(None, min=0, allownone=True) | |
"""Index of the deck that a card is being dragged into.""" | |
insertion_card = BoundedNumericProperty(None, min=0, allownone=True) | |
"""Index within the current deck that a card is being dragged into.""" | |
dragging = ObjectProperty(None, allownone=True) | |
def __init__(self, **kwargs): | |
"""Bind most of my custom properties to ``_trigger_layout``.""" | |
super(self.__class__, self).__init__(**kwargs) | |
self.bind( | |
decks=self._trigger_layout, | |
insertion_deck=self._trigger_layout, | |
insertion_card=self._trigger_layout | |
) | |
def on_decks(self, *args): | |
"""Inform the cards of their deck and their index within the deck; | |
extend the ``_hint_offsets`` properties as needed; and trigger | |
a layout. | |
""" | |
if None in ( | |
self.canvas, | |
self.decks, | |
): | |
Clock.schedule_once(self.on_decks, 0) | |
return | |
decknum = 0 | |
for deck in self.decks: | |
cardnum = 0 | |
for card in deck: | |
if not isinstance(card, Card): | |
raise TypeError("You must only put Card in decks") | |
if card not in self.children: | |
self.add_widget(card) | |
if card.deck != decknum: | |
card.deck = decknum | |
if card.idx != cardnum: | |
card.idx = cardnum | |
cardnum += 1 | |
decknum += 1 | |
self._trigger_layout() | |
def on_touch_down(self, touch): | |
"""If I'm the first card to collide this touch, grab it, store my | |
metadata in its userdict, and store the relative coords upon | |
me where the collision happened. | |
""" | |
if not self.collide_point(*touch.pos): | |
return | |
for child in self.children: | |
if not isinstance(child, Card): | |
continue | |
if child.collide_point(*touch.pos) and ( | |
self.dragging is None or ( | |
child.idx > self.dragging.idx or | |
child.deck > self.dragging.deck | |
) | |
): | |
self.dragging = child | |
if self.dragging is not None: | |
self.dragging.collide_x = touch.x - self.dragging.x | |
self.dragging.collide_y = touch.y - self.dragging.y | |
touch.grab(self) | |
def on_touch_move(self, touch): | |
"""If a card is being dragged, move other cards out of the way to show | |
where the dragged card will go if you drop it. | |
""" | |
if touch.grab_current != self: | |
return | |
self.dragging.pos = ( | |
touch.x - self.dragging.collide_x, | |
touch.y - self.dragging.collide_y | |
) | |
if not hasattr(self.dragging, '_topdecked'): | |
self.canvas.after.add(self.dragging.canvas) | |
self.dragging._topdecked = True | |
i = 0 | |
for deck in self.decks: | |
cards = [card for card in deck if card != self.dragging] | |
maxidx = max(card.idx for card in cards) if cards else 0 | |
cards.reverse() | |
cards_collided = [ | |
card for card in cards if card.collide_point(*touch.pos) | |
] | |
if cards_collided: | |
collided = cards_collided.pop() | |
for card in cards_collided: | |
if card.idx > collided.idx: | |
collided = card | |
if self.dragging and collided.deck == self.dragging.deck: | |
self.insertion_card = ( | |
1 if collided.idx == 0 else | |
maxidx + 1 if collided.idx == maxidx else | |
collided.idx + 1 if collided.idx > self.dragging.idx | |
else collided.idx | |
) | |
else: | |
dropdeck = self.decks[collided.deck] | |
maxidx = max(card.idx for card in dropdeck) | |
self.insertion_card = ( | |
1 if collided.idx == 0 else | |
maxidx + 1 if collided.idx == maxidx else | |
collided.idx + 1 | |
) | |
if self.insertion_deck != collided.deck: | |
self.insertion_deck = collided.deck | |
return | |
else: | |
if self.insertion_deck == i: | |
if self.insertion_card in (0, len(deck)): | |
pass | |
elif touch.y > cards[0].top or touch.x < cards[0].x: | |
self.insertion_card = len(deck) | |
elif touch.y < cards[0].y or touch.x > cards[0].right: | |
self.insertion_card = 0 | |
i += 1 | |
def on_touch_up(self, touch): | |
"""If a card is being dragged, put it in the place it was just dropped | |
and trigger a layout. | |
""" | |
if touch.grab_current != self: | |
return | |
touch.ungrab(self) | |
if hasattr(self.dragging, '_topdecked'): | |
self.canvas.after.remove(self.dragging.canvas) | |
del self.dragging._topdecked | |
if None not in (self.insertion_deck, self.insertion_card): | |
# need to sync to adapter.data?? | |
del self.decks[self.dragging.deck][self.dragging.idx] | |
for i in range(0, len(self.decks[self.dragging.deck])): | |
self.decks[self.dragging.deck][i].idx = i | |
deck = self.decks[self.insertion_deck] | |
if self.insertion_card >= len(deck): | |
deck.append(self.dragging) | |
else: | |
deck.insert(self.insertion_card, self.dragging) | |
self.dragging.deck = self.insertion_deck | |
self.dragging.idx = self.insertion_card | |
self.decks[self.insertion_deck] = deck | |
self.insertion_deck = self.insertion_card = self.dragging = None | |
self._trigger_layout() | |
def on_insertion_card(self, *args): | |
"""Trigger a layout""" | |
if self.insertion_card is not None: | |
self._trigger_layout() | |
def do_layout(self, *args): | |
"""Layout each of my decks""" | |
if self.size == [1, 1]: | |
return | |
self.clear_widgets() | |
for i in range(0, len(self.decks)): | |
self.layout_deck(i) | |
def layout_deck(self, i): | |
"""Stack the cards, starting at my deck's foundation, and proceeding | |
by ``card_pos_hint`` | |
""" | |
def get_dragidx(cards): | |
j = 0 | |
for card in cards: | |
if card == self.dragging: | |
return j | |
j += 1 | |
# Put a None in the card list in place of the card you're | |
# hovering over, if you're dragging another card. This will | |
# result in an empty space where the card will go if you drop | |
# it now. | |
cards = list(self.decks[i]) | |
dragidx = get_dragidx(cards) | |
if self.insertion_deck == i and self.insertion_card is not None: | |
insdx = self.insertion_card | |
if dragidx is not None and insdx > dragidx: | |
insdx -= 1 | |
cards.insert(insdx, None) | |
cards.reverse() | |
# Work out the initial pos_hint for this deck | |
phx = 0.1 | |
phy = 0.6 | |
phx += i * 0.4 | |
(w, h) = self.size | |
(x, y) = self.pos | |
# start assigning pos and size to cards | |
for card in cards: | |
if card is not None: | |
(shw, shh) = (0.15, 0.3) | |
if card != self.dragging: | |
card.pos = ( | |
x + phx * w, | |
y + phy * h | |
) | |
card.size = (w * shw, h * shh) | |
self.add_widget(card) | |
phx += 0.05 | |
phy -= 0.1 | |
class DeckBuilderView(DeckBuilderLayout, StencilView): | |
"""Just a :class:`DeckBuilderLayout` mixed with | |
:class:`StencilView`. | |
""" | |
pass | |
kv = """ | |
<Card>: | |
canvas: | |
Color: | |
rgba: [1, 1, 1, 1] | |
Rectangle: | |
pos: root.pos | |
size: root.size | |
BoxLayout: | |
size_hint: 0.9, 0.9 | |
pos_hint: {'x': 0.05, 'y': 0.05} | |
orientation: 'vertical' | |
Widget: | |
id: foreground | |
canvas: | |
Color: | |
rgba: root.foreground_color | |
Rectangle: | |
size: self.size | |
pos: self.pos | |
Label: | |
text: root.text | |
color: root.text_color | |
markup: root.markup | |
font_name: root.font_name | |
font_size: root.font_size | |
text_size: foreground.size | |
size_hint: (None, None) | |
size: self.texture_size | |
pos: foreground.pos | |
valign: 'top' | |
<DeckBuilderScrollBar>: | |
ScrollBarBar: | |
id: bar | |
color: root.bar_color if root.scrolling else root.bar_inactive_color | |
texture: root.bar_texture | |
""" | |
Builder.load_string(kv) | |
if __name__ == '__main__': | |
from kivy.uix.button import Button | |
def decko(): | |
return [ | |
Card( | |
foreground_color=[0, 0, 1, 1], | |
text='The quick brown fox jumps over the lazy dog', | |
text_color=[1, 1, 1, 1], | |
) | |
for i in range(0, 4) | |
] | |
deck0 = decko() | |
def deckun(): | |
return [ | |
Card( | |
foreground_color=[1, 0, 0, 1], | |
text='Have a steak at the porter house bar', | |
text_color=[1, 1, 0, 1], | |
) | |
for i in range(0, 4) | |
] | |
deck1 = deckun() | |
from kivy.base import runTouchApp | |
from kivy.core.window import Window | |
from kivy.modules import inspector | |
builder = DeckBuilderLayout( | |
pos_hint={'x': 0, 'y': 0}, | |
decks=[deck0, deck1] | |
) | |
layout = BoxLayout(orientation='vertical') | |
layout.add_widget(builder) | |
def redeux(*args): | |
builder.decks = [decko(), deckun()] | |
layout.add_widget(Button( | |
text='Reset', | |
size_hint_y=0.1, | |
on_release=redeux | |
)) | |
inspector.create_inspector(Window, layout) | |
runTouchApp(layout) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment