Skip to content

Fix TikZ copy/paste to preserve parameter metadata across windows#433

Open
Bumsparkle wants to merge 13 commits intozxcalc:masterfrom
Bumsparkle:fix-copy-paste-metadata
Open

Fix TikZ copy/paste to preserve parameter metadata across windows#433
Bumsparkle wants to merge 13 commits intozxcalc:masterfrom
Bumsparkle:fix-copy-paste-metadata

Conversation

@Bumsparkle
Copy link

Issue summary:

Copying/pasting ZX diagrams would drop parameter metadata.

Root cause:

  • Variable type metadata was not consistently propagated through subgraph copy/merge paths.
  • Cross-instance transfer goes through TikZ, and symbolic-phase TikZ parsing is fragile in some cases.

Fix summary:

  • Added explicit variable-type metadata propagation helpers.
  • Preserved metadata during selection Tikz copy and paste merge paths (including same-instance tab workflows).
  • Kept TikZ clipboard compatibility while preventing metadata loss in normal ZXLive-to-ZXLive workflows.

Manual verification:

  • In one window, set a spider phase to a symbolic variable and set variable type in Variables panel.
  • Copy in window A and paste in window B using TikZ copy/paste (Cmd/Ctrl+Shift+C, Cmd/Ctrl+Shift+V).
  • Result: diagram pasted across windows and variable metadata/type preserved.
  • Copy/paste within same instance (tab-to-tab) also preserved metadata/type.

Issue link

Fixes #363

@RazinShaikh
Copy link
Collaborator

RazinShaikh commented Feb 16, 2026

Hi, thanks for the PR. It seems like a reasonable approach to append the additional metadata to tikz for copy/paste across windows. However, when I tested this, it didn't work.

I copied from the left window to the right window in the screenshot below and the variables b and d changed from boolean to parametric.

image

And for copy-paste between tabs of the same window, the variable viewer is empty so it didn't copy the parameters:

image

@jvdwetering
Copy link
Collaborator

Thanks for the implementation!
I'm not so familiar with mime types, would this still make it possible to copy-paste from ZXLive to tikzit and back, or would it no longer be recognized as text?
Also, this would change the behaviour of copy-pasting if I'm not mistaken, making the ctrl-shift-v/c commands no longer necessary (they were the commands that produced tikz instead of the internal graph type).

@Bumsparkle
Copy link
Author

Fixed my implementation.

Yes, Tikzit copy/paste still works. The clipboard still contains plain TikZ text (mime.setText(...)), so Tikzit continues to recognize it.

Ctrl+Shift+C / Ctrl+Shift+V are still meaningful and kept as TikZ-only copy/paste paths.
Regular Ctrl+C / Ctrl+V now uses internal ZXLive MIME when available (to preserve metadata like variable types) and falls back to TikZ text otherwise, so behavior is improved without removing the TikZ workflow.

Also, I noticed someone else has opened a related PR sooner, so happy to close this one.

@lia-approves
Copy link
Collaborator

Hi @Bumsparkle, thanks for fixing the 2nd issue Razin pointed out in his most recent comment above, which is that the variable viewer is no longer empty when pasting between tabs of the same window.
The first issue Razin pointed out is still present: when copy pasting between windows (and now also for between tabs of the same window), all Boolean variable types get changed to Parametric variable types - could that be fixed?
We will also check the other submitted PR for the issue but it's the first working one that will be accepted.

@Bumsparkle
Copy link
Author

Thanks for testing and for the detailed report. I’ve now fixed the remaining issue: boolean variable types are preserved on copy/paste between windows and between tabs (normal Ctrl+C / Ctrl+V), and no longer get converted to parametric.

@Bumsparkle
Copy link
Author

I’ve resolved the merge conflicts and re-tested the boolean variable type issue: Ctrl+C/Ctrl+V preserves Boolean vs Parametric types across windows and tabs.

@RazinShaikh
Copy link
Collaborator

RazinShaikh commented Feb 23, 2026

Hi @Bumsparkle can you please clean up the PR? There are many changes that are not relevant to the issue; I believe they come from merging incorrectly from the main branch. Can you also make the checks pass?

@lia-approves
Copy link
Collaborator

lia-approves commented Feb 23, 2026

I've tested that in ZXLive, copy paste works both with and without Shift, on both different tabs and different windows, preserving the parameter metadata.

The tikz copy (Ctrl/Command + Shift + C in ZXLive) didn't work pasting (Ctrl/Command + V) into Tikzit as-is, which thankfully has a one-line fix in the above commit. Tikzit doesn't preserve the metadata, but that's a feature that could be added to tikzit's parser that I would say isn't necessary for this unitary design bounty.

@Bumsparkle
Copy link
Author

I’ve finished cleaning up this PR and rebased it so it now only contains the copy/paste variable-type fix (removed unrelated merge-noise files and resolved conflicts).

Could someone with write access please re-run the failed [test (3.11)] job? The failure is in Setup Audio (apt-get install pulseaudio) with Ubuntu mirror 404s, so I think it looks infrastructure-related rather than a code/test failure.

@RazinShaikh
Copy link
Collaborator

Thanks for cleaning up. We are looking into why setup audio is failing; this is happening across all PRs.

@RazinShaikh
Copy link
Collaborator

I am looking at the code now and I am a bit confused by the copy_variable_types method. It looks like you are calling this every time a graph is copied. In theory, the code in PyZX should already handle this and include the correct types for variables in the copied graph. Does that mean the copy in PyZX is not working correctly? If that's the case, we should move your copy_variable_types method to PyZX rather than keeping this in ZXLive.
(don't worry, this shouldn't affect the bounty as long as the issue is closed in the end.)

@Bumsparkle
Copy link
Author

Good point, I’m cautious about over-claiming this as purely a PyZX bug. From what I found, ZXLive’s copy/paste path (subgraph_from_vertices + merge/update), boolean type info was not reliably preserved, which caused variables to appear as parametric after paste. copy_variable_types() is then a defensive fix in ZXLive to keep behaviour stable for users right now.

I agree the cleaner long-term fix is likely to preserve this metadata in PyZX for those graph operations, then simplify/remove the ZXLive workaround.

@RazinShaikh
Copy link
Collaborator

RazinShaikh commented Feb 25, 2026

I see, thanks. Can you document this somewhere in the code so that we can remove the workaround once it has been fixed in PyZX? And do you mind creating an issue in the PyZX repo with a minimal example?

@Bumsparkle
Copy link
Author

I’ve added a TODO in ZXLive that its a temporary workaround and opened PyZX issue zxcalc/pyzx#408 with a minimal repro.

@RazinShaikh
Copy link
Collaborator

RazinShaikh commented Feb 26, 2026

Hello, it looks like the PyZX issue is already closed (so fast, within 4 hours after you created the issue😅). Do you mind having a look at your PR again and see if the temporary workaround can now be removed? Sorry, I was thinking of leaving the workaround but since PyZX is already fixed, it might be worth cleaning it up before the PR is merged.

@Bumsparkle
Copy link
Author

Hey, no worries, I've removed the temporary ZXLive-side variable type workaround

zxlive/common.py Outdated
Comment on lines 147 to 169
def get_variable_types(graph: GraphT) -> dict[str, bool]:
"""Returns variable type metadata, falling back to symbolic phase vars."""
variable_types = {
str(name): bool(graph.var_registry.get_type(name, default=False))
for name in graph.var_registry.vars()
}

for v in graph.vertices():
phase = graph.phase(v)
if not hasattr(phase, "free_vars"):
continue
try:
for var in phase.free_vars():
name = str(var)
inferred = bool(getattr(var, "is_bool", False))
if name not in variable_types:
variable_types[name] = inferred
else:
variable_types[name] = variable_types[name] or inferred
except Exception:
continue

return variable_types
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this method necessary now that this is fixed in PyZX? Would something break if you simplified to this?

Suggested change
def get_variable_types(graph: GraphT) -> dict[str, bool]:
"""Returns variable type metadata, falling back to symbolic phase vars."""
variable_types = {
str(name): bool(graph.var_registry.get_type(name, default=False))
for name in graph.var_registry.vars()
}
for v in graph.vertices():
phase = graph.phase(v)
if not hasattr(phase, "free_vars"):
continue
try:
for var in phase.free_vars():
name = str(var)
inferred = bool(getattr(var, "is_bool", False))
if name not in variable_types:
variable_types[name] = inferred
else:
variable_types[name] = variable_types[name] or inferred
except Exception:
continue
return variable_types
def get_variable_types(graph: GraphT) -> dict[str, bool]:
"""Returns variable type metadata."""
variable_types = {
str(name): bool(graph.var_registry.get_type(name, default=False))
for name in graph.var_registry.vars()
}
return variable_types

Copy link
Author

Choose a reason for hiding this comment

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

Don't think anything breaks. Variable types are now taken only from var_registry, which matches PyZX.

zxlive/common.py Outdated
Comment on lines 226 to 231
def _coerce_bool(value: object) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.strip().lower() in ("true", "1", "yes", "on")
return bool(value)
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this method necessary because the from_tikz method will only interact with the metadata encoded by you in _encode_tikz_metadata method? Couldn't you have just done bool(is_bool) instead of _coerce_bool(is_bool)?

Copy link
Author

Choose a reason for hiding this comment

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

You’re right _coerce_bool was just defensive, but as _from_tikz only ever sees data produced by _encode_tikz_metadata, I’ll simplify this to bool(is_bool).

zxlive/common.py Outdated
Comment on lines 172 to 185
def normalize_symbolic_phase_types(graph: GraphT) -> None:
"""Reparse symbolic phases so Var.is_bool matches graph.var_registry."""
for v in graph.vertices():
if graph.type(v) not in (VertexType.Z, VertexType.X):
continue
phase = graph.phase(v)
if not hasattr(phase, "free_vars"):
continue
try:
if not phase.free_vars():
continue
graph.set_phase(v, string_to_phase(str(phase), graph))
except Exception:
continue
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need to parse again and which part of the code makes sure that Var.is_bool matches graph.var_registry? Would graph.rebind_variables_to_registry not suffice instead of normalize_symbolic_phase_types?

Copy link
Author

Choose a reason for hiding this comment

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

Yes graph.rebind_variables_to_registry would work.

Comment on lines 56 to 61
def _parse_bool(value: object) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.strip().lower() in ("true", "1", "yes", "on")
return bool(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 is the same code is _coerce_bool and as I mentioned there, I am not sure if this is really necessary?

Copy link
Author

Choose a reason for hiding this comment

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

Yep, same story as _coerce_bool, just defensive, I’ve simplified it to bool(v) and removed _parse_bool.

if include_internal:
payload = json.dumps({
"graph_json": graph.to_json(),
"variable_types": get_variable_types(graph),
Copy link
Collaborator

Choose a reason for hiding this comment

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

is it necessary to store variable types seperately for json? I thought json from pyzx already includes the correct variable types

Copy link
Author

Choose a reason for hiding this comment

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

Yes. PyZX’s graph_to_dict already puts variable_types in the graph JSON, and from_json restores the registry. Storing it again in the clipboard was redundant.

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.

Thanks for removing the workaround code. Could you please have a look at the above comments?

Comment on lines +155 to +160
def normalize_symbolic_phase_types(graph: GraphT) -> None:
"""Ensure phase variables are consistent with the graph's var_registry."""
# Prefer the library helper if it's available.
rebind = getattr(graph, "rebind_variables_to_registry", None)
if callable(rebind):
rebind()
Copy link
Collaborator

Choose a reason for hiding this comment

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

This method just calls rebind_variables_to_registry in a complicated way. Can you delete this method and replace the instances of normalize_symbolic_phase_types(g) with g.rebind_variables_to_registry?

@RazinShaikh
Copy link
Collaborator

@Bumsparkle Thanks for the changes. Looking at the code now, I realized that the changes made in this PR are of two types:

  1. fixing to_tikz and from_tikz to preserve metadata
  2. making the zxlive UI work with copy/paste where it calls the correct methods and add variables/types in the variable viewer.

The first one should really be in PyZX rather than ZXLive and ZXLive should expect the PyZX methods to preserve metadata appropriately. It turns out that similar issues were open in PyZX already but I didn't realize that they are related.

I think it would be better to move your code related to the first point to PyZX and that would also end up fixing the existing PyZX issue zxcalc/pyzx#367

@Bumsparkle
Copy link
Author

@RazinShaikh That makes a lot of sense. I’m going to be away for today and tomorrow morning, but I’ll get started on splitting these changes out this Sunday late afternoon if that's okay?

@RazinShaikh
Copy link
Collaborator

No worries, I believe the issues need to be closed by Monday end of day for the hackathon

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.

Copy and paste doesn't bring parameters

4 participants