Lewati ke isi

File Management dalam GUI Tkinter

Bagian 1: File Dialogs dan Operasi File Dasar

File management adalah salah satu aspek penting dalam pengembangan aplikasi desktop. Pengguna perlu dapat membuka, menyimpan, dan mengelola file dengan mudah melalui interface yang intuitif. Dalam bagian ini, kita akan mempelajari cara menggunakan file dialogs dan melakukan operasi file dasar dalam aplikasi GUI.

1.1 Memahami File Dialogs

File dialog adalah jendela standar sistem operasi yang memungkinkan pengguna untuk memilih file atau direktori. Dialog ini memberikan interface yang familiar dan konsisten di semua aplikasi, sehingga pengguna tidak perlu mempelajari cara baru untuk setiap aplikasi.

Tkinter menyediakan beberapa jenis file dialog melalui modul filedialog:

Open File Dialog: Digunakan untuk memilih file yang akan dibuka. Dialog ini menampilkan struktur direktori dan memungkinkan pengguna untuk navigasi dan memilih file.

Save File Dialog: Digunakan untuk menentukan lokasi dan nama file yang akan disimpan. Dialog ini juga memungkinkan pengguna untuk membuat direktori baru.

Directory Dialog: Digunakan untuk memilih direktori atau folder, bukan file individual.

1.2 Keuntungan Menggunakan File Dialogs

Menggunakan file dialogs standar memberikan beberapa keuntungan:

Konsistensi: Pengguna sudah familiar dengan tampilan dan cara kerja dialog standar sistem operasi.

Fitur Lengkap: Dialog standar sudah dilengkapi dengan fitur seperti preview, sorting, filtering, dan navigasi yang canggih.

Keamanan: Dialog standar menangani validasi path dan permission secara otomatis.

Accessibility: Dialog standar sudah mendukung fitur accessibility untuk pengguna dengan kebutuhan khusus.

1.3 Praktikum 1: File Explorer Sederhana

Mari kita buat aplikasi file explorer sederhana untuk memahami penggunaan file dialogs.

Praktikum 1.1: Setup Aplikasi File Explorer

Buat file baru bernama file_explorer.py:

import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import os
from datetime import datetime
import shutil

class FileExplorer:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("File Explorer - File Management Demo")
        self.window.geometry("800x600")
        self.window.configure(bg="lightsteelblue")

        # Variabel untuk menyimpan informasi file
        self.current_file = None
        self.current_directory = os.getcwd()
        self.selected_files = []

        self.buat_interface()
        self.update_directory_info()

    def buat_interface(self):
        # Header dengan informasi direktori
        header_frame = tk.Frame(self.window, bg="darkblue", height=60)
        header_frame.pack(fill=tk.X)
        header_frame.pack_propagate(False)

        tk.Label(
            header_frame,
            text="FILE EXPLORER",
            font=("Arial", 16, "bold"),
            fg="white",
            bg="darkblue"
        ).pack(pady=15)

        # Frame untuk path dan navigasi
        nav_frame = tk.Frame(self.window, bg="lightgray", height=40)
        nav_frame.pack(fill=tk.X)
        nav_frame.pack_propagate(False)

        tk.Label(
            nav_frame,
            text="Current Directory:",
            font=("Arial", 10, "bold"),
            bg="lightgray"
        ).pack(side=tk.LEFT, padx=10, pady=10)

        self.path_label = tk.Label(
            nav_frame,
            text=self.current_directory,
            font=("Arial", 10),
            bg="lightgray",
            fg="blue",
            anchor="w"
        )
        self.path_label.pack(side=tk.LEFT, fill=tk.X, expand=True, pady=10)

Tugas: Jalankan kode ini dan pastikan header dan navigation bar muncul dengan informasi direktori saat ini.

Praktikum 1.2: Menambahkan Toolbar dengan File Operations

Tambahkan toolbar dengan tombol-tombol untuk operasi file:

        # Toolbar dengan tombol operasi file
        toolbar = tk.Frame(self.window, bg="lightgray", relief=tk.RAISED, bd=1)
        toolbar.pack(fill=tk.X, padx=2, pady=2)

        # Tombol Open File
        btn_open = tk.Button(
            toolbar,
            text="📁 Open File",
            font=("Arial", 10),
            bg="lightblue",
            command=self.open_file,
            width=12
        )
        btn_open.pack(side=tk.LEFT, padx=2, pady=2)

        # Tombol Open Directory
        btn_open_dir = tk.Button(
            toolbar,
            text="📂 Open Directory",
            font=("Arial", 10),
            bg="lightgreen",
            command=self.open_directory,
            width=15
        )
        btn_open_dir.pack(side=tk.LEFT, padx=2, pady=2)

        # Tombol Create File
        btn_create = tk.Button(
            toolbar,
            text="📄 Create File",
            font=("Arial", 10),
            bg="lightyellow",
            command=self.create_file,
            width=12
        )
        btn_create.pack(side=tk.LEFT, padx=2, pady=2)

        # Tombol Delete File
        btn_delete = tk.Button(
            toolbar,
            text="🗑️ Delete",
            font=("Arial", 10),
            bg="lightcoral",
            command=self.delete_file,
            width=10
        )
        btn_delete.pack(side=tk.LEFT, padx=2, pady=2)

        # Tombol Refresh
        btn_refresh = tk.Button(
            toolbar,
            text="🔄 Refresh",
            font=("Arial", 10),
            bg="lightcyan",
            command=self.refresh_view,
            width=10
        )
        btn_refresh.pack(side=tk.LEFT, padx=2, pady=2)

Tugas: Jalankan dan lihat toolbar dengan berbagai tombol operasi file.

Praktikum 1.3: Membuat Area Informasi File

Tambahkan area untuk menampilkan informasi file yang dipilih:

        # Main content area
        content_frame = tk.Frame(self.window, bg="lightsteelblue")
        content_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        # Frame kiri untuk file list
        left_frame = tk.LabelFrame(
            content_frame,
            text="File Information",
            font=("Arial", 12, "bold"),
            bg="lightsteelblue"
        )
        left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5)

        # Text widget untuk menampilkan informasi file
        self.info_text = tk.Text(
            left_frame,
            font=("Courier", 10),
            bg="white",
            wrap=tk.WORD,
            height=15
        )

        # Scrollbar untuk info text
        info_scrollbar = tk.Scrollbar(left_frame, orient=tk.VERTICAL, command=self.info_text.yview)
        self.info_text.configure(yscrollcommand=info_scrollbar.set)

        self.info_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
        info_scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)

        # Frame kanan untuk preview
        right_frame = tk.LabelFrame(
            content_frame,
            text="File Preview",
            font=("Arial", 12, "bold"),
            bg="lightsteelblue"
        )
        right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5)

        # Text widget untuk preview file
        self.preview_text = tk.Text(
            right_frame,
            font=("Courier", 9),
            bg="lightyellow",
            wrap=tk.WORD,
            height=15,
            state=tk.DISABLED
        )

        # Scrollbar untuk preview
        preview_scrollbar = tk.Scrollbar(right_frame, orient=tk.VERTICAL, command=self.preview_text.yview)
        self.preview_text.configure(yscrollcommand=preview_scrollbar.set)

        self.preview_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
        preview_scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)

Tugas: Jalankan dan lihat layout dengan dua panel: informasi file dan preview.

Praktikum 1.4: Implementasi Open File Dialog

Tambahkan method untuk membuka file:

    def open_file(self):
        """Method untuk membuka file menggunakan file dialog"""
        # Definisikan jenis file yang didukung
        file_types = [
            ("Text files", "*.txt"),
            ("Python files", "*.py"),
            ("JSON files", "*.json"),
            ("CSV files", "*.csv"),
            ("All files", "*.*")
        ]

        # Tampilkan dialog open file
        filename = filedialog.askopenfilename(
            title="Select a file to open",
            filetypes=file_types,
            initialdir=self.current_directory
        )

        if filename:  # Jika user memilih file (tidak cancel)
            try:
                self.current_file = filename
                self.current_directory = os.path.dirname(filename)
                self.update_directory_info()
                self.show_file_info(filename)
                self.preview_file(filename)

                messagebox.showinfo("Success", f"File opened: {os.path.basename(filename)}")

            except Exception as e:
                messagebox.showerror("Error", f"Cannot open file: {str(e)}")

    def show_file_info(self, filepath):
        """Method untuk menampilkan informasi detail file"""
        try:
            # Dapatkan informasi file
            stat_info = os.stat(filepath)
            file_size = stat_info.st_size
            modified_time = datetime.fromtimestamp(stat_info.st_mtime)
            created_time = datetime.fromtimestamp(stat_info.st_ctime)

            # Format informasi
            info = f"""FILE INFORMATION
{'='*50}

File Name: {os.path.basename(filepath)}
Full Path: {filepath}
Directory: {os.path.dirname(filepath)}

File Size: {self.format_file_size(file_size)}
Extension: {os.path.splitext(filepath)[1] or 'No extension'}

Created: {created_time.strftime('%Y-%m-%d %H:%M:%S')}
Modified: {modified_time.strftime('%Y-%m-%d %H:%M:%S')}

Permissions:
- Readable: {'Yes' if os.access(filepath, os.R_OK) else 'No'}
- Writable: {'Yes' if os.access(filepath, os.W_OK) else 'No'}
- Executable: {'Yes' if os.access(filepath, os.X_OK) else 'No'}

File Type: {self.get_file_type(filepath)}
"""

            # Tampilkan di info text
            self.info_text.delete(1.0, tk.END)
            self.info_text.insert(1.0, info)

        except Exception as e:
            error_info = f"Error getting file information: {str(e)}"
            self.info_text.delete(1.0, tk.END)
            self.info_text.insert(1.0, error_info)

    def format_file_size(self, size_bytes):
        """Method untuk format ukuran file ke format yang mudah dibaca"""
        if size_bytes < 1024:
            return f"{size_bytes} bytes"
        elif size_bytes < 1024**2:
            return f"{size_bytes/1024:.1f} KB"
        elif size_bytes < 1024**3:
            return f"{size_bytes/1024**2:.1f} MB"
        else:
            return f"{size_bytes/1024**3:.1f} GB"

    def get_file_type(self, filepath):
        """Method untuk menentukan jenis file berdasarkan ekstensi"""
        extension = os.path.splitext(filepath)[1].lower()

        file_types = {
            '.txt': 'Text Document',
            '.py': 'Python Script',
            '.json': 'JSON Data',
            '.csv': 'CSV Data',
            '.html': 'HTML Document',
            '.css': 'CSS Stylesheet',
            '.js': 'JavaScript',
            '.jpg': 'JPEG Image',
            '.png': 'PNG Image',
            '.pdf': 'PDF Document',
            '.docx': 'Word Document',
            '.xlsx': 'Excel Spreadsheet'
        }

        return file_types.get(extension, 'Unknown File Type')

Tugas: Jalankan aplikasi dan coba buka berbagai jenis file. Perhatikan informasi detail yang ditampilkan di panel kiri.

Praktikum 1.5: Implementasi File Preview

Tambahkan method untuk preview file:

    def preview_file(self, filepath):
        """Method untuk preview isi file"""
        try:
            file_size = os.path.getsize(filepath)

            # Batasi preview untuk file besar
            if file_size > 1024 * 1024:  # 1MB
                preview_content = f"File too large for preview ({self.format_file_size(file_size)})\n"
                preview_content += "Please use appropriate application to view this file."
            else:
                # Coba baca file sebagai text
                try:
                    with open(filepath, 'r', encoding='utf-8') as file:
                        content = file.read()

                        # Batasi jumlah karakter yang ditampilkan
                        if len(content) > 5000:
                            preview_content = content[:5000] + "\n\n... (Content truncated for preview)"
                        else:
                            preview_content = content

                except UnicodeDecodeError:
                    # Jika tidak bisa dibaca sebagai text, coba sebagai binary
                    try:
                        with open(filepath, 'rb') as file:
                            binary_content = file.read(500)  # Baca 500 bytes pertama

                        preview_content = "Binary file detected. Showing first 500 bytes as hex:\n\n"
                        preview_content += ' '.join(f'{byte:02x}' for byte in binary_content)

                    except Exception:
                        preview_content = "Cannot preview this file type."

            # Tampilkan preview
            self.preview_text.config(state=tk.NORMAL)
            self.preview_text.delete(1.0, tk.END)
            self.preview_text.insert(1.0, preview_content)
            self.preview_text.config(state=tk.DISABLED)

        except Exception as e:
            error_preview = f"Error previewing file: {str(e)}"
            self.preview_text.config(state=tk.NORMAL)
            self.preview_text.delete(1.0, tk.END)
            self.preview_text.insert(1.0, error_preview)
            self.preview_text.config(state=tk.DISABLED)

Tugas: Test preview dengan membuka file teks, Python script, dan file binary. Perhatikan bagaimana aplikasi menangani berbagai jenis file.

Praktikum 1.6: Implementasi Open Directory

Tambahkan method untuk membuka direktori:

    def open_directory(self):
        """Method untuk membuka direktori menggunakan directory dialog"""
        directory = filedialog.askdirectory(
            title="Select a directory",
            initialdir=self.current_directory
        )

        if directory:
            try:
                self.current_directory = directory
                self.update_directory_info()
                self.show_directory_contents(directory)

                messagebox.showinfo("Success", f"Directory opened: {directory}")

            except Exception as e:
                messagebox.showerror("Error", f"Cannot open directory: {str(e)}")

    def show_directory_contents(self, directory):
        """Method untuk menampilkan isi direktori"""
        try:
            # Dapatkan daftar file dan folder
            items = os.listdir(directory)

            # Pisahkan folder dan file
            folders = []
            files = []

            for item in items:
                item_path = os.path.join(directory, item)
                if os.path.isdir(item_path):
                    folders.append(item)
                else:
                    files.append(item)

            # Sort alphabetically
            folders.sort()
            files.sort()

            # Format informasi direktori
            dir_info = f"""DIRECTORY CONTENTS
{'='*50}

Directory: {directory}
Total Items: {len(items)} ({len(folders)} folders, {len(files)} files)

FOLDERS ({len(folders)}):
{'-'*30}
"""

            for folder in folders:
                folder_path = os.path.join(directory, folder)
                try:
                    folder_size = self.get_directory_size(folder_path)
                    dir_info += f"📁 {folder} ({self.format_file_size(folder_size)})\n"
                except:
                    dir_info += f"📁 {folder} (Size unknown)\n"

            dir_info += f"\nFILES ({len(files)}):\n{'-'*30}\n"

            for file in files:
                file_path = os.path.join(directory, file)
                try:
                    file_size = os.path.getsize(file_path)
                    file_ext = os.path.splitext(file)[1]
                    dir_info += f"📄 {file} ({self.format_file_size(file_size)}) {file_ext}\n"
                except:
                    dir_info += f"📄 {file} (Size unknown)\n"

            # Tampilkan di info text
            self.info_text.delete(1.0, tk.END)
            self.info_text.insert(1.0, dir_info)

            # Clear preview
            self.preview_text.config(state=tk.NORMAL)
            self.preview_text.delete(1.0, tk.END)
            self.preview_text.insert(1.0, "Select a file to preview its contents")
            self.preview_text.config(state=tk.DISABLED)

        except Exception as e:
            error_info = f"Error reading directory: {str(e)}"
            self.info_text.delete(1.0, tk.END)
            self.info_text.insert(1.0, error_info)

    def get_directory_size(self, directory):
        """Method untuk menghitung ukuran total direktori"""
        total_size = 0
        try:
            for dirpath, dirnames, filenames in os.walk(directory):
                for filename in filenames:
                    filepath = os.path.join(dirpath, filename)
                    try:
                        total_size += os.path.getsize(filepath)
                    except:
                        continue
        except:
            pass
        return total_size

    def update_directory_info(self):
        """Method untuk update informasi direktori di navigation bar"""
        self.path_label.config(text=self.current_directory)

Tugas: Test open directory dan lihat bagaimana aplikasi menampilkan daftar file dan folder dengan informasi ukuran.

Praktikum 1.7: Implementasi Create dan Delete File

Tambahkan method untuk membuat dan menghapus file:

    def create_file(self):
        """Method untuk membuat file baru"""
        # Dialog untuk memilih lokasi dan nama file
        filename = filedialog.asksaveasfilename(
            title="Create new file",
            defaultextension=".txt",
            filetypes=[
                ("Text files", "*.txt"),
                ("Python files", "*.py"),
                ("JSON files", "*.json"),
                ("All files", "*.*")
            ],
            initialdir=self.current_directory
        )

        if filename:
            try:
                # Buat file kosong
                with open(filename, 'w', encoding='utf-8') as file:
                    file.write("")  # File kosong

                self.current_file = filename
                self.current_directory = os.path.dirname(filename)
                self.update_directory_info()
                self.show_file_info(filename)

                # Clear preview
                self.preview_text.config(state=tk.NORMAL)
                self.preview_text.delete(1.0, tk.END)
                self.preview_text.insert(1.0, "New empty file created")
                self.preview_text.config(state=tk.DISABLED)

                messagebox.showinfo("Success", f"File created: {os.path.basename(filename)}")

            except Exception as e:
                messagebox.showerror("Error", f"Cannot create file: {str(e)}")

    def delete_file(self):
        """Method untuk menghapus file"""
        if not self.current_file:
            messagebox.showwarning("Warning", "No file selected for deletion!")
            return

        # Konfirmasi penghapusan
        filename = os.path.basename(self.current_file)
        if messagebox.askyesno("Confirm Delete", f"Are you sure you want to delete '{filename}'?\n\nThis action cannot be undone."):
            try:
                os.remove(self.current_file)

                # Clear displays
                self.info_text.delete(1.0, tk.END)
                self.info_text.insert(1.0, "File deleted successfully")

                self.preview_text.config(state=tk.NORMAL)
                self.preview_text.delete(1.0, tk.END)
                self.preview_text.insert(1.0, "No file selected")
                self.preview_text.config(state=tk.DISABLED)

                self.current_file = None

                messagebox.showinfo("Success", f"File '{filename}' has been deleted")

            except Exception as e:
                messagebox.showerror("Error", f"Cannot delete file: {str(e)}")

    def refresh_view(self):
        """Method untuk refresh tampilan"""
        if self.current_file and os.path.exists(self.current_file):
            self.show_file_info(self.current_file)
            self.preview_file(self.current_file)
        elif os.path.exists(self.current_directory):
            self.show_directory_contents(self.current_directory)
        else:
            # Jika direktori tidak ada, kembali ke home directory
            self.current_directory = os.path.expanduser("~")
            self.update_directory_info()
            self.show_directory_contents(self.current_directory)

        messagebox.showinfo("Refresh", "View refreshed successfully")

    def jalankan(self):
        """Method untuk menjalankan aplikasi"""
        self.window.mainloop()

# Untuk menjalankan aplikasi
if __name__ == "__main__":
    app = FileExplorer()
    app.jalankan()

Tugas: Test semua fitur file explorer: buka file, buka direktori, buat file baru, hapus file, dan refresh. Perhatikan bagaimana aplikasi menangani berbagai skenario dan memberikan feedback yang sesuai.


Bagian 2: Text Editor dengan File Management

Dalam bagian ini, kita akan membuat text editor yang lebih canggih dengan fitur file management yang lengkap. Text editor adalah salah satu aplikasi yang paling umum menggunakan file operations, sehingga cocok untuk mempelajari konsep file management secara praktis.

2.1 Fitur-Fitur Text Editor Modern

Text editor modern memiliki beberapa fitur standar yang harus ada:

File Operations: New, Open, Save, Save As, dan Recent Files.

Edit Operations: Undo, Redo, Cut, Copy, Paste, Find, Replace.

View Options: Word wrap, line numbers, syntax highlighting.

Status Information: File status, cursor position, file encoding.

2.2 State Management dalam Text Editor

Text editor perlu mengelola berbagai state:

File State: Apakah file sudah disimpan, sudah dimodifikasi, atau masih baru.

Edit State: History untuk undo/redo operations.

View State: Pengaturan tampilan seperti font, theme, dan layout.

2.3 Praktikum 2: Advanced Text Editor

Mari kita buat text editor yang canggih dengan fitur file management lengkap.

Praktikum 2.1: Setup Text Editor dengan Menu System

Buat file baru bernama advanced_text_editor.py:

import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, font
import os
from datetime import datetime

class AdvancedTextEditor:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Advanced Text Editor")
        self.window.geometry("900x700")
        self.window.configure(bg="white")

        # File management variables
        self.current_file = None
        self.is_modified = False
        self.recent_files = []
        self.max_recent_files = 10

        # Editor settings
        self.current_font = ("Consolas", 12)
        self.word_wrap = True
        self.show_line_numbers = True

        # Undo/Redo stacks
        self.undo_stack = []
        self.redo_stack = []

        self.buat_interface()
        self.buat_menu_system()
        self.bind_events()
        self.update_title()
        self.update_status()

    def buat_interface(self):
        # Toolbar
        self.toolbar = tk.Frame(self.window, bg="lightgray", relief=tk.RAISED, bd=1)
        self.toolbar.pack(fill=tk.X)

        # Toolbar buttons
        self.buat_toolbar_buttons()

        # Main editor frame
        editor_frame = tk.Frame(self.window)
        editor_frame.pack(fill=tk.BOTH, expand=True, padx=2, pady=2)

        # Line numbers frame (optional)
        self.line_numbers_frame = tk.Frame(editor_frame, bg="lightgray", width=50)

        # Text area dengan scrollbar
        self.text_area = scrolledtext.ScrolledText(
            editor_frame,
            wrap=tk.WORD if self.word_wrap else tk.NONE,
            undo=True,
            font=self.current_font,
            bg="white",
            fg="black",
            insertbackground="black",
            selectbackground="lightblue"
        )
        self.text_area.pack(fill=tk.BOTH, expand=True)

        # Status bar
        self.status_bar = tk.Frame(self.window, relief=tk.SUNKEN, bd=1)
        self.status_bar.pack(fill=tk.X, side=tk.BOTTOM)

        # Status bar labels
        self.status_label = tk.Label(
            self.status_bar, 
            text="Ready", 
            anchor=tk.W,
            font=("Arial", 9)
        )
        self.status_label.pack(side=tk.LEFT, padx=5)

        self.position_label = tk.Label(
            self.status_bar, 
            text="Line: 1, Column: 1", 
            anchor=tk.E,
            font=("Arial", 9)
        )
        self.position_label.pack(side=tk.RIGHT, padx=5)

        self.file_info_label = tk.Label(
            self.status_bar, 
            text="Untitled", 
            anchor=tk.CENTER,
            font=("Arial", 9)
        )
        self.file_info_label.pack(side=tk.RIGHT, padx=20)

Tugas: Jalankan kode ini dan lihat layout dasar text editor dengan toolbar dan status bar.

Praktikum 2.2: Membuat Toolbar Buttons

Tambahkan method untuk membuat tombol toolbar:

    def buat_toolbar_buttons(self):
        """Method untuk membuat tombol-tombol di toolbar"""
        # New file button
        btn_new = tk.Button(
            self.toolbar,
            text="📄 New",
            command=self.new_file,
            relief=tk.FLAT,
            bg="lightgray",
            font=("Arial", 9)
        )
        btn_new.pack(side=tk.LEFT, padx=2, pady=2)

        # Open file button
        btn_open = tk.Button(
            self.toolbar,
            text="📁 Open",
            command=self.open_file,
            relief=tk.FLAT,
            bg="lightgray",
            font=("Arial", 9)
        )
        btn_open.pack(side=tk.LEFT, padx=2, pady=2)

        # Save file button
        btn_save = tk.Button(
            self.toolbar,
            text="💾 Save",
            command=self.save_file,
            relief=tk.FLAT,
            bg="lightgray",
            font=("Arial", 9)
        )
        btn_save.pack(side=tk.LEFT, padx=2, pady=2)

        # Separator
        separator1 = tk.Frame(self.toolbar, width=2, bg="gray", relief=tk.SUNKEN, bd=1)
        separator1.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=2)

        # Undo button
        btn_undo = tk.Button(
            self.toolbar,
            text="↶ Undo",
            command=self.undo_action,
            relief=tk.FLAT,
            bg="lightgray",
            font=("Arial", 9)
        )
        btn_undo.pack(side=tk.LEFT, padx=2, pady=2)

        # Redo button
        btn_redo = tk.Button(
            self.toolbar,
            text="↷ Redo",
            command=self.redo_action,
            relief=tk.FLAT,
            bg="lightgray",
            font=("Arial", 9)
        )
        btn_redo.pack(side=tk.LEFT, padx=2, pady=2)

        # Separator
        separator2 = tk.Frame(self.toolbar, width=2, bg="gray", relief=tk.SUNKEN, bd=1)
        separator2.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=2)

        # Find button
        btn_find = tk.Button(
            self.toolbar,
            text="🔍 Find",
            command=self.show_find_dialog,
            relief=tk.FLAT,
            bg="lightgray",
            font=("Arial", 9)
        )
        btn_find.pack(side=tk.LEFT, padx=2, pady=2)

        # Font button
        btn_font = tk.Button(
            self.toolbar,
            text="🔤 Font",
            command=self.change_font,
            relief=tk.FLAT,
            bg="lightgray",
            font=("Arial", 9)
        )
        btn_font.pack(side=tk.LEFT, padx=2, pady=2)

Tugas: Jalankan dan lihat toolbar dengan berbagai tombol yang sudah dikelompokkan dengan separator.

Praktikum 2.3: Membuat Menu System

Tambahkan method untuk membuat menu system yang lengkap:

    def buat_menu_system(self):
        """Method untuk membuat menu system lengkap"""
        menubar = tk.Menu(self.window)
        self.window.config(menu=menubar)

        # File Menu
        file_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="File", menu=file_menu)

        file_menu.add_command(label="New", command=self.new_file, accelerator="Ctrl+N")
        file_menu.add_command(label="Open...", command=self.open_file, accelerator="Ctrl+O")
        file_menu.add_separator()
        file_menu.add_command(label="Save", command=self.save_file, accelerator="Ctrl+S")
        file_menu.add_command(label="Save As...", command=self.save_as_file, accelerator="Ctrl+Shift+S")
        file_menu.add_separator()

        # Recent files submenu
        self.recent_menu = tk.Menu(file_menu, tearoff=0)
        file_menu.add_cascade(label="Recent Files", menu=self.recent_menu)
        self.update_recent_menu()

        file_menu.add_separator()
        file_menu.add_command(label="Exit", command=self.on_closing, accelerator="Ctrl+Q")

        # Edit Menu
        edit_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Edit", menu=edit_menu)

        edit_menu.add_command(label="Undo", command=self.undo_action, accelerator="Ctrl+Z")
        edit_menu.add_command(label="Redo", command=self.redo_action, accelerator="Ctrl+Y")
        edit_menu.add_separator()
        edit_menu.add_command(label="Cut", command=self.cut_text, accelerator="Ctrl+X")
        edit_menu.add_command(label="Copy", command=self.copy_text, accelerator="Ctrl+C")
        edit_menu.add_command(label="Paste", command=self.paste_text, accelerator="Ctrl+V")
        edit_menu.add_separator()
        edit_menu.add_command(label="Select All", command=self.select_all, accelerator="Ctrl+A")
        edit_menu.add_command(label="Find...", command=self.show_find_dialog, accelerator="Ctrl+F")
        edit_menu.add_command(label="Replace...", command=self.show_replace_dialog, accelerator="Ctrl+H")

        # View Menu
        view_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="View", menu=view_menu)

        # Word wrap option
        self.wrap_var = tk.BooleanVar(value=self.word_wrap)
        view_menu.add_checkbutton(
            label="Word Wrap", 
            variable=self.wrap_var, 
            command=self.toggle_word_wrap
        )

        # Line numbers option
        self.line_numbers_var = tk.BooleanVar(value=self.show_line_numbers)
        view_menu.add_checkbutton(
            label="Show Line Numbers", 
            variable=self.line_numbers_var, 
            command=self.toggle_line_numbers
        )

        view_menu.add_separator()
        view_menu.add_command(label="Change Font...", command=self.change_font)

        # Tools Menu
        tools_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Tools", menu=tools_menu)

        tools_menu.add_command(label="Word Count", command=self.show_word_count)
        tools_menu.add_command(label="Character Count", command=self.show_char_count)
        tools_menu.add_separator()
        tools_menu.add_command(label="Insert Date/Time", command=self.insert_datetime)

        # Help Menu
        help_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="Help", menu=help_menu)

        help_menu.add_command(label="About", command=self.show_about)

Tugas: Jalankan dan explore menu system yang lengkap. Perhatikan struktur menu dan submenu.

Praktikum 2.4: Implementasi File Operations

Tambahkan method untuk operasi file:

    def new_file(self):
        """Method untuk membuat file baru"""
        if self.is_modified:
            if not self.confirm_save():
                return

        self.text_area.delete(1.0, tk.END)
        self.current_file = None
        self.is_modified = False
        self.update_title()
        self.update_status("New file created")

    def open_file(self):
        """Method untuk membuka file"""
        if self.is_modified:
            if not self.confirm_save():
                return

        file_types = [
            ("Text files", "*.txt"),
            ("Python files", "*.py"),
            ("JavaScript files", "*.js"),
            ("HTML files", "*.html"),
            ("CSS files", "*.css"),
            ("JSON files", "*.json"),
            ("All files", "*.*")
        ]

        filename = filedialog.askopenfilename(
            title="Open File",
            filetypes=file_types
        )

        if filename:
            try:
                with open(filename, 'r', encoding='utf-8') as file:
                    content = file.read()

                self.text_area.delete(1.0, tk.END)
                self.text_area.insert(1.0, content)

                self.current_file = filename
                self.is_modified = False
                self.add_to_recent_files(filename)
                self.update_title()
                self.update_status(f"Opened: {os.path.basename(filename)}")

            except Exception as e:
                messagebox.showerror("Error", f"Cannot open file: {str(e)}")

    def save_file(self):
        """Method untuk menyimpan file"""
        if self.current_file:
            try:
                content = self.text_area.get(1.0, tk.END)
                with open(self.current_file, 'w', encoding='utf-8') as file:
                    file.write(content)

                self.is_modified = False
                self.update_title()
                self.update_status(f"Saved: {os.path.basename(self.current_file)}")
                return True

            except Exception as e:
                messagebox.showerror("Error", f"Cannot save file: {str(e)}")
                return False
        else:
            return self.save_as_file()

    def save_as_file(self):
        """Method untuk save as"""
        file_types = [
            ("Text files", "*.txt"),
            ("Python files", "*.py"),
            ("JavaScript files", "*.js"),
            ("HTML files", "*.html"),
            ("CSS files", "*.css"),
            ("JSON files", "*.json"),
            ("All files", "*.*")
        ]

        filename = filedialog.asksaveasfilename(
            title="Save As",
            filetypes=file_types,
            defaultextension=".txt"
        )

        if filename:
            try:
                content = self.text_area.get(1.0, tk.END)
                with open(filename, 'w', encoding='utf-8') as file:
                    file.write(content)

                self.current_file = filename
                self.is_modified = False
                self.add_to_recent_files(filename)
                self.update_title()
                self.update_status(f"Saved as: {os.path.basename(filename)}")
                return True

            except Exception as e:
                messagebox.showerror("Error", f"Cannot save file: {str(e)}")
                return False

        return False

Tugas: Test operasi file dasar: new, open, save, dan save as. Perhatikan bagaimana status bar dan title window terupdate.

Praktikum 2.5: Recent Files Management

Tambahkan method untuk mengelola recent files:

    def add_to_recent_files(self, filename):
        """Method untuk menambah file ke recent files list"""
        # Hapus file dari list jika sudah ada
        if filename in self.recent_files:
            self.recent_files.remove(filename)

        # Tambah di awal list
        self.recent_files.insert(0, filename)

        # Batasi jumlah recent files
        if len(self.recent_files) > self.max_recent_files:
            self.recent_files = self.recent_files[:self.max_recent_files]

        # Update menu
        self.update_recent_menu()

    def update_recent_menu(self):
        """Method untuk update recent files menu"""
        # Clear existing menu items
        self.recent_menu.delete(0, tk.END)

        if not self.recent_files:
            self.recent_menu.add_command(label="No recent files", state=tk.DISABLED)
        else:
            for i, filename in enumerate(self.recent_files):
                display_name = os.path.basename(filename)
                if len(display_name) > 30:
                    display_name = display_name[:27] + "..."

                self.recent_menu.add_command(
                    label=f"{i+1}. {display_name}",
                    command=lambda f=filename: self.open_recent_file(f)
                )

            self.recent_menu.add_separator()
            self.recent_menu.add_command(
                label="Clear Recent Files",
                command=self.clear_recent_files
            )

    def open_recent_file(self, filename):
        """Method untuk membuka file dari recent files"""
        if not os.path.exists(filename):
            messagebox.showerror("Error", f"File not found: {filename}")
            self.recent_files.remove(filename)
            self.update_recent_menu()
            return

        if self.is_modified:
            if not self.confirm_save():
                return

        try:
            with open(filename, 'r', encoding='utf-8') as file:
                content = file.read()

            self.text_area.delete(1.0, tk.END)
            self.text_area.insert(1.0, content)

            self.current_file = filename
            self.is_modified = False
            self.add_to_recent_files(filename)  # Move to top of recent list
            self.update_title()
            self.update_status(f"Opened: {os.path.basename(filename)}")

        except Exception as e:
            messagebox.showerror("Error", f"Cannot open file: {str(e)}")

    def clear_recent_files(self):
        """Method untuk clear recent files list"""
        if messagebox.askyesno("Clear Recent Files", "Clear all recent files from the list?"):
            self.recent_files.clear()
            self.update_recent_menu()

Tugas: Test recent files functionality dengan membuka beberapa file dan lihat bagaimana menu recent files terupdate.

Praktikum 2.6: Edit Operations

Tambahkan method untuk operasi edit:

    def undo_action(self):
        """Method untuk undo"""
        try:
            self.text_area.edit_undo()
            self.update_status("Undo performed")
        except tk.TclError:
            self.update_status("Nothing to undo")

    def redo_action(self):
        """Method untuk redo"""
        try:
            self.text_area.edit_redo()
            self.update_status("Redo performed")
        except tk.TclError:
            self.update_status("Nothing to redo")

    def cut_text(self):
        """Method untuk cut text"""
        try:
            self.text_area.event_generate("<<Cut>>")
            self.update_status("Text cut to clipboard")
        except:
            self.update_status("Cannot cut text")

    def copy_text(self):
        """Method untuk copy text"""
        try:
            self.text_area.event_generate("<<Copy>>")
            self.update_status("Text copied to clipboard")
        except:
            self.update_status("Cannot copy text")

    def paste_text(self):
        """Method untuk paste text"""
        try:
            self.text_area.event_generate("<<Paste>>")
            self.update_status("Text pasted from clipboard")
        except:
            self.update_status("Cannot paste text")

    def select_all(self):
        """Method untuk select all text"""
        self.text_area.tag_add(tk.SEL, "1.0", tk.END)
        self.text_area.mark_set(tk.INSERT, "1.0")
        self.text_area.see(tk.INSERT)
        self.update_status("All text selected")

Tugas: Test semua operasi edit: undo, redo, cut, copy, paste, dan select all. Perhatikan feedback di status bar.

Praktikum 2.7: Find dan Replace Dialog

Tambahkan method untuk find dan replace:

    def show_find_dialog(self):
        """Method untuk menampilkan dialog find"""
        self.find_dialog = tk.Toplevel(self.window)
        self.find_dialog.title("Find")
        self.find_dialog.geometry("400x150")
        self.find_dialog.transient(self.window)
        self.find_dialog.grab_set()

        # Find input
        tk.Label(self.find_dialog, text="Find:").pack(pady=5)
        self.find_entry = tk.Entry(self.find_dialog, width=40)
        self.find_entry.pack(pady=5)
        self.find_entry.focus()

        # Options frame
        options_frame = tk.Frame(self.find_dialog)
        options_frame.pack(pady=5)

        self.case_sensitive_var = tk.BooleanVar()
        tk.Checkbutton(
            options_frame,
            text="Case sensitive",
            variable=self.case_sensitive_var
        ).pack(side=tk.LEFT, padx=5)

        self.whole_word_var = tk.BooleanVar()
        tk.Checkbutton(
            options_frame,
            text="Whole word",
            variable=self.whole_word_var
        ).pack(side=tk.LEFT, padx=5)

        # Buttons frame
        buttons_frame = tk.Frame(self.find_dialog)
        buttons_frame.pack(pady=10)

        tk.Button(
            buttons_frame,
            text="Find Next",
            command=self.find_next,
            width=10
        ).pack(side=tk.LEFT, padx=5)

        tk.Button(
            buttons_frame,
            text="Find All",
            command=self.find_all,
            width=10
        ).pack(side=tk.LEFT, padx=5)

        tk.Button(
            buttons_frame,
            text="Close",
            command=self.find_dialog.destroy,
            width=10
        ).pack(side=tk.LEFT, padx=5)

        # Bind Enter key
        self.find_entry.bind("<Return>", lambda e: self.find_next())

    def find_next(self):
        """Method untuk find next occurrence"""
        search_text = self.find_entry.get()
        if not search_text:
            return

        # Get current cursor position
        start_pos = self.text_area.index(tk.INSERT)

        # Search options
        case_sensitive = self.case_sensitive_var.get()

        # Perform search
        pos = self.text_area.search(
            search_text,
            start_pos,
            tk.END,
            nocase=not case_sensitive
        )

        if pos:
            # Select found text
            end_pos = f"{pos}+{len(search_text)}c"
            self.text_area.tag_remove(tk.SEL, "1.0", tk.END)
            self.text_area.tag_add(tk.SEL, pos, end_pos)
            self.text_area.mark_set(tk.INSERT, end_pos)
            self.text_area.see(pos)
            self.update_status(f"Found: {search_text}")
        else:
            # Search from beginning
            pos = self.text_area.search(
                search_text,
                "1.0",
                start_pos,
                nocase=not case_sensitive
            )

            if pos:
                end_pos = f"{pos}+{len(search_text)}c"
                self.text_area.tag_remove(tk.SEL, "1.0", tk.END)
                self.text_area.tag_add(tk.SEL, pos, end_pos)
                self.text_area.mark_set(tk.INSERT, end_pos)
                self.text_area.see(pos)
                self.update_status(f"Found: {search_text} (wrapped)")
            else:
                messagebox.showinfo("Find", f"'{search_text}' not found")

    def find_all(self):
        """Method untuk find all occurrences"""
        search_text = self.find_entry.get()
        if not search_text:
            return

        # Remove previous highlights
        self.text_area.tag_remove("found", "1.0", tk.END)

        # Search all occurrences
        start_pos = "1.0"
        count = 0

        while True:
            pos = self.text_area.search(
                search_text,
                start_pos,
                tk.END,
                nocase=not self.case_sensitive_var.get()
            )

            if not pos:
                break

            end_pos = f"{pos}+{len(search_text)}c"
            self.text_area.tag_add("found", pos, end_pos)
            count += 1
            start_pos = end_pos

        # Configure highlight tag
        self.text_area.tag_config("found", background="yellow", foreground="black")

        if count > 0:
            self.update_status(f"Found {count} occurrences of '{search_text}'")
        else:
            messagebox.showinfo("Find All", f"'{search_text}' not found")

    def show_replace_dialog(self):
        """Method untuk menampilkan dialog replace"""
        self.replace_dialog = tk.Toplevel(self.window)
        self.replace_dialog.title("Replace")
        self.replace_dialog.geometry("400x200")
        self.replace_dialog.transient(self.window)
        self.replace_dialog.grab_set()

        # Find input
        tk.Label(self.replace_dialog, text="Find:").pack(pady=5)
        self.replace_find_entry = tk.Entry(self.replace_dialog, width=40)
        self.replace_find_entry.pack(pady=5)

        # Replace input
        tk.Label(self.replace_dialog, text="Replace with:").pack(pady=5)
        self.replace_with_entry = tk.Entry(self.replace_dialog, width=40)
        self.replace_with_entry.pack(pady=5)

        self.replace_find_entry.focus()

        # Options frame
        options_frame = tk.Frame(self.replace_dialog)
        options_frame.pack(pady=5)

        self.replace_case_var = tk.BooleanVar()
        tk.Checkbutton(
            options_frame,
            text="Case sensitive",
            variable=self.replace_case_var
        ).pack(side=tk.LEFT, padx=5)

        # Buttons frame
        buttons_frame = tk.Frame(self.replace_dialog)
        buttons_frame.pack(pady=10)

        tk.Button(
            buttons_frame,
            text="Replace",
            command=self.replace_current,
            width=12
        ).pack(side=tk.LEFT, padx=2)

        tk.Button(
            buttons_frame,
            text="Replace All",
            command=self.replace_all,
            width=12
        ).pack(side=tk.LEFT, padx=2)

        tk.Button(
            buttons_frame,
            text="Close",
            command=self.replace_dialog.destroy,
            width=12
        ).pack(side=tk.LEFT, padx=2)

    def replace_current(self):
        """Method untuk replace current selection"""
        find_text = self.replace_find_entry.get()
        replace_text = self.replace_with_entry.get()

        if not find_text:
            return

        try:
            # Get current selection
            selected_text = self.text_area.get(tk.SEL_FIRST, tk.SEL_LAST)

            # Check if selection matches find text
            if (self.replace_case_var.get() and selected_text == find_text) or \
               (not self.replace_case_var.get() and selected_text.lower() == find_text.lower()):
                # Replace selected text
                self.text_area.delete(tk.SEL_FIRST, tk.SEL_LAST)
                self.text_area.insert(tk.INSERT, replace_text)
                self.update_status(f"Replaced: {find_text} -> {replace_text}")
            else:
                messagebox.showinfo("Replace", "No matching text selected")

        except tk.TclError:
            messagebox.showinfo("Replace", "No text selected")

    def replace_all(self):
        """Method untuk replace all occurrences"""
        find_text = self.replace_find_entry.get()
        replace_text = self.replace_with_entry.get()

        if not find_text:
            return

        content = self.text_area.get("1.0", tk.END)

        if self.replace_case_var.get():
            new_content = content.replace(find_text, replace_text)
            count = content.count(find_text)
        else:
            # Case insensitive replace
            import re
            pattern = re.compile(re.escape(find_text), re.IGNORECASE)
            new_content = pattern.sub(replace_text, content)
            count = len(pattern.findall(content))

        if count > 0:
            self.text_area.delete("1.0", tk.END)
            self.text_area.insert("1.0", new_content)
            self.update_status(f"Replaced {count} occurrences")
            messagebox.showinfo("Replace All", f"Replaced {count} occurrences of '{find_text}'")
        else:
            messagebox.showinfo("Replace All", f"'{find_text}' not found")

Tugas: Test find dan replace functionality. Coba find next, find all, replace current, dan replace all dengan berbagai opsi.

Praktikum 2.8: View Options dan Tools

Tambahkan method untuk view options dan tools:

    def toggle_word_wrap(self):
        """Method untuk toggle word wrap"""
        self.word_wrap = self.wrap_var.get()
        self.text_area.config(wrap=tk.WORD if self.word_wrap else tk.NONE)
        self.update_status(f"Word wrap {'enabled' if self.word_wrap else 'disabled'}")

    def toggle_line_numbers(self):
        """Method untuk toggle line numbers"""
        self.show_line_numbers = self.line_numbers_var.get()
        # Implementation for line numbers would go here
        self.update_status(f"Line numbers {'enabled' if self.show_line_numbers else 'disabled'}")

    def change_font(self):
        """Method untuk mengubah font"""
        # Simple font dialog
        font_dialog = tk.Toplevel(self.window)
        font_dialog.title("Change Font")
        font_dialog.geometry("300x200")
        font_dialog.transient(self.window)
        font_dialog.grab_set()

        # Font family
        tk.Label(font_dialog, text="Font Family:").pack(pady=5)
        font_families = ["Arial", "Times New Roman", "Courier New", "Consolas", "Verdana"]
        font_var = tk.StringVar(value=self.current_font[0])
        font_combo = ttk.Combobox(font_dialog, textvariable=font_var, values=font_families)
        font_combo.pack(pady=5)

        # Font size
        tk.Label(font_dialog, text="Font Size:").pack(pady=5)
        size_var = tk.IntVar(value=self.current_font[1])
        size_spinbox = tk.Spinbox(font_dialog, from_=8, to=72, textvariable=size_var)
        size_spinbox.pack(pady=5)

        # Buttons
        def apply_font():
            new_font = (font_var.get(), size_var.get())
            self.current_font = new_font
            self.text_area.config(font=new_font)
            self.update_status(f"Font changed to {new_font[0]} {new_font[1]}")
            font_dialog.destroy()

        buttons_frame = tk.Frame(font_dialog)
        buttons_frame.pack(pady=20)

        tk.Button(buttons_frame, text="Apply", command=apply_font).pack(side=tk.LEFT, padx=5)
        tk.Button(buttons_frame, text="Cancel", command=font_dialog.destroy).pack(side=tk.LEFT, padx=5)

    def show_word_count(self):
        """Method untuk menampilkan word count"""
        content = self.text_area.get("1.0", tk.END)
        words = len(content.split())
        lines = content.count('\n')

        messagebox.showinfo("Word Count", f"Words: {words}\nLines: {lines}")

    def show_char_count(self):
        """Method untuk menampilkan character count"""
        content = self.text_area.get("1.0", tk.END)
        chars = len(content)
        chars_no_spaces = len(content.replace(' ', '').replace('\n', '').replace('\t', ''))

        messagebox.showinfo("Character Count", f"Characters: {chars}\nCharacters (no spaces): {chars_no_spaces}")

    def insert_datetime(self):
        """Method untuk insert current date/time"""
        current_datetime = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.text_area.insert(tk.INSERT, current_datetime)
        self.update_status("Date/time inserted")

    def show_about(self):
        """Method untuk menampilkan about dialog"""
        about_text = """Advanced Text Editor
Version 1.0

A feature-rich text editor built with Python Tkinter.

Features:
• File operations (New, Open, Save, Save As)
• Recent files management
• Find and Replace
• Undo/Redo
• Word wrap and line numbers
• Font customization
• Word and character count
• Date/time insertion

Created for educational purposes."""

        messagebox.showinfo("About", about_text)

Praktikum 2.9: Event Binding dan Helper Methods

Tambahkan method untuk event binding dan helper methods:

    def bind_events(self):
        """Method untuk binding keyboard shortcuts dan events"""
        # File operations
        self.window.bind('<Control-n>', lambda e: self.new_file())
        self.window.bind('<Control-o>', lambda e: self.open_file())
        self.window.bind('<Control-s>', lambda e: self.save_file())
        self.window.bind('<Control-Shift-S>', lambda e: self.save_as_file())
        self.window.bind('<Control-q>', lambda e: self.on_closing())

        # Edit operations
        self.window.bind('<Control-z>', lambda e: self.undo_action())
        self.window.bind('<Control-y>', lambda e: self.redo_action())
        self.window.bind('<Control-x>', lambda e: self.cut_text())
        self.window.bind('<Control-c>', lambda e: self.copy_text())
        self.window.bind('<Control-v>', lambda e: self.paste_text())
        self.window.bind('<Control-a>', lambda e: self.select_all())

        # Find and replace
        self.window.bind('<Control-f>', lambda e: self.show_find_dialog())
        self.window.bind('<Control-h>', lambda e: self.show_replace_dialog())

        # Text change event
        self.text_area.bind('<Key>', self.on_text_change)
        self.text_area.bind('<Button-1>', self.on_cursor_move)
        self.text_area.bind('<KeyRelease>', self.on_cursor_move)

        # Window close event
        self.window.protocol("WM_DELETE_WINDOW", self.on_closing)

    def on_text_change(self, event=None):
        """Method yang dipanggil saat text berubah"""
        if not self.is_modified:
            self.is_modified = True
            self.update_title()

        # Update cursor position after a short delay
        self.window.after(10, self.update_cursor_position)

    def on_cursor_move(self, event=None):
        """Method yang dipanggil saat cursor bergerak"""
        self.window.after(10, self.update_cursor_position)

    def update_cursor_position(self):
        """Method untuk update posisi cursor di status bar"""
        try:
            cursor_pos = self.text_area.index(tk.INSERT)
            line, column = cursor_pos.split('.')
            self.position_label.config(text=f"Line: {line}, Column: {int(column)+1}")
        except:
            pass

    def update_title(self):
        """Method untuk update title window"""
        title = "Advanced Text Editor - "
        if self.current_file:
            title += os.path.basename(self.current_file)
            self.file_info_label.config(text=os.path.basename(self.current_file))
        else:
            title += "Untitled"
            self.file_info_label.config(text="Untitled")

        if self.is_modified:
            title += " *"

        self.window.title(title)

    def update_status(self, message):
        """Method untuk update status bar"""
        self.status_label.config(text=message)
        # Clear status message after 3 seconds
        self.window.after(3000, lambda: self.status_label.config(text="Ready"))

    def confirm_save(self):
        """Method untuk konfirmasi save sebelum operasi lain"""
        if not self.is_modified:
            return True

        response = messagebox.askyesnocancel(
            "Save Changes",
            "Do you want to save changes to the current document?"
        )

        if response:  # Yes
            return self.save_file()
        elif response is False:  # No
            return True
        else:  # Cancel
            return False

    def on_closing(self):
        """Method yang dipanggil saat window akan ditutup"""
        if self.confirm_save():
            self.window.destroy()

    def jalankan(self):
        """Method untuk menjalankan aplikasi"""
        self.window.mainloop()

# Untuk menjalankan aplikasi
if __name__ == "__main__":
    app = AdvancedTextEditor()
    app.jalankan()

Tugas: Jalankan text editor lengkap dan test semua fitur: file operations, edit operations, find/replace, view options, tools, dan keyboard shortcuts. Perhatikan bagaimana status bar dan title terupdate secara real-time.


Bagian 3: Menangani Format File Berbeda

Dalam bagian ini, kita akan mempelajari cara menangani berbagai format file seperti CSV, JSON, dan XML. Setiap format memiliki struktur dan cara penanganan yang berbeda, sehingga aplikasi kita perlu dapat beradaptasi dengan format yang sesuai.

3.1 Memahami Format File Terstruktur

Format file terstruktur memiliki aturan dan sintaks khusus yang harus diikuti:

CSV (Comma Separated Values): Format tabular sederhana dengan pemisah koma atau karakter lain.

JSON (JavaScript Object Notation): Format pertukaran data yang ringan dan mudah dibaca manusia.

XML (eXtensible Markup Language): Format markup yang fleksibel untuk menyimpan dan transport data.

3.2 Keuntungan Menangani Multiple Format

Aplikasi yang dapat menangani multiple format file memberikan fleksibilitas lebih kepada pengguna dan dapat diintegrasikan dengan berbagai sistem lain.

3.3 Praktikum 3: Multi-Format File Manager

Mari kita buat aplikasi yang dapat menangani berbagai format file dengan interface yang intuitif.

Praktikum 3.1: Setup Multi-Format File Manager

Buat file baru bernama multi_format_manager.py:

import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import csv
import json
import xml.etree.ElementTree as ET
import os
from datetime import datetime

class MultiFormatManager:
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Multi-Format File Manager")
        self.window.geometry("1000x700")
        self.window.configure(bg="lightgray")

        # Data storage
        self.current_file = None
        self.current_format = None
        self.data = None

        # Supported formats
        self.supported_formats = {
            '.csv': 'CSV',
            '.json': 'JSON',
            '.xml': 'XML',
            '.txt': 'Text'
        }

        self.buat_interface()

    def buat_interface(self):
        # Header
        header_frame = tk.Frame(self.window, bg="darkblue", height=60)
        header_frame.pack(fill=tk.X)
        header_frame.pack_propagate(False)

        tk.Label(
            header_frame,
            text="MULTI-FORMAT FILE MANAGER",
            font=("Arial", 16, "bold"),
            fg="white",
            bg="darkblue"
        ).pack(pady=15)

        # Toolbar
        toolbar = tk.Frame(self.window, bg="lightgray", relief=tk.RAISED, bd=1)
        toolbar.pack(fill=tk.X, padx=2, pady=2)

        # Load file button
        btn_load = tk.Button(
            toolbar,
            text="📁 Load File",
            command=self.load_file,
            bg="lightblue",
            font=("Arial", 10),
            width=12
        )
        btn_load.pack(side=tk.LEFT, padx=2, pady=2)

        # Save file button
        btn_save = tk.Button(
            toolbar,
            text="💾 Save File",
            command=self.save_file,
            bg="lightgreen",
            font=("Arial", 10),
            width=12
        )
        btn_save.pack(side=tk.LEFT, padx=2, pady=2)

        # Convert format button
        btn_convert = tk.Button(
            toolbar,
            text="🔄 Convert",
            command=self.show_convert_dialog,
            bg="lightyellow",
            font=("Arial", 10),
            width=12
        )
        btn_convert.pack(side=tk.LEFT, padx=2, pady=2)

        # Validate button
        btn_validate = tk.Button(
            toolbar,
            text="✓ Validate",
            command=self.validate_file,
            bg="lightcoral",
            font=("Arial", 10),
            width=12
        )
        btn_validate.pack(side=tk.LEFT, padx=2, pady=2)

        # File info label
        self.file_info_label = tk.Label(
            toolbar,
            text="No file loaded",
            font=("Arial", 10),
            bg="lightgray"
        )
        self.file_info_label.pack(side=tk.RIGHT, padx=10)

Tugas: Jalankan kode ini dan lihat interface dasar dengan toolbar.

Praktikum 3.2: Membuat Notebook untuk Multiple Views

Tambahkan notebook dengan tabs untuk berbagai view:

        # Main content dengan notebook
        self.notebook = ttk.Notebook(self.window)
        self.notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        # Tab 1: Raw Data View
        self.raw_frame = tk.Frame(self.notebook)
        self.notebook.add(self.raw_frame, text="Raw Data")

        # Text widget untuk raw data
        self.raw_text = tk.Text(
            self.raw_frame,
            font=("Courier", 10),
            wrap=tk.WORD
        )
        raw_scrollbar = tk.Scrollbar(self.raw_frame, orient=tk.VERTICAL, command=self.raw_text.yview)
        self.raw_text.configure(yscrollcommand=raw_scrollbar.set)

        self.raw_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
        raw_scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)

        # Tab 2: Structured View
        self.structured_frame = tk.Frame(self.notebook)
        self.notebook.add(self.structured_frame, text="Structured View")

        # Treeview untuk structured data
        self.tree = ttk.Treeview(self.structured_frame)
        tree_scrollbar_v = ttk.Scrollbar(self.structured_frame, orient=tk.VERTICAL, command=self.tree.yview)
        tree_scrollbar_h = ttk.Scrollbar(self.structured_frame, orient=tk.HORIZONTAL, command=self.tree.xview)

        self.tree.configure(yscrollcommand=tree_scrollbar_v.set, xscrollcommand=tree_scrollbar_h.set)

        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
        tree_scrollbar_v.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
        tree_scrollbar_h.pack(side=tk.BOTTOM, fill=tk.X, padx=5)

        # Tab 3: Statistics
        self.stats_frame = tk.Frame(self.notebook)
        self.notebook.add(self.stats_frame, text="Statistics")

        # Text widget untuk statistics
        self.stats_text = tk.Text(
            self.stats_frame,
            font=("Courier", 11),
            wrap=tk.WORD,
            state=tk.DISABLED
        )
        stats_scrollbar = tk.Scrollbar(self.stats_frame, orient=tk.VERTICAL, command=self.stats_text.yview)
        self.stats_text.configure(yscrollcommand=stats_scrollbar.set)

        self.stats_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
        stats_scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)

Tugas: Jalankan dan lihat notebook dengan tiga tabs untuk berbagai view data.

Praktikum 3.3: Implementasi Load File untuk Multiple Format

Tambahkan method untuk load berbagai format file:

    def load_file(self):
        """Method untuk load file dengan format detection"""
        file_types = [
            ("CSV files", "*.csv"),
            ("JSON files", "*.json"),
            ("XML files", "*.xml"),
            ("Text files", "*.txt"),
            ("All files", "*.*")
        ]

        filename = filedialog.askopenfilename(
            title="Select file to load",
            filetypes=file_types
        )

        if filename:
            try:
                self.current_file = filename
                self.detect_format(filename)
                self.load_file_content(filename)
                self.update_file_info()

                messagebox.showinfo("Success", f"File loaded: {os.path.basename(filename)}")

            except Exception as e:
                messagebox.showerror("Error", f"Cannot load file: {str(e)}")

    def detect_format(self, filename):
        """Method untuk detect format file berdasarkan ekstensi"""
        _, ext = os.path.splitext(filename.lower())
        self.current_format = self.supported_formats.get(ext, 'Unknown')

    def load_file_content(self, filename):
        """Method untuk load content berdasarkan format"""
        if self.current_format == 'CSV':
            self.load_csv_file(filename)
        elif self.current_format == 'JSON':
            self.load_json_file(filename)
        elif self.current_format == 'XML':
            self.load_xml_file(filename)
        else:
            self.load_text_file(filename)

        # Update all views
        self.update_raw_view()
        self.update_structured_view()
        self.update_statistics()

    def load_csv_file(self, filename):
        """Method untuk load CSV file"""
        self.data = {
            'type': 'csv',
            'headers': [],
            'rows': [],
            'raw_content': ''
        }

        with open(filename, 'r', encoding='utf-8') as file:
            # Baca raw content
            file.seek(0)
            self.data['raw_content'] = file.read()

            # Parse CSV
            file.seek(0)
            csv_reader = csv.reader(file)

            # Baca header
            try:
                self.data['headers'] = next(csv_reader)
            except StopIteration:
                self.data['headers'] = []

            # Baca rows
            for row in csv_reader:
                self.data['rows'].append(row)

    def load_json_file(self, filename):
        """Method untuk load JSON file"""
        with open(filename, 'r', encoding='utf-8') as file:
            raw_content = file.read()

        self.data = {
            'type': 'json',
            'content': json.loads(raw_content),
            'raw_content': raw_content
        }

    def load_xml_file(self, filename):
        """Method untuk load XML file"""
        with open(filename, 'r', encoding='utf-8') as file:
            raw_content = file.read()

        tree = ET.parse(filename)
        root = tree.getroot()

        self.data = {
            'type': 'xml',
            'tree': tree,
            'root': root,
            'raw_content': raw_content
        }

    def load_text_file(self, filename):
        """Method untuk load text file"""
        with open(filename, 'r', encoding='utf-8') as file:
            content = file.read()

        self.data = {
            'type': 'text',
            'content': content,
            'raw_content': content
        }

Tugas: Test load file dengan berbagai format dan lihat bagaimana aplikasi mendeteksi format secara otomatis.

Praktikum 3.4: Update Views untuk Setiap Format

Tambahkan method untuk update berbagai view:

    def update_raw_view(self):
        """Method untuk update raw data view"""
        if self.data:
            self.raw_text.delete(1.0, tk.END)
            self.raw_text.insert(1.0, self.data['raw_content'])

    def update_structured_view(self):
        """Method untuk update structured view"""
        # Clear existing items
        for item in self.tree.get_children():
            self.tree.delete(item)

        if not self.data:
            return

        if self.data['type'] == 'csv':
            self.update_csv_tree_view()
        elif self.data['type'] == 'json':
            self.update_json_tree_view()
        elif self.data['type'] == 'xml':
            self.update_xml_tree_view()
        else:
            self.update_text_tree_view()

    def update_csv_tree_view(self):
        """Method untuk update CSV tree view"""
        # Setup columns
        if self.data['headers']:
            self.tree["columns"] = self.data['headers']
            self.tree["show"] = "headings"

            # Configure column headers
            for header in self.data['headers']:
                self.tree.heading(header, text=header)
                self.tree.column(header, width=100)

            # Insert data rows
            for i, row in enumerate(self.data['rows']):
                # Pad row if it has fewer columns than headers
                padded_row = row + [''] * (len(self.data['headers']) - len(row))
                self.tree.insert("", tk.END, values=padded_row[:len(self.data['headers'])])
        else:
            # No headers, show as simple list
            self.tree["columns"] = ("Data",)
            self.tree["show"] = "headings"
            self.tree.heading("Data", text="Data")

            for i, row in enumerate(self.data['rows']):
                self.tree.insert("", tk.END, values=(str(row),))

    def update_json_tree_view(self):
        """Method untuk update JSON tree view"""
        self.tree["show"] = "tree"
        self.tree["columns"] = ("Value",)
        self.tree.heading("#0", text="Key")
        self.tree.heading("Value", text="Value")

        def add_json_node(parent, key, value, path=""):
            if isinstance(value, dict):
                node = self.tree.insert(parent, tk.END, text=key, values=("Object",))
                for k, v in value.items():
                    add_json_node(node, k, v, f"{path}.{k}" if path else k)
            elif isinstance(value, list):
                node = self.tree.insert(parent, tk.END, text=key, values=(f"Array ({len(value)} items)",))
                for i, item in enumerate(value):
                    add_json_node(node, f"[{i}]", item, f"{path}[{i}]" if path else f"[{i}]")
            else:
                self.tree.insert(parent, tk.END, text=key, values=(str(value),))

        if isinstance(self.data['content'], dict):
            for key, value in self.data['content'].items():
                add_json_node("", key, value)
        elif isinstance(self.data['content'], list):
            for i, item in enumerate(self.data['content']):
                add_json_node("", f"[{i}]", item)
        else:
            self.tree.insert("", tk.END, text="Root", values=(str(self.data['content']),))

    def update_xml_tree_view(self):
        """Method untuk update XML tree view"""
        self.tree["show"] = "tree"
        self.tree["columns"] = ("Attributes", "Text")
        self.tree.heading("#0", text="Element")
        self.tree.heading("Attributes", text="Attributes")
        self.tree.heading("Text", text="Text Content")

        def add_xml_node(parent, element):
            # Format attributes
            attrs = ", ".join([f"{k}={v}" for k, v in element.attrib.items()]) if element.attrib else ""

            # Get text content (only direct text, not from children)
            text_content = (element.text or "").strip()

            node = self.tree.insert(
                parent, 
                tk.END, 
                text=element.tag, 
                values=(attrs, text_content)
            )

            # Add child elements
            for child in element:
                add_xml_node(node, child)

        add_xml_node("", self.data['root'])

    def update_text_tree_view(self):
        """Method untuk update text tree view"""
        self.tree["show"] = "tree"
        self.tree["columns"] = ("Content",)
        self.tree.heading("#0", text="Line")
        self.tree.heading("Content", text="Content")

        lines = self.data['content'].split('\n')
        for i, line in enumerate(lines, 1):
            self.tree.insert("", tk.END, text=f"Line {i}", values=(line,))

Tugas: Load berbagai format file dan lihat bagaimana structured view menampilkan data sesuai dengan format masing-masing.

Praktikum 3.5: Generate Statistics untuk Setiap Format

Tambahkan method untuk generate statistics:

    def update_statistics(self):
        """Method untuk update statistics view"""
        self.stats_text.config(state=tk.NORMAL)
        self.stats_text.delete(1.0, tk.END)

        if not self.data:
            self.stats_text.insert(1.0, "No data loaded")
            self.stats_text.config(state=tk.DISABLED)
            return

        stats = self.generate_statistics()
        self.stats_text.insert(1.0, stats)
        self.stats_text.config(state=tk.DISABLED)

    def generate_statistics(self):
        """Method untuk generate statistics berdasarkan format"""
        if self.data['type'] == 'csv':
            return self.generate_csv_statistics()
        elif self.data['type'] == 'json':
            return self.generate_json_statistics()
        elif self.data['type'] == 'xml':
            return self.generate_xml_statistics()
        else:
            return self.generate_text_statistics()

    def generate_csv_statistics(self):
        """Method untuk generate CSV statistics"""
        stats = f"""CSV FILE STATISTICS
{'='*50}

File: {os.path.basename(self.current_file) if self.current_file else 'Unknown'}
Format: CSV (Comma Separated Values)

STRUCTURE:
- Headers: {len(self.data['headers'])}
- Data Rows: {len(self.data['rows'])}
- Total Rows: {len(self.data['rows']) + (1 if self.data['headers'] else 0)}

HEADERS:
"""

        for i, header in enumerate(self.data['headers'], 1):
            stats += f"{i:2d}. {header}\n"

        if self.data['rows']:
            stats += f"\nDATA ANALYSIS:\n"

            # Analyze each column
            for i, header in enumerate(self.data['headers']):
                column_data = []
                for row in self.data['rows']:
                    if i < len(row) and row[i].strip():
                        column_data.append(row[i].strip())

                stats += f"\nColumn '{header}':\n"
                stats += f"  - Non-empty values: {len(column_data)}\n"
                stats += f"  - Empty values: {len(self.data['rows']) - len(column_data)}\n"

                if column_data:
                    # Check if numeric
                    numeric_values = []
                    for value in column_data:
                        try:
                            numeric_values.append(float(value))
                        except ValueError:
                            pass

                    if numeric_values:
                        stats += f"  - Numeric values: {len(numeric_values)}\n"
                        stats += f"  - Min: {min(numeric_values)}\n"
                        stats += f"  - Max: {max(numeric_values)}\n"
                        stats += f"  - Average: {sum(numeric_values)/len(numeric_values):.2f}\n"
                    else:
                        # Text analysis
                        unique_values = set(column_data)
                        stats += f"  - Unique values: {len(unique_values)}\n"
                        if len(unique_values) <= 10:
                            stats += f"  - Values: {', '.join(sorted(unique_values))}\n"

        return stats

    def generate_json_statistics(self):
        """Method untuk generate JSON statistics"""
        stats = f"""JSON FILE STATISTICS
{'='*50}

File: {os.path.basename(self.current_file) if self.current_file else 'Unknown'}
Format: JSON (JavaScript Object Notation)

"""

        def analyze_json_structure(obj, path="root", depth=0):
            result = ""
            indent = "  " * depth

            if isinstance(obj, dict):
                result += f"{indent}{path}: Object ({len(obj)} keys)\n"
                for key, value in obj.items():
                    result += analyze_json_structure(value, key, depth + 1)
            elif isinstance(obj, list):
                result += f"{indent}{path}: Array ({len(obj)} items)\n"
                if obj:
                    # Analyze first few items
                    for i, item in enumerate(obj[:3]):
                        result += analyze_json_structure(item, f"[{i}]", depth + 1)
                    if len(obj) > 3:
                        result += f"{indent}  ... and {len(obj) - 3} more items\n"
            else:
                value_type = type(obj).__name__
                value_str = str(obj)
                if len(value_str) > 50:
                    value_str = value_str[:47] + "..."
                result += f"{indent}{path}: {value_type} = {value_str}\n"

            return result

        stats += "STRUCTURE:\n"
        stats += analyze_json_structure(self.data['content'])

        # Count different types
        def count_types(obj, counts=None):
            if counts is None:
                counts = {}

            obj_type = type(obj).__name__
            counts[obj_type] = counts.get(obj_type, 0) + 1

            if isinstance(obj, dict):
                for value in obj.values():
                    count_types(value, counts)
            elif isinstance(obj, list):
                for item in obj:
                    count_types(item, counts)

            return counts

        type_counts = count_types(self.data['content'])

        stats += f"\nTYPE DISTRIBUTION:\n"
        for obj_type, count in sorted(type_counts.items()):
            stats += f"- {obj_type}: {count}\n"

        return stats

    def generate_xml_statistics(self):
        """Method untuk generate XML statistics"""
        stats = f"""XML FILE STATISTICS
{'='*50}

File: {os.path.basename(self.current_file) if self.current_file else 'Unknown'}
Format: XML (eXtensible Markup Language)

ROOT ELEMENT: {self.data['root'].tag}

"""

        # Count elements
        element_counts = {}
        attribute_counts = {}
        total_elements = 0
        elements_with_text = 0

        def analyze_element(element):
            nonlocal total_elements, elements_with_text

            total_elements += 1

            # Count element types
            tag = element.tag
            element_counts[tag] = element_counts.get(tag, 0) + 1

            # Count attributes
            if element.attrib:
                for attr in element.attrib:
                    attribute_counts[attr] = attribute_counts.get(attr, 0) + 1

            # Check for text content
            if element.text and element.text.strip():
                elements_with_text += 1

            # Recurse through children
            for child in element:
                analyze_element(child)

        analyze_element(self.data['root'])

        stats += f"STRUCTURE ANALYSIS:\n"
        stats += f"- Total elements: {total_elements}\n"
        stats += f"- Elements with text: {elements_with_text}\n"
        stats += f"- Unique element types: {len(element_counts)}\n"
        stats += f"- Unique attributes: {len(attribute_counts)}\n"

        stats += f"\nELEMENT DISTRIBUTION:\n"
        for element, count in sorted(element_counts.items(), key=lambda x: x[1], reverse=True):
            stats += f"- {element}: {count}\n"

        if attribute_counts:
            stats += f"\nATTRIBUTE DISTRIBUTION:\n"
            for attr, count in sorted(attribute_counts.items(), key=lambda x: x[1], reverse=True):
                stats += f"- {attr}: {count}\n"

        return stats

    def generate_text_statistics(self):
        """Method untuk generate text statistics"""
        content = self.data['content']

        stats = f"""TEXT FILE STATISTICS
{'='*50}

File: {os.path.basename(self.current_file) if self.current_file else 'Unknown'}
Format: Plain Text

BASIC COUNTS:
- Characters: {len(content)}
- Characters (no spaces): {len(content.replace(' ', '').replace('\n', '').replace('\t', ''))}
- Words: {len(content.split())}
- Lines: {content.count(chr(10)) + 1}
- Paragraphs: {len([p for p in content.split('\n\n') if p.strip()])}

CHARACTER ANALYSIS:
- Spaces: {content.count(' ')}
- Tabs: {content.count('\t')}
- Newlines: {content.count('\n')}
- Digits: {sum(1 for c in content if c.isdigit())}
- Letters: {sum(1 for c in content if c.isalpha())}
- Uppercase: {sum(1 for c in content if c.isupper())}
- Lowercase: {sum(1 for c in content if c.islower())}

"""

        # Word frequency (top 10)
        words = content.lower().split()
        word_freq = {}
        for word in words:
            # Remove punctuation
            clean_word = ''.join(c for c in word if c.isalnum())
            if clean_word:
                word_freq[clean_word] = word_freq.get(clean_word, 0) + 1

        if word_freq:
            stats += "MOST FREQUENT WORDS:\n"
            sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)
            for word, count in sorted_words[:10]:
                stats += f"- {word}: {count}\n"

        return stats

Tugas: Load berbagai format file dan lihat statistics yang dihasilkan untuk setiap format. Perhatikan bagaimana aplikasi menganalisis struktur dan konten file.

Praktikum 3.6: File Validation dan Conversion

Tambahkan method untuk validasi dan konversi file:

    def validate_file(self):
        """Method untuk validasi file"""
        if not self.data:
            messagebox.showwarning("Warning", "No file loaded!")
            return

        try:
            validation_result = self.perform_validation()
            messagebox.showinfo("Validation Result", validation_result)
        except Exception as e:
            messagebox.showerror("Validation Error", f"Validation failed: {str(e)}")

    def perform_validation(self):
        """Method untuk perform validation berdasarkan format"""
        if self.data['type'] == 'csv':
            return self.validate_csv()
        elif self.data['type'] == 'json':
            return self.validate_json()
        elif self.data['type'] == 'xml':
            return self.validate_xml()
        else:
            return self.validate_text()

    def validate_csv(self):
        """Method untuk validasi CSV"""
        issues = []

        # Check for consistent column count
        if self.data['headers']:
            expected_cols = len(self.data['headers'])
            for i, row in enumerate(self.data['rows'], 2):  # Start from row 2 (after header)
                if len(row) != expected_cols:
                    issues.append(f"Row {i}: Expected {expected_cols} columns, found {len(row)}")

        # Check for empty cells
        empty_cells = 0
        for i, row in enumerate(self.data['rows'], 2):
            for j, cell in enumerate(row):
                if not cell.strip():
                    empty_cells += 1

        result = f"CSV Validation Results:\n\n"
        result += f"✓ File structure: Valid CSV format\n"
        result += f"✓ Headers: {len(self.data['headers'])} columns\n"
        result += f"✓ Data rows: {len(self.data['rows'])}\n"

        if issues:
            result += f"\n⚠ Issues found:\n"
            for issue in issues[:10]:  # Show max 10 issues
                result += f"  - {issue}\n"
            if len(issues) > 10:
                result += f"  ... and {len(issues) - 10} more issues\n"
        else:
            result += f"✓ Column consistency: All rows have correct number of columns\n"

        if empty_cells > 0:
            result += f"⚠ Empty cells: {empty_cells} found\n"
        else:
            result += f"✓ Data completeness: No empty cells\n"

        return result

    def validate_json(self):
        """Method untuk validasi JSON"""
        result = f"JSON Validation Results:\n\n"
        result += f"✓ Syntax: Valid JSON format\n"

        # Check structure
        if isinstance(self.data['content'], dict):
            result += f"✓ Root type: Object with {len(self.data['content'])} keys\n"
        elif isinstance(self.data['content'], list):
            result += f"✓ Root type: Array with {len(self.data['content'])} items\n"
        else:
            result += f"✓ Root type: {type(self.data['content']).__name__}\n"

        # Check for common issues
        def check_json_issues(obj, path=""):
            issues = []

            if isinstance(obj, dict):
                # Check for empty keys
                if "" in obj:
                    issues.append(f"Empty key found at {path}")

                # Check for null values
                for key, value in obj.items():
                    if value is None:
                        issues.append(f"Null value at {path}.{key}")
                    else:
                        issues.extend(check_json_issues(value, f"{path}.{key}" if path else key))

            elif isinstance(obj, list):
                for i, item in enumerate(obj):
                    issues.extend(check_json_issues(item, f"{path}[{i}]" if path else f"[{i}]"))

            return issues

        issues = check_json_issues(self.data['content'])

        if issues:
            result += f"\n⚠ Potential issues:\n"
            for issue in issues[:10]:
                result += f"  - {issue}\n"
            if len(issues) > 10:
                result += f"  ... and {len(issues) - 10} more issues\n"
        else:
            result += f"✓ Structure: No issues detected\n"

        return result

    def validate_xml(self):
        """Method untuk validasi XML"""
        result = f"XML Validation Results:\n\n"
        result += f"✓ Syntax: Well-formed XML\n"
        result += f"✓ Root element: {self.data['root'].tag}\n"

        # Count elements and check for issues
        total_elements = 0
        empty_elements = 0
        elements_with_attrs = 0

        def check_element(element):
            nonlocal total_elements, empty_elements, elements_with_attrs

            total_elements += 1

            if not element.text or not element.text.strip():
                if len(element) == 0:  # No children either
                    empty_elements += 1

            if element.attrib:
                elements_with_attrs += 1

            for child in element:
                check_element(child)

        check_element(self.data['root'])

        result += f"✓ Total elements: {total_elements}\n"
        result += f"✓ Elements with attributes: {elements_with_attrs}\n"

        if empty_elements > 0:
            result += f"⚠ Empty elements: {empty_elements} found\n"
        else:
            result += f"✓ Content: All elements have content or children\n"

        return result

    def validate_text(self):
        """Method untuk validasi text"""
        content = self.data['content']

        result = f"Text File Validation Results:\n\n"
        result += f"✓ File readable as text\n"
        result += f"✓ Character count: {len(content)}\n"
        result += f"✓ Line count: {content.count(chr(10)) + 1}\n"

        # Check encoding issues
        try:
            content.encode('utf-8')
            result += f"✓ Encoding: UTF-8 compatible\n"
        except UnicodeEncodeError:
            result += f"⚠ Encoding: Contains non-UTF-8 characters\n"

        # Check for very long lines
        lines = content.split('\n')
        long_lines = [i+1 for i, line in enumerate(lines) if len(line) > 200]

        if long_lines:
            result += f"⚠ Long lines: {len(long_lines)} lines exceed 200 characters\n"
        else:
            result += f"✓ Line length: All lines under 200 characters\n"

        return result

    def show_convert_dialog(self):
        """Method untuk menampilkan dialog konversi"""
        if not self.data:
            messagebox.showwarning("Warning", "No file loaded!")
            return

        # Create conversion dialog
        convert_dialog = tk.Toplevel(self.window)
        convert_dialog.title("Convert File Format")
        convert_dialog.geometry("400x300")
        convert_dialog.transient(self.window)
        convert_dialog.grab_set()

        tk.Label(
            convert_dialog,
            text=f"Convert from {self.current_format}",
            font=("Arial", 14, "bold")
        ).pack(pady=10)

        tk.Label(
            convert_dialog,
            text="Select target format:",
            font=("Arial", 12)
        ).pack(pady=5)

        # Format selection
        format_var = tk.StringVar(value="JSON")
        formats = ["CSV", "JSON", "XML", "Text"]

        for fmt in formats:
            if fmt != self.current_format:
                tk.Radiobutton(
                    convert_dialog,
                    text=fmt,
                    variable=format_var,
                    value=fmt,
                    font=("Arial", 11)
                ).pack(anchor="w", padx=50)

        # Buttons
        button_frame = tk.Frame(convert_dialog)
        button_frame.pack(pady=20)

        tk.Button(
            button_frame,
            text="Convert",
            command=lambda: self.perform_conversion(format_var.get(), convert_dialog),
            bg="green",
            fg="white",
            width=10
        ).pack(side=tk.LEFT, padx=5)

        tk.Button(
            button_frame,
            text="Cancel",
            command=convert_dialog.destroy,
            bg="red",
            fg="white",
            width=10
        ).pack(side=tk.LEFT, padx=5)

    def perform_conversion(self, target_format, dialog):
        """Method untuk perform file conversion"""
        try:
            if target_format == "CSV":
                converted_data = self.convert_to_csv()
                extension = ".csv"
            elif target_format == "JSON":
                converted_data = self.convert_to_json()
                extension = ".json"
            elif target_format == "XML":
                converted_data = self.convert_to_xml()
                extension = ".xml"
            else:  # Text
                converted_data = self.convert_to_text()
                extension = ".txt"

            # Save converted file
            filename = filedialog.asksaveasfilename(
                title=f"Save as {target_format}",
                defaultextension=extension,
                filetypes=[(f"{target_format} files", f"*{extension}"), ("All files", "*.*")]
            )

            if filename:
                with open(filename, 'w', encoding='utf-8') as file:
                    file.write(converted_data)

                dialog.destroy()
                messagebox.showinfo("Success", f"File converted and saved as {target_format}")

        except Exception as e:
            messagebox.showerror("Conversion Error", f"Cannot convert file: {str(e)}")

    def convert_to_csv(self):
        """Method untuk convert ke CSV"""
        if self.data['type'] == 'csv':
            return self.data['raw_content']
        elif self.data['type'] == 'json':
            # Convert JSON to CSV (flatten if possible)
            if isinstance(self.data['content'], list) and self.data['content']:
                if isinstance(self.data['content'][0], dict):
                    # List of objects - can convert to CSV
                    import io
                    output = io.StringIO()

                    # Get all possible keys
                    all_keys = set()
                    for item in self.data['content']:
                        if isinstance(item, dict):
                            all_keys.update(item.keys())

                    fieldnames = sorted(all_keys)
                    writer = csv.DictWriter(output, fieldnames=fieldnames)
                    writer.writeheader()

                    for item in self.data['content']:
                        if isinstance(item, dict):
                            writer.writerow(item)

                    return output.getvalue()

            # Fallback: convert to simple key-value CSV
            output = "Key,Value\n"
            def flatten_json(obj, prefix=""):
                if isinstance(obj, dict):
                    for key, value in obj.items():
                        new_key = f"{prefix}.{key}" if prefix else key
                        if isinstance(value, (dict, list)):
                            flatten_json(value, new_key)
                        else:
                            output += f'"{new_key}","{value}"\n'
                elif isinstance(obj, list):
                    for i, item in enumerate(obj):
                        new_key = f"{prefix}[{i}]" if prefix else f"[{i}]"
                        if isinstance(item, (dict, list)):
                            flatten_json(item, new_key)
                        else:
                            output += f'"{new_key}","{item}"\n'

            flatten_json(self.data['content'])
            return output

        else:
            # Convert other formats to simple CSV
            return f"Data,Value\n\"{self.current_format}\",\"{self.data.get('raw_content', '')[:100]}...\""

    def convert_to_json(self):
        """Method untuk convert ke JSON"""
        if self.data['type'] == 'json':
            return self.data['raw_content']
        elif self.data['type'] == 'csv':
            # Convert CSV to JSON
            result = []
            for row in self.data['rows']:
                row_dict = {}
                for i, header in enumerate(self.data['headers']):
                    row_dict[header] = row[i] if i < len(row) else ""
                result.append(row_dict)
            return json.dumps(result, indent=2, ensure_ascii=False)
        else:
            # Convert other formats to JSON
            return json.dumps({
                "format": self.current_format,
                "content": self.data.get('raw_content', ''),
                "converted_at": datetime.now().isoformat()
            }, indent=2)

    def convert_to_xml(self):
        """Method untuk convert ke XML"""
        if self.data['type'] == 'xml':
            return self.data['raw_content']
        elif self.data['type'] == 'csv':
            # Convert CSV to XML
            xml_content = '<?xml version="1.0" encoding="UTF-8"?>\n<data>\n'
            for i, row in enumerate(self.data['rows']):
                xml_content += f'  <row id="{i+1}">\n'
                for j, header in enumerate(self.data['headers']):
                    value = row[j] if j < len(row) else ""
                    xml_content += f'    <{header}>{value}</{header}>\n'
                xml_content += '  </row>\n'
            xml_content += '</data>'
            return xml_content
        elif self.data['type'] == 'json':
            # Convert JSON to XML
            def json_to_xml(obj, tag="item"):
                if isinstance(obj, dict):
                    xml = f"<{tag}>\n"
                    for key, value in obj.items():
                        xml += json_to_xml(value, key)
                    xml += f"</{tag}>\n"
                    return xml
                elif isinstance(obj, list):
                    xml = f"<{tag}>\n"
                    for i, item in enumerate(obj):
                        xml += json_to_xml(item, f"item_{i}")
                    xml += f"</{tag}>\n"
                    return xml
                else:
                    return f"<{tag}>{obj}</{tag}>\n"

            xml_content = '<?xml version="1.0" encoding="UTF-8"?>\n'
            xml_content += json_to_xml(self.data['content'], "root")
            return xml_content
        else:
            # Convert other formats to XML
            return f'''<?xml version="1.0" encoding="UTF-8"?>
<document>
  <format>{self.current_format}</format>
  <content>{self.data.get('raw_content', '')}</content>
</document>'''

    def convert_to_text(self):
        """Method untuk convert ke text"""
        if self.data['type'] == 'text':
            return self.data['raw_content']
        else:
            return self.data.get('raw_content', '')

    def save_file(self):
        """Method untuk save file"""
        if not self.data:
            messagebox.showwarning("Warning", "No file loaded!")
            return

        # Get current content from raw view (in case user edited it)
        current_content = self.raw_text.get(1.0, tk.END).rstrip('\n')

        if self.current_file:
            # Save to current file
            try:
                with open(self.current_file, 'w', encoding='utf-8') as file:
                    file.write(current_content)
                messagebox.showinfo("Success", f"File saved: {os.path.basename(self.current_file)}")
            except Exception as e:
                messagebox.showerror("Error", f"Cannot save file: {str(e)}")
        else:
            # Save as new file
            self.save_as_file(current_content)

    def save_as_file(self, content=None):
        """Method untuk save as new file"""
        if content is None:
            if not self.data:
                messagebox.showwarning("Warning", "No file loaded!")
                return
            content = self.raw_text.get(1.0, tk.END).rstrip('\n')

        # Determine file types based on current format
        if self.current_format == 'CSV':
            file_types = [("CSV files", "*.csv"), ("All files", "*.*")]
            default_ext = ".csv"
        elif self.current_format == 'JSON':
            file_types = [("JSON files", "*.json"), ("All files", "*.*")]
            default_ext = ".json"
        elif self.current_format == 'XML':
            file_types = [("XML files", "*.xml"), ("All files", "*.*")]
            default_ext = ".xml"
        else:
            file_types = [("Text files", "*.txt"), ("All files", "*.*")]
            default_ext = ".txt"

        filename = filedialog.asksaveasfilename(
            title="Save File As",
            filetypes=file_types,
            defaultextension=default_ext
        )

        if filename:
            try:
                with open(filename, 'w', encoding='utf-8') as file:
                    file.write(content)

                self.current_file = filename
                self.update_file_info()
                messagebox.showinfo("Success", f"File saved as: {os.path.basename(filename)}")
            except Exception as e:
                messagebox.showerror("Error", f"Cannot save file: {str(e)}")

    def update_file_info(self):
        """Method untuk update file info label"""
        if self.current_file:
            filename = os.path.basename(self.current_file)
            self.file_info_label.config(text=f"{filename} ({self.current_format})")
        else:
            self.file_info_label.config(text="No file loaded")

    def jalankan(self):
        """Method untuk menjalankan aplikasi"""
        self.window.mainloop()

# Untuk menjalankan aplikasi
if __name__ == "__main__":
    app = MultiFormatManager()
    app.jalankan()

Tugas Akhir Pertemuan 2:

Test aplikasi Multi-Format File Manager secara lengkap:

  1. Load berbagai format file (CSV, JSON, XML, TXT)
  2. Lihat structured view untuk setiap format
  3. Analyze statistics yang dihasilkan
  4. Validate file dan lihat hasil validasi
  5. Convert antar format dan save hasil konversi
  6. Edit raw data dan save perubahan

Buat juga sample files untuk testing:

Sample CSV (students.csv):

  0123
  NameAgeGradeCityJohn Doe20AJakartaJane Smith19BBandungBob Johnson21ASurabaya

Sample JSON (products.json):

{
  "products": [
    {
      "id": 1,
      "name": "Laptop",
      "price": 15000000,
      "category": "Electronics"
    },
    {
      "id": 2,
      "name": "Book",
      "price": 50000,
      "category": "Education"
    }
  ]
}

Sample XML (books.xml):

<?xml version="1.0" encoding="UTF-8"?>
<library>
  <book id="1">
    <title>Python Programming</title>
    <author>John Smith</author>
    <year>2023</year>
  </book>
  <book id="2">
    <title>Web Development</title>
    <author>Jane Doe</author>
    <year>2022</year>
  </book>
</library>