Python
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
import datetime
import json
import os
class ModernTodoApp:
def __init__(self, root):
self.root = root
self.root.title("To-Do-List")
self.root.geometry("800x600")
self.root.configure(bg="#121212")
self.root.resizable(True, True)
# Set up data file
self.data_file = "todo_data.json"
# Modern color scheme
self.dark_bg = "#121212"
self.card_bg = "#1e1e1e"
self.light_text = "#ffffff"
self.muted_text = "#aaaaaa"
self.primary_color = "#bb86fc"
self.secondary_color = "#03dac6"
self.accent_color = "#cf6679"
self.completed_color = "#4CAF50"
# Set up app fonts
self.title_font = ("Segoe UI", 28, "bold")
self.header_font = ("Segoe UI", 14)
self.normal_font = ("Segoe UI", 12)
self.small_font = ("Segoe UI", 10)
# Create tasks list
self.tasks = []
# Create UI first - must be done before notification attempts
self.create_widgets()
# Now load tasks since UI is ready
self.load_tasks()
# Save tasks when closing the app
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def load_tasks(self):
"""Load tasks from JSON file if it exists"""
if os.path.exists(self.data_file):
try:
with open(self.data_file, 'r') as f:
data = json.load(f)
# Convert date strings back to datetime objects
for task in data:
if 'created' in task:
task['created'] = datetime.datetime.strptime(task['created'], "%Y-%m-%d %H:%M:%S")
self.tasks = data
self.show_notification(f"Loaded {len(self.tasks)} saved tasks")
except Exception as e:
self.show_notification(f"Error loading tasks: {str(e)}")
self.tasks = []
else:
self.show_notification("No saved tasks found")
def save_tasks(self):
"""Save tasks to JSON file"""
try:
# Convert datetime objects to strings for JSON serialization
tasks_to_save = []
for task in self.tasks:
task_copy = task.copy()
if 'created' in task_copy:
task_copy['created'] = task_copy['created'].strftime("%Y-%m-%d %H:%M:%S")
tasks_to_save.append(task_copy)
with open(self.data_file, 'w') as f:
json.dump(tasks_to_save, f, indent=2)
self.show_notification("Tasks saved successfully")
return True
except Exception as e:
self.show_notification(f"Error saving tasks: {str(e)}")
return False
def on_closing(self):
"""Handle window closing event"""
if self.save_tasks():
self.root.destroy()
def create_widgets(self):
# Header frame
header_frame = tk.Frame(self.root, bg=self.dark_bg)
header_frame.pack(fill=tk.X, padx=30, pady=(30, 15))
# Title label
title_label = tk.Label(
header_frame,
text="To-Do-List",
font=self.title_font,
fg=self.light_text,
bg=self.dark_bg,
)
title_label.pack(side=tk.LEFT, anchor="w")
# Stats label
self.stats_label = tk.Label(
header_frame,
text=f"{len(self.tasks)} tasks",
font=self.small_font,
fg=self.muted_text,
bg=self.dark_bg,
)
self.stats_label.pack(side=tk.RIGHT, anchor="e", padx=10)
# Input frame
input_frame = tk.Frame(self.root, bg=self.dark_bg)
input_frame.pack(fill=tk.X, padx=30, pady=(0, 20))
# Task entry
self.task_entry = tk.Entry(
input_frame,
font=self.normal_font,
bg=self.card_bg,
fg=self.light_text,
insertbackground=self.light_text,
relief=tk.FLAT,
width=40
)
self.task_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=8, padx=(0, 10))
self.task_entry.bind("", self.add_task_from_entry)
self.task_entry.focus_set()
# Add placeholder text
self.task_entry.insert(0, "Add a new task...")
self.task_entry.bind("", lambda e: self.clear_placeholder() if self.task_entry.get() == "Add a new task..." else None)
self.task_entry.bind("", lambda e: self.restore_placeholder() if not self.task_entry.get() else None)
# Add task button
add_btn = tk.Button(
input_frame,
text="+ Add",
font=self.normal_font,
bg=self.primary_color,
fg="#000000",
activebackground="#d0a0ff",
relief=tk.FLAT,
command=self.add_task_from_entry,
padx=15,
cursor="hand2"
)
add_btn.pack(side=tk.LEFT)
# Filter frame
filter_frame = tk.Frame(self.root, bg=self.dark_bg)
filter_frame.pack(fill=tk.X, padx=30, pady=(0, 15))
filter_label = tk.Label(
filter_frame,
text="Filter tasks:",
font=self.small_font,
bg=self.dark_bg,
fg=self.muted_text
)
filter_label.pack(side=tk.LEFT, padx=(0, 15))
# Filter options with modern toggle
self.filter_var = tk.StringVar(value="all")
filter_btn_all = tk.Button(
filter_frame,
text="All",
font=self.small_font,
bg=self.card_bg if self.filter_var.get() == "all" else self.dark_bg,
fg=self.light_text if self.filter_var.get() == "all" else self.muted_text,
relief=tk.FLAT,
command=lambda: self.set_filter("all"),
padx=10,
cursor="hand2"
)
filter_btn_all.pack(side=tk.LEFT, padx=(0, 10))
filter_btn_active = tk.Button(
filter_frame,
text="Active",
font=self.small_font,
bg=self.card_bg if self.filter_var.get() == "active" else self.dark_bg,
fg=self.light_text if self.filter_var.get() == "active" else self.muted_text,
relief=tk.FLAT,
command=lambda: self.set_filter("active"),
padx=10,
cursor="hand2"
)
filter_btn_active.pack(side=tk.LEFT, padx=(0, 10))
filter_btn_completed = tk.Button(
filter_frame,
text="Completed",
font=self.small_font,
bg=self.card_bg if self.filter_var.get() == "completed" else self.dark_bg,
fg=self.light_text if self.filter_var.get() == "completed" else self.muted_text,
relief=tk.FLAT,
command=lambda: self.set_filter("completed"),
padx=10,
cursor="hand2"
)
filter_btn_completed.pack(side=tk.LEFT)
# Save filter buttons for updating
self.filter_buttons = {
"all": filter_btn_all,
"active": filter_btn_active,
"completed": filter_btn_completed
}
# Tasks frame with scrollbar
self.canvas = tk.Canvas(self.root, bg=self.dark_bg, highlightthickness=0)
self.scrollbar = ttk.Scrollbar(self.root, orient="vertical", command=self.canvas.yview)
self.scrollable_frame = tk.Frame(self.canvas, bg=self.dark_bg)
self.scrollable_frame.bind(
"",
lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all"))
)
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
self.canvas.configure(yscrollcommand=self.scrollbar.set)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(30, 0))
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y, padx=(0, 30))
# Bind mousewheel for scrolling
self.canvas.bind_all("", self.on_mousewheel)
# Empty state
self.empty_state_label = tk.Label(
self.scrollable_frame,
text="No tasks yet\nAdd your first task to get started",
font=self.header_font,
fg=self.muted_text,
bg=self.dark_bg,
justify="center",
pady=50
)
# Footer with action buttons
footer_frame = tk.Frame(self.root, bg=self.dark_bg, height=40)
footer_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=(0, 10), padx=30)
# Clear completed button
tk.Button(
footer_frame,
text="Clear Completed",
font=self.small_font,
bg=self.dark_bg,
fg=self.muted_text,
activebackground="#2a2a2a",
relief=tk.FLAT,
command=self.clear_completed,
cursor="hand2"
).pack(side=tk.LEFT, padx=(0, 15))
# Export tasks button
tk.Button(
footer_frame,
text="Export Tasks",
font=self.small_font,
bg=self.dark_bg,
fg=self.muted_text,
activebackground="#2a2a2a",
relief=tk.FLAT,
command=self.export_tasks,
cursor="hand2"
).pack(side=tk.LEFT)
# Save tasks button
tk.Button(
footer_frame,
text="Save Now",
font=self.small_font,
bg=self.dark_bg,
fg=self.primary_color,
activebackground="#2a2a2a",
relief=tk.FLAT,
command=self.save_tasks,
cursor="hand2"
).pack(side=tk.LEFT, padx=(15, 0))
# Add search functionality
search_frame = tk.Frame(footer_frame, bg=self.dark_bg)
search_frame.pack(side=tk.RIGHT)
self.search_var = tk.StringVar()
self.search_var.trace("w", self.update_task_list)
search_entry = tk.Entry(
search_frame,
textvariable=self.search_var,
font=self.small_font,
bg=self.card_bg,
fg=self.light_text,
relief=tk.FLAT,
width=20
)
search_entry.pack(side=tk.LEFT, padx=(0, 5), ipady=2)
search_entry.insert(0, "Search tasks...")
search_entry.bind("", lambda e: search_entry.delete(0, "end") if search_entry.get() == "Search tasks..." else None)
search_entry.bind("", lambda e: search_entry.insert(0, "Search tasks...") if search_entry.get() == "" else None)
# Notification system - MUST be created last in the UI
self.notification_label = tk.Label(
self.root,
text="",
font=self.small_font,
bg=self.dark_bg,
fg=self.primary_color
)
self.notification_label.pack(side=tk.BOTTOM, fill=tk.X, pady=5)
# Update task list to show initial state
self.update_task_list()
def clear_placeholder(self):
if self.task_entry.get() == "Add a new task...":
self.task_entry.delete(0, "end")
self.task_entry.config(fg=self.light_text)
def restore_placeholder(self):
if not self.task_entry.get():
self.task_entry.insert(0, "Add a new task...")
self.task_entry.config(fg=self.muted_text)
def set_filter(self, filter_type):
self.filter_var.set(filter_type)
self.update_filter_buttons()
self.update_task_list()
def update_filter_buttons(self):
for filter_type, button in self.filter_buttons.items():
if filter_type == self.filter_var.get():
button.config(bg=self.card_bg, fg=self.light_text)
else:
button.config(bg=self.dark_bg, fg=self.muted_text)
def add_task(self, task_text):
if task_text.strip() and task_text != "Add a new task...":
task = {"task": task_text, "completed": False, "created": datetime.datetime.now()}
self.tasks.append(task)
self.update_task_list()
self.show_notification(f"Task added: {task_text}")
self.task_entry.delete(0, tk.END)
self.restore_placeholder()
self.save_tasks() # Auto-save after adding
def add_task_from_entry(self, event=None):
task_text = self.task_entry.get()
self.add_task(task_text)
def mark_task_complete(self, index):
if 0 <= index < len(self.tasks):
self.tasks[index]["completed"] = not self.tasks[index]["completed"]
status = "completed" if self.tasks[index]["completed"] else "reopened"
self.update_task_list()
self.show_notification(f"Task marked as {status}: {self.tasks[index]['task']}")
self.save_tasks() # Auto-save after status change
def edit_task(self, index):
if 0 <= index < len(self.tasks):
new_text = simpledialog.askstring(
"Edit Task",
"Edit your task:",
initialvalue=self.tasks[index]["task"]
)
if new_text and new_text.strip():
self.tasks[index]["task"] = new_text.strip()
self.update_task_list()
self.show_notification("Task updated")
self.save_tasks() # Auto-save after edit
def delete_task(self, index):
if 0 <= index < len(self.tasks):
task_text = self.tasks[index]["task"]
del self.tasks[index]
self.update_task_list()
self.show_notification(f"Task deleted: {task_text}")
self.save_tasks() # Auto-save after delete
def clear_completed(self):
initial_count = len(self.tasks)
self.tasks = [task for task in self.tasks if not task["completed"]]
removed_count = initial_count - len(self.tasks)
self.update_task_list()
if removed_count:
self.show_notification(f"Removed {removed_count} completed tasks")
self.save_tasks() # Auto-save after clearing
def export_tasks(self):
export_text = "Modern To-Do List Export\n" + "="*30 + "\n"
export_text += f"Generated on: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}\n"
export_text += f"Total tasks: {len(self.tasks)}\n\n"
for i, task in enumerate(self.tasks):
status = "✓" if task["completed"] else "✗"
created = task["created"].strftime("%b %d, %H:%M")
export_text += f"{i+1}. [{status}] {task['task']} ({created})\n"
# In a real app, you would save this to a file
messagebox.showinfo(
"Export Tasks",
"Tasks exported to clipboard!\n\n" + export_text
)
self.root.clipboard_clear()
self.root.clipboard_append(export_text)
self.show_notification("Tasks copied to clipboard")
def update_task_list(self, *args):
# Remove the empty state label if tasks exist
if self.tasks:
self.empty_state_label.pack_forget()
# Clear the scrollable frame
for widget in self.scrollable_frame.winfo_children():
if widget != self.empty_state_label:
widget.destroy()
# Show empty state if no tasks
if not self.tasks:
self.empty_state_label.pack(fill=tk.X, pady=50)
self.stats_label.config(text="0 tasks")
return
# Apply filters
filter_type = self.filter_var.get()
search_text = self.search_var.get().lower()
filtered_tasks = []
for task in self.tasks:
# Apply search filter
if search_text and search_text != "search tasks..." and search_text not in task["task"].lower():
continue
# Apply status filter
if filter_type == "all":
filtered_tasks.append(task)
elif filter_type == "active" and not task["completed"]:
filtered_tasks.append(task)
elif filter_type == "completed" and task["completed"]:
filtered_tasks.append(task)
# Update stats
total_tasks = len(self.tasks)
completed_tasks = sum(1 for task in self.tasks if task["completed"])
self.stats_label.config(text=f"{total_tasks} tasks ({completed_tasks} completed)")
# Add tasks to the scrollable frame
for i, task in enumerate(filtered_tasks):
task_frame = tk.Frame(
self.scrollable_frame,
bg=self.card_bg,
padx=15,
pady=12,
highlightthickness=0
)
task_frame.pack(fill=tk.X, pady=5)
# Create a custom checkbox
checkbox = tk.Label(
task_frame,
text="✓" if task["completed"] else " ",
font=("Segoe UI", 14),
bg=self.card_bg,
fg=self.primary_color,
width=2,
height=1,
relief="solid",
bd=1,
highlightbackground="#333333"
)
checkbox.grid(row=0, column=0, rowspan=2, padx=(0, 15), sticky="n")
checkbox.bind("", lambda e, idx=i: self.mark_task_complete(self.get_task_index(filtered_tasks[idx])))
checkbox.config(cursor="hand2")
# Task text
task_text = tk.Label(
task_frame,
text=task["task"],
font=self.normal_font,
bg=self.card_bg,
fg=self.light_text if not task["completed"] else "#666666",
anchor="w",
wraplength=600,
justify="left"
)
if task["completed"]:
task_text.config(font=("Segoe UI", 12, "overstrike"))
task_text.grid(row=0, column=1, sticky="w")
# Task metadata
created_str = task["created"].strftime("%b %d, %H:%M")
meta_label = tk.Label(
task_frame,
text=created_str,
font=self.small_font,
bg=self.card_bg,
fg=self.muted_text,
anchor="w"
)
meta_label.grid(row=1, column=1, sticky="w", pady=(5, 0))
# Task actions
actions_frame = tk.Frame(
task_frame,
bg=self.card_bg
)
actions_frame.grid(row=0, column=2, rowspan=2, sticky="e")
# Edit button
edit_btn = tk.Label(
actions_frame,
text="✎",
font=("Segoe UI", 12),
fg=self.muted_text,
bg=self.card_bg,
cursor="hand2"
)
edit_btn.pack(side=tk.LEFT, padx=(0, 15))
edit_btn.bind("", lambda e, idx=i: self.edit_task(self.get_task_index(filtered_tasks[idx])))
# Delete button
delete_btn = tk.Label(
actions_frame,
text="✕",
font=("Segoe UI", 14),
fg=self.accent_color,
bg=self.card_bg,
cursor="hand2"
)
delete_btn.pack(side=tk.LEFT)
delete_btn.bind("", lambda e, idx=i: self.delete_task(self.get_task_index(filtered_tasks[idx])))
def get_task_index(self, task):
return self.tasks.index(task)
def show_notification(self, message):
if hasattr(self, 'notification_label'): # Safeguard
self.notification_label.config(text=message)
self.root.after(3000, lambda: self.notification_label.config(text=""))
def on_mousewheel(self, event):
self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")
def run(self):
self.root.mainloop()
if __name__ == "__main__":
root = tk.Tk()
app = ModernTodoApp(root)
app.run()