From: mar77i Date: Wed, 2 Jul 2025 15:03:30 +0000 (+0200) Subject: long-time update: restructuring color_menu, not reparent TabBar and better abstract... X-Git-Url: https://git.mar77i.info/?a=commitdiff_plain;h=7f2c69bce4b9d5c2f0d11c0c58d917e09574ee88;p=zenbook_gui long-time update: restructuring color_menu, not reparent TabBar and better abstract spinner value --- diff --git a/bookpaint/book_manager.py b/bookpaint/book_manager.py index dbe0ec3..6f7e275 100644 --- a/bookpaint/book_manager.py +++ b/bookpaint/book_manager.py @@ -46,7 +46,7 @@ class BookManager: self.current_file = self.get_new_file() self.pages.append(self.current_file) self.free_page_no += 1 - pygame.image.save(self.parent.draw_image.surface, self.current_file) + pygame.image.save(self.parent.draw_image.image, self.current_file) class Nav(Enum): FIRST = auto() diff --git a/bookpaint/bookpaint.py b/bookpaint/bookpaint.py index 002531d..1912efd 100644 --- a/bookpaint/bookpaint.py +++ b/bookpaint/bookpaint.py @@ -31,7 +31,7 @@ class BookPaint(Root): self.draw_image = DrawImage(self, pygame.Color("white")) self.color_menu = ColorMenu(self) self.line_menu = LineMenu(self) - self.book_manager = BookManager(self, Path("book")) + self.book_manager = BookManager(self, Path(__file__).parents[1] / "book") self.page_menu = PageMenu(self) self.menu = Menu(self) self.page_menu.update_label() diff --git a/bookpaint/color_menu.py b/bookpaint/color_menu.py index 992ce63..b585f6c 100644 --- a/bookpaint/color_menu.py +++ b/bookpaint/color_menu.py @@ -1,9 +1,10 @@ from colorsys import hsv_to_rgb, rgb_to_hsv +from math import atan2, cos, pi, sin, sqrt, tau from functools import partial import pygame -from ui import Child, Label, Slider, Spinner, TextInput +from ui import Child, Label, Slider, Spinner, TabBar, TextInput from .base_menu import BaseMenu @@ -18,7 +19,243 @@ class ColorLabel(Child): pygame.draw.rect(self.surf, self.color, self.rect) +def draw_cursor(surf, color, pos): + r = 16 + pygame.draw.polygon( + surf, + color, + ( + (pos[0], pos[1] - r), + (pos[0] + r, pos[1]), + (pos[0], pos[1] + r), + (pos[0] - r, pos[1]), + ) + ) + + +class ColorCircle(Child): + def render_hue_circle(self): + surf = pygame.Surface(self.rect.size, 0, 24) + center = tuple(a // 2 for a in surf.get_size()) + rs = self.radii_squared + with pygame.PixelArray(surf) as pa: + for x, col in enumerate(pa): + for y in range(len(col)): + rel = (x - center[0], y - center[1]) + if rs[0] <= rel[0] ** 2 + rel[1] ** 2 <= rs[1]: + angle = atan2(-rel[1], rel[0]) + if angle < 0: + angle += tau + pa[x][y] = ColorMenu.hsv_to_color( + (int(angle * 255 / tau), 255, 255) + ) + return surf + + def get_overlay_surfs(self): + size = self.hue_surf.get_size() + overlay_surfs = ( + pygame.Surface(size, pygame.SRCALPHA, 32), + pygame.Surface(size, pygame.SRCALPHA, 32), + ) + for i, surf in enumerate(overlay_surfs): + with pygame.PixelArray(surf) as pa: + for x, col in enumerate(pa): + for y in range(len(col)): + pa[x][y] = pygame.Color( + y // 2, y // 2, y // 2, 255 - (y, x)[i] * 255 // max(size), + ) + return overlay_surfs + + def __init__(self, parent, rect): + super().__init__(parent) + self.rect = rect + self.radii = (378, min(rect.size) // 2) + self.radii_squared = tuple(r ** 2 for r in self.radii) + self.avg_radius = sum(self.radii) / len(self.radii) + self.hue_circle = self.render_hue_circle() + self.hue_angle = 0 + sz = (512, 512) + self.hue_surf = pygame.Surface(sz, pygame.SRCALPHA, 32) + self.hue_surf_rect = pygame.Rect( + (self.rect.centerx - sz[0] // 2, self.rect.centery - sz[1] // 2), sz + ) + self.sv_pos = (0, 0) + self.overlay_surfs = self.get_overlay_surfs() + self.pressed = None + + def get_hue_surf_angle(self): + angle = self.hue_angle + tau / 8 + if angle >= tau: + angle -= tau + return angle + + def draw(self): + self.surf.blit(self.hue_circle, self.rect.topleft) + draw_cursor( + self.surf, + "black", + ( + self.rect.centerx + cos(self.hue_angle) * self.avg_radius, + self.rect.centery - sin(self.hue_angle) * self.avg_radius, + ), + ) + color = ColorMenu.hsv_to_color((self.hue_angle * 255 / tau, 255, 255)) + self.hue_surf.fill(color) + self.hue_surf.blit(self.overlay_surfs[0], (0, 0)) + self.hue_surf.blit(self.overlay_surfs[1], (0, 0)) + angle = self.get_hue_surf_angle() + rotated_hue_surf = pygame.transform.rotate(self.hue_surf, angle * 360 / tau) + sz = rotated_hue_surf.get_size() + self.surf.blit( + rotated_hue_surf, + (self.rect.centerx - sz[0] // 2, self.rect.centery - sz[1] // 2) + ) + + sz = self.hue_surf_rect.size + sv_rel = (self.sv_pos[0] - sz[0] // 2, self.sv_pos[1] - sz[1] // 2) + rel_dist = sqrt(sv_rel[0] ** 2 + sv_rel[1] ** 2) + rel_angle = atan2(sv_rel[1], sv_rel[0]) - angle + flipped_hue_angle = self.hue_angle - pi / 2 + if flipped_hue_angle < 0: + flipped_hue_angle += tau + draw_cursor( + self.surf, + ColorMenu.hsv_to_color((flipped_hue_angle * 255 / tau, 255, 255)), + ( + self.rect.centerx + cos(rel_angle) * rel_dist, + self.rect.centery + sin(rel_angle) * rel_dist, + ), + ) + + def handle_mousebuttondown(self, ev): + if ev.button != 1 or not self.rect.collidepoint(ev.pos): + return + rel = (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery) + rs = self.radii_squared + sq = rel[0] ** 2 + rel[1] ** 2 + if rs[0] <= sq <= rs[1]: + self.pressed = 1 + self.set_rel_hue(rel) + elif rs[0] > sq: + self.pressed = 2 + self.set_rel_sv(rel, sq) + + def handle_mousemotion(self, ev): + if self.pressed == 1: + self.set_rel_hue( + (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery) + ) + elif self.pressed == 2: + self.set_rel_sv( + (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery) + ) + + def handle_mousebuttonup(self, ev): + if self.pressed == 1: + self.set_rel_hue( + (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery) + ) + self.pressed = None + elif self.pressed == 2: + self.set_rel_sv( + (ev.pos[0] - self.rect.centerx, ev.pos[1] - self.rect.centery) + ) + self.pressed = None + + def set_rel_hue(self, rel): + angle = atan2(-rel[1], rel[0]) + if angle < 0: + angle += tau + self.hue_angle = angle + self.parent.set_channel(angle * 255 / tau, "h", self) + + def set_rel_sv(self, rel, sq=None): + rel_dist = sqrt(sq if sq is not None else rel[0] ** 2 + rel[1] ** 2) + rel_angle = atan2(rel[1], rel[0]) + self.get_hue_surf_angle() + sz = self.hue_surf_rect.size + x, y = ( + cos(rel_angle) * rel_dist + sz[0] // 2, + sin(rel_angle) * rel_dist + sz[1] // 2, + ) + if x < 0: + x = 0 + elif x > sz[0]: + x = sz[0] + if y < 0: + y = 0 + elif y > sz[1]: + y = sz[1] + self.sv_pos = (x, y) + self.parent.set_channel(x * 255 / sz[0], "s", self) + self.parent.set_channel(y * 255 / sz[1], "v", self) + + +class HSMap(Child): + @staticmethod + def get_hs_surf(size): + surf = pygame.Surface(size, 0, 24) + with pygame.PixelArray(surf) as pa: + for x, col in enumerate(pa): + for y in range(len(col)): + col[y] = pygame.Color( + *( + int(a * 255) + for a in hsv_to_rgb(x / size[0], 1 - y / (size[1] - 1), 1.0) + ) + ) + return surf + + def __init__(self, parent, rect): + super().__init__(parent) + self.rect = rect + self.hs_surf = self.get_hs_surf(rect.size) + self.hs_pos = (0, 0) + + def draw(self): + pygame.draw.rect(self.surf, "green", self.rect, 1) + self.surf.blit(self.hs_surf, self.rect.topleft) + color = ColorMenu.hsv_to_color( + (self.hs_pos[0] * 255 / self.rect.width, 255, 255) + ) + draw_cursor( + self.surf, + color, + (self.rect.left + self.hs_pos[0], self.rect.bottom - (1 + self.hs_pos[1])), + ) + + def set_hsv(self, value, dest): + self.parent.set_hsv(value, dest, type(self)) + + +class HSSlider(Slider): + def draw_cursor(self): + draw_cursor(self.surf, "blue", self.get_cursor_rect()[0].center) + + class ColorMenu(BaseMenu): + def prev_tab(self): + if self.tab_bar.current_tab > 0: + self.tab_bar.click(self.tab_bar.current_tab - 1) + + def next_tab(self): + if self.tab_bar.current_tab < len(self.tab_bar.buttons) - 1: + self.tab_bar.click(self.tab_bar.current_tab + 1) + + key_methods = { + frozenset(): { + pygame.K_LEFT: prev_tab, + pygame.K_RIGHT: next_tab, + **BaseMenu.key_methods[frozenset()], + } + } + + def add_control(self, ui_type, *args, **kwargs): + dest = kwargs.pop("dest") + instance = ui_type(self, *args, None, **kwargs) + instance.callback = partial(self.set_channel, dest=dest, sender=instance) + self.channel_slots[dest].append(instance) + return instance + def __init__(self, parent): super().__init__(parent) size = self.surf.get_size() @@ -46,127 +283,211 @@ class ColorMenu(BaseMenu): slider_right = label_right + label_width input_right = slider_right + slider_width - y += button_height * 2 - Label( - self, - pygame.Rect((label_left, y), (label_width, button_height)), - "R", - Label.HAlign.CENTER, - ) - self.red_slider = Slider( - self, + y += button_height + 16 + tabbar_y = y + y += (button_height + 16) * 2 + + self.channel_slots = {"r": [], "g": [], "b": [], "h": [], "s": [], "v": []} + red_slider = self.add_control( + Slider, pygame.Rect((slider_left, y), (slider_width, button_height)), Slider.Direction.HORIZONTAL, 0, button_height, - partial(self.set_rgb, dest="r", sender_type=Slider), + dest="r", ) - self.red_input = Spinner( - self, + red_spinner = self.add_control( + Spinner, pygame.Rect((input_left, y), (input_width, button_height)), - partial(self.set_rgb, dest="r", sender_type=Spinner), + dest="r", ) - Label( - self, - pygame.Rect((label_right, y), (label_width, button_height)), - "H", - Label.HAlign.CENTER, - ) - self.hue_slider = Slider( - self, + hue_slider = self.add_control( + Slider, pygame.Rect((slider_right, y), (slider_width, button_height)), Slider.Direction.HORIZONTAL, 0, button_height, - partial(self.set_hsv, dest=0, sender_type=Slider), + dest="h", ) - self.hue_input = Spinner( - self, + hue_spinner = self.add_control( + Spinner, pygame.Rect((input_right, y), (input_width, button_height)), - partial(self.set_hsv, dest=0, sender_type=Spinner), + dest="h", ) - y += button_height + 16 - Label( - self, - pygame.Rect((label_left, y), (label_width, button_height)), - "G", - Label.HAlign.CENTER, - ) - self.green_slider = Slider( - self, + green_slider = self.add_control( + Slider, pygame.Rect((slider_left, y), (slider_width, button_height)), Slider.Direction.HORIZONTAL, 0, button_height, - partial(self.set_rgb, dest="g", sender_type=Slider), + dest="g", ) - self.green_input = Spinner( - self, + green_spinner = self.add_control( + Spinner, pygame.Rect((input_left, y), (input_width, button_height)), - partial(self.set_rgb, dest="g", sender_type=Spinner), + dest="g", ) - Label( - self, - pygame.Rect((label_right, y), (label_width, button_height)), - "S", - Label.HAlign.CENTER, - ) - self.saturation_slider = Slider( - self, + saturation_slider = self.add_control( + Slider, pygame.Rect((slider_right, y), (slider_width, button_height)), Slider.Direction.HORIZONTAL, 0, button_height, - partial(self.set_hsv, dest=1, sender_type=Slider), + dest="s", ) - self.saturation_input = Spinner( - self, + saturation_spinner = self.add_control( + Spinner, pygame.Rect((input_right, y), (input_width, button_height)), - partial(self.set_hsv, dest=1, sender_type=Spinner), + dest="s", ) - y += button_height + 16 - Label( - self, - pygame.Rect((label_left, y), (label_width, button_height)), - "B", - Label.HAlign.CENTER, - ) - self.blue_slider = Slider( - self, + blue_slider = self.add_control( + Slider, pygame.Rect((slider_left, y), (slider_width, button_height)), Slider.Direction.HORIZONTAL, 0, button_height, - partial(self.set_rgb, dest="b", sender_type=Slider), + dest="b", ) - self.blue_input = Spinner( - self, + blue_spinner = self.add_control( + Spinner, pygame.Rect((input_left, y), (input_width, button_height)), - partial(self.set_rgb, dest="b", sender_type=Spinner), + dest="b", ) - Label( - self, - pygame.Rect((label_right, y), (label_width, button_height)), - "V", - Label.HAlign.CENTER, - ) - self.value_slider = Slider( - self, + value_slider = self.add_control( + Slider, pygame.Rect((slider_right, y), (slider_width, button_height)), Slider.Direction.HORIZONTAL, 0, button_height, - partial(self.set_hsv, dest=2, sender_type=Slider), + dest="v", ) - self.value_input = Spinner( - self, + value_spinner = self.add_control( + Spinner, pygame.Rect((input_right, y), (input_width, button_height)), - partial(self.set_hsv, dest=2, sender_type=Spinner), + dest="v", + ) + self.color_circle = ColorCircle( + self, + pygame.Rect( + (self.modal_rect.centerx - 450, tabbar_y + button_height + 16), + (900, 900), + ), + ) + hs_map_width = 1336 + hs_slider_width = 64 + self.hs_map = HSMap( + self, + pygame.Rect( + ( + self.modal_rect.centerx - hs_map_width // 2, + tabbar_y + button_height + 16, + ), + (hs_map_width - hs_slider_width - 16, 900), + ), + ) + value_slider2 = self.add_control( + HSSlider, + pygame.Rect( + ( + self.modal_rect.centerx + hs_map_width // 2 - hs_slider_width, + tabbar_y + button_height + 16, + ), + (hs_slider_width, 900), + ), + Slider.Direction.VERTICAL, + 0, + button_height // 2, + dest="v", ) - y += button_height * 2 + self.tab_bar = TabBar( + self, + pygame.Rect( + (self.modal_rect.left, tabbar_y), + (self.modal_rect.width, button_height), + ), + ( + "Sliders", + "Hue Circle", + "Hue-Saturation Map", + ), + ( + ( + Label( + self, + pygame.Rect( + (label_left, red_slider.rect.top), + (label_width, button_height), + ), + "R", + Label.HAlign.CENTER, + ), + red_slider, + red_spinner, + Label( + self, + pygame.Rect( + (label_right, hue_slider.rect.top), + (label_width, button_height) + ), + "H", + Label.HAlign.CENTER, + ), + hue_slider, + hue_spinner, + Label( + self, + pygame.Rect( + (label_left, green_slider.rect.top), + (label_width, button_height), + ), + "G", + Label.HAlign.CENTER, + ), + green_slider, + green_spinner, + Label( + self, + pygame.Rect( + (label_right, saturation_slider.rect.top), + (label_width, button_height), + ), + "S", + Label.HAlign.CENTER, + ), + saturation_slider, + saturation_spinner, + Label( + self, + pygame.Rect( + (label_left, blue_slider.rect.top), + (label_width, button_height), + ), + "B", + Label.HAlign.CENTER, + ), + blue_slider, + blue_spinner, + Label( + self, + pygame.Rect( + (label_right, value_slider.rect.top), + (label_width, button_height), + ), + "V", + Label.HAlign.CENTER, + ), + value_slider, + value_spinner, + ), + (self.color_circle,), + (self.hs_map, value_slider2,), + ), + ) + + y = tabbar_y + button_height + 16 + 900 + 16 half_width = self.modal_rect.width // 2 self.hex_input = TextInput( self, @@ -202,26 +523,44 @@ class ColorMenu(BaseMenu): *(int(c * 255) for c in hsv_to_rgb(*(x / 255 for x in hsv))) ) - def update_controls(self, skip_hsv=False): - color = self.parent.draw_image.color - self.color_label.color = color - self.hex_input.value = f"{int(color) >> 8:06x}" - controls = ( - (self.red_slider, self.red_input, color.r), - (self.green_slider, self.green_input, color.g), - (self.blue_slider, self.blue_input, color.b), - ) - if not skip_hsv: + def update_controls(self, color=None, hsv=None, sender=None): + if hsv is None: + if color is None: + color = self.parent.draw_image.color + else: + self.parent.draw_image.color = color hsv = self.color_to_hsv(color) - controls = ( - *controls, - (self.hue_slider, self.hue_input, int(hsv[0])), - (self.saturation_slider, self.saturation_input, int(hsv[1])), - (self.value_slider, self.value_input, int(hsv[2])), - ) - for slider, spinner, value in controls: - slider.value = slider.extent * value // 255 - spinner.value = value + else: + assert color is None + color = self.hsv_to_color(hsv) + self.parent.draw_image.color = color + self.hex_input.value = f"{int(color) >> 8:06x}" + self.color_label.color = color + assert tuple(color)[:3] == (color.r, color.g, color.b) + values = { + "r": color.r, + "g": color.g, + "b": color.b, + "h": hsv[0], + "s": hsv[1], + "v": hsv[2], + } + for key, controls in self.channel_slots.items(): + value = values[key] + for control in controls: + if control is sender: + continue + control.value = ( + value + if not isinstance(control, Slider) + else value * control.extent // 255 + ) + if sender != self.color_circle: + self.color_circle.hue_angle = values["h"] * tau / 255 + sz = self.color_circle.hue_surf_rect.size + self.color_circle.sv_pos = (values["s"] * sz[0] / 255, values["v"] * sz[1] / 255) + if sender != self.hs_map: + ... def draw_modal(self): super().draw_modal() @@ -230,58 +569,50 @@ class ColorMenu(BaseMenu): pygame.draw.rect(self.surf, "black", rect) pygame.draw.rect(self.surf, "gray", rect, 1) - def set_rgb(self, value, dest, sender_type): - slider, spinner = { - "r": (self.red_slider, self.red_input), - "g": (self.green_slider, self.green_input), - "b": (self.blue_slider, self.blue_input), - }[dest] - if sender_type is Slider: - value = value * 255 // slider.extent - elif sender_type is Spinner: - pass - else: - raise KeyError(sender_type) - setattr(self.parent.draw_image.color, dest, max(min(value, 255), 0)) - self.update_controls() - self.dirty = True + RGB_LETTERS = ("r", "g", "b") + HSV_LETTERS = ("h", "s", "v") - def set_hsv(self, value, dest, sender_type): - slider, spinner = ( - (self.hue_slider, self.hue_input), - (self.saturation_slider, self.saturation_input), - (self.value_slider, self.value_input), - )[dest] - if sender_type is Slider: - limited_value = max(min(value, slider.extent), 0) - if limited_value != value: - value = limited_value - slider.value = value - value = value * 255 // slider.extent - spinner.value = int(value) - elif sender_type is Spinner: - limited_value = max(min(int(value), 255), 0) - if limited_value != value: - value = limited_value - spinner.value = value - slider.value = value * slider.extent // 255 + def set_channel(self, value, dest, sender): + if isinstance(sender, Slider): + value = max(min(value, sender.extent), 0) + if getattr(sender, "value", value) != value: + sender.value = value + value = value * 255 // sender.extent else: - raise KeyError(sender_type) - self.parent.draw_image.color = self.hsv_to_color( - ( - value if dest == 0 else self.hue_input.value, - value if dest == 1 else self.saturation_input.value, - value if dest == 2 else self.value_input.value, - ) - ) - self.update_controls(True) + value = max(min(value, 255), 0) + if getattr(sender, "value", value) != value: + sender.value = value + kwargs = {} + if dest in self.RGB_LETTERS: + color = pygame.Color(self.parent.draw_image.color) + if getattr(color, dest) == value: + return + setattr(color, dest, value) + kwargs["color"] = color + elif dest in self.HSV_LETTERS: + hsv_controls = [ + next( + d for d in self.channel_slots[c] if not isinstance(d, Slider) + ) + for c in self.HSV_LETTERS + ] + index = self.HSV_LETTERS.index(dest) + kwargs["hsv"] = [d.value for d in hsv_controls] + if sender != hsv_controls[index]: + if kwargs["hsv"][index] == value: + return + kwargs["hsv"][index] = value + self.update_controls(sender=sender, **kwargs) self.dirty = True - def set_color(self, value): - try: - color = pygame.Color(value) - except: - color = pygame.Color(f"0x{value}") + def set_color(self, color_like): + if isinstance(color_like, pygame.Color): + color = color_like + else: + try: + color = pygame.Color(color_like) + except: + color = pygame.Color(f"0x{color_like}") self.parent.draw_image.color = color self.update_controls() self.dirty = True diff --git a/memory.py b/memory.py index 2771efc..129c98f 100755 --- a/memory.py +++ b/memory.py @@ -9,8 +9,8 @@ from ui import Button, Child, Label, Modal, Root, TextInput class QuittableModal(Modal): - def handle_quit(self, ev): - self.parent.handle_quit(ev) + def handle_quit(self, _=None): + self.parent.handle_quit() key_methods = {frozenset(): {pygame.K_ESCAPE: handle_quit}} diff --git a/ui/spinner.py b/ui/spinner.py index 43547df..726b254 100644 --- a/ui/spinner.py +++ b/ui/spinner.py @@ -82,7 +82,6 @@ class Spinner(Parent, Child): super().__init__(parent) self.callback = callback self._value = 0 - self.value = value button_size = (rect.height // 2, rect.height // 2) self.text_input = TextInput( self, @@ -93,6 +92,7 @@ class Spinner(Parent, Child): str(value), re.compile(r"[-+]?\d*").fullmatch, ) + self.value = value RepeatButtonRotate( self, pygame.Rect( @@ -124,8 +124,8 @@ class Spinner(Parent, Child): return if int_value == self.value: return - self.value = int_value - str_value = str(self.value) + self._value = int_value + str_value = str(int_value) if str_value != self.text_input.value: self.text_input.value = str_value self.dirty = True @@ -156,7 +156,7 @@ class FloatSpinner(Spinner): if float_value == self.value: return self.value = float_value - str_value = str(self.value) + str_value = str(float_value) if str_value != self.text_input.value: self.text_input.value = str_value self.dirty = True diff --git a/ui/tab_bar.py b/ui/tab_bar.py index 8ee76fc..da3c328 100644 --- a/ui/tab_bar.py +++ b/ui/tab_bar.py @@ -8,7 +8,7 @@ from .parent import Parent class TabBar(Parent, Child): - def __init__(self, parent, rect, labels, groups, current_tab): + def __init__(self, parent, rect, labels, groups, current_tab=0): super().__init__(parent) self.labels = labels self.groups = groups @@ -22,32 +22,28 @@ class TabBar(Parent, Child): (rect.width // num_labels, rect.height), ), labels[i], - partial(self.update_children, i), + partial(self.click, i), False, ) for i in range(num_labels) ] self.update_children() - def update_children(self, i=None): - if i is not None: - if self.current_tab == i: - return - self.current_tab = i + def click(self, i): + if self.current_tab == i: + return + self.current_tab = i + self.update_children() + + def update_children(self): for i, (button, group) in enumerate(zip(self.buttons, self.groups)): is_current_tab = i == self.current_tab if button.highlight != is_current_tab: button.highlight = is_current_tab self.dirty = True for item in group: - if item.parent is not self: - item.parent = self - is_child_enabled = item.enabled - if is_current_tab == is_child_enabled: + if is_current_tab == item.enabled: continue - if is_current_tab: - item.enabled = True - elif is_child_enabled: - item.enabled = False + item.enabled = is_current_tab self.dirty = True assert len(self.children) == len(set(self.children))