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('', self._tab) self.bind('', self._shift_tab) self.bind('', 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('', 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""" {self._simple_markdown_to_html(markdown_text)} """ # 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("# ", "

").replace("\n# ", "

\n

") + "

" html = html.replace("## ", "

").replace("\n## ", "

\n

") + "

" html = html.replace("### ", "

").replace("\n### ", "

\n

") + "

" html = html.replace("**", "").replace("**", "") html = html.replace("*", "").replace("*", "") html = html.replace("`", "").replace("`", "") html = html.replace("\n", "
") 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()