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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
|
"""
Part of the tagit module.
A copy of the license is provided with the project.
Author: Matthias Baumgartner, 2022
"""
# standard imports
from collections import defaultdict
from functools import reduce, partial
import io
import logging
import math
import operator
import os
import typing
# kivy imports
from kivy.clock import Clock
from kivy.core.image.img_pil import ImageLoaderPIL
from kivy.lang import Builder
from kivy.resources import resource_find
from kivy.uix.gridlayout import GridLayout
from kivy.uix.image import AsyncImage
from kivy.uix.relativelayout import RelativeLayout
import kivy.properties as kp
# tagit imports
from tagit import config
from tagit.external.setproperty import SetProperty
from tagit.utils import Frame, Resolution, Struct, clamp, ns, ttime, rmatcher
from tagit.utils.bsfs import ast
# inner-module imports
from .loader import Loader
from .session import StorageAwareMixin, ConfigAwareMixin
# exports
__all__: typing.Sequence[str] = (
'Browser',
)
## code ##
logger = logging.getLogger(__name__)
# load kv
Builder.load_file(os.path.join(os.path.dirname(__file__), 'browser.kv'))
# classes
class ImageLoaderTagit(ImageLoaderPIL):
def load(self, filename):
data = super(ImageLoaderTagit, self).load(filename)
if len(data) > 1:
# source features multiple images
res = [(im.width, im.height) for im in data]
if len(set(res)) > 1:
# images have different resolutions; I'm guessing
# it's multiple previews embedded in the same image file.
# keep only the largest one.
idx = res.index(max(res, key=lambda wh: wh[0]*wh[1]))
data = [data[idx]]
return data
class ItemIndex(list):
"""A list with constant time in index and contains operations.
List items must be hashable. Assumes the list is to be immutable.
Trades space for time by constructing an index and set at creation time.
"""
def __init__(self, items):
super(ItemIndex, self).__init__(items)
self._item_set = set(items) # FIXME: mb/port: collect into a nodes instance?
self._index = {itm: idx for idx, itm in enumerate(items)}
def index(self, item):
return self._index[item]
def __contains__(self, value):
return value in self._item_set
def as_set(self):
return self._item_set
class Browser(GridLayout, StorageAwareMixin, ConfigAwareMixin):
"""The browser displays a grid of item previews."""
# root reference
root = kp.ObjectProperty(None)
# select modes
SELECT_SINGLE = 0
SELECT_MULTI = 1
SELECT_RANGE = 2
SELECT_ADDITIVE = 4
SELECT_SUBTRACTIVE = 8
# selection extras
range_base = []
range_origin = None
# mode
select_mode = kp.NumericProperty(SELECT_SINGLE)
# content
change_view = kp.BooleanProperty(False)
change_grid = kp.BooleanProperty(True)
items = kp.ObjectProperty(ItemIndex([]))
folds = kp.DictProperty()
# frame
offset = kp.NumericProperty(0)
cursor = kp.ObjectProperty(None, allownone=True)
selection = SetProperty()
# grid mode
GRIDMODE_GRID = 'grid'
GRIDMODE_LIST = 'list'
gridmode = kp.OptionProperty('grid', options=[GRIDMODE_GRID, GRIDMODE_LIST])
# grid size
cols = kp.NumericProperty(3)
rows = kp.NumericProperty(3)
# page_size is defined in kivy such that it updates automatically
# delayed view update event
_draw_view_evt = None
## initialization
def on_root(self, wx, root):
StorageAwareMixin.on_root(self, wx, root)
ConfigAwareMixin.on_root(self, wx, root)
def on_config_changed(self, session, key, value):
with self:
if key == ('ui', 'standalone', 'browser', 'cols'):
self.cols = max(1, value)
elif key == ('ui', 'standalone', 'browser', 'rows'):
self.rows = max(1, value)
elif key == ('ui', 'standalone', 'browser', 'gridmode'):
self.gridmode = value
elif key == ('ui', 'standalone', 'browser', 'fold_threshold'):
self.redraw() # FIXME: redraw doesn't exist
elif key == ('ui', 'standalone', 'browser', 'select_color'):
self.change_grid = True
def on_cfg(self, wx, cfg):
with self:
self.cols = max(1, cfg('ui', 'standalone', 'browser', 'cols'))
self.rows = max(1, cfg('ui', 'standalone', 'browser', 'rows'))
self.gridmode = cfg('ui', 'standalone', 'browser', 'gridmode')
def on_storage(self, wx, storage):
with self:
self.frame = Frame()
self.items = ItemIndex([])
## functions
def set_items(self, items):
"""Set the items. Should be used instead of setting items directly
to get the correct folding behaviour.
"""
items, folds = self.fold(items)
self.folds = folds
self.items = ItemIndex(items)
self.change_view = True
def fold(self, items):
"""Replace items in *items* if they are grouped.
Return the new item list and the dict of representatives.
"""
# get groups and their shadow (group's members in items)
groups = defaultdict(set)
all_items = reduce(operator.add, items, self.root.session.storage.empty(ns.bsn.Entity))
for obj, grp in all_items.group(node=True, view=list):
groups[grp].add(obj)
# don't fold groups if few members
fold_threshold = self.root.session.cfg('ui', 'standalone', 'browser', 'fold_threshold')
groups = {grp: objs for grp, objs in groups.items() if len(objs) > fold_threshold}
# don't fold groups that make up all items
groups = {grp: objs for grp, objs in groups.items() if len(objs) < len(items)}
def superset_exists(grp):
"""Helper fu to detect subsets."""
for objs in groups.values():
if objs != groups[grp] and groups[grp].issubset(objs):
return True
return False
# create folds
folds = {
grp.represented_by(): Struct(
group=grp,
shadow=objs,
)
for grp, objs in groups.items()
if not superset_exists(grp)
}
# add representatives
for rep in folds:
# add representative in place of the first of its members
idx = min([items.index(obj) for obj in folds[rep].shadow])
items.insert(idx, rep)
# remove folded items
for obj in {obj for fold in folds.values() for obj in fold.shadow}:
items.remove(obj)
return items, folds
def unfold(self, items):
"""Replace group representatives by their group members."""
# fetch each item or their shadow if applicable
unfolded = set()
for itm in items:
if itm in self.folds:
unfolded |= self.folds[itm].shadow
else:
unfolded |= {itm}
return reduce(operator.add, unfolded, self.root.session.storage.empty(ns.bsn.Entity))
def neighboring_unselected(self):
"""Return the item closest to the cursor and not being selected. May return None."""
if self.cursor in self.selection:
# set cursor to nearest neighbor
cur_idx = self.items.index(self.cursor)
sel_idx = {self.items.index(obj) for obj in self.selection}
# find available items
n_right = {clamp(idx + 1, self.n_items - 1) for idx in sel_idx}
n_left = {clamp(idx - 1, self.n_items - 1) for idx in sel_idx}
cand = sorted((n_left | n_right) - sel_idx)
# find closest to cursor
c_dist = [abs(idx - cur_idx) for idx in cand]
if len(c_dist) == 0:
return None
else:
# set cursor to item at candidate with minimum distance to cursor
return self.items[cand[c_dist.index(min(c_dist))]]
else:
# cursor isn't selected
return self.cursor
## properties
@property
def frame(self):
return Frame(self.cursor, self.selection, self.offset)
@frame.setter
def frame(self, frame):
self.offset = frame.offset
self.cursor = frame.cursor
self.selection = frame.selection
@property
def n_items(self):
return len(self.items)
@property
def max_offset(self):
return max(0,
self.n_items + (self.cols - (self.n_items % self.cols)) % self.cols - self.page_size)
## property listeners
def on_cols(self, sender, cols):
#self.page_size = self.cols * self.rows
self.change_grid = True
def on_rows(self, sender, rows):
#self.page_size = self.cols * self.rows
self.change_grid = True
def on_offset(self, sender, offset):
self.change_view = True
def on_cursor(self, sender, cursor):
if cursor is not None:
self.root.status.dispatch('on_status', cursor.filename(default=''))
def on_items(self, sender, items):
self.change_view = True
# items might have changed; start caching
#if self.root.session.cfg('ui', 'standalone', 'browser', 'cache_all'):
# self._preload_all()
def on_gridmode(self, sender, mode):
self.change_grid = True
# resolution might have changed; start caching
#if self.root.session.cfg('ui', 'standalone', 'browser', 'cache_all'):
# self._preload_all()
## context
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
# ensure valid values for cursor, selection, and offset
# necessary if old frames were loaded while search filters have changed
if self.root.session.cfg('session', 'verbose') > 0:
# warn about changes
if self.cursor is not None and self.cursor not in self.items:
logger.warn(f'Fixing: cursor ({self.cursor})')
if not self.selection.issubset(self.items.as_set()):
logger.warn('Fixing: selection')
if self.offset > self.max_offset or self.offset < 0:
logger.warn(f'Fixing: offset ({self.offset} not in [0, {self.max_offset}])')
self.cursor = self.cursor if self.cursor in self.items else None
self.selection = self.items.as_set() & self.selection
self.offset = clamp(self.offset, self.max_offset)
# issue redraw
if self.change_grid:
# grid change requires view change
self.draw_grid()
self.draw_view()
elif self.change_view:
timeout = self.root.session.cfg('ui', 'standalone', 'browser', 'page_delay') / 1000
if timeout > 0:
self._draw_view_evt = Clock.schedule_once(lambda dt: self.draw_view(), timeout)
else:
self.draw_view()
# reset flags
self.change_grid = False
self.change_view = False
def draw_grid(self):
if self.gridmode == self.GRIDMODE_LIST:
factory = BrowserDescription
elif self.gridmode == self.GRIDMODE_GRID:
factory = BrowserImage
else:
raise UserError(f'gridmode has to be {self.GRIDMODE_GRID} or {self.GRIDMODE_LIST}')
self.clear_widgets()
for itm in range(self.page_size):
wx = factory(browser=self)
self.bind(selection=wx.on_selection)
self.bind(cursor=wx.on_cursor)
self.add_widget(wx)
def _cell_resolution(self):
return Resolution(self.width/self.cols, self.height/self.rows)
def on_change_view(self, wx, change_view):
# the view will be updated, hence preloading should be interrupted
# if it were active. That's done here since to capture the earliest
# time where a view change becomes apparent.
if change_view and self._draw_view_evt is not None:
self._draw_view_evt.cancel()
self._draw_view_evt = None
def draw_view(self):
self._draw_view_evt = None
# revoke images that are still wait to being loaded
Loader.clear()
#if not self.root.session.cfg('ui', 'standalone', 'browser', 'cache_all'):
# Loader.clear()
# fetch items
items = self.items[self.offset:self.offset+self.page_size]
childs = iter(self.children) # reversed since child widgets are in reverse order
# preload neighbouring pages
n_pages = self.root.session.cfg('ui', 'standalone', 'browser', 'cache_items')
n_pages = math.ceil(n_pages / self.page_size)
if n_pages > 0:
lo = clamp(self.offset - n_pages * self.page_size, self.n_items)
cu = clamp(self.offset + self.page_size, self.n_items)
hi = clamp(self.offset + (n_pages + 1) * self.page_size, self.n_items)
# load previous page
# previous before next such that scrolling downwards is prioritised
self._preload_items(self.items[lo:self.offset])
# load next page
# reversed such that the loader prioritises earlier previews
self._preload_items(reversed(self.items[cu:hi]))
# clear unused cells
for _ in range(self.page_size - len(items)):
next(childs).clear()
if len(items) == 0: # FIXME: mb/port
return
resolution = self._cell_resolution()
previews = self._fetch_previews(items, resolution)
default = resource_find('no_preview.png')
for ent, child in zip(reversed(items), childs):
if ent in previews:
buf = previews[ent]
else:
buf = open(default, 'rb')
child.update(ent, buf, f'{ent}x{resolution}')
def _fetch_previews(self, items, resolution):
"""Fetch previews matching *resolution* for *items*.
Return a dict with items as key and a BytesIO as value.
Items without valid asset are omitted from the dict.
"""
# fetch previews
node_preview = reduce(operator.add, items).get(ns.bse.preview, node=True)
previews = {p for previews in node_preview.values() for p in previews}
previews = reduce(operator.add, previews) # FIXME: empty previews
# fetch preview resolutions
res_preview = previews.get(ns.bsp.width, ns.bsp.height, node=True)
# select a preview for each item
chosen = {}
for ent in items:
try:
# get previews and their resolution for this ent
options = []
for preview in node_preview[ent]:
# unpack resolution
res = res_preview[preview]
width = res.get(ns.bsp.width, 0)
height = res.get(ns.bsp.height, 0)
options.append((preview, Resolution(width, height)))
# select the best fitting preview
chosen[ent] = rmatcher.by_area_min(resolution, options)
except (KeyError, IndexError):
# skip objects w/o preview (KeyError in node_preview)
# skip objects w/o valid preview (IndexError in rmatcher)
pass
# fetch assets
assets = reduce(operator.add, chosen.values()).asset(node=True) # FIXME: empty chosen
# build ent -> asset mapping and convert raw data to io buffer
return {
ent: io.BytesIO(assets[thumb])
for ent, thumb
in chosen.items()
if thumb in assets
}
#def _preload_all(self):
# # prefer loading from start to end
# self._preload_items(reversed(self.items))
def _preload_items(self, items, resolution=None):
"""Load an item into the kivy *Cache* without displaying the image anywhere."""
def _buf_loader(buffer, fname):
# helper method to load the image from a raw buffer
with buffer as buf:
return ImageLoaderTagit(filename=fname, inline=True, rawdata=buf)
resolution = resolution if resolution is not None else self._cell_resolution()
try:
foo = self._fetch_previews(items, resolution) # FIXME: _fetch_previews fails on empty previews/chosen
except TypeError:
return
for obj, buffer in foo.items():
guid = ','.join(obj.guids)
source = f'{guid}x{resolution}'
Loader.image(source,
nocache=False, mipmap=False,
anim_delay=0,
load_callback=partial(_buf_loader, buffer) # mb: pass load_callback
)
class BrowserAwareMixin(object):
"""Widget that binds to the browser."""
browser = None
def on_root(self, wx, root):
root.bind(browser=self.on_browser)
if root.browser is not None:
# initialize with the current browser
# Going through the event dispatcher ensures that the object
# is initialized properly before on_browser is called.
Clock.schedule_once(lambda dt: self.on_browser(root, root.browser))
def on_browser(self, sender, browser):
pass
class BrowserItem(RelativeLayout):
"""Just some space for an object."""
browser = kp.ObjectProperty()
obj = kp.ObjectProperty(allownone=True)
is_cursor = kp.BooleanProperty(False)
is_selected = kp.BooleanProperty(False)
is_group = kp.BooleanProperty(False)
def update(self, obj):
self.obj = obj
def clear(self):
self.obj = None
def on_obj(self, wx, obj):
self.on_cursor(self.browser, self.browser.cursor)
self.on_selection(self.browser, self.browser.selection)
self.is_group = obj in self.browser.folds if obj is not None else False
def on_cursor(self, browser, cursor):
self.is_cursor = (cursor == self.obj) if self.obj is not None else False
def on_selection(self, browser, selection):
self.is_selected = self.obj in selection if self.obj is not None else False
def on_touch_down(self, touch):
"""Click on item."""
if self.obj is not None and self.collide_point(*touch.pos):
if touch.button == 'left':
# shift counts as double tap
if touch.is_double_tap and not self.browser.root.keys.shift_pressed:
# open
logger.debug('Item: Double touch in {}'.format(str(self.obj)))
if not self.is_selected:
self.browser.root.trigger('Select', self.obj)
self.browser.root.trigger('OpenExternal')
else:
# set cursor
logger.debug('Item: Touchdown in {}'.format(str(self.obj)))
self.browser.root.trigger('SetCursor', self.obj)
# must call the parent's method to ensure OpenGroup gets a chance to handle
# the mouse event. Also, this must happen *after* processing the event here
# so that the cursor is set correctly.
return super(BrowserItem, self).on_touch_down(touch)
def on_touch_move(self, touch):
"""Move over item."""
if self.obj is not None and self.collide_point(*touch.pos):
if touch.button == 'left':
if not self.collide_point(*touch.ppos):
self.browser.root.trigger('Select', self.obj)
return super(BrowserItem, self).on_touch_move(touch)
class BrowserImage(BrowserItem):
def update(self, obj, buffer, source):
super(BrowserImage, self).update(obj)
self.preview.load_image(buffer, source, 1)
#self.preview.load_image(buffer, source, obj.orientation(default=1)) # FIXME: mb/port
self.preview.set_size(self.size)
def clear(self):
super(BrowserImage, self).clear()
self.preview.clear_image()
def on_size(self, wx, size):
self.preview.set_size(self.size)
class BrowserDescription(BrowserItem):
text = kp.StringProperty()
def update(self, obj, buffer, source):
super(BrowserDescription, self).update(obj)
self.preview.load_image(buffer, source, 1)
#self.preview.load_image(buffer, source, obj.orientation(default=1)) # FIXME: mb/port
self.preview.set_size((self.height, self.height))
def clear(self):
super(BrowserDescription, self).clear()
self.preview.clear_image()
def on_size(self, wx, size):
self.preview.set_size((self.height, self.height))
def on_obj(self, wx, obj):
super(BrowserDescription, self).on_obj(wx, obj)
if self.is_group:
# get group and its members
grp = self.browser.folds[self.obj].group
# FIXME: Here we could actually use a predicate reversal for Nodes.get
# members = grp.get(ast.fetch.Node(ast.fetch.Predicate(ns.bse.group, reverse=True)))
members = self.browser.root.session.storage.get(ns.bsn.Entity,
ast.filter.Any(ns.bse.group, ast.filter.Is(grp)))
# get group member's tags
member_tags = members.tag.label(node=True)
tags_all = set.intersection(*member_tags.values())
tags_any = {tag for tags in member_tags.values() for tag in tags}
# get remaining info from representative
preds = self.obj.get(
ns.bse.mime,
ns.bsm.t_created,
)
self.text = '{name} [size=20sp]x{count}[/size], {mime}, {time}\n[color=8f8f8f][b]{tags_all}[/b], [size=14sp]{tags_any}[/size][/color]'.format(
name=os.path.basename(next(iter(grp.guids))),
count=len(members),
mime=preds.get(ns.bse.mime, ''),
time=ttime.from_timestamp_loc(
preds.get(ns.bsm.t_created, ttime.timestamp_min)).strftime('%Y-%m-%d %H:%M'),
tags_all=', '.join(sorted(tags_all)),
tags_any=', '.join(sorted(tags_any - tags_all)),
)
elif self.obj is not None:
preds = self.obj.get(
ns.bse.filename,
ns.bse.filesize,
ns.bse.mime,
ns.bsm.t_created,
(ns.bse.tag, ns.bst.label),
)
self.text = '[size=20sp]{filename}[/size], [size=18sp][i]{mime}[/i][/size] -- {time} -- {filesize}\n[color=8f8f8f][size=14sp]{tags}[/size][/color]'.format(
filename=preds.get(ns.bse.filename, 'n/a'),
mime=preds.get(ns.bse.mime, ''),
time=ttime.from_timestamp_loc(
preds.get(ns.bsm.t_created, ttime.timestamp_min)).strftime('%Y-%m-%d %H:%M'),
filesize=preds.get(ns.bse.filesize, 0),
tags=', '.join(sorted(preds.get((ns.bse.tag, ns.bst.label), []))),
)
else:
self.text = ''
class AsyncBufferImage(AsyncImage):
"""Replacement for kivy.uix.image.AsyncImage that allows to pass a *load_callback*
method. The load_callback (fu(filename) -> ImageLoaderTagit) can be used to read a file
from something else than a path. However, note that if caching is desired, a filename
(i.e. source) should still be given.
"""
orientation = kp.NumericProperty(1)
buffer = kp.ObjectProperty(None, allownone=True)
mirror = kp.BooleanProperty(False)
angle = kp.NumericProperty(0)
def load_image(self, buffer, source, orientation):
self.orientation = orientation
self.buffer = buffer
# triggers actual loading
self.source = source
# make visible
self.opacity = 1
def clear_image(self):
# make invisible
self.opacity = 0
def set_size(self, size):
width, height = size
# swap dimensions if the image is rotated
self.size = (height, width) if self.orientation in (5,6,7,8) else (width, height)
# ensure the correct positioning via the center
self.center = width / 2.0, height / 2.0
# note that the widget's bounding box will be overlapping with other grid
# cells, however the content will be confined in the correct grid box.
def on_orientation(self, wx, orientation):
if orientation in (2, 4, 5, 7): # Mirror
self.mirror = True
if orientation in (3, 4): # Rotate 180deg
self.angle = 180
elif orientation in (5, 6): # Rotate clockwise, 90 deg
self.angle = -90
elif orientation in (7, 8): # Rotate counter-clockwise, 90 deg
self.angle = 90
else:
self.angle = 0
self.mirror = False
@staticmethod
def loader(buffer, fname):
# helper method to load the image from a raw buffer
with buffer as buf:
return ImageLoaderTagit(filename=fname, inline=True, rawdata=buf)
def _load_source(self, *args):
# overwrites method from parent class
source = self.source
if not source:
if self._coreimage is not None:
self._coreimage.unbind(on_texture=self._on_tex_change)
self._coreimage.unbind(on_load=self.post_source_load)
self.texture = None
self._coreimage = None
else:
if self._coreimage is not None:
# unbind old image
self._coreimage.unbind(on_load=self._on_source_load)
self._coreimage.unbind(on_error=self._on_source_error)
self._coreimage.unbind(on_texture=self._on_tex_change)
del self._coreimage
self._coreimage = None
self._coreimage = image = Loader.image(self.source,
nocache=self.nocache, mipmap=self.mipmap,
anim_delay=self.anim_delay,
load_callback=partial(self.loader, self.buffer), # mb: pass load_callback
)
# bind new image
image.bind(on_load=self._on_source_load)
image.bind(on_error=self._on_source_error)
image.bind(on_texture=self._on_tex_change)
self.texture = image.texture
## config ##
config.declare(('ui', 'standalone', 'browser', 'cols'), config.Unsigned(), 3,
__name__, 'Browser columns', 'Default number of columns in the browser. Is at least one. Note that an odd number of columns and rows gives a more intuitive behaviour when zooming. A large value might make the program respond slowly.')
config.declare(('ui', 'standalone', 'browser', 'rows'), config.Unsigned(), 3,
__name__, 'Browser rows', 'Default number of rows in the grid view. Is at least one. Note that an odd number of columns and rows gives a more intuitive behaviour when zooming. A large value might make the program respond slowly.')
config.declare(('ui', 'standalone', 'browser', 'fold_threshold'), config.Unsigned(), 1,
__name__, 'Folding', "Define at which threshold groups will be folded. The default (1) folds every group unless it consists of only a single item (which isn't really a group anyhow).")
config.declare(('ui', 'standalone', 'browser', 'gridmode'),
config.Enum(Browser.GRIDMODE_GRID, Browser.GRIDMODE_LIST), Browser.GRIDMODE_GRID,
__name__, 'Display style', 'The grid mode shows only the preview image of each item. The list mode shows the preview and some additional information of each item. Note that rows and cols can be specified for both options. It is recommended that they are set to the same value in grid mode, and to a single column in list mode.')
config.declare(('ui', 'standalone', 'browser', 'cache_items'), config.Unsigned(), 20,
__name__, 'Page pre-loading', 'Number of items that are loaded into the cache before they are actually shown. The effective number of loaded items the specified value rounded up to the page size times two (since it affects pages before and after the current one). E.g. a value of one loads the page before and after the current one irrespective of the page size. If zero, preemptive caching is disabled.')
config.declare(('ui', 'standalone', 'browser', 'page_delay'), config.Unsigned(), 50,
__name__, 'Page setup delay', 'Quickly scrolling through pages incurs an overhead due to loading images that will be discarded shortly afterwards. This overhead can be reduced by delaying the browser page setup for a short amount of time. If small enough the delay will not be noticable. Specify in milliseconds. Set to zero to disable the delay completely.')
# FIXME: Also add select_alpha or maybe even select_style (left/right/over/under bar; overlay; recolor; others?)
# FIXME: Also add cursor style config (left/right/under/over bar; borders; others?)
config.declare(('ui', 'standalone', 'browser', 'select_color'),
config.List(config.Unsigned()), [0,0,1],
__name__, '', '') # FIXME
#config.declare(('ui', 'standalone', 'browser', 'cache_all'), config.Bool(), False,
# __name__, 'Cache everything', 'Cache all preview images in the background. The cache size (`ui.standalone.browser.cache_size`) should be large enough to hold the library at least once (some reserve for different resolutions is advised). Can incur a small delay when opening the library. May consume a lot of memory.')
## EOF ##
|