import tkinter as tk from tkinter import filedialog from moviepy import VideoFileClip, ImageClip, CompositeVideoClip import threading import json import pysrt import os from PIL import Image, ImageDraw, ImageFont import numpy as np def find_system_font(): """Find a working system font for PIL""" print("Finding system fonts for PIL...") fonts_to_try = [ "C:/Windows/Fonts/arial.ttf", "C:/Windows/Fonts/calibri.ttf", "C:/Windows/Fonts/times.ttf", "C:/Windows/Fonts/verdana.ttf", "C:/Windows/Fonts/tahoma.ttf", "C:/Windows/Fonts/segoeui.ttf", ] for font_path in fonts_to_try: try: if os.path.exists(font_path): # Test the font by creating a small text image font = ImageFont.truetype(font_path, 20) img = Image.new('RGB', (100, 50), color='white') draw = ImageDraw.Draw(img) draw.text((10, 10), "Test", font=font, fill='black') print(f"Using font: {font_path}") return font_path except Exception as e: print(f"Font test failed for {font_path}: {e}") continue print("Using default font") return None def create_text_image(text, font_size=50, color='white', stroke_color='black', stroke_width=3, font_path=None): """Create a text image using PIL""" try: # Load font if font_path and os.path.exists(font_path): font = ImageFont.truetype(font_path, font_size) else: # Try to use default font try: font = ImageFont.load_default() except: # Last resort - create a basic font font = ImageFont.load_default() # Get text dimensions temp_img = Image.new('RGB', (1, 1)) temp_draw = ImageDraw.Draw(temp_img) try: bbox = temp_draw.textbbox((0, 0), text, font=font) text_width = bbox[2] - bbox[0] text_height = bbox[3] - bbox[1] except: # Fallback method for older PIL versions text_width, text_height = temp_draw.textsize(text, font=font) # Add padding padding = max(stroke_width * 2, 10) img_width = text_width + padding * 2 img_height = text_height + padding * 2 # Create transparent image img = Image.new('RGBA', (img_width, img_height), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) # Calculate text position (centered) x = (img_width - text_width) // 2 y = (img_height - text_height) // 2 # Convert color names to RGB if isinstance(color, str): if color == 'white': color = (255, 255, 255, 255) elif color.startswith('#'): # Convert hex to RGB hex_color = color.lstrip('#') color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + (255,) if isinstance(stroke_color, str): if stroke_color == 'black': stroke_color = (0, 0, 0, 255) elif stroke_color.startswith('#'): hex_color = stroke_color.lstrip('#') stroke_color = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + (255,) # Draw text with stroke if stroke_width > 0 and stroke_color: # Draw stroke by drawing text in multiple positions for dx in range(-stroke_width, stroke_width + 1): for dy in range(-stroke_width, stroke_width + 1): if dx*dx + dy*dy <= stroke_width*stroke_width: draw.text((x + dx, y + dy), text, font=font, fill=stroke_color) # Draw main text draw.text((x, y), text, font=font, fill=color) # Convert to numpy array for MoviePy img_array = np.array(img) return img_array, img_width, img_height except Exception as e: print(f"Text image creation failed: {e}") # Create a simple colored rectangle as fallback img = Image.new('RGBA', (200, 50), (255, 255, 255, 255)) return np.array(img), 200, 50 def debug_moviepy_text(): """Debug MoviePy TextClip functionality""" print("🔍 Debugging MoviePy TextClip...") # Set the correct ImageMagick path imagemagick_path = r"C:\Program Files\ImageMagick-7.1.2-Q16-HDRI\magick.exe" os.environ["IMAGEMAGICK_BINARY"] = imagemagick_path print(f"� Setting ImageMagick path: {imagemagick_path}") # Test different approaches to fix the font issue methods_to_try = [ {"name": "Basic TextClip", "params": {"font_size": 20}}, {"name": "TextClip with method='caption'", "params": {"font_size": 20, "method": "caption"}}, {"name": "TextClip with method='label'", "params": {"font_size": 20, "method": "label"}}, {"name": "TextClip color only", "params": {"color": "white"}}, {"name": "TextClip minimal", "params": {}}, ] for i, method in enumerate(methods_to_try, 1): try: print(f"Test {i}: {method['name']}...") clip = TextClip("test", **method["params"]) clip.close() print(f"✅ {method['name']} works") return method["params"] except Exception as e: print(f"❌ {method['name']} failed: {e}") # Try additional environment variables print("🔧 Attempting additional font fixes...") try: os.environ["FONTCONFIG_PATH"] = "C:/Windows/Fonts" os.environ["MAGICK_FONT_PATH"] = "C:/Windows/Fonts" clip = TextClip("test", font_size=20) clip.close() print("✅ Font fix successful") return {"font_size": 20} except Exception as e: print(f"❌ Font fix failed: {e}") print("❌ All TextClip methods failed") print("🆘 For now, the app will work without text overlays") return None # Find working font at startup SYSTEM_FONT = find_system_font() # Global settings with defaults settings = { "subtitle_y_px": 1550, "highlight_offset": -8, "font_size_subtitle": 65, "font_size_highlight": 68, "highlight_x_offset": 0, "video_path": None, "font": "Arial", "srt_path": None, "highlight_word_index": -1, # Index of word to highlight (-1 = last word) "highlight_start_time": 0.1, # When highlight appears (seconds) "highlight_duration": 1.5 # How long highlight lasts (seconds) } preset_file = "subtitle_gui_presets.json" word_presets_file = "word_presets.json" # Store word selections per subtitle subtitles = [] current_index = 0 word_presets = {} # Dictionary to store word index for each subtitle current_preset_slot = 1 # Currently selected preset slot def save_presets(): global current_preset_slot preset_file_numbered = f"subtitle_gui_presets_slot_{current_preset_slot}.json" word_presets_file_numbered = f"word_presets_slot_{current_preset_slot}.json" with open(preset_file_numbered, "w") as f: json.dump(settings, f) # Save word-specific presets with open(word_presets_file_numbered, "w") as f: json.dump(word_presets, f) print(f"📂 Preset slot {current_preset_slot} saved!") update_preset_display() def load_presets(): global settings, word_presets, current_preset_slot preset_file_numbered = f"subtitle_gui_presets_slot_{current_preset_slot}.json" word_presets_file_numbered = f"word_presets_slot_{current_preset_slot}.json" try: with open(preset_file_numbered, "r") as f: loaded = json.load(f) settings.update(loaded) # Load word-specific presets try: with open(word_presets_file_numbered, "r") as f: word_presets = json.load(f) except FileNotFoundError: word_presets = {} print(f"✅ Preset slot {current_preset_slot} loaded!") sync_gui() update_preset_display() except FileNotFoundError: print(f"⚠️ No preset found in slot {current_preset_slot}.") def change_preset_slot(slot_number): global current_preset_slot current_preset_slot = slot_number print(f"🔄 Switched to preset slot {current_preset_slot}") update_preset_display() def update_preset_display(): preset_label.config(text=f"Current Preset Slot: {current_preset_slot}") # Check if preset file exists and update button colors preset_file_numbered = f"subtitle_gui_presets_slot_{current_preset_slot}.json" if os.path.exists(preset_file_numbered): preset_label.config(bg="lightgreen") load_preset_btn.config(text=f"📂 Load Preset {current_preset_slot}", state="normal") else: preset_label.config(bg="lightcoral") load_preset_btn.config(text=f"📂 Load Preset {current_preset_slot} (Empty)", state="disabled") save_btn.config(text=f"📂 Save Preset {current_preset_slot}") def sync_gui(): sub_y_slider.set(settings["subtitle_y_px"]) highlight_slider.set(settings["highlight_offset"]) highlight_x_slider.set(settings["highlight_x_offset"]) sub_font_slider.set(settings["font_size_subtitle"]) highlight_font_slider.set(settings["font_size_highlight"]) font_dropdown_var.set(settings["font"]) highlight_start_slider.set(settings["highlight_start_time"]) highlight_duration_slider.set(settings["highlight_duration"]) update_highlight_word_display() update_preset_display() def load_srt(): global subtitles, current_index, word_presets path = filedialog.askopenfilename(filetypes=[("SRT files", "*.srt")]) if path: settings["srt_path"] = path subtitles = pysrt.open(path) current_index = 0 # Load word presets for this SRT file load_word_presets_for_srt(path) print(f"📜 Loaded subtitles: {path}") update_highlight_word_display() def load_word_presets_for_srt(srt_path): """Load word presets specific to this SRT file""" global word_presets try: srt_key = os.path.basename(srt_path) if srt_key in word_presets: print(f"✅ Loaded word selections for {srt_key}") else: word_presets[srt_key] = {} print(f"📝 Created new word selection storage for {srt_key}") except Exception as e: print(f"⚠️ Error loading word presets: {e}") word_presets = {} def get_current_word_index(): """Get the word index for the current subtitle""" if not subtitles or not settings["srt_path"]: return settings["highlight_word_index"] srt_key = os.path.basename(settings["srt_path"]) subtitle_key = str(current_index) if srt_key in word_presets and subtitle_key in word_presets[srt_key]: return word_presets[srt_key][subtitle_key] else: return settings["highlight_word_index"] # Default def set_current_word_index(word_index): """Set the word index for the current subtitle""" if not subtitles or not settings["srt_path"]: settings["highlight_word_index"] = word_index return srt_key = os.path.basename(settings["srt_path"]) subtitle_key = str(current_index) if srt_key not in word_presets: word_presets[srt_key] = {} word_presets[srt_key][subtitle_key] = word_index print(f"💾 Saved word selection for subtitle {current_index + 1}: word {word_index + 1}") def create_safe_textclip(text, font_size=50, color='white', stroke_color=None, stroke_width=0): """Create a text clip using PIL instead of MoviePy TextClip""" try: # Create text image using PIL if stroke_color is None: stroke_color = 'black' if stroke_width == 0: stroke_width = 3 img_array, img_width, img_height = create_text_image( text=text, font_size=font_size, color=color, stroke_color=stroke_color, stroke_width=stroke_width, font_path=SYSTEM_FONT ) # Convert PIL image to MoviePy ImageClip from moviepy import ImageClip clip = ImageClip(img_array, duration=1) print(f"Created text clip: '{text}' ({img_width}x{img_height})") return clip except Exception as e: print(f"Text clip creation failed: {e}") # Return a simple colored clip as placeholder from moviepy import ColorClip placeholder = ColorClip(size=(800, 100), color=(255, 255, 255)).with_duration(1) return placeholder def render_preview(save_path=None): if not settings["video_path"]: print("⚠️ No video selected.") return if not subtitles: print("⚠️ No subtitles loaded.") return sub = subtitles[current_index] subtitle_text = sub.text.replace("\n", " ").strip() words = subtitle_text.split() start_time = sub.start.ordinal / 1000.0 end_time = sub.end.ordinal / 1000.0 clip = VideoFileClip(settings["video_path"]).subclipped(start_time, min(end_time, start_time + 3)) vertical_clip = clip.resized(height=1920).cropped(width=1080, x_center=clip.w / 2) # Create base subtitle using safe method try: base_subtitle = create_safe_textclip( subtitle_text, font_size=settings["font_size_subtitle"], color='white', stroke_color='black', stroke_width=5 ).with_duration(clip.duration).with_position(('center', settings["subtitle_y_px"])) print("✅ Base subtitle created successfully") except Exception as e: print(f"❌ Base subtitle creation failed: {e}") return # For preview mode (not export), show only current selected word if save_path is None: # Get highlight word based on index for preview if len(words) > 0: word_index = get_current_word_index() # Use subtitle-specific word index if word_index < 0 or word_index >= len(words): word_index = len(words) - 1 # Default to last word highlight_word = words[word_index] else: highlight_word = "word" # Fallback full_text = subtitle_text.upper() words_upper = full_text.split() if highlight_word.upper() not in words_upper: highlight_word = words_upper[-1] # Fallback highlight_index = words_upper.index(highlight_word.upper()) chars_before = sum(len(w) + 1 for w in words_upper[:highlight_index]) char_width = 35 total_width = len(full_text) * char_width x_offset = (chars_before * char_width) - (total_width // 2) + settings["highlight_x_offset"] # Create single highlighted word for preview try: highlighted_word = create_safe_textclip( highlight_word, font_size=settings["font_size_highlight"], color='#FFD700', stroke_color='#FF6B35', stroke_width=5 ).with_duration(min(settings["highlight_duration"], clip.duration)).with_start(settings["highlight_start_time"]).with_position((540 + x_offset, settings["subtitle_y_px"] + settings["highlight_offset"])) print(f"✅ Preview highlighted word created: '{highlight_word}'") except Exception as e: print(f"❌ Highlighted word creation failed: {e}") highlighted_word = None # Compose preview video if highlighted_word is not None: final = CompositeVideoClip([vertical_clip, base_subtitle, highlighted_word], size=(1080, 1920)) print("✅ Preview video composed with highlight") else: final = CompositeVideoClip([vertical_clip, base_subtitle], size=(1080, 1920)) print("✅ Preview video composed without highlight") else: # For export mode, create highlights for ALL words in sequence print(f"🎬 Creating export with ALL {len(words)} words highlighted in sequence...") highlighted_clips = [] full_text = subtitle_text.upper() words_upper = full_text.split() # Store current settings to restore later original_slot = current_preset_slot original_settings = settings.copy() # Calculate timing for each word highlight if len(words) > 0: time_per_word = clip.duration / len(words) for i, word in enumerate(words): try: # Calculate preset slot for this word (cycle through slots 1-5) word_slot = (i % 5) + 1 # Load preset for this word's slot (without changing global current_preset_slot) preset_file_numbered = f"subtitle_gui_presets_slot_{word_slot}.json" if os.path.exists(preset_file_numbered): with open(preset_file_numbered, "r") as f: slot_settings = json.load(f) # Apply slot settings for this word word_font_size = slot_settings.get("font_size_highlight", original_settings["font_size_highlight"]) word_y_offset = slot_settings.get("highlight_offset", original_settings["highlight_offset"]) word_x_offset = slot_settings.get("highlight_x_offset", original_settings["highlight_x_offset"]) print(f"📂 Using preset slot {word_slot} for word '{word}' (font: {word_font_size}, y: {word_y_offset}, x: {word_x_offset})") else: # Use original settings if preset doesn't exist word_font_size = original_settings["font_size_highlight"] word_y_offset = original_settings["highlight_offset"] word_x_offset = original_settings["highlight_x_offset"] print(f"⚠️ No preset in slot {word_slot}, using defaults for word '{word}'") # Calculate position for this word chars_before = sum(len(w) + 1 for w in words_upper[:i]) char_width = 35 total_width = len(full_text) * char_width x_offset = (chars_before * char_width) - (total_width // 2) + word_x_offset # Calculate timing for this word word_start_time = i * time_per_word word_duration = min(time_per_word * 1.5, original_settings["highlight_duration"]) # Slight overlap # Create highlighted word clip with slot-specific settings highlighted_word = create_safe_textclip( word, font_size=word_font_size, color='#FFD700', stroke_color='#FF6B35', stroke_width=5 ).with_duration(word_duration).with_start(word_start_time).with_position((540 + x_offset, original_settings["subtitle_y_px"] + word_y_offset)) highlighted_clips.append(highlighted_word) print(f"✅ Created highlight {i+1}/{len(words)}: '{word}' at {word_start_time:.1f}s with slot {word_slot} settings") except Exception as e: print(f"❌ Failed to create highlight for word '{word}': {e}") # No need to restore settings since we didn't modify the global variables # Compose final video with all highlights all_clips = [vertical_clip, base_subtitle] + highlighted_clips final = CompositeVideoClip(all_clips, size=(1080, 1920)) print(f"✅ Export video composed with {len(highlighted_clips)} word highlights using different presets") if save_path: srt_output_path = os.path.splitext(save_path)[0] + ".srt" with open(srt_output_path, "w", encoding='utf-8') as srt_file: srt_file.write(str(sub)) final.write_videofile(save_path, fps=24) print(f"✅ Exported SRT: {srt_output_path}") else: # Create a smaller preview version to fit 1080p screen better # Scale down to 50% size: 540x960 instead of 1080x1920 preview_final = final.resized(0.5) print("🎬 Opening preview window (540x960 - scaled to fit 1080p screen)") preview_final.preview(fps=24) preview_final.close() clip.close() final.close() def update_setting(var_name, value): if var_name in ["highlight_start_time", "highlight_duration"]: settings[var_name] = float(value) else: settings[var_name] = int(value) if var_name.startswith("font_size") or "offset" in var_name or "y_px" in var_name else value def update_font(value): settings["font"] = value def open_video(): file_path = filedialog.askopenfilename(filetypes=[("MP4 files", "*.mp4")]) if file_path: settings["video_path"] = file_path print(f"📂 Loaded video: {file_path}") def start_preview_thread(): threading.Thread(target=render_preview).start() def export_clip(): if settings["video_path"]: base_name = os.path.splitext(os.path.basename(settings["video_path"]))[0] out_path = os.path.join(os.path.dirname(settings["video_path"]), f"{base_name}_clip_exported.mp4") threading.Thread(target=render_preview, args=(out_path,)).start() print(f"💾 Exporting to: {out_path}") def prev_sub(): global current_index if current_index > 0: current_index -= 1 update_highlight_word_display() start_preview_thread() def next_sub(): global current_index if current_index < len(subtitles) - 1: current_index += 1 update_highlight_word_display() start_preview_thread() def prev_highlight_word(): """Switch to the previous preset slot, select previous word, AND load the preset""" global current_preset_slot # First, change the highlighted word if subtitles: sub = subtitles[current_index] words = sub.text.replace("\n", " ").strip().split() if len(words) > 1: current_word_index = get_current_word_index() if current_word_index < 0: current_word_index = len(words) - 1 new_index = current_word_index - 1 if new_index < 0: new_index = len(words) - 1 # Wrap to last word set_current_word_index(new_index) update_highlight_word_display() # Then, switch to previous preset slot new_slot = current_preset_slot - 1 if new_slot < 1: new_slot = 5 # Wrap to last slot change_preset_slot(new_slot) # Automatically load the preset for this slot load_presets() # Start preview to show changes start_preview_thread() def next_highlight_word(): """Switch to the next preset slot, select next word, AND load the preset""" global current_preset_slot # First, change the highlighted word if subtitles: sub = subtitles[current_index] words = sub.text.replace("\n", " ").strip().split() if len(words) > 1: current_word_index = get_current_word_index() if current_word_index < 0: current_word_index = len(words) - 1 new_index = (current_word_index + 1) % len(words) set_current_word_index(new_index) update_highlight_word_display() # Then, switch to next preset slot new_slot = current_preset_slot + 1 if new_slot > 5: new_slot = 1 # Wrap to first slot change_preset_slot(new_slot) # Automatically load the preset for this slot load_presets() # Start preview to show changes start_preview_thread() def update_highlight_word_display(): """Update the display showing which word is selected for highlight""" if not subtitles: highlight_word_label.config(text="Highlighted Word: None") return sub = subtitles[current_index] words = sub.text.replace("\n", " ").strip().split() if len(words) > 0: word_index = get_current_word_index() if word_index < 0 or word_index >= len(words): word_index = len(words) - 1 set_current_word_index(word_index) # Save the default highlight_word = words[word_index] highlight_word_label.config(text=f"Highlighted Word: '{highlight_word}' ({word_index + 1}/{len(words)})") else: highlight_word_label.config(text="Highlighted Word: None") def handle_drop(event): path = event.data if path.endswith(".mp4"): settings["video_path"] = path print(f"🎥 Dropped video: {path}") elif path.endswith(".srt"): settings["srt_path"] = path global subtitles, current_index subtitles = pysrt.open(path) current_index = 0 print(f"📜 Dropped subtitles: {path}") # GUI Setup root = tk.Tk() root.title("Subtitle Positioning Tool") root.geometry("420x850") root.drop_target_register = getattr(root, 'drop_target_register', lambda *args: None) root.dnd_bind = getattr(root, 'dnd_bind', lambda *args, **kwargs: None) try: import tkinterdnd2 as tkdnd root.drop_target_register(tkdnd.DND_FILES) root.dnd_bind('<>', handle_drop) except: pass load_btn = tk.Button(root, text="🎥 Load Video", command=open_video) load_btn.pack(pady=5) load_srt_btn = tk.Button(root, text="📜 Load SRT", command=load_srt) load_srt_btn.pack(pady=5) tk.Label(root, text="Subtitle Y Position").pack() sub_y_slider = tk.Scale(root, from_=1000, to=1800, orient="horizontal", command=lambda v: update_setting("subtitle_y_px", v)) sub_y_slider.set(settings["subtitle_y_px"]) sub_y_slider.pack() tk.Label(root, text="Highlight Y Offset").pack() highlight_slider = tk.Scale(root, from_=-100, to=100, orient="horizontal", command=lambda v: update_setting("highlight_offset", v)) highlight_slider.set(settings["highlight_offset"]) highlight_slider.pack() tk.Label(root, text="Highlight X Offset").pack() highlight_x_slider = tk.Scale(root, from_=-300, to=300, orient="horizontal", command=lambda v: update_setting("highlight_x_offset", v)) highlight_x_slider.set(settings["highlight_x_offset"]) highlight_x_slider.pack() tk.Label(root, text="Subtitle Font Size").pack() sub_font_slider = tk.Scale(root, from_=30, to=100, orient="horizontal", command=lambda v: update_setting("font_size_subtitle", v)) sub_font_slider.set(settings["font_size_subtitle"]) sub_font_slider.pack() tk.Label(root, text="Highlight Font Size").pack() highlight_font_slider = tk.Scale(root, from_=30, to=100, orient="horizontal", command=lambda v: update_setting("font_size_highlight", v)) highlight_font_slider.set(settings["font_size_highlight"]) highlight_font_slider.pack() tk.Label(root, text="Highlight Start Time (seconds)").pack() highlight_start_slider = tk.Scale(root, from_=0.0, to=3.0, resolution=0.1, orient="horizontal", command=lambda v: update_setting("highlight_start_time", v)) highlight_start_slider.set(settings["highlight_start_time"]) highlight_start_slider.pack() tk.Label(root, text="Highlight Duration (seconds)").pack() highlight_duration_slider = tk.Scale(root, from_=0.1, to=5.0, resolution=0.1, orient="horizontal", command=lambda v: update_setting("highlight_duration", v)) highlight_duration_slider.set(settings["highlight_duration"]) highlight_duration_slider.pack() font_dropdown_var = tk.StringVar(value=settings["font"]) tk.Label(root, text="Font").pack() font_dropdown = tk.OptionMenu( root, font_dropdown_var, "Arial", "Courier", "Times-Roman", "Helvetica-Bold", "Verdana", "Georgia", "Impact", command=update_font ) font_dropdown.pack(pady=5) # Preset Slot Navigation + Word Selection tk.Label(root, text="Preset Slot Navigation + Word Selection").pack() highlight_word_label = tk.Label(root, text="Highlighted Word: None", bg="lightgray", relief="sunken") highlight_word_label.pack(pady=5) word_nav_frame = tk.Frame(root) word_nav_frame.pack(pady=5) tk.Button(word_nav_frame, text="⏮️ Prev Slot+Word", command=prev_highlight_word).pack(side="left", padx=2) tk.Button(word_nav_frame, text="⏭️ Next Slot+Word", command=next_highlight_word).pack(side="left", padx=2) preview_btn = tk.Button(root, text="▶️ Preview Clip", command=start_preview_thread) preview_btn.pack(pady=10) export_btn = tk.Button(root, text="💾 Export Clip", command=export_clip) export_btn.pack(pady=5) # Preset slot selection tk.Label(root, text="Preset Slots").pack() preset_label = tk.Label(root, text="Current Preset Slot: 1", bg="lightcoral", relief="sunken") preset_label.pack(pady=2) preset_slot_frame = tk.Frame(root) preset_slot_frame.pack(pady=2) tk.Button(preset_slot_frame, text="Slot 1", command=lambda: change_preset_slot(1)).pack(side="left", padx=1) tk.Button(preset_slot_frame, text="Slot 2", command=lambda: change_preset_slot(2)).pack(side="left", padx=1) tk.Button(preset_slot_frame, text="Slot 3", command=lambda: change_preset_slot(3)).pack(side="left", padx=1) tk.Button(preset_slot_frame, text="Slot 4", command=lambda: change_preset_slot(4)).pack(side="left", padx=1) tk.Button(preset_slot_frame, text="Slot 5", command=lambda: change_preset_slot(5)).pack(side="left", padx=1) save_btn = tk.Button(root, text="📂 Save Preset 1", command=save_presets) save_btn.pack(pady=5) load_preset_btn = tk.Button(root, text="📂 Load Preset 1", command=load_presets) load_preset_btn.pack(pady=5) root.mainloop()