569 lines
21 KiB
Python
569 lines
21 KiB
Python
import tkinter as tk
|
|
from tkinter import ttk, scrolledtext, messagebox, filedialog
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
import webbrowser
|
|
try:
|
|
from tkinterweb import HtmlFrame # For markdown preview
|
|
except ImportError:
|
|
# Fallback if tkinterweb is not available
|
|
HtmlFrame = None
|
|
|
|
# For syntax highlighting
|
|
try:
|
|
from pygments import lex
|
|
from pygments.lexers import PythonLexer
|
|
from pygments.styles import get_style_by_name
|
|
PYGMENTS_AVAILABLE = True
|
|
except ImportError:
|
|
PYGMENTS_AVAILABLE = False
|
|
|
|
class CodeEditor(scrolledtext.ScrolledText):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.bind('<Tab>', self._tab)
|
|
self.bind('<Shift-Tab>', self._shift_tab)
|
|
self.bind('<KeyRelease>', self._on_key_release)
|
|
self.configure(font=('Consolas', 10))
|
|
|
|
# Setup tags for syntax highlighting if available
|
|
if PYGMENTS_AVAILABLE:
|
|
self._setup_tags()
|
|
|
|
def _tab(self, event):
|
|
self.insert(tk.INSERT, " " * 4)
|
|
return "break" # Prevent default behavior
|
|
|
|
def _shift_tab(self, event):
|
|
# Remove indentation
|
|
sel_start = self.index(tk.SEL_FIRST)
|
|
sel_end = self.index(tk.SEL_LAST)
|
|
|
|
if sel_start and sel_end:
|
|
# Get the lines in the selection
|
|
start_line = int(sel_start.split('.')[0])
|
|
end_line = int(sel_end.split('.')[0])
|
|
|
|
for line_num in range(start_line, end_line + 1):
|
|
line_start = f"{line_num}.0"
|
|
line_end = f"{line_num}.end"
|
|
line_text = self.get(line_start, line_end)
|
|
|
|
if line_text.startswith(" " * 4):
|
|
self.delete(line_start, f"{line_num}.4")
|
|
elif line_text.startswith("\t"):
|
|
self.delete(line_start, f"{line_num}.1")
|
|
|
|
return "break"
|
|
|
|
def _on_key_release(self, event):
|
|
if event.keysym == 'Return':
|
|
# Auto-indent new line
|
|
current_line = self.get("insert linestart", "insert")
|
|
indent = len(current_line) - len(current_line.lstrip())
|
|
self.insert("insert", " " * indent)
|
|
|
|
# Syntax highlighting if available
|
|
if PYGMENTS_AVAILABLE:
|
|
self.highlight_syntax()
|
|
|
|
def _setup_tags(self):
|
|
try:
|
|
self.style = get_style_by_name('default')
|
|
|
|
# Create tags for different token types
|
|
for token, style in self.style:
|
|
color = style.get('color', '')
|
|
if color and self._is_valid_color(color):
|
|
self.tag_configure(str(token), foreground=f'#{color}')
|
|
except Exception as e:
|
|
print(f"Error setting up syntax highlighting: {e}")
|
|
|
|
def _is_valid_color(self, color_str):
|
|
"""Check if a string is a valid hex color"""
|
|
if not color_str:
|
|
return False
|
|
# Remove # if present
|
|
if color_str.startswith('#'):
|
|
color_str = color_str[1:]
|
|
# Check if it's a valid hex color (3 or 6 digits)
|
|
if len(color_str) not in (3, 6):
|
|
return False
|
|
try:
|
|
int(color_str, 16)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
def highlight_syntax(self):
|
|
if not PYGMENTS_AVAILABLE:
|
|
return
|
|
|
|
try:
|
|
# Remove previous highlighting
|
|
for tag in self.tag_names():
|
|
if tag != "sel":
|
|
self.tag_remove(tag, "1.0", "end")
|
|
|
|
# Get the text
|
|
code = self.get("1.0", "end-1c")
|
|
|
|
# Lex the code and apply tags
|
|
pos = 0
|
|
for token, text in lex(code, PythonLexer()):
|
|
if text.strip(): # Only apply tags to non-whitespace text
|
|
start = f"1.0+{pos}c"
|
|
end = f"1.0+{pos+len(text)}c"
|
|
self.tag_add(str(token), start, end)
|
|
pos += len(text)
|
|
except Exception as e:
|
|
print(f"Error during syntax highlighting: {e}")
|
|
|
|
class MarkdownEditor:
|
|
def __init__(self, parent):
|
|
self.parent = parent
|
|
|
|
# Create a paned window for split view
|
|
self.paned = ttk.PanedWindow(parent, orient=tk.HORIZONTAL)
|
|
self.paned.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Left side - text editor
|
|
self.editor_frame = ttk.Frame(self.paned)
|
|
self.editor = scrolledtext.ScrolledText(
|
|
self.editor_frame, wrap=tk.WORD, font=('Consolas', 10)
|
|
)
|
|
self.editor.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Right side - preview (if available)
|
|
self.preview_frame = ttk.Frame(self.paned)
|
|
if HtmlFrame:
|
|
self.preview = HtmlFrame(self.preview_frame)
|
|
self.preview.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Add both frames to the paned window
|
|
self.paned.add(self.editor_frame, weight=1)
|
|
self.paned.add(self.preview_frame, weight=1)
|
|
|
|
# Bind key events
|
|
self.editor.bind('<KeyRelease>', self.update_preview)
|
|
else:
|
|
# If tkinterweb is not available, just show the editor
|
|
self.paned.add(self.editor_frame, weight=1)
|
|
self.preview = None
|
|
|
|
def update_preview(self, event=None):
|
|
if not self.preview:
|
|
return
|
|
|
|
# Get the markdown text
|
|
markdown_text = self.editor.get("1.0", "end-1c")
|
|
|
|
# Convert to HTML (simple conversion for demonstration)
|
|
# In a real application, you might want to use a markdown library
|
|
html_content = f"""
|
|
<html>
|
|
<head>
|
|
<style>
|
|
body {{ font-family: Arial, sans-serif; padding: 10px; }}
|
|
code {{ background-color: #f4f4f4; padding: 2px 4px; border-radius: 3px; }}
|
|
pre {{ background-color: #f4f4f4; padding: 10px; border-radius: 5px; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{self._simple_markdown_to_html(markdown_text)}
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
# Update the preview
|
|
self.preview.load_html(html_content)
|
|
|
|
def _simple_markdown_to_html(self, text):
|
|
# Very basic markdown to HTML conversion
|
|
# For a real application, consider using a proper markdown library
|
|
html = text
|
|
html = html.replace("# ", "<h1>").replace("\n# ", "</h1>\n<h1>") + "</h1>"
|
|
html = html.replace("## ", "<h2>").replace("\n## ", "</h2>\n<h2>") + "</h2>"
|
|
html = html.replace("### ", "<h3>").replace("\n### ", "</h3>\n<h3>") + "</h3>"
|
|
html = html.replace("**", "<strong>").replace("**", "</strong>")
|
|
html = html.replace("*", "<em>").replace("*", "</em>")
|
|
html = html.replace("`", "<code>").replace("`", "</code>")
|
|
html = html.replace("\n", "<br>")
|
|
return html
|
|
|
|
def get(self, start, end):
|
|
return self.editor.get(start, end)
|
|
|
|
def insert(self, index, text):
|
|
self.editor.insert(index, text)
|
|
if self.preview:
|
|
self.update_preview()
|
|
|
|
def delete(self, start, end):
|
|
self.editor.delete(start, end)
|
|
if self.preview:
|
|
self.update_preview()
|
|
|
|
class ProblemCreatorApp:
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.root.title("Coding Problem Creator")
|
|
self.root.geometry("1000x800")
|
|
self.root.minsize(800, 600)
|
|
|
|
# Configure style
|
|
style = ttk.Style()
|
|
style.theme_use('clam')
|
|
|
|
self.create_widgets()
|
|
|
|
def create_widgets(self):
|
|
# Create notebook for tabs
|
|
self.notebook = ttk.Notebook(self.root)
|
|
self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|
|
|
# Main info tab
|
|
self.main_frame = ttk.Frame(self.notebook, padding="10")
|
|
self.notebook.add(self.main_frame, text="Problem Info")
|
|
|
|
# Configure grid weights for responsiveness
|
|
self.main_frame.columnconfigure(1, weight=1)
|
|
|
|
# Title
|
|
title_label = ttk.Label(self.main_frame, text="Coding Problem Creator",
|
|
font=('Arial', 16, 'bold'))
|
|
title_label.grid(row=0, column=0, columnspan=2, pady=(0, 20))
|
|
|
|
# Problem Name
|
|
ttk.Label(self.main_frame, text="Problem Name:", font=('Arial', 10, 'bold')).grid(
|
|
row=1, column=0, sticky=tk.W, pady=(0, 5))
|
|
self.problem_name = ttk.Entry(self.main_frame, width=40, font=('Arial', 10))
|
|
self.problem_name.grid(row=1, column=1, sticky=(tk.W, tk.E), pady=(0, 10))
|
|
|
|
# Difficulty
|
|
ttk.Label(self.main_frame, text="Difficulty:", font=('Arial', 10, 'bold')).grid(
|
|
row=2, column=0, sticky=tk.W, pady=(0, 5))
|
|
self.difficulty = ttk.Combobox(self.main_frame, values=["easy", "medium", "hard"],
|
|
state="readonly", width=15)
|
|
self.difficulty.set("medium")
|
|
self.difficulty.grid(row=2, column=1, sticky=tk.W, pady=(0, 10))
|
|
|
|
# Description tab
|
|
self.desc_frame = ttk.Frame(self.notebook)
|
|
self.notebook.add(self.desc_frame, text="Description")
|
|
|
|
# Create markdown editor for description
|
|
self.description = MarkdownEditor(self.desc_frame)
|
|
|
|
# Test Code tab
|
|
self.test_frame = ttk.Frame(self.notebook)
|
|
self.notebook.add(self.test_frame, text="Test Code")
|
|
|
|
# Create code editor for test code
|
|
self.test_code = CodeEditor(self.test_frame, width=80, height=20)
|
|
self.test_code.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# Insert template code
|
|
template_code = '''import unittest
|
|
|
|
class TestCase(unittest.TestCase):
|
|
def test(self):
|
|
# Write your test cases here
|
|
# Example:
|
|
# from solution import your_function
|
|
# self.assertEqual(your_function("input"), "expected_output")
|
|
pass'''
|
|
|
|
self.test_code.insert("1.0", template_code)
|
|
if PYGMENTS_AVAILABLE:
|
|
self.test_code.highlight_syntax()
|
|
|
|
# Buttons frame at the bottom
|
|
button_frame = ttk.Frame(self.root)
|
|
button_frame.pack(fill=tk.X, padx=10, pady=10)
|
|
|
|
create_button = ttk.Button(button_frame, text="Create Problem",
|
|
command=self.create_problem)
|
|
create_button.pack(side=tk.LEFT, padx=(0, 10))
|
|
|
|
clear_button = ttk.Button(button_frame, text="Clear All",
|
|
command=self.clear_all)
|
|
clear_button.pack(side=tk.LEFT, padx=(0, 10))
|
|
|
|
load_button = ttk.Button(button_frame, text="Load Existing",
|
|
command=self.load_existing)
|
|
load_button.pack(side=tk.LEFT)
|
|
|
|
# Status bar
|
|
self.status_var = tk.StringVar()
|
|
self.status_var.set("Ready to create a new problem...")
|
|
status_bar = ttk.Label(self.root, textvariable=self.status_var,
|
|
relief=tk.SUNKEN, anchor=tk.W)
|
|
status_bar.pack(fill=tk.X, padx=10, pady=(0, 10))
|
|
|
|
def show_help(self):
|
|
help_text = """Test Code Help:
|
|
|
|
Your test code should follow this structure:
|
|
|
|
import unittest
|
|
|
|
class TestCase(unittest.TestCase):
|
|
def test(self):
|
|
# Import your solution function
|
|
from solution import your_function_name
|
|
|
|
# Write test assertions
|
|
self.assertEqual(your_function_name(input), expected_output)
|
|
self.assertTrue(condition)
|
|
self.assertFalse(condition)
|
|
|
|
Tips:
|
|
- Replace 'your_function_name' with the actual function name
|
|
- Add multiple test cases with different inputs
|
|
- Use descriptive variable names
|
|
- Test edge cases (empty inputs, large inputs, etc.)
|
|
|
|
Example for a palindrome checker:
|
|
from solution import is_palindrome
|
|
self.assertTrue(is_palindrome("racecar"))
|
|
self.assertFalse(is_palindrome("hello"))
|
|
self.assertTrue(is_palindrome(""))
|
|
"""
|
|
messagebox.showinfo("Test Code Help", help_text)
|
|
|
|
def validate_inputs(self):
|
|
if not self.problem_name.get().strip():
|
|
messagebox.showerror("Error", "Problem name is required!")
|
|
return False
|
|
|
|
if not self.description.get("1.0", tk.END).strip():
|
|
messagebox.showerror("Error", "Description is required!")
|
|
return False
|
|
|
|
test_code = self.test_code.get("1.0", tk.END).strip()
|
|
if not test_code or test_code == "pass" or len(test_code) < 50:
|
|
messagebox.showerror("Error", "Please provide proper test code!")
|
|
return False
|
|
|
|
# Validate problem name (should be filesystem-safe)
|
|
name = self.problem_name.get().strip()
|
|
if not name.replace("_", "").replace("-", "").replace(" ", "").isalnum():
|
|
messagebox.showerror("Error", "Problem name should only contain letters, numbers, spaces, hyphens, and underscores!")
|
|
return False
|
|
|
|
return True
|
|
|
|
def create_problem(self):
|
|
if not self.validate_inputs():
|
|
return
|
|
|
|
try:
|
|
# Get values
|
|
problem_name = self.problem_name.get().strip()
|
|
description_text = self.description.get("1.0", tk.END).strip()
|
|
difficulty = self.difficulty.get()
|
|
test_code = self.test_code.get("1.0", tk.END).strip()
|
|
|
|
# Create safe folder name (replace spaces with underscores)
|
|
folder_name = problem_name.replace(" ", "_")
|
|
|
|
# Create directory structure
|
|
base_path = Path("src/problems")
|
|
problem_path = base_path / folder_name
|
|
|
|
# Create directories if they don't exist
|
|
problem_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create manifest.json
|
|
manifest = {
|
|
"title": problem_name,
|
|
"description": description_text,
|
|
"description_md": f"problems/{folder_name}/description.md",
|
|
"test_code": f"problems/{folder_name}/test.py",
|
|
"difficulty": difficulty
|
|
}
|
|
|
|
manifest_path = problem_path / "manifest.json"
|
|
with open(manifest_path, 'w', encoding='utf-8') as f:
|
|
json.dump(manifest, f, indent=4, ensure_ascii=False)
|
|
|
|
# Create description.md
|
|
description_md_path = problem_path / "description.md"
|
|
with open(description_md_path, 'w', encoding='utf-8') as f:
|
|
f.write(description_text)
|
|
|
|
# Create test.py
|
|
test_py_path = problem_path / "test.py"
|
|
with open(test_py_path, 'w', encoding='utf-8') as f:
|
|
f.write(test_code)
|
|
|
|
# Create empty solution.py template
|
|
solution_py_path = problem_path / "solution.py"
|
|
if not solution_py_path.exists():
|
|
with open(solution_py_path, 'w', encoding='utf-8') as f:
|
|
f.write(f"# {problem_name} Solution\n")
|
|
f.write("# Implement your solution here\n\n")
|
|
f.write("def your_function():\n")
|
|
f.write(" pass\n")
|
|
|
|
self.status_var.set(f"✓ Problem '{problem_name}' created successfully in {problem_path}")
|
|
|
|
result = messagebox.askyesno("Success",
|
|
f"Problem '{problem_name}' created successfully!\n\n"
|
|
f"Location: {problem_path}\n\n"
|
|
"Would you like to open the folder?")
|
|
|
|
if result:
|
|
self.open_folder(problem_path)
|
|
|
|
except Exception as e:
|
|
error_msg = f"Error creating problem: {str(e)}"
|
|
self.status_var.set(error_msg)
|
|
messagebox.showerror("Error", error_msg)
|
|
|
|
def open_folder(self, path):
|
|
"""Cross-platform folder opening"""
|
|
import subprocess
|
|
import platform
|
|
|
|
try:
|
|
if platform.system() == "Windows":
|
|
os.startfile(path)
|
|
elif platform.system() == "Darwin": # macOS
|
|
subprocess.run(["open", path])
|
|
else: # Linux and other Unix-like
|
|
subprocess.run(["xdg-open", path])
|
|
except Exception as e:
|
|
messagebox.showwarning("Warning", f"Could not open folder: {str(e)}")
|
|
|
|
def clear_all(self):
|
|
result = messagebox.askyesno("Confirm", "Clear all fields?")
|
|
if result:
|
|
self.problem_name.delete(0, tk.END)
|
|
self.description.delete("1.0", tk.END)
|
|
self.difficulty.set("medium")
|
|
self.test_code.delete("1.0", tk.END)
|
|
# Re-insert template
|
|
template_code = '''import unittest
|
|
|
|
class TestCase(unittest.TestCase):
|
|
def test(self):
|
|
# Write your test cases here
|
|
# Example:
|
|
# from solution import your_function
|
|
# self.assertEqual(your_function("input"), "expected_output")
|
|
pass'''
|
|
self.test_code.insert("1.0", template_code)
|
|
if PYGMENTS_AVAILABLE:
|
|
self.test_code.highlight_syntax()
|
|
self.status_var.set("All fields cleared.")
|
|
|
|
def load_existing(self):
|
|
try:
|
|
base_path = Path("src/problems")
|
|
if not base_path.exists():
|
|
messagebox.showwarning("Warning", "No problems directory found!")
|
|
return
|
|
|
|
# Get list of existing problems
|
|
problems = [d.name for d in base_path.iterdir() if d.is_dir()]
|
|
|
|
if not problems:
|
|
messagebox.showinfo("Info", "No existing problems found!")
|
|
return
|
|
|
|
# Create selection dialog
|
|
dialog = tk.Toplevel(self.root)
|
|
dialog.title("Load Existing Problem")
|
|
dialog.geometry("400x300")
|
|
dialog.transient(self.root)
|
|
dialog.grab_set()
|
|
|
|
ttk.Label(dialog, text="Select a problem to load:",
|
|
font=('Arial', 10, 'bold')).pack(pady=10)
|
|
|
|
# Listbox with scrollbar
|
|
frame = ttk.Frame(dialog)
|
|
frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
|
|
|
|
scrollbar = ttk.Scrollbar(frame)
|
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
listbox = tk.Listbox(frame, yscrollcommand=scrollbar.set)
|
|
listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
scrollbar.config(command=listbox.yview)
|
|
|
|
for problem in sorted(problems):
|
|
listbox.insert(tk.END, problem)
|
|
|
|
def load_selected():
|
|
selection = listbox.curselection()
|
|
if not selection:
|
|
messagebox.showwarning("Warning", "Please select a problem!")
|
|
return
|
|
|
|
selected_problem = problems[selection[0]]
|
|
self.load_problem_data(selected_problem)
|
|
dialog.destroy()
|
|
|
|
button_frame = ttk.Frame(dialog)
|
|
button_frame.pack(pady=10)
|
|
|
|
ttk.Button(button_frame, text="Load", command=load_selected).pack(side=tk.LEFT, padx=5)
|
|
ttk.Button(button_frame, text="Cancel", command=dialog.destroy).pack(side=tk.LEFT, padx=5)
|
|
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Error loading existing problems: {str(e)}")
|
|
|
|
def load_problem_data(self, problem_name):
|
|
try:
|
|
problem_path = Path("src/problems") / problem_name
|
|
manifest_path = problem_path / "manifest.json"
|
|
test_path = problem_path / "test.py"
|
|
desc_path = problem_path / "description.md"
|
|
|
|
# Load manifest
|
|
with open(manifest_path, 'r', encoding='utf-8') as f:
|
|
manifest = json.load(f)
|
|
|
|
# Load test code
|
|
test_code = ""
|
|
if test_path.exists():
|
|
with open(test_path, 'r', encoding='utf-8') as f:
|
|
test_code = f.read()
|
|
|
|
# Load description
|
|
description = ""
|
|
if desc_path.exists():
|
|
with open(desc_path, 'r', encoding='utf-8') as f:
|
|
description = f.read()
|
|
|
|
# Populate fields
|
|
self.problem_name.delete(0, tk.END)
|
|
self.problem_name.insert(0, manifest["title"])
|
|
|
|
self.description.delete("1.0", tk.END)
|
|
self.description.insert("1.0", description)
|
|
|
|
self.difficulty.set(manifest["difficulty"])
|
|
|
|
self.test_code.delete("1.0", tk.END)
|
|
self.test_code.insert("1.0", test_code)
|
|
if PYGMENTS_AVAILABLE:
|
|
self.test_code.highlight_syntax()
|
|
|
|
self.status_var.set(f"Loaded problem: {problem_name}")
|
|
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Error loading problem data: {str(e)}")
|
|
|
|
def main():
|
|
root = tk.Tk()
|
|
app = ProblemCreatorApp(root)
|
|
root.mainloop()
|
|
|
|
if __name__ == "__main__":
|
|
main() |