Akascape / CTkListbox

A simple listbox for customtkinter (extenstion/add-on)
MIT License
130 stars 14 forks source link

Unable to get drag-and-drop working #67

Open ohshitgorillas opened 1 month ago

ohshitgorillas commented 1 month ago

I'm trying to create a drag-and-drop GUI interface featuring several CTkListboxes, however, I can't get the drag-drop functionality to work. I've bound the items to <ButtonPress-1> but the items never register as being clicked. I get no output from "on_click" print statements unless the click is out of bounds and the response is "Nearest index: None"

This works perfectly with a regular tk listbox (and the code is much neater due to the presence of .nearest()), but I'm not sure why the CTkListbox doesn't work.

Note that on_drop is probably broken, haven't gotten to the point of being able to debug it yet.

import customtkinter as ctk
from CTkListbox import *

class DragDropGUI(ctk.CTk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # Create the list of objects
        objects = ["Object 1", "Object 2", "Object 3", "Object 4"]
        ctk.set_appearance_mode('light')

        # Create the listbox for objects
        self.object_listbox = CTkListbox(self, width=200, height=400)
        for obj in objects:
            self.object_listbox.insert(ctk.END, obj)
        self.object_listbox.pack(side=ctk.LEFT, padx=10, pady=10)

        # Create the bin listboxes
        self.bin1_listbox = CTkListbox(self, width=200, height=400)
        self.bin1_listbox.pack(side=ctk.LEFT, padx=10, pady=10)

        self.bin2_listbox = CTkListbox(self, width=200, height=400)
        self.bin2_listbox.pack(side=ctk.LEFT, padx=10, pady=10)

        self.bin3_listbox = CTkListbox(self, width=200, height=400)
        self.bin3_listbox.pack(side=ctk.LEFT, padx=10, pady=10)

        self.bin4_listbox = CTkListbox(self, width=200, height=400)
        self.bin4_listbox.pack(side=ctk.LEFT, padx=10, pady=10)

        # Enable drag and drop functionality
        self.object_listbox.bind("<ButtonPress-1>", self.on_start_drag)
        self.bin1_listbox.bind("<ButtonPress-1>", self.on_start_drag)
        self.bin2_listbox.bind("<ButtonPress-1>", self.on_start_drag)
        self.bin3_listbox.bind("<ButtonPress-1>", self.on_start_drag)
        self.bin4_listbox.bind("<ButtonPress-1>", self.on_start_drag)

        self.object_listbox.bind("<B1-Motion>", self.on_drag_motion)
        self.bin1_listbox.bind("<B1-Motion>", self.on_drag_motion)
        self.bin2_listbox.bind("<B1-Motion>", self.on_drag_motion)
        self.bin3_listbox.bind("<B1-Motion>", self.on_drag_motion)
        self.bin4_listbox.bind("<B1-Motion>", self.on_drag_motion)

        self.object_listbox.bind("<ButtonRelease-1>", self.on_drop)
        self.bin1_listbox.bind("<ButtonRelease-1>", self.on_drop)
        self.bin2_listbox.bind("<ButtonRelease-1>", self.on_drop)
        self.bin3_listbox.bind("<ButtonRelease-1>", self.on_drop)
        self.bin4_listbox.bind("<ButtonRelease-1>", self.on_drop)

    def on_start_drag(self, event):
        # Get the selected item and its index
        widget = event.widget
        y = event.y
        nearest_index = None
        min_distance = float("inf")

        asdf, height = widget.size()

        for i in range(height):
            item_y, item_height = widget.bbox(i)[1:3]
            item_center_y = item_y + item_height / 2
            distance = abs(y - item_center_y)
            if distance < min_distance:
                min_distance = distance
                nearest_index = i

        print("Nearest index:", nearest_index)

        if nearest_index is not None:
            try:
                self.drag_data = {"widget": widget, "index": nearest_index, "text": widget.get(nearest_index)}
                print("Drag data:", self.drag_data)
                print('item grabbed')
            except AttributeError: # clicks between objects
                self.drag_data = None
        else:
            self.drag_data = None

    def on_drag_motion(self, event):
        # Change the cursor to a hand symbol
        event.widget.config(cursor="hand2")

    def on_drop(self, event):
        # Get the widget we dropped on
        target_widget = self.winfo_containing(event.x_root, event.y_root)

        # Check if the target widget is a listbox and the y-coordinate is within the target widget
        if isinstance(target_widget, CTkListbox) and 0 <= event.y < target_widget.winfo_height():
            # Get the position where the item was dropped
            # Calculate the drop index manually
            drop_index = None
            asdf, height = target_widget.size()
            y = event.y - target_widget.winfo_rooty() - target_widget.winfo_y()
            for i in range(height):
                item_y = target_widget.bbox(i)[1]
                if y < item_y:
                    drop_index = i
                    break
            if drop_index is None:
                drop_index = target_widget.size()

            print("Drop index:", drop_index)

            # Add the dragged item to the target listbox
            target_widget.insert(drop_index, self.drag_data["text"])
            print('item dropped')

            # Remove the item from the original listbox
            self.drag_data["widget"].delete(self.drag_data["index"])

            # force the target listbox to update
            target_widget.activate(drop_index)

            # Reset the drag_data
            self.drag_data = {"widget": None, "index": None, "text": None}

if __name__ == "__main__":
    app = DragDropGUI()
    app.mainloop()
Akascape commented 1 month ago

@ohshitgorillas Drag and drop is not implemented in this ctk listbox, but it can be achieved if you modify the list button by binding different events. I will try to add this feature in next version.

ohshitgorillas commented 1 month ago

As a related enhancement, can you also please add .nearest()?