aboutsummaryrefslogtreecommitdiffstats
path: root/tagit/widgets/dock.py
blob: 41ff642a86ff3f6e8ef14cc5dd0ac588bf788866 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
"""

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.lang import Builder
from kivy.uix.gridlayout import GridLayout
from kivy.uix.stacklayout import StackLayout
from kivy.uix.widget import Widget
import kivy.properties as kp

# tagit imports
from tagit import config
from tagit.actions import ActionBuilder
from tagit.tiles import TileBuilder
from tagit.utils import errors
from tagit.utils.builder import InvalidFactoryName

# inner-module imports
from .session import ConfigAwareMixin

# exports
__all__ = ('Dock', )


## code ##

logger = logging.getLogger(__name__)

# load kv
Builder.load_file(os.path.join(os.path.dirname(__file__), 'dock.kv'))

# classes
class DockBase(Widget, ConfigAwareMixin):
    """A Dock is a container that holds configurable items."""
    # root reference
    root = kp.ObjectProperty(None)

    def on_cfg(self, wx, cfg):
        """Construct the dock from config."""
        errors.abstract()

    def populate(self, config):
        """Fill the dock with content."""
        errors.abstract()


class TileDock(GridLayout, DockBase):
    """A TileDock holds a number of Tiles."""

    # dock's name for loading from config
    name = kp.StringProperty('')
    # tile decoration
    decoration = kp.ObjectProperty(None)
    # tile visiblity
    visible = kp.BooleanProperty(False)

    def on_config_changed(self, session, key, value):
        if key == ('ui', 'standalone', 'tiledocks'):
            self.on_cfg(session, session.cfg)

    def on_cfg(self, wx, cfg):
        """Construct the Tiles from the config item matching dock's name."""
        if self.name != '':
            self.populate(cfg('ui', 'standalone', 'tiledocks').get(self.name, {}))
            # FIXME: Since dictionaries are not ordered, the tiles might change
            # their position at every application start. Switching to a list would
            # solve this issue. E.g. [{tile: 'tile name', **kwargs}]

    def populate(self, tiles):
        """Construct the Tiles."""
        # clear old items
        self.clear_widgets()

        # add new items
        n_tiles_max = self.cols * self.rows
        builder = TileBuilder()
        for idx, tid in enumerate(sorted(tiles)):
            if idx >= n_tiles_max:
                logger.warn(f'number of tiles exceeds space ({len(tiles)} > {n_tiles_max})')
                break

            try:
                kwargs = tiles[tid]
                tile = builder.build(tid, root=self.root, **kwargs)
                self.add_widget(self.decoration(client=tile))
            except InvalidFactoryName:
                logger.error(f'invalid tile name: {tid}')

        # create and attach widgets before setting visibility
        # to ensure that the widget initialization has finished.
        self.on_visible(self, self.visible)

    def on_size(self, *args):
        # FIXME: If dashboard is loaded, resizing the window becomes painfully slow.
        # Something to do with the code here, e.g. delayed sizing?
        for child in self.children:
            # TODO: Allow default_size or tile_size to specify relative sizes (<1)
            # determine size
            width = self.tile_width
            width = child.default_size[0] if width is None else width
            #width = self.width if width is None and self.size_hint_x is None else width
            height = self.tile_height
            height = child.default_size[1] if height is None else height
            #height = self.height if height is None and self.size_hint_y is None else height
            size = width if width is not None else 1, height if height is not None else 1
            size_hint = None if width is not None else 1, None if height is not None else 1
            # set size; will be propagated from the decorator to the client
            child.size = size
            child.size_hint = size_hint

    def on_visible(self, wx, visible):
        """Propagate visibility update to Tiles."""
        for child in self.children:
            child.client.visible = visible

    # FIXME: move events in the browser are only triggered if the move event is also
    # handled here with an empty body (no super!).
    # No idea why this happens (e.g. doing it in desktop or tab doesn't work).
    def on_touch_move(self, touch):
        pass


class ButtonDock(StackLayout, DockBase):
    """A ButtonDock holds a number of Actions."""

    # dock's name for loading from config
    name = kp.StringProperty('')

    def on_config_changed(self, session, key, value):
        if key == ('ui', 'standalone', 'buttondocks'):
            self.on_cfg(session, session.cfg)

    def on_cfg(self, wx, cfg):
        """Construct the Actions from config item matching the dock's name."""
        if self.name != '':
            # name is empty if created via the Buttons tile
            self.populate(cfg('ui', 'standalone', 'buttondocks').get(self.name, []))

    def populate(self, actions):
        """Construct the Actions."""
        # clear old items
        self.clear_widgets()

        # add new items
        n_buttons_max = float('inf') if self.n_buttons_max is None else self.n_buttons_max
        builder = ActionBuilder()
        for idx, action in enumerate(actions):
            if idx >= n_buttons_max:
                logger.warn(f'number of buttons exceeds space ({len(actions)} > {n_buttons_max})')
                break

            try:
                self.add_widget(builder.build(action,
                    root=self.root,
                    size=(self.button_width, self.button_height),
                    show=self.button_show,
                    autowidth=False,
                    ))
            except InvalidFactoryName:
                logger.error(f'invalid button name: {action}')


class KeybindDock(DockBase):
    """The KeybindDock holds a number of invisible Actions that can be triggered by key presses."""

    def on_config_changed(self, session, key, value):
        if key == ('ui', 'standalone', 'keytriggers'):
            self.on_cfg(session, session.cfg)

    def on_cfg(self, wx, cfg):
        """Construct the Actions from config."""
        self.populate(cfg('ui', 'standalone', 'keytriggers'))

    def populate(self, actions):
        """Construct the Actions."""
        # clear old items
        self.clear_widgets()

        # add new items
        builder = ActionBuilder()
        for action in actions:
            try:
                self.add_widget(builder.build(
                    action,
                    root=self.root,
                    # process key events only
                    touch_trigger=False,
                    key_trigger=True,
                    # no need to specify show (default is empty)
                    ))

            except InvalidFactoryName:
                logger.error(f'invalid button name: {action}')


## config ##

config.declare(('ui', 'standalone', 'keytriggers'),
    config.List(config.Enum(set(ActionBuilder.keys()))), [],
    __name__, 'Key triggers',
    'Actions that can be triggered by a key but have no visible button', '')

config.declare(('ui', 'standalone', 'tiledocks'),
    config.Dict(config.String(), config.Dict(config.String(), config.Dict(config.String(), config.Any()))), {},
    __name__, 'Tile docks', '''Tiles can be placed in several locations of the UI. A tile usually displays some information about the current program state, such as information about the library in general, visible or selected items, etc.

The configuration of a tile consists the its name as string and additional parameters to that tile as a dict. A tile dock is configured by a dictionary with the tile names as key and their parameters as value:

{
    "Hints": {},
    "ButtonDock": {"buttons: ["Save", "SaveAs", "Index"]}
}

The order of the items in the UI is generally the same as in the config dict.

To show a list of available tiles, execute:

$ tagger info tile

''')

config.declare(('ui', 'standalone', 'buttondocks'),
    config.Dict(config.String(), config.List(config.Enum(set(ActionBuilder.keys())))), {},
    __name__, 'Buttons', '''Every possible action in the UI is triggered via a button. Hence, buttons are found in various places in the UI, organized in button docks. Each dock is identified by name and lists the names of the buttons it contains.

To show a list of available buttons, execute:

$ tagger info action

''')

## EOF ##