Skip to content

Enhance ProofModel by group expansion functionality.#431

Open
axif0 wants to merge 16 commits intozxcalc:masterfrom
axif0:copy
Open

Enhance ProofModel by group expansion functionality.#431
axif0 wants to merge 16 commits intozxcalc:masterfrom
axif0:copy

Conversation

@axif0
Copy link
Contributor

@axif0 axif0 commented Feb 15, 2026

Enhance ProofStepView with expandable grouped steps and improve item rendering. Added functionality to toggle group expansion on mouse click and updated rendering logic in ProofStepItemDelegate for grouped steps.

Closed: #292

proof

@lia-approves
Copy link
Collaborator

Hello, thank you for the PR. Can you add the missing type annotations to pass the lint checks? Thanks, Lia

@axif0
Copy link
Contributor Author

axif0 commented Feb 16, 2026

Hi @lia-approves , I’ve added the missing type annotations and pushed the update.

@RazinShaikh
Copy link
Collaborator

Hi @axif0 , thanks for the PR. When the grouped rewrites are expanded, the rewrites that were part of the group do not appear to be selectable. It looks like you only make a visual change of how the grouped rewrites are displayed. Can you make it such that the rewrites are clickable and when you click on a sub rewrite, you see the changes in the diagram according to the rewrite?

…eps, displaying their graphs and highlighting them in the UI.
@axif0
Copy link
Contributor Author

axif0 commented Feb 17, 2026

Thank you for your review. @RazinShaikh . Updated by individual sub-steps within grouped proof steps
proof_1

@RazinShaikh
Copy link
Collaborator

Thanks, the main functionality works now. Just a few things that need to be polished now:

  1. The grouped steps are in bold when not selected:
image
  1. Clicking anywhere on the group rewrite seems to expand/contract it. It should only do that when the user actually intends to do it, for instance by clicking on the triangle.
group.mp4
  1. If we rename the grouped rewrite, it only renames in the unexpanded form. Upon expanding, the name becomes "Grouped Steps".
rename.mp4
  1. This is mostly cosmetic but can you make the background of the grouped rewrite lighter when the sub-step is selected as it's otherwise quite hard to see.
image
  1. When the user hovers the mouse over the steps, the background changes implying that the step is clickable. But when hovering over the grouped steps, the sub-steps don't highlight; instead the whole grouped step is highlighted. This indicates to the user that the sub-steps are not really clickable (even though they are).
hover.mp4

Once these things have been polished, I can review the code and then we can merge it

@axif0
Copy link
Contributor Author

axif0 commented Feb 17, 2026

Updated reviews accordingly, thanks for your in detailed suggetion @RazinShaikh .

final

@boldar99
Copy link
Collaborator

Hi, this is starting to look quite complete.

  • Before merging, would you be able to adjust the way the steps are displayed to look something like this:
image
  • Currently, you cannot rename steps inside a group.

  • To rename the group, it would be better to do this when right clicking on the "grouped steps" rather than on individual rewrites inside the group.

@axif0
Copy link
Contributor Author

axif0 commented Feb 18, 2026

Thanks for the suggetion @boldar99 .

@boldar99
Copy link
Collaborator

This is looking great! A couple of minor things:

  • The grouped rewrites look different to the rest. Could you make the style match? Also the green circle of the group now has a black line inside it.
  • Could you make the highlighting inside the group reach the left of the box, also, could you make it so that it doesn't cover the line like in the style of the ungrouped rewrites?
  • Is it possible to highlight the group name when the mouse is over it similar to how it looks with the rest of them?
Screen.Recording.2026-02-24.at.22.51.58.mov

anim_before, anim_after = make_animation(self, panel, g, matches_list, rem_verts_list)
panel.undo_stack.push(cmd, anim_before=anim_before, anim_after=anim_after)

# TODO: Narrow down the type of the first return value.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a TODO here

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment was not added by @axif0 . I am looking at the changes and it seems that when @axif0 merged the main branch to the current branch, some things were merged incorrectly. Because I deleted this block and code in #441 which fixes mypy and flake8 errors but it is being added back by this PR due to some original merge issues here.

Copy link
Collaborator

@RazinShaikh RazinShaikh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like when you merged main to the current branch, several files (all changes other than proof.py) were merged incorrectly. Can you fix that? Also, can you make sure the checks pass?

zxlive/proof.py Outdated
Comment on lines 117 to 122
def rowCount(self, parent: Union[QModelIndex, QPersistentModelIndex] = QModelIndex()) -> int:
"""The number of rows"""
# This is a quirk of Qt list models: Since they are based on tree models, the
# user has to specify the index of the parent. In a list, we always expect the
# parent to be `None` or the empty `QModelIndex()`
if not index or not index.isValid():
if not parent or not parent.isValid():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change index to parent?

zxlive/proof.py Outdated
Comment on lines 9 to 11
QPoint, QPointF, QRect, QRectF, QSize, Qt)
from PySide6.QtGui import QColor, QFont, QFontMetrics, QMouseEvent, QPainter, QPainterPath, QPen, QPolygonF
from PySide6.QtWidgets import (QAbstractItemView, QInputDialog, QLineEdit, QListView, QMenu,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some imports are unused

zxlive/proof.py Outdated
Comment on lines 607 to 610
def _rename_sub_step_dialog(self, step_idx: int, sub_idx: int) -> None:
# Kept for compatibility if needed, but implementation redirects or is replaced.
# Ideally removed, but for this refactor we can just remove the old method body
pass
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this? Is this some AI generated code leftover?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is unrelated to unfusion so it shouldn't change anything in this file.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is unrelated to this file so it shouldn't change anything here.

anim_before, anim_after = make_animation(self, panel, g, matches_list, rem_verts_list)
panel.undo_stack.push(cmd, anim_before=anim_before, anim_after=anim_after)

# TODO: Narrow down the type of the first return value.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment was not added by @axif0 . I am looking at the changes and it seems that when @axif0 merged the main branch to the current branch, some things were merged incorrectly. Because I deleted this block and code in #441 which fixes mypy and flake8 errors but it is being added back by this PR due to some original merge issues here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is unrelated to this file so it shouldn't change anything here.

…grouped step headers and sub-steps, improving hover and selection highlighting.
@axif0
Copy link
Contributor Author

axif0 commented Feb 25, 2026

@RazinShaikh Apologies for the merge issue on my side.
updated as-
final

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances the proof visualization feature by adding expandable/collapsible functionality for grouped proof steps. The implementation allows users to click a triangle icon to expand grouped steps and see individual sub-steps in a tree-like structure, addressing issue #292.

Changes:

  • Added expandable group UI with triangle indicators and sub-step tree visualization in the delegate paint method
  • Implemented mouse interaction handlers for expanding/collapsing groups, selecting sub-steps, and inline renaming of sub-steps
  • Enhanced the ProofModel with sub-step renaming capabilities and synchronized name updates between group headers and sub-steps
Comments suppressed due to low confidence (6)

zxlive/proof.py:208

  • The arrow symbol was changed from "🡒" to "\u2192" (→). While both are valid Unicode arrows, this change affects the display. Ensure that the new arrow character is properly rendered in all fonts and on all platforms that the application targets. The Unicode character U+2192 (→) is more commonly supported than U+1F852 (🡒), so this is likely an improvement, but verify visual consistency across different operating systems.
            "Grouped Steps: " + " \u2192 ".join(self.steps[i].display_name for i in range(start_index, end_index + 1)),

zxlive/proof.py:954

  • The sub-step circle colors are hard-coded (QColor(30, 100, 200) for selected and QColor(70, 130, 180) for default) and don't respect the dark mode setting. For consistency with the rest of the UI, these colors should either use display_setting.effective_colors or have different values for dark vs light mode. Compare with line 826 which uses display_setting.effective_colors["z_spider"] for the main circle.
            if is_sub_selected:
                painter.setBrush(QColor(30, 100, 200))  # brighter blue when selected
                r = self.sub_circle_radius + 1
            else:
                painter.setBrush(QColor(70, 130, 180))  # default steel-blue
                r = self.sub_circle_radius

zxlive/proof.py:549

  • The lambda captures step_idx and sub_idx from the enclosing scope, which creates a closure. In Python, lambdas in loops or conditional blocks can have unexpected behavior due to late binding. While this particular case should work because the lambda is created and stored in the action_function_map within the same scope, and is executed before the scope exits, it's still a potential source of bugs if the code is refactored. Consider using a default argument to capture the values explicitly: lambda si=step_idx, ssi=sub_idx: self._open_sub_step_editor(si, ssi)
                        action_function_map[rename_sub_action] = lambda: self._open_sub_step_editor(step_idx, sub_idx)

zxlive/proof.py:373

  • There are two consecutive blank lines here. According to PEP 8, there should be at most one blank line between method definitions within a class. Consider removing one of these blank lines to maintain consistent style.
    zxlive/proof.py:317
  • When setting a new model, the active sub-step editor (if any) should be cleaned up. If an editor is currently open and a new model is set, the editor will become orphaned and may reference invalid indices. Consider adding cleanup for _active_sub_editor here, such as calling deleteLater() on it if it exists and setting it to None.
    def set_model(self, model: ProofModel) -> None:
        self.setModel(model)
        self.expanded_groups.clear()
        self.selected_sub_step = None
        # it looks like the selectionModel is linked to the model, so after updating the model we need to reconnect the selectionModel signals.
        self.selectionModel().selectionChanged.connect(self.proof_step_selected)
        self.setCurrentIndex(model.index(len(model.steps), 0))

zxlive/proof.py:330

  • When collapsing a group, if there's an active sub-step editor open for one of its sub-steps, the editor should be closed and cleaned up. The current code clears the selected_sub_step state but doesn't handle the _active_sub_editor. This could leave an orphaned editor visible after the group is collapsed. Consider adding a check to close and delete the editor if it exists and belongs to this step_index.
    def toggle_group_expansion(self, step_index: int) -> None:
        """Toggle the expanded/collapsed state of a grouped step."""
        if step_index in self.expanded_groups:
            self.expanded_groups.discard(step_index)
            # Clear sub-step selection when collapsing
            if self.selected_sub_step and self.selected_sub_step[0] == step_index:
                self.selected_sub_step = None
        else:
            self.expanded_groups.add(step_index)
        model_index = self.model().index(step_index + 1, 0)
        self.model().dataChanged.emit(model_index, model_index, [])
        self.doItemsLayout()

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

zxlive/proof.py Outdated
self.steps[step_index] = Rewrite(old_step.display_name, old_step.rule, old_step.graph, new_grouped)

# Rerender
modelIndex = self.createIndex(step_index, 0)
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as line 182-183: this should be self.createIndex(step_index + 1, 0) to correctly emit the dataChanged signal for the right model row. The step_index parameter is a 0-based index into the steps array, but model rows are offset by 1.

Suggested change
modelIndex = self.createIndex(step_index, 0)
modelIndex = self.createIndex(step_index + 1, 0)

Copilot uses AI. Check for mistakes.
zxlive/proof.py Outdated
Comment on lines 182 to 183
modelIndex = self.createIndex(index, 0)
self.dataChanged.emit(modelIndex, modelIndex, [])
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The modelIndex creation uses createIndex(index, 0) but the parameter index refers to the steps array index (0-based), while the model's row numbering is offset by 1 (row 0 is "START", row N corresponds to steps[N-1]). This emits a dataChanged signal for the wrong row. Based on the pattern seen in line 644 and the data() method at line 96, this should be self.createIndex(index + 1, 0) to correctly notify about the change in the view's row coordinate system.

Copilot uses AI. Check for mistakes.
@RazinShaikh
Copy link
Collaborator

@axif0 are the comments in copilot's review about the index correct? I am not quite sure.

@axif0
Copy link
Contributor Author

axif0 commented Feb 25, 2026

Found a bug. like If you edit the position of an edge or node on a sub-step now, it should smoothly animate into place, save to the stack and leave your sub-panels completely intact!

final2

are the comments in copilot's review about the index correct? I am not quite sure.

ya, I think, any element at position i in self.steps corresponds to i + 1 in the view's model rows. they need to issue a dataChanged event for row index + 1 rather than index.

@boldar99
Copy link
Collaborator

I have made some minor edits to make the subtree look better.

Can you please fix the following:

  • When you click on a sub-step, it selects the parent first, and we need to click again to actually select the desired step.
  • The highlighting is still looking odd. Can you make it so that the blue background reaches the very left of the box, not the right of the circle.
Screen.Recording.2026-02-26.at.18.17.28.mov

@axif0
Copy link
Contributor Author

axif0 commented Feb 26, 2026

Updated and use from __future__ import annotations and Optional, Union, and Dict imports were removed.

@boldar99 @RazinShaikh

Peek 2026-02-27 02-42

@boldar99
Copy link
Collaborator

Highlighting still seems to block the dots and lines inside the expanded view but not when outside. Could you fix that?

@axif0
Copy link
Contributor Author

axif0 commented Feb 26, 2026

blue background reaches the very left of the box, not the right of the circle.

Sorry I misinterpreted you earlier. @boldar99
Updated-

final

@axif0
Copy link
Contributor Author

axif0 commented Feb 27, 2026

Hello, Anything else I need to do on my side?

@boldar99
Copy link
Collaborator

Hi,

I'm quite pleased with how it looks and thought this was effectively done, so I did some testing which unfortunately resulted in finding a bug.
If you do a rewrites from a step inside the grouped rewrites, the current behaviour is not what should be happening.

Rather, you should drop all the rewrites from the group after the step from which you did the new rewrite, as well as everything after the group. If the size of the group is 1 then ungroup this. And then add the new rewrites after outside of the group.

Screen.Recording.2026-02-27.at.18.19.31.mov

…runcation and ungrouping, with corresponding undo functionality.
@axif0
Copy link
Contributor Author

axif0 commented Feb 27, 2026

So, I understand like -

  • Pops the new rewrite
  • Removes the truncated/ungrouped step at the group's position
  • Re-inserts the original group (with all 4 sub-steps: _old_group_rewrite)
  • Re-inserts the dropped main steps (Step B, Step C) in the right order
  • Restores the list view selection to the group row

zxlive/proof.py Outdated
Comment on lines 489 to 500
if index.isValid() and index.row() > 0:
step_idx = index.row() - 1
if step_idx < len(self.model().steps):
step = self.model().steps[step_idx]
if step.grouped_rewrites is not None:
if step_idx in self.expanded_groups:
rect = self.visualRect(index)
sub_idx = self._sub_step_hit_test(
step_idx, int(event.pos().y()), rect.y()
)
if sub_idx >= 0:
new_step = step_idx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no better way to do this than 5 nested if statements?

zxlive/proof.py Outdated
Comment on lines 423 to 432
if index.isValid() and index.row() > 0:
step_idx = index.row() - 1
if step_idx < len(self.model().steps):
step = self.model().steps[step_idx]
if step.grouped_rewrites is not None:
delegate = self.itemDelegate()
if isinstance(delegate, ProofStepItemDelegate):
rect = self.visualRect(index)
# Check if clicking on the triangle
if self._triangle_hit_test(step_idx, int(event.pos().x()), int(event.pos().y()),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there no better way to do this than 5 nested if statements?

@axif0
Copy link
Contributor Author

axif0 commented Feb 28, 2026

Removed the 5-level deep nested if-statements and use helper method for better readability. @RazinShaikh

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (6)

zxlive/commands.py:94

  • ProofModeCommand calls ProofStepView._navigate_to_sub_step, which is a private method (leading underscore) and couples command logic to view internals. Consider making this a public method (e.g., navigate_to_sub_step) or exposing a small public API for selecting/navigating sub-steps so future refactors don’t break undo/redo behavior.
    def undo(self) -> None:
        if self.sub_step:
            self.step_view.selectionModel().blockSignals(True)
            self.step_view.setCurrentIndex(self.step_view.model().index(self.proof_step_index, 0, QModelIndex()))
            self.step_view.selectionModel().blockSignals(False)
            self.step_view._navigate_to_sub_step(self.sub_step[0], self.sub_step[1])
        else:
            self.step_view.move_to_step(self.proof_step_index)
        self.command.undo()
        if self.sub_step:
            self.step_view.model().set_sub_graph(self.sub_step[0], self.sub_step[1], self.command.g)
        else:
            self.step_view.model().set_graph(self.proof_step_index, self.command.g)

    def redo(self) -> None:
        if self.sub_step:
            self.step_view.selectionModel().blockSignals(True)
            self.step_view.setCurrentIndex(self.step_view.model().index(self.proof_step_index, 0, QModelIndex()))
            self.step_view.selectionModel().blockSignals(False)
            self.step_view._navigate_to_sub_step(self.sub_step[0], self.sub_step[1])
        else:

zxlive/commands.py:473

  • AddRewriteStep introduces _old_group_tail, but it’s never read anywhere (undo uses _old_group_rewrite instead). Leaving unused state like this makes the command harder to reason about and can drift from the actual behavior. Either remove _old_group_tail or use it to restore the truncated group in undo (if that was the intent).
    # When rewriting from inside a grouped sub-step, stores the tail sub-steps
    # that were dropped from the group (for undo restoration).
    _old_group_tail: list[Rewrite] | None = field(default=None, init=False)
    # The original Rewrite object for the group step (before truncation).
    _old_group_rewrite: Rewrite | None = field(default=None, init=False)

zxlive/proof.py:840

  • is_last is computed but no longer used in paint(), which is easy to miss when maintaining the drawing logic. Consider removing it or reintroducing its usage (e.g., to stop the timeline line at the last item) to avoid dead code.
        # ── Main timeline line ──────────────────────────────────────────
        is_last = index.row() == index.model().rowCount() - 1

zxlive/proof.py:355

  • expanded_groups stores 0-based indices into model.steps, but those indices will become stale when steps are inserted/removed (e.g., add/pop/group/ungroup), causing the wrong group to appear expanded or hit-testing/hover to mismatch. Consider tracking expansion by QPersistentModelIndex/stable IDs, or at least clearing/updating expanded_groups (and selected_sub_step) on rowsInserted/rowsRemoved/modelReset signals.
        self.expanded_groups: set[int] = set()
        # Track currently selected sub-step: (step_index, sub_step_index) or None
        self.selected_sub_step: tuple[int, int] | None = None
        self._active_sub_editor: QLineEdit | None = None
        self.setModel(ProofModel(self.graph_view.graph_scene.g))
        self.setCurrentIndex(self.model().index(0, 0))
        self.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked)
        # Set background color for dark mode (panel background)
        if display_setting.dark_mode:
            self.setStyleSheet("background-color: #23272e;")
        else:
            self.setStyleSheet("")
        # Set background color for dark mode
        pal = self.palette()
        if display_setting.dark_mode:
            pal.setColor(self.backgroundRole(), QColor(35, 39, 46))
            pal.setColor(self.viewport().backgroundRole(), QColor(35, 39, 46))
        else:
            pal.setColor(self.backgroundRole(), QColor(255, 255, 255))
            pal.setColor(self.viewport().backgroundRole(), QColor(255, 255, 255))
        self.setPalette(pal)
        self.setSpacing(0)
        self.setMouseTracking(True)
        self.setSelectionMode(QAbstractItemView.SelectionMode.ContiguousSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        self.setResizeMode(QListView.ResizeMode.Adjust)
        self.setUniformItemSizes(False)
        self.setAlternatingRowColors(True)
        self.viewport().setAttribute(Qt.WidgetAttribute.WA_Hover)
        self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_context_menu)
        self.selectionModel().selectionChanged.connect(self.proof_step_selected)
        self.setItemDelegate(ProofStepItemDelegate(self))

    # overriding this method to change the return type and stop mypy from complaining
    def model(self) -> ProofModel:
        model = super().model()
        assert isinstance(model, ProofModel)
        return model

    def set_model(self, model: ProofModel) -> None:
        self.setModel(model)
        self.expanded_groups.clear()
        self.selected_sub_step = None
        # it looks like the selectionModel is linked to the model, so after updating the model we need to reconnect the selectionModel signals.
        self.selectionModel().selectionChanged.connect(self.proof_step_selected)
        self.setCurrentIndex(model.index(len(model.steps), 0))

    def toggle_group_expansion(self, step_index: int) -> None:
        """Toggle the expanded/collapsed state of a grouped step."""
        if step_index in self.expanded_groups:
            self.expanded_groups.discard(step_index)
            # Clear sub-step selection when collapsing
            if self.selected_sub_step and self.selected_sub_step[0] == step_index:
                self.selected_sub_step = None
        else:
            self.expanded_groups.add(step_index)
        model_index = self.model().index(step_index + 1, 0)
        self.model().dataChanged.emit(model_index, model_index, [])
        self.doItemsLayout()

zxlive/proof.py:395

  • _sub_step_hit_test / _triangle_hit_test compute row_height using QFontMetrics(self.font()), but the delegate paints using option.font (and the model provides a per-item FontRole). If the view font differs from the item font, hit-testing and hover/click detection will be misaligned. Use the same font metrics as painting (e.g., fetch the row font via index.data(Qt.FontRole) for the clicked row) when computing row_height.
    def _sub_step_hit_test(self, step_idx: int, event_pos_y: int, rect_y: int) -> int:
        """Determine which sub-step (if any) was clicked in an expanded group.

        Returns the 0-based sub-step index, or -1 if the click was not on a sub-step.
        """
        delegate = self.itemDelegate()
        if not isinstance(delegate, ProofStepItemDelegate):
            return -1
        step = self.model().steps[step_idx]
        if step.grouped_rewrites is None:
            return -1
        text_h = QFontMetrics(self.font()).height()
        row_height = text_h + 2 * delegate.vert_padding
        header_h = row_height  # header occupies first row_height pixels
        click_y = event_pos_y - rect_y
        if click_y <= header_h:
            return -1  # Click is in the header area
        sub_y = click_y - header_h
        sub_idx = int(sub_y // row_height)
        if 0 <= sub_idx < len(step.grouped_rewrites):
            return sub_idx
        return -1

    def _triangle_hit_test(self, step_idx: int, event_pos_x: int, event_pos_y: int, rect_x: int, rect_y: int) -> bool:
        """Check if the click was on the collapse/expand triangle."""
        delegate = self.itemDelegate()
        if not isinstance(delegate, ProofStepItemDelegate):
            return False
        text_h = QFontMetrics(self.font()).height()
        row_height = text_h + 2 * delegate.vert_padding
        text_x_base = rect_x + delegate.line_width + 2 * delegate.line_padding
        tri_size = delegate.triangle_size
        tri_cy = rect_y + row_height / 2
        click_x = event_pos_x
        click_y = event_pos_y
        # Triangle bounds: roughly text_x_base to text_x_base + tri_size*2
        # and tri_cy - tri_size to tri_cy + tri_size
        return (text_x_base <= click_x <= text_x_base + tri_size * 2 and
                tri_cy - tri_size <= click_y <= tri_cy + tri_size)

zxlive/proof.py:655

  • _open_sub_step_editor places the QLineEdit using coordinates from visualRect() (viewport coordinates), but creates the editor with parent self (the QListView). This can offset the editor when the view has a frame/scrolling because the coordinate systems differ. Prefer parenting the editor to self.viewport() (or mapping the rect to the view coordinate system) and include rect.x() in the x-position calculations so it stays aligned under horizontal scrolling/styling.
        # Ensure the item is visible
        model_idx = self.model().index(step_idx + 1, 0)
        self.scrollTo(model_idx)

        # Get the visual rect of the main item
        rect = self.visualRect(model_idx)

        # Calculate sub-step position logic (matching delegate.paint)
        font = self.font()
        text_height = QFontMetrics(font).height()
        row_height = text_height + 2 * delegate.vert_padding

        main_cx = delegate.line_padding + delegate.line_width / 2
        sub_tree_x = main_cx + delegate.sub_indent

        # Calculate vertical position of the sub-step
        # Header + sub_steps before this one
        header_height = row_height
        sub_step_top = rect.y() + header_height + sub_idx * row_height

        sub_text_x = int(sub_tree_x + delegate.circle_radius + 10)

        editor_x = sub_text_x
        # Center vertically in the row
        editor_y = int(sub_step_top + delegate.vert_padding)
        editor_w = rect.width() - sub_text_x - 5  # Small padding on right
        editor_h = text_height

        editor = QLineEdit(self)
        step = self.model().steps[step_idx]
        if step.grouped_rewrites:
            editor.setText(step.grouped_rewrites[sub_idx].display_name)

        editor.setGeometry(editor_x, editor_y, editor_w, editor_h)
        # Style to blend in or look like an editor
        if display_setting.dark_mode:
            editor.setStyleSheet("QLineEdit { background-color: #2c313a; color: #e0e0e0; border: 1px solid #4b5362; }")
        else:
            editor.setStyleSheet("QLineEdit { background-color: white; color: black; border: 1px solid #ccc; }")

        # Connect signals
        editor.editingFinished.connect(lambda: self._finish_sub_step_edit(step_idx, sub_idx, editor))

        editor.show()
        editor.setFocus()
        self._active_sub_editor = editor

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expandable group rewrites

5 participants