Files
katrain-qt/katrain/gui/kivyutils.py
T
2020-05-30 01:43:15 +02:00

583 lines
19 KiB
Python

import json
import random
import re
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.graphics import *
from kivy.properties import (
BooleanProperty,
ListProperty,
NumericProperty,
StringProperty,
OptionProperty,
ObjectProperty,
)
from kivy.uix.behaviors import ToggleButtonBehavior, ButtonBehavior
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button
from kivy.uix.checkbox import CheckBox
from kivy.uix.filechooser import FileChooserLayout, FileChooserListLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.label import Label
from kivy.uix.scrollview import ScrollView
from kivy.uix.spinner import Spinner
from kivy.uix.textinput import TextInput
from kivy.uix.widget import Widget
# --new
from kivymd.app import MDApp
from kivymd.uix.behaviors import RectangularRippleBehavior, CircularRippleBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.button import BasePressedButton, BaseFlatButton
from kivymd.uix.navigationdrawer import MDNavigationDrawer
from kivy.core.text import Label as CoreLabel
from katrain.core.constants import GAME_TYPES
from katrain.core.utils import i18n
#
# --- new mixins
from katrain.gui.style import WHITE, DEFAULT_FONT
class BackgroundMixin(Widget):
background_color = ListProperty([0, 0, 0, 0])
background_radius = NumericProperty(0)
outline_color = ListProperty([0, 0, 0, 0])
outline_width = NumericProperty(1)
class LeftButtonBehavior(ButtonBehavior): # stops buttons etc activating on right click
def __init__(self, **kwargs):
self.register_event_type("on_left_release")
self.register_event_type("on_left_press")
super().__init__(**kwargs)
def on_touch_down(self, touch):
return super().on_touch_down(touch)
def on_release(self):
if not self.last_touch or self.last_touch.button == "left":
self.dispatch("on_left_release")
return super().on_release()
def on_press(self):
if not self.last_touch or self.last_touch.button == "left":
self.dispatch("on_left_press")
return super().on_press()
def on_left_release(self):
pass
def on_left_press(self):
pass
# -- resizeable buttons
class SizedButton(LeftButtonBehavior, RectangularRippleBehavior, BasePressedButton, BaseFlatButton, BackgroundMixin): # avoid baserectangular for sizing
text = StringProperty("")
text_color = ListProperty(WHITE)
text_size = ListProperty([100, 100])
halign = OptionProperty("center", options=["left", "center", "right", "justify", "auto"])
label = ObjectProperty(None)
padding_x = NumericProperty(6)
padding_y = NumericProperty(0)
_font_size = NumericProperty(None)
font_name = StringProperty(DEFAULT_FONT)
class AutoSizedButton(SizedButton):
pass
class SizedRectangleButton(SizedButton):
pass
class AutoSizedRectangleButton(AutoSizedButton):
pass
class ToggleButtonMixin(ToggleButtonBehavior):
inactive_outline_color = ListProperty([0.5, 0.5, 0.5, 0])
active_outline_color = ListProperty([1, 1, 1, 0])
inactive_background_color = ListProperty([0.5, 0.5, 0.5, 1])
active_background_color = ListProperty([1, 1, 1, 1])
@property
def active(self):
return self.state == "down"
class SizedToggleButton(ToggleButtonMixin, SizedButton):
pass
class SizedRectangleToggleButton(ToggleButtonMixin, SizedRectangleButton):
pass
class AutoSizedRectangleToggleButton(ToggleButtonMixin, AutoSizedRectangleButton):
pass
class TransparentIconButton(CircularRippleBehavior, Button):
icon_size = ListProperty([25, 25])
icon = StringProperty("")
class PauseButton(CircularRippleBehavior, LeftButtonBehavior, Widget):
active = BooleanProperty(True)
active_line_color = ListProperty([0.5, 0.5, 0.8, 1])
inactive_line_color = ListProperty([1, 1, 1, 1])
active_fill_color = ListProperty([0.5, 0.5, 0.5, 1])
inactive_fill_color = ListProperty([1, 1, 1, 0])
line_width = NumericProperty(5)
fill_color = ListProperty([0.5, 0.5, 0.5, 1])
line_color = ListProperty([0.5, 0.5, 0.5, 1])
min_size = NumericProperty(100)
# -- basic styles
class LightLabel(Label):
pass
class StatsLabel(MDBoxLayout):
text = StringProperty("")
label = StringProperty("")
color = ListProperty([1, 1, 1, 1])
hidden = BooleanProperty(False)
font_name = StringProperty(DEFAULT_FONT)
class MyNavigationDrawer(MDNavigationDrawer): # in PR - closes NavDrawer on any outside click
def on_touch_down(self, touch):
return super().on_touch_down(touch)
def on_touch_up(self, touch):
if self.status == "opened" and self.close_on_click and not self.collide_point(touch.ox, touch.oy):
self.set_state("close", animation=True)
return True
return super().on_touch_up(touch)
class CircleWithText(Widget):
text = StringProperty("0")
player = OptionProperty("B", options=["B", "W"])
min_size = NumericProperty(50)
class BGBoxLayout(BoxLayout, BackgroundMixin):
pass
# -- new gui elements
class StyledSpinner(Spinner):
sync_height_frac = NumericProperty(1.0)
value_refs = ListProperty()
font_name = StringProperty(DEFAULT_FONT)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(size=self.update_dropdown_props, pos=self.update_dropdown_props, value_refs=self.i18n_values)
self.i18n_values()
@property
def selected(self):
try:
selected = self.values.index(self.text)
return selected, self.value_refs[selected], self.values[selected]
except (ValueError, IndexError):
return 0, "", ""
def select_key(self, key):
try:
ix = self.value_refs.index(key)
self.text = self.values[ix]
except:
pass
def i18n_values(self, *_args):
if self.value_refs:
selected = self.selected[0]
self.values = [i18n._(ref) for ref in self.value_refs]
self.text = self.values[selected]
self.font_name = i18n.font_name
self.update_dropdown_props()
def update_dropdown_props(self, *largs):
if not self.sync_height_frac:
return
dp = self._dropdown
if not dp:
return
container = dp.container
if not container:
return
h = self.height
fsz = self.font_size
for item in container.children[:]:
item.height = h * self.sync_height_frac
item.font_size = fsz
item.font_name = self.font_name
class PlayerSetup(MDBoxLayout):
player = OptionProperty("B", options=["B", "W"])
def __init__(self, **kwargs):
super().__init__(**kwargs)
Clock.schedule_once(self.setup_options, 0)
def setup_options(self, *_args):
self.player_subtype.clear_widgets()
if self.player_type.selected[0] == 0: # human
self.player_subtype_label.text = i18n._("gametype")
self.player_subtype.value_refs = GAME_TYPES
else:
self.player_subtype_label.text = i18n._("aistrategy")
self.player_subtype.value_refs = MDApp.get_running_app().gui.ai_strategies # TODO:
self.player_subtype.text = self.player_subtype.values[0]
self.update_global_player_info()
@property
def player_type_dump(self):
return {"player_type": self.player_type.selected[1], "player_subtype": self.player_subtype.selected[1]}
def update_widget(self, player_type, player_subtype):
self.player_type.select_key(player_type) # should trigger setup options
self.player_subtype.select_key(player_subtype) # should trigger setup options
def update_global_player_info(self):
if self.parent and self.parent.update_global:
katrain = MDApp.get_running_app().gui
if katrain.game and katrain.game.current_node:
katrain.update_player(self.player, **self.player_type_dump)
class PlayerSetupBlock(MDBoxLayout):
players = ObjectProperty(None)
black = ObjectProperty(None)
white = ObjectProperty(None)
update_global = BooleanProperty(False)
INSTANCES = []
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.black = PlayerSetup(player="B")
self.white = PlayerSetup(player="W")
self.players = {"B": self.black, "W": self.white}
self.add_widget(self.black)
self.add_widget(self.white)
PlayerSetupBlock.INSTANCES.append(self)
def update_players(self, bw, player_info): # update sub widget based on gui state change
self.players[bw].update_widget(player_type=player_info.player_type, player_subtype=player_info.player_subtype)
class PlayerInfo(MDBoxLayout, BackgroundMixin):
captures = NumericProperty(0)
player = OptionProperty("B", options=["B", "W"])
player_type = StringProperty("Player")
player_subtype = StringProperty("")
active = BooleanProperty(True)
class Timer(BGBoxLayout):
state = ListProperty([30, 5, 1])
class AnalysisToggle(MDBoxLayout):
text = StringProperty("")
default_active = BooleanProperty(False)
font_name = StringProperty(DEFAULT_FONT)
def trigger_action(self, *args, **kwargs):
return self.checkbox.trigger_action(*args, **kwargs)
@property
def active(self):
return self.checkbox.active
class MainMenuItem(RectangularRippleBehavior, LeftButtonBehavior, MDBoxLayout, BackgroundMixin):
__events__ = ["on_action"]
icon = StringProperty("")
text = StringProperty("")
shortcut = StringProperty("")
font_name = StringProperty(DEFAULT_FONT)
def on_left_release(self):
self.anim_complete() # kill ripple
MDApp.get_running_app().gui.nav_drawer.set_state("close")
self.dispatch("on_action")
def on_action(self):
pass
class CollapsablePanelHeader(MDBoxLayout):
pass
class CollapsablePanelTab(AutoSizedRectangleToggleButton):
pass
class CollapsablePanel(MDBoxLayout):
__events__ = ["on_option_state"]
options = ListProperty([])
options_height = NumericProperty(25)
options_spacing = NumericProperty(6)
option_labels = ListProperty([])
option_active = ListProperty([])
option_colors = ListProperty([])
closed_label = StringProperty("Closed Panel")
size_hint_y_open = NumericProperty(1)
height_open = NumericProperty(None)
state = OptionProperty("open", options=["open", "close"])
close_icon = "img/flaticon/previous5.png"
open_icon = "img/flaticon/next5.png"
def __init__(self, **kwargs):
self.header, self.contents, self.open_close_button = None, None, None
self.option_buttons = []
super().__init__(**kwargs)
self.orientation = "vertical"
self.bind(
options=self.build_options, option_colors=self.build_options, options_height=self.build_options, option_active=self.build_options, options_spacing=self.build_options,
)
self.bind(state=self.build, size_hint_y_open=self.build, height_open=self.build)
self.build_options()
def build_options(self, *args, **kwargs):
self.header = CollapsablePanelHeader(height=self.options_height, size_hint_y=None, spacing=self.options_spacing, padding=[1, 0, 0, 0])
self.option_buttons = []
option_labels = self.option_labels or [i18n._(f"tab:{opt}") for opt in self.options]
for ix, (lbl, opt_col, active) in enumerate(zip(option_labels, self.option_colors, self.option_active)):
button = CollapsablePanelTab(text=lbl, active_outline_color=opt_col, height=self.options_height, state="down" if active else "normal")
self.option_buttons.append(button)
button.bind(state=lambda *_args, _ix=ix: self.trigger_select(_ix))
self.open_close_button = TransparentIconButton( # << / >> collapse button
icon=self.open_close_icon(),
icon_size=[0.5 * self.options_height, 0.5 * self.options_height],
width=0.75 * self.options_height,
size_hint_x=None,
on_press=lambda *_args: self.set_state("toggle"),
)
self.bind(state=lambda *_args: self.open_close_button.setter("icon")(None, self.open_close_icon()))
self.build()
def build(self, *args, **kwargs):
self.header.clear_widgets()
if self.state == "open":
for button in self.option_buttons:
self.header.add_widget(button)
self.header.add_widget(Label()) # spacer
self.trigger_select(ix=None)
else:
self.header.add_widget(Label(text=i18n._(self.closed_label), halign="right", height=self.options_height))
self.header.add_widget(self.open_close_button)
super().clear_widgets()
super().add_widget(self.header)
height, size_hint_y = 1, None
if self.state == "open" and self.contents:
super().add_widget(self.contents)
if self.height_open:
height = self.height_open
else:
size_hint_y = self.size_hint_y_open
else:
height = self.header.height
self.height, self.size_hint_y = height, size_hint_y
def open_close_icon(self):
return self.open_icon if self.state == "open" else self.close_icon
def add_widget(self, widget, index=0, **_kwargs):
if self.contents:
raise ValueError("CollapsablePanel can only have one child")
self.contents = widget
self.build()
def set_state(self, state="toggle"):
if state == "toggle":
state = "close" if self.state == "open" else "open"
self.state = state
self.build()
if self.state == "open":
self.trigger_select(ix=None)
def trigger_select(self, ix):
if ix is not None and self.option_buttons:
self.option_active[ix] = self.option_buttons[ix].state == "down"
if self.state == "open":
self.dispatch("on_option_state", {opt: btn.active for opt, btn in zip(self.options, self.option_buttons)})
return False
def on_option_state(self, options):
pass
class StatsBox(MDBoxLayout, BackgroundMixin):
winrate = StringProperty("...")
score = StringProperty("...")
points_lost = NumericProperty(None, allownone=True)
player = StringProperty("")
# --- not checked
class ToolTipLabel(Label):
pass
class ToolTipBehavior(object): # TODO restyle
tooltip_text = StringProperty("")
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.tooltip = ToolTipLabel()
self.open = False
def on_touch_up(self, touch):
inside = self.collide_point(*self.to_widget(*touch.pos))
if inside and touch.button == "right" and self.tooltip_text:
if not self.open:
self.display_tooltip(touch.pos)
Clock.schedule_once(lambda _: self.set_position(touch.pos), 0)
elif not inside and self.open:
self.close_tooltip()
return super().on_touch_up(touch)
def close_tooltip(self):
self.open = False
Window.remove_widget(self.tooltip)
def on_size(self, *args):
mid = (self.pos[0] + self.width / 2, self.pos[1] + self.height / 2)
self.set_position(mid)
def set_position(self, pos):
self.tooltip.pos = (pos[0] - self.tooltip.texture_size[0], pos[1])
def display_tooltip(self, pos):
self.open = True
self.tooltip.text = self.tooltip_text
Window.add_widget(self.tooltip)
class ScaledLightLabel(LightLabel, ToolTipBehavior):
num_lines = NumericProperty(1)
class ClickableLabel(LeftButtonBehavior, Label):
pass
class LightHelpLabel(ScaledLightLabel):
pass
class ScrollableLabel(ScrollView, BackgroundMixin):
__events__ = ["on_ref_press"]
outline_color = ListProperty([0, 0, 0, 0]) # mixin not working for some reason
text = StringProperty("")
markup = BooleanProperty(False)
def on_ref_press(self, ref):
pass
class StyledButton(Button, LeftButtonBehavior, ToolTipBehavior):
button_color = ListProperty([])
button_color_down = ListProperty([])
radius = ListProperty((0,))
class StyledToggleButton(StyledButton, ToggleButtonBehavior, ToolTipBehavior):
value = StringProperty("")
allow_no_selection = BooleanProperty(False)
def _do_press(self):
if (self.last_touch and self.last_touch.button != "left") or (not self.allow_no_selection and self.state == "down"):
return
self._release_group(self)
self.state = "normal" if self.state == "down" else "down"
class ToggleButtonContainer(GridLayout):
__events__ = ("on_selection",)
options = ListProperty([])
labels = ListProperty([])
tooltips = ListProperty(None)
selected = StringProperty("")
group = StringProperty(None)
autosize = BooleanProperty(True)
spacing = ListProperty((1, 1))
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.rows = 1
self.cols = len(self.options)
Clock.schedule_once(self._build, 0)
def on_selection(self, *args):
pass
def _build(self, _dt):
self.cols = len(self.options)
self.group = self.group or str(random.random())
if not self.selected and self.options:
self.selected = self.options[0]
if len(self.labels) < len(self.options):
self.labels += self.options[len(self.labels) + 1 :]
def state_handler(*args):
self.dispatch("on_selection")
for i, opt in enumerate(self.options):
state = "down" if opt == self.selected else "normal"
tooltip = self.tooltips[i] if self.tooltips else None
self.add_widget(StyledToggleButton(group=self.group, text=self.labels[i], value=opt, state=state, on_press=state_handler, tooltip_text=tooltip,))
Clock.schedule_once(self._size, 0)
def _size(self, _dt):
if self.autosize:
for tb in self.children:
tb.size_hint = (tb.texture_size[0] + 10, 1)
@property
def value(self):
for tb in self.children:
if tb.state == "down":
return tb.value
if self.options:
return self.options[0]
def draw_text(pos, text, font_name=None, **kw):
label = CoreLabel(text=text, bold=True, font_name=font_name or i18n.font_name, **kw) #
label.refresh()
Rectangle(
texture=label.texture, pos=(pos[0] - label.texture.size[0] / 2, pos[1] - label.texture.size[1] / 2), size=label.texture.size,
)
def draw_circle(pos, r, col):
Color(*col)
Ellipse(pos=(pos[0] - r, pos[1] - r), size=(2 * r, 2 * r))