"""Button for proxy actions. Part of the tagit module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ # standard imports import logging import os # kivy imports from kivy import metrics from kivy.animation import Animation from kivy.lang import Builder from kivy.uix.behaviors import ButtonBehavior from kivy.uix.boxlayout import BoxLayout from kivy.uix.image import Image from kivy.uix.label import Label import kivy.properties as kp # tagit imports from tagit.external.tooltip import Tooltip # exports __all__ = ('Action', ) ## code ## logger = logging.getLogger(__name__) # load kv Builder.load_file(os.path.join(os.path.dirname(__file__), 'action.kv')) class Action(ButtonBehavior, BoxLayout, Tooltip): """ An Action can be triggered in three ways: * Touch event: Clicking or touching on the button * Key event: Entering a keyboard shortcut * Single-shot: Programmatically triggered once, without UI attachment For the last, use the *single_shot* classmethod. For the first two, declare the Action in a kv file or in code. Note that the remarks below about kv do not apply if the object is created in code. When an Action is declared in kv, two restrictions apply (also see examples below): * To disable touch_trigger, it must be declared last * show must be declared after all other properties Enable key triggers, but hide the Action in the UI: Action: show: [] Action: # alias for the one above Show text, image, or both, with default height and the width stretched as necessary: Action: show: 'text', Action: show: 'image', Action: show: 'text', 'image' Action: width: 200 # has no effect unless autowidth is False show: 'image', 'text' Make the Action larger: Action: # increased height. The image width scales accordingly height: 80 show: 'image', 'text' Action: # scales to parent widget's width autowidth: False size_hint_x: 1 show: 'image', 'text' Action: # fixed width and height width: 150 height: 80 autowidth: False show: 'image', 'text' # must be declared **last** Show the button but disable touch events: Action: height: 80 show: 'image', 'text' touch_trigger: False # must be declared **after** show Do the same in code: >>> Action( ... size=(130, 80), ... autowidth=False, ... show=('image', 'text'), ... text='foobar', ... touch_trigger=False) """ # content tooltip = kp.StringProperty() source = kp.StringProperty() text = kp.StringProperty() # visibility flags show = kp.ListProperty([]) # sizing default_height = kp.NumericProperty(30) autowidth = kp.BooleanProperty(True) # responsiveness key_trigger = kp.BooleanProperty(True) touch_trigger = kp.BooleanProperty(True) # required such that properties can be set via constructor font_size = kp.StringProperty("15sp") # FIXME: Check why I have to pass size instead of height/width height = kp.NumericProperty(0) width = kp.NumericProperty(0) # internal properties root = kp.ObjectProperty(None) selected_alpha = kp.NumericProperty(0) _image = kp.ObjectProperty(None) _label = kp.ObjectProperty(None) def __init__(self, **kwargs): show = kwargs.pop('show', []) touch_trigger = kwargs.pop('touch_trigger', None) super(Action, self).__init__(**kwargs) # delay such that on_show is executed once the other # properties are set self.show = show if touch_trigger is not None: self.touch_trigger = touch_trigger ## display def on_show(self, wx, show): if self._image is not None: self.remove_widget(self._image) if self._label is not None: self.remove_widget(self._label) if 'image' in show: self.height = self.default_height if self.height == 0 else self.height self.touch_trigger = True self._image = Image( source=self.source, # size height=self.height, width=self.height, # square image size_hint=(None, None), ) self.add_widget(self._image) if self.autowidth: self.width = self.height if 'text' in show: self.height = self.default_height if self.height == 0 else self.height self.touch_trigger = True self._label = Label( # text text=self.text, halign='left', valign='middle', padding=(metrics.dp(10), 0), # size font_size=self.font_size, height=self.height, size_hint=(None, None), ) self._label.bind(texture_size=self.on_texture_size) self.add_widget(self._label) def on_size(self, wx, size): for child in self.children: child.height = self.height if isinstance(child, Image): child.width = self.height elif not self.autowidth: # must be the label self.on_texture_size(child, None) def on_texture_size(self, label, texture_size): if self.autowidth: # adapt the width to the label's width self.width = max(0, sum([child.width for child in self.children])) else: # adapt the label's width others = sum([child.width for child in self.children if child != label]) label.width = self.width - others label.height = self.height label.text_size = (self.width - others, self.height) def on_tooltip(self, callee, text): self.set_tooltip(text) ## properties @property def cfg(self): return self.root.session.cfg ## action triggering @classmethod def single_shot(cls, root, *args, **kwargs): #logger.info(f'action: {cls.__name__}, {args}, {kwargs}') root.action_log.append(str(cls.__name__)) return cls(root=root).apply(*args, **kwargs) def on_root(self, wx, root): root.keys.bind(on_press=self.on_keyboard) def on_press(self): self.selected_alpha = 1 def on_release(self): if self.touch_trigger: Animation(selected_alpha=0, d=.25, t='out_quad').start(self) #logger.info(f'action: {type(self).__name__}') self.root.action_log.append(str(type(self).__name__)) self.apply() def on_keyboard(self, wx, evt): if self.key_trigger and self.ktrigger(evt): #logger.info(f'action: {type(self).__name__}') self.root.action_log.append(str(type(self).__name__)) self.apply() # stop the event from further processing return True ## interfaces for subtypes def ktrigger(self, evt): """Return True if the action should be triggered by keyboard event *evt*.""" return False def apply(self, *args, **kwargs): """Execute the action.""" pass ## EOF ##