Skip to content

Commit 13af6ee

Browse files
authored
Render tree to include drag-and-drop to move nodes (#388)
* feat: render tree add moving * feat: render tree formatting * feat: render tree formatting * docs: update docs * docs: update CHANGELOG
1 parent e8e8058 commit 13af6ee

File tree

2 files changed

+69
-12
lines changed

2 files changed

+69
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added:
9+
- Tree Render: `render_tree` to support drag-and-drop to shift nodes.
810

911
## [0.29.1] - 2025-05-11
1012
### Added:

bigtree/tree/construct/render.py

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import tkinter as tk
22
from tkinter import ttk
3-
from typing import TYPE_CHECKING, Any
3+
from typing import TYPE_CHECKING, Any, Optional
44

55
from bigtree.node import node
66

@@ -12,6 +12,57 @@
1212
__all__ = ["render_tree"]
1313

1414

15+
class DragDropTree(ttk.Treeview):
16+
def __init__(self, master: tk.Tk, **kwargs: Any):
17+
super().__init__(master, **kwargs)
18+
self.bind("<ButtonPress-1>", self.on_button_press)
19+
self.bind("<B1-Motion>", self.on_motion)
20+
self.bind("<ButtonRelease-1>", self.on_button_release)
21+
self.tag_configure("highlight", background="lightblue")
22+
23+
self._dragging_item: Optional[str] = None
24+
self._drop_target: Optional[str] = None
25+
26+
def on_button_press(self, event: TkEvent) -> None:
27+
"""Assign dragging item to pressed object"""
28+
item = self.identify_row(event.y)
29+
if item:
30+
self._dragging_item = item
31+
32+
def on_motion(self, event: TkEvent) -> None:
33+
"""Highlight drop target"""
34+
if not self._dragging_item:
35+
return
36+
37+
# Highlight drop target
38+
new_target = self.identify_row(event.y)
39+
if new_target != self._drop_target:
40+
self._clear_highlight()
41+
if new_target and new_target != self._dragging_item:
42+
self.item(new_target, tags=("highlight",))
43+
self._drop_target = new_target
44+
45+
def _clear_highlight(self) -> None:
46+
"""Clear highlight"""
47+
if self._drop_target:
48+
self.item(self._drop_target, tags=())
49+
self._drop_target = None
50+
51+
def on_button_release(self, event: TkEvent) -> None:
52+
"""Assign dragging item to first child of drop target"""
53+
if not self._dragging_item:
54+
return
55+
56+
target = self.identify_row(event.y)
57+
if target and target != self._dragging_item:
58+
self.item(target, open=True)
59+
self.move(self._dragging_item, target, 0)
60+
61+
self._clear_highlight()
62+
self._dragging_item = None
63+
self._drop_target = None
64+
65+
1566
class TkinterTree:
1667
def __init__(
1768
self,
@@ -29,11 +80,10 @@ def __init__(
2980
self.counter = 0
3081

3182
root.title(title)
32-
33-
tree = ttk.Treeview(root)
83+
root.minsize(width=400, height=200)
84+
tree = DragDropTree(root)
3485
tree.pack(fill=tk.BOTH, expand=True)
3586

36-
# Hidden entry for inline editing
3787
entry = tk.Entry(root)
3888
entry.bind("<FocusOut>", lambda e: entry.place_forget())
3989
entry.bind("<Return>", self.on_return)
@@ -45,10 +95,14 @@ def __init__(
4595
# Insert nodes
4696
tree_root = tree.insert("", "end", iid=self.get_iid(), text=root_name)
4797

48-
# Add button
49-
tk.Button(root, text="Add Child", command=self.on_plus).pack()
50-
tk.Button(root, text="Print Tree", command=self.print_tree).pack()
51-
tk.Button(root, text="Export Tree", command=self.export_tree).pack()
98+
# Add buttons
99+
button_frame = tk.Frame(root)
100+
button_frame.pack()
101+
b_add = tk.Button(button_frame, text="Add Child", command=self.on_plus)
102+
b_print = tk.Button(button_frame, text="Print Tree", command=self.print_tree)
103+
b_export = tk.Button(button_frame, text="Export Tree", command=self.export_tree)
104+
for button in [b_add, b_print, b_export]:
105+
button.pack(side="left", padx=5)
52106

53107
self.tree = tree
54108
self.tree_root = tree_root
@@ -97,13 +151,13 @@ def _add_child(_node: node.Node, node_iid: str) -> None:
97151

98152
def print_tree(self) -> None:
99153
"""Export tree, print tree to console. Tree can be constructed into a bigtree object using
100-
bigtree.tree.construct.str_to_tree."""
154+
`bigtree.tree.construct.str_to_tree`."""
101155
tree = self.get_tree()
102156
tree.show()
103157

104158
def export_tree(self) -> None:
105159
"""Export tree, print tree dictionary to console. Tree can be constructed into a bigtree object using
106-
bigtree.tree.construct.dict_to_tree"""
160+
`bigtree.tree.construct.dict_to_tree`"""
107161
from pprint import pprint
108162

109163
from bigtree.tree import export
@@ -173,7 +227,7 @@ def render_tree(
173227
title: str = "Tree Render",
174228
root_name: str = "Root",
175229
) -> None:
176-
"""Renders tree with tkinter, exports tree to JSON file.
230+
"""Renders tree in windows pop-up, powered by tkinter. Able to export tree to console.
177231
178232
Viewing Interaction:
179233
@@ -184,6 +238,7 @@ def render_tree(
184238
- Add node: Press "+" / Click "Add Child" button
185239
- Delete node: Press "Delete"
186240
- Rename node: Double click
241+
- Move node: Drag-and-drop to assign as (first) child
187242
188243
Export Interaction:
189244
@@ -195,7 +250,7 @@ def render_tree(
195250
root_name: initial root name of tree
196251
197252
Returns:
198-
Tree render in window pop-up
253+
Tree rendered in window pop-up
199254
"""
200255
root = tk.Tk()
201256
TkinterTree(root, title, root_name)

0 commit comments

Comments
 (0)