+class GalleryItem(GObject.GObject):
+ """Gallery representation of filesystem entry, base to DirItem, ImgItem."""
+ _to_hash = ['name', 'full_path']
+
+ def __init__(self, path: str, name: str) -> None:
+ super().__init__()
+ self.name = name
+ self.full_path = path_join(path, self.name)
+ self.slot: GallerySlot
+
+ def __hash__(self) -> int:
+ hashable_values = []
+ for attr_name in self._to_hash:
+ hashable_values += [getattr(self, attr_name)]
+ return hash(tuple(hashable_values))
+
+
+class DirItem(GalleryItem):
+ """Gallery representation of filesystem entry for directory."""
+
+ def __init__(self, path: str, name: str, is_parent: bool = False) -> None:
+ super().__init__(path, name)
+ if is_parent:
+ self.full_path = path
+
+
+class ImgItem(GalleryItem):
+ """Gallery representation of filesystem entry for image file."""
+ _to_hash = (['name', 'full_path', 'last_mod_time', 'bookmarked',
+ 'with_others']
+ + [k.lower() for k in GEN_PARAMS])
+
+ def __init__(self, path: str, name: str, cache: Cache) -> None:
+ super().__init__(path, name)
+ mtime = getmtime(self.full_path)
+ dt = datetime.fromtimestamp(mtime, tz=timezone.utc)
+ iso8601_str = dt.isoformat(timespec='microseconds')
+ self.last_mod_time = iso8601_str.replace('+00:00', 'Z')
+ self.bookmarked = False
+ self.with_others = False
+ self.has_metadata = False
+ for param_name in GEN_PARAMS:
+ if param_name in GEN_PARAMS_STR:
+ setattr(self, param_name.lower(), '')
+ else:
+ setattr(self, param_name.lower(), 0)
+ if self.full_path in cache:
+ if self.last_mod_time in cache[self.full_path]:
+ self.has_metadata = True
+ cached = cache[self.full_path][self.last_mod_time]
+ for k in cached.keys():
+ setattr(self, k, cached[k])
+
+ def set_metadata(self, cache: Cache) -> None:
+ """Set attrs from file's GenParams PNG chunk, update into cache."""
+ img = Image.open(self.full_path)
+ if isinstance(img, PngImageFile):
+ gen_params_as_str = img.text.get('generation_parameters', '')
+ if gen_params_as_str:
+ gen_params = GenParams.from_str(gen_params_as_str)
+ for k, v_ in gen_params.as_dict.items():
+ setattr(self, k, v_)
+ cached = {}
+ for k in (k.lower() for k in GEN_PARAMS):
+ cached[k] = getattr(self, k)
+ cache[self.full_path] = {self.last_mod_time: cached}
+
+ def bookmark(self, positive: bool = True) -> None:
+ """Set self.bookmark to positive, and update CSS class mark."""
+ self.bookmarked = positive
+ self.slot.mark('bookmarked', positive)
+
+
+class GallerySlotsGeometry:
+ """Collect variable sizes shared among all GallerySlots."""
+
+ def __init__(self) -> None:
+ self._margin = GALLERY_SLOT_MARGIN
+ assert 0 == self._margin % 2 # avoid ._margin != 2 * .side_margin
+ self.side_margin = self._margin // 2
+ self.size, self.size_sans_margins = -1, -1
+
+ def set_size(self, size: int) -> None:
+ """Not only set .size but also update .size_sans_margins."""
+ self.size = size
+ self.size_sans_margins = self.size - self._margin
+
+
+class GallerySlot(Gtk.Button):
+ """Slot in Gallery representing a GalleryItem."""
+
+ def __init__(self,
+ item: GalleryItem,
+ slots_geometry: GallerySlotsGeometry,
+ on_click_file: Optional[Callable] = None
+ ) -> None:
+ super().__init__()
+ self._geometry = slots_geometry
+ self.add_css_class('slot')
+ self.set_hexpand(True)
+ self.item = item
+ self.item.slot = self
+ if on_click_file:
+ self.connect('clicked', lambda _: on_click_file())
+
+ def mark(self, css_class: str, do_add: bool = True) -> None:
+ """Add or remove css_class from self."""
+ if do_add:
+ self.add_css_class(css_class)
+ else:
+ self.remove_css_class(css_class)
+
+ def ensure_slot_size(self) -> None:
+ """Call ._size_widget to size .props.child; if none, make empty one."""
+ if self.get_child() is None:
+ self.set_child(Gtk.Label(label='+'))
+ self._size_widget()
+
+ def _size_widget(self) -> None:
+ for s in ('bottom', 'top', 'start', 'end'):
+ setattr(self.get_child().props, f'margin_{s}',
+ self._geometry.side_margin)
+ self.get_child().set_size_request(self._geometry.size_sans_margins,
+ self._geometry.size_sans_margins)
+
+ def update_widget(self, is_in_vp: bool) -> None:
+ """(Un-)load slot, for Imgs if (not) is_in_vp, update CSS class."""
+ new_content = None
+ if isinstance(self.item, ImgItem):
+ if is_in_vp and not isinstance(self.item, Gtk.Image):
+ new_content = Gtk.Image.new_from_file(self.item.full_path)
+ if self.item.with_others:
+ new_content.set_vexpand(True)
+ box = Gtk.Box(orientation=OR_V)
+ box.append(new_content)
+ msg = 'and one or more other images of this configuration'
+ box.append(Gtk.Label(label=msg))
+ new_content = box
+ elif (not is_in_vp) and not isinstance(self.item, Gtk.Label):
+ new_content = Gtk.Label(label='?')
+ elif (isinstance(self.item, DirItem)
+ and self.get_child().props.label == '+'):
+ new_content = Gtk.Label(label=self.item.name)
+ if new_content:
+ self.set_child(new_content)
+ self._size_widget()
+ if isinstance(self.item, ImgItem):
+ self.mark('bookmarked', self.item.bookmarked)
+
+