Friday, December 13, 2024

My own Mind Map program in Java script and Python

I had been searching online mindmap apps for my study for a while and never got one that I am really happy with. 

Then I asked myself what I really need for a mind map to do? My requirements are pretty simple:

It allows me to put in text and create child nodes. It can be saved and re-opened for later editing. I should be able to print it as a picture/pdf.

With some help from ChatGPT, I was actually able to create one that works very well so I am sharing it with all of you here.




Full source code in Python.


import tkinter as tk

from tkinter import filedialog

import json



class MindMapApp:

    def __init__(self, root):

        self.root = root

        self.root.title("Mind Map Tool")

        self.root.geometry("800x600")


        # Main canvas

        self.canvas = tk.Canvas(root, width=780, height=500, bg="lightgray")

        self.canvas.pack(pady=10)


        # Buttons

        button_frame = tk.Frame(root)

        button_frame.pack()

        tk.Button(button_frame, text="Add Text Box", command=lambda: self.add_text_box()).pack(side=tk.LEFT, padx=5)

        tk.Button(button_frame, text="Save", command=self.save_mind_map).pack(side=tk.LEFT, padx=5)

        tk.Button(button_frame, text="Open", command=self.open_mind_map).pack(side=tk.LEFT, padx=5)


        self.text_boxes = []  # List to track all text boxes

        self.connections = []  # List to track parent-child relationships for drawing lines


    def add_text_box(self, parent=None, x=None, y=None, text=""):

        """Add a new text box. If a parent is provided, connect the sub-note to it."""

        if x is None or y is None:

            # Add a top-level text box with default placement

            x, y = 50 + len(self.text_boxes) * 30, 50 + len(self.text_boxes) * 30


        # Create the text box and "+" button

        new_text_box = tk.Text(self.root, width=20, height=2, wrap="word")

        new_text_box.place(x=x, y=y)

        new_text_box.insert("1.0", text)

        new_text_box.bind("<KeyRelease>", lambda e: self.adjust_text_box_size(new_text_box))

        new_text_box.focus_set()  # Automatically set focus to the new text box


        add_button = tk.Button(self.root, text="+", command=lambda: self.add_text_box(new_text_box))

        add_button.place(x=x - 30, y=y)


        # Link the "+" button to the text box

        new_text_box._add_button = add_button


        self.text_boxes.append(new_text_box)


        # Add dragging functionality

        self.make_draggable(new_text_box)


        # Add right-click menu for deletion

        self.add_context_menu(new_text_box)


        # Draw a curved, dotted line connecting parent and child

        if parent is not None:

            # Determine non-overlapping position

            x, y = self.find_non_overlapping_position(parent)

            line_id = self.canvas.create_line(

                parent.winfo_x() + 90, parent.winfo_y() + 10,  # Parent's right center

                x, y + 10,  # Child's left center

                fill="black",

                dash=(4, 2),

                smooth=True,

            )

            self.connections.append((parent, new_text_box, line_id))


    def find_non_overlapping_position(self, parent):

        """Find a non-overlapping position for a new child text box."""

        x = parent.winfo_x() + 150

        y = parent.winfo_y()

        while any(

            abs(x - t.winfo_x()) < 100 and abs(y - t.winfo_y()) < 50

            for t in self.text_boxes

        ):

            y += 50  # Move down until a free space is found

        return x, y


    def adjust_text_box_size(self, text_box):

        """Adjust the size of the text box based on its content."""

        content = text_box.get("1.0", "end-1c")

        lines = content.split("\n")

        width = max(len(line) for line in lines)

        height = len(lines)

        text_box.config(width=max(20, width), height=max(2, height))


    def make_draggable(self, widget):

        """Make a widget draggable."""

        def start_drag(event):

            widget._drag_start_x = event.x

            widget._drag_start_y = event.y


        def drag(event):

            # Calculate new position

            new_x = widget.winfo_x() + event.x - widget._drag_start_x

            new_y = widget.winfo_y() + event.y - widget._drag_start_y

            widget.place(x=new_x, y=new_y)


            # Update "+" button position

            if hasattr(widget, "_add_button"):

                widget._add_button.place(x=new_x - 30, y=new_y)


            # Redraw all lines

            self.redraw_lines()


        widget.bind("<Button-1>", start_drag)

        widget.bind("<B1-Motion>", drag)


    def add_context_menu(self, widget):

        """Add a right-click context menu to delete the widget."""

        menu = tk.Menu(self.root, tearoff=0)

        menu.add_command(label="Delete", command=lambda: self.delete_text_box(widget))


        def show_context_menu(event):

            menu.tk_popup(event.x_root, event.y_root)


        widget.bind("<Button-3>", show_context_menu)


    def delete_text_box(self, widget):

        """Delete a text box, its "+" button, and associated lines."""

        # Remove associated "+" button

        if hasattr(widget, "_add_button"):

            widget._add_button.destroy()


        # Remove connections and lines related to the widget

        for connection in self.connections[:]:

            parent, child, line_id = connection

            if parent == widget or child == widget:

                self.canvas.delete(line_id)

                self.connections.remove(connection)


        # Destroy the widget

        widget.destroy()


        # Remove from text_boxes list

        self.text_boxes.remove(widget)


    def redraw_lines(self):

        """Redraw all lines to keep connections intact when widgets are moved."""

        for parent, child, line_id in self.connections:

            # Update line coordinates based on the current positions

            self.canvas.coords(

                line_id,

                parent.winfo_x() + 90, parent.winfo_y() + 10,  # Parent's right center

                child.winfo_x(), child.winfo_y() + 10,  # Child's left center

            )


    def save_mind_map(self):

        """Save the current mind map to a JSON file."""

        mind_map = []

        for text_box in self.text_boxes:

            mind_map.append({

                "x": text_box.winfo_x(),

                "y": text_box.winfo_y(),

                "text": text_box.get("1.0", "end-1c"),

            })


        connections = []

        for parent, child, _ in self.connections:

            connections.append({

                "parent_index": self.text_boxes.index(parent),

                "child_index": self.text_boxes.index(child),

            })


        data = {"text_boxes": mind_map, "connections": connections}


        file_path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON files", "*.json")])

        if file_path:

            with open(file_path, "w") as file:

                json.dump(data, file)


    def open_mind_map(self):

        """Open a mind map from a JSON file."""

        file_path = filedialog.askopenfilename(filetypes=[("JSON files", "*.json")])

        if file_path:

            with open(file_path, "r") as file:

                data = json.load(file)


            # Clear current mind map

            for text_box in self.text_boxes[:]:

                self.delete_text_box(text_box)


            # Add text boxes

            for item in data["text_boxes"]:

                self.add_text_box(x=item["x"], y=item["y"], text=item["text"])


            # Add connections

            for conn in data["connections"]:

                parent = self.text_boxes[conn["parent_index"]]

                child = self.text_boxes[conn["child_index"]]

                line_id = self.canvas.create_line(

                    parent.winfo_x() + 90, parent.winfo_y() + 10,

                    child.winfo_x(), child.winfo_y() + 10,

                    fill="black", dash=(4, 2), smooth=True

                )

                self.connections.append((parent, child, line_id))



# Run the application

root = tk.Tk()

app = MindMapApp(root)


# Periodically redraw lines to ensure they're updated

def periodic_redraw():

    app.redraw_lines()

    root.after(100, periodic_redraw)



periodic_redraw()

root.mainloop()



My own Mind Map program in Java script and Python

I had been searching online mindmap apps for my study for a while and never got one that I am really happy with.  Then I asked myself what I...