Skip to content

Commit d2d41d6

Browse files
Merge pull request #18 from tmaier-kettering/copilot/update-file-selection-drag-drop
Copilot/update file selection drag drop
2 parents b165d76 + cfc622f commit d2d41d6

8 files changed

Lines changed: 210 additions & 4 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,6 @@ cython_debug/
205205
marimo/_static/
206206
marimo/_lsp/
207207
__marimo__/
208+
209+
# Test files
210+
test_drag_drop_gui.py

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ CodebookAI is a tool designed to assist qualitative researchers in processing la
55

66
## Getting Started
77

8-
- CodebookAI required you to supply an OpenAI API key in the Settings (File > Settings) ([Get one here](https://platform.openai.com/api-keys)). This keeps CodebookAI open-source and free to use. You control your own API key and are responsible for any costs incurred through your usage of OpenAI's services.
98
- Download the latest .exe ([Get one here](https://github.com/tmaier-kettering/CodebookAI/releases)). This is a standalone application that does not require installation. Just double-click to run. Only tested on Windows 10.
9+
- CodebookAI required you to supply an OpenAI API key in the Settings (File > Settings) ([Get one here](https://platform.openai.com/api-keys)). This keeps CodebookAI open-source and free to use. You control your own API key and are responsible for any costs incurred through your usage of OpenAI's services.
1010
- Not sure what CodebookAI can do? Check out the [Example](wiki/Example/Example.md) section.
1111

1212
## Help & Documentation

file_handling/data_import.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@
2222
import tkinter as tk
2323
from tkinter import filedialog, messagebox, ttk
2424

25+
# Import drag-and-drop support
26+
try:
27+
from ui.drag_drop import enable_file_drop
28+
from tkinterdnd2 import TkinterDnD
29+
_HAS_DND = True
30+
except ImportError:
31+
enable_file_drop = None
32+
_HAS_DND = False
33+
2534
# Optional pandas for robust Excel/CSV reading; gracefully degrade if unavailable
2635
try:
2736
import pandas as _pd # type: ignore
@@ -35,7 +44,13 @@
3544
def _ensure_parent(parent: Optional[tk.Misc]) -> tuple[tk.Misc, Optional[tk.Tk]]:
3645
if parent is not None:
3746
return parent, None
38-
root = tk.Tk()
47+
48+
# Create root with DnD support if available
49+
if _HAS_DND:
50+
root = TkinterDnD.Tk()
51+
else:
52+
root = tk.Tk()
53+
3954
root.withdraw()
4055
try:
4156
root.attributes("-topmost", True)
@@ -230,6 +245,23 @@ def import_data(
230245
file_var = tk.StringVar()
231246
file_entry = ttk.Entry(dlg, textvariable=file_var, width=56)
232247
file_entry.grid(row=1, column=1, padx=4, pady=4, sticky="we")
248+
249+
# Enable drag-and-drop on the file entry
250+
if enable_file_drop is not None:
251+
def _handle_drop(path):
252+
file_var.set(path)
253+
_refresh_preview()
254+
255+
# Extract allowed extensions from filetypes
256+
allowed_extensions = []
257+
for name, pattern in filetypes:
258+
if pattern != "*.*":
259+
# Parse patterns like "*.csv *.txt" or "*.xlsx *.xls"
260+
exts = pattern.replace("*", "").split()
261+
allowed_extensions.extend(exts)
262+
263+
enable_file_drop(file_entry, _handle_drop,
264+
allowed_extensions if allowed_extensions else None)
233265

234266
def browse_file():
235267
path = filedialog.askopenfilename(parent=dlg, title=title, filetypes=list(filetypes))

live_processing/sampler.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
from tkinter import ttk, filedialog, messagebox
44
from typing import Optional
55

6+
# Import drag-and-drop support
7+
try:
8+
from ui.drag_drop import enable_file_drop
9+
except ImportError:
10+
enable_file_drop = None
11+
612
try:
713
import pandas as pd
814
except ImportError:
@@ -164,6 +170,18 @@ def _build_ui(self):
164170
self.entry_path.grid(row=1, column=0, sticky="ew", padx=(0, 8))
165171
btn_browse = ttk.Button(file_frame, text="Browse…", command=self._browse_file)
166172
btn_browse.grid(row=1, column=1, sticky="e")
173+
174+
# Enable drag-and-drop on the entry
175+
if enable_file_drop is not None:
176+
def _handle_drop(path):
177+
self.selected_path = path
178+
self.entry_path.delete(0, tk.END)
179+
self.entry_path.insert(0, path)
180+
self._load_dataframe()
181+
182+
# Extract allowed extensions from FILE_TYPES
183+
allowed_extensions = ['.csv', '.tsv', '.tab', '.xlsx', '.xls', '.parquet']
184+
enable_file_drop(self.entry_path, _handle_drop, allowed_extensions)
167185

168186
file_frame.columnconfigure(0, weight=1)
169187

main.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@
1717

1818
# Import and run the main UI
1919
import tkinter as tk
20+
21+
# Try to import TkinterDnD for drag-and-drop support
22+
try:
23+
from tkinterdnd2 import TkinterDnD
24+
_HAS_DND = True
25+
except ImportError:
26+
_HAS_DND = False
27+
2028
from ui.main_window import build_ui
2129

2230
def _warm_models_cache():
@@ -27,6 +35,12 @@ def _warm_models_cache():
2735

2836
if __name__ == "__main__":
2937
threading.Thread(target=_warm_models_cache, daemon=True).start()
30-
root = tk.Tk()
38+
39+
# Create root window with drag-and-drop support if available
40+
if _HAS_DND:
41+
root = TkinterDnD.Tk()
42+
else:
43+
root = tk.Tk()
44+
3145
build_ui(root)
3246
root.mainloop()

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ matplotlib>=3.7.0 # Data visualization
1010

1111
# System integration
1212
keyring>=25.0.0 # Secure credential storage across platforms
13+
tkinterdnd2>=0.4.0 # Drag-and-drop support for file selection on Windows
1314

1415
# Type hints support for older Python versions
1516
typing-extensions>=4.0.0 # Enhanced type annotations (backport features)

ui/drag_drop.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""
2+
Drag-and-drop utility for file selection widgets.
3+
4+
Provides a reusable function to enable drag-and-drop file selection on tkinter widgets,
5+
specifically for Windows Explorer file drag-and-drop support.
6+
"""
7+
8+
import os
9+
from typing import Callable, Optional
10+
11+
try:
12+
from tkinterdnd2 import DND_FILES, TkinterDnD
13+
_HAS_DND = True
14+
except ImportError:
15+
_HAS_DND = False
16+
17+
18+
def enable_file_drop(widget, callback: Callable[[str], None],
19+
file_types: Optional[list[str]] = None) -> bool:
20+
"""
21+
Enable drag-and-drop file selection on a tkinter widget.
22+
23+
Args:
24+
widget: The tkinter widget to enable drag-and-drop on
25+
callback: Function to call with the file path when a file is dropped
26+
file_types: Optional list of allowed file extensions (e.g., ['.csv', '.xlsx'])
27+
If None, all file types are allowed
28+
29+
Returns:
30+
True if drag-and-drop was successfully enabled, False otherwise
31+
32+
Example:
33+
>>> import tkinter as tk
34+
>>> entry = tk.Entry(root)
35+
>>> enable_file_drop(entry, lambda path: entry_var.set(path))
36+
"""
37+
if not _HAS_DND:
38+
return False
39+
40+
def _on_drop(event):
41+
"""Handle the drop event."""
42+
# The event.data can be a list of files or a single file path
43+
# It comes in a format like: {path1} {path2} or just path
44+
# We need to parse it correctly
45+
files = event.data
46+
47+
# Handle different formats
48+
if isinstance(files, (list, tuple)):
49+
# Already a list
50+
file_list = files
51+
else:
52+
# Parse the string - files can be separated by spaces or in curly braces
53+
file_list = []
54+
if files.startswith('{'):
55+
# Format: {path1} {path2}
56+
import re
57+
file_list = re.findall(r'\{([^}]+)\}', files)
58+
else:
59+
# Simple space-separated or single file
60+
# But be careful with paths containing spaces
61+
if os.path.exists(files.strip()):
62+
file_list = [files.strip()]
63+
else:
64+
# Try to split, but this might not work with spaces in paths
65+
file_list = [f.strip() for f in files.split() if f.strip()]
66+
67+
# Get the first valid file
68+
for file_path in file_list:
69+
file_path = file_path.strip().strip('{}')
70+
71+
# Validate file exists
72+
if not os.path.exists(file_path):
73+
continue
74+
75+
# Validate file type if specified
76+
if file_types:
77+
ext = os.path.splitext(file_path)[1].lower()
78+
if ext not in file_types:
79+
continue
80+
81+
# Call the callback with the first valid file
82+
callback(file_path)
83+
break
84+
85+
try:
86+
# Register the widget for file drop
87+
widget.drop_target_register(DND_FILES)
88+
widget.dnd_bind('<<Drop>>', _on_drop)
89+
return True
90+
except Exception:
91+
return False
92+
93+
94+
def make_window_dnd_compatible(window):
95+
"""
96+
Make a tkinter window compatible with drag-and-drop.
97+
98+
This should be called on the root window or Toplevel to enable DnD support.
99+
For regular tk.Tk() or tk.Toplevel(), this will upgrade them to support DnD.
100+
101+
Args:
102+
window: A tk.Tk() or tk.Toplevel() window
103+
104+
Returns:
105+
The window (potentially upgraded to TkinterDnD version)
106+
107+
Note:
108+
If using TkinterDnD, you should create your root window as TkinterDnD.Tk()
109+
instead of tk.Tk() for best results. This function is a helper for existing windows.
110+
"""
111+
if not _HAS_DND:
112+
return window
113+
114+
# If the window is already DnD-compatible, return it as-is
115+
if hasattr(window, 'drop_target_register'):
116+
return window
117+
118+
# Otherwise, we can't upgrade an existing window easily
119+
# Just return the original window
120+
return window

ui/two_page_wizard.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
import pandas as pd
99
from ui.ui_utils import center_window # keep your existing helper
1010

11+
# Import drag-and-drop support
12+
try:
13+
from ui.drag_drop import enable_file_drop
14+
except ImportError:
15+
enable_file_drop = None
16+
1117

1218
# ----------------------------- Utility: Data Loading -----------------------------
1319

@@ -183,8 +189,20 @@ def __init__(self, master, page_index: int, total_pages: int, title: str,
183189
ttk.Button(file_row, text="Choose file…", command=self._choose_file).pack(side="left")
184190
ttk.Checkbutton(file_row, text="Has header row", variable=self.has_header, command=self._reload_if_possible)\
185191
.pack(side="left", padx=12)
186-
self.path_label = ttk.Label(file_row, text="", foreground="#555")
192+
self.path_label = ttk.Label(file_row, text="(drag file here or use Choose file button)", foreground="#555")
187193
self.path_label.pack(side="left", padx=8)
194+
195+
# Enable drag-and-drop on the path label
196+
if enable_file_drop is not None:
197+
def _handle_drop(path):
198+
self.current_path = path
199+
self.path_label.configure(text=os.path.basename(path))
200+
self.name_selector.set_file_name(path)
201+
self._load_and_preview(path)
202+
203+
# Allow common data file types
204+
allowed_extensions = ['.csv', '.tsv', '.txt', '.xlsx', '.xls', '.xlsm']
205+
enable_file_drop(self.path_label, _handle_drop, allowed_extensions)
188206

189207
# Preview (top 5)
190208
preview = ttk.LabelFrame(self, text="Preview (top 5)")

0 commit comments

Comments
 (0)