Sub Menu "Setelan Preferensi Nutrisi MMAG"

  Isi Menu "Setting"

(Konten Sub Menu "Setelan Preferensi Nutrisi MMAG ")


2. Sub Menu Setelan Preferensi Nutrisi MMAG

Sekarang kita masuk ke  Menu " Pengaturan kita lanjut membuat konten Sub Menu  "Setelan Preferensi Nutrisi MMAG"

Sip 🙌, kita lanjut ke Sub Menu "Setelan Preferensi Nutrisi MMAG" .
Supaya konsisten dengan struktur menu lain (seperti Ubah Profil Pengguna dan Kelompok Bahan ), kami membuat file baru:

📂/storage/emulated/0/AppJadi/SmartNutriChefAI/Setting/sub_menu_preferensi_nutrisi_mmag.py

Isi kode dasarnya seperti ini:

# sub_menu_preferensi_nutrisi_mmag.py
import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3, os

# Lokasi database
DB_PATH = "/storage/emulated/0/SmartNutriChefAI/Database/preferensi_nutrisi_mmag.db"

def get_connection():
    # Pastikan folder database ada
    os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute("""
        CREATE TABLE IF NOT EXISTS preferensi_nutrisi (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            nama TEXT,
            kalori REAL,
            protein REAL,
            lemak REAL,
            karbohidrat REAL,
            catatan TEXT
        )
    """)
    conn.commit()
    return conn


def show_sub_menu_preferensi_nutrisi_mmag(root_frame):
    for w in root_frame.winfo_children():
        w.destroy()

    tk.Label(root_frame, text="Setelan Preferensi Nutrisi MMAG", font=("Arial", 14, "bold")).pack(pady=10)

    # Frame tabel
    table_frame = tk.Frame(root_frame)
    table_frame.pack(fill="both", expand=True, padx=10, pady=8)

    columns = ("id", "nama", "kalori", "protein", "lemak", "karbohidrat", "catatan")
    tree = ttk.Treeview(table_frame, columns=columns, show="headings")

    for col in columns:
        tree.heading(col, text=col.capitalize())
        tree.column(col, width=120, anchor="center")

    # Scrollbar vertikal & horizontal
    vsb = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
    hsb = ttk.Scrollbar(table_frame, orient="horizontal", command=tree.xview)
    tree.configure(yscroll=vsb.set, xscroll=hsb.set)

    tree.grid(row=0, column=0, sticky="nsew")
    vsb.grid(row=0, column=1, sticky="ns")
    hsb.grid(row=1, column=0, sticky="ew")
    table_frame.grid_rowconfigure(0, weight=1)
    table_frame.grid_columnconfigure(0, weight=1)

    tampilkan_data(tree)

    # Tombol aksi
    frame_btn_top = tk.Frame(root_frame)
    frame_btn_top.pack(pady=(10, 2))

    tk.Button(frame_btn_top, text="Simpan", width=12, bg="#388E3C", fg="white", command=lambda: tambah_data(root_frame, tree)).pack(side="left", padx=5)
    tk.Button(frame_btn_top, text="Hapus", width=12, bg="#D32F2F", fg="white", command=lambda: hapus_data(tree)).pack(side="left", padx=5)

    frame_btn_bottom = tk.Frame(root_frame)
    frame_btn_bottom.pack(pady=(2, 10))

    def kembali():
        for w in root_frame.winfo_children():
            w.destroy()
        from menu_pengaturan import show_menu_pengaturan
        show_menu_pengaturan(root_frame)

    tk.Button(frame_btn_bottom, text="↩ Kembali", width=20, bg="#757575", fg="white", command=kembali).pack(pady=5)


def tampilkan_data(tree):
    for item in tree.get_children():
        tree.delete(item)

    conn = get_connection()
    cur = conn.cursor()
    cur.execute("SELECT * FROM preferensi_nutrisi ORDER BY id")
    rows = cur.fetchall()
    conn.close()

    for r in rows:
        tree.insert("", "end", values=r)


def tambah_data(root_frame, tree):
    form = tk.Toplevel(root_frame)
    form.title("Tambah Preferensi Nutrisi")
    form.geometry("400x350")

    labels = ["Nama", "Kalori", "Protein", "Lemak", "Karbohidrat", "Catatan"]
    entries = {}

    for i, lbl in enumerate(labels):
        tk.Label(form, text=lbl).grid(row=i, column=0, sticky="w", padx=8, pady=4)
        ent = tk.Entry(form, width=28)
        ent.grid(row=i, column=1, padx=8, pady=4)
        entries[lbl] = ent

    def _num(s):
        try:
            return float(str(s).strip() or 0)
        except:
            return 0.0

    def simpan():
        data = [
            entries["Nama"].get().strip(),
            _num(entries["Kalori"].get()),
            _num(entries["Protein"].get()),
            _num(entries["Lemak"].get()),
            _num(entries["Karbohidrat"].get()),
            entries["Catatan"].get().strip()
        ]
        if not data[0]:
            messagebox.showerror("Error", "Nama wajib diisi!")
            return
        conn = get_connection()
        cur = conn.cursor()
        cur.execute("INSERT INTO preferensi_nutrisi (nama, kalori, protein, lemak, karbohidrat, catatan) VALUES (?,?,?,?,?,?)", data)
        conn.commit()
        conn.close()
        messagebox.showinfo("Sukses", "Preferensi berhasil disimpan.")
        form.destroy()
        tampilkan_data(tree)

    tk.Button(form, text="Simpan", bg="#4CAF50", fg="white", command=simpan).grid(row=len(labels), columnspan=2, pady=10)


def hapus_data(tree):
    selected = tree.selection()
    if not selected:
        messagebox.showwarning("Hapus", "Pilih baris yang ingin dihapus.")
        return
    item = tree.item(selected[0])["values"]
    pref_id = item[0]

    if not messagebox.askyesno("Konfirmasi", f"Hapus preferensi ID {pref_id}?"):
        return

    conn = get_connection()
    cur = conn.cursor()
    cur.execute("DELETE FROM preferensi_nutrisi WHERE id=?", (pref_id,))
    conn.commit()
    conn.close()
    tree.delete(selected[0])
    messagebox.showinfo("Hapus", "Data berhasil dihapus.")

📌 Alurnya sama dengan profil pengguna :

  • Tabel SQLite otomatis terbentuk ( preferensi_nutrisi).
  • Kolom: id, nama, kalori, protein, lemak, karbohidrat, catatan .
  • Tersedia tombol Simpan , Hapus , dan Kembali .
  • Ada scroll horizontal & vertikal biar tabel terbaca jelas.

Mau saya sekalian update juga file menu_pengaturan.py biar tombol "Setelan Preferensi Nutrisi" langsung memanggil submenu ini?


Pada kode bagian TopLevel form, bantu membuat kotak entri catatan teks tinggi dengan height=14.

Siap 👍, untuk kolom Catatan lebih cocok pakai tk.Textdaripada tk.Entry, karena Entrytidak punya parameter height.

Jadi saya ubah kode bagian tambah_data()pada form TopLevel seperti ini:

def tambah_data(root_frame, tree):
    form = tk.Toplevel(root_frame)
    form.title("Tambah Preferensi Nutrisi")
    form.geometry("420x480")

    labels = ["Nama", "Kalori", "Protein", "Lemak", "Karbohidrat", "Catatan"]
    entries = {}

    for i, lbl in enumerate(labels[:-1]):  # semua kecuali Catatan
        tk.Label(form, text=lbl).grid(row=i, column=0, sticky="w", padx=8, pady=4)
        ent = tk.Entry(form, width=32)
        ent.grid(row=i, column=1, padx=8, pady=4)
        entries[lbl] = ent

    # Khusus Catatan -> Text area dengan height=14
    tk.Label(form, text="Catatan").grid(row=len(labels)-1, column=0, sticky="nw", padx=8, pady=4)
    catatan_txt = tk.Text(form, width=32, height=14, wrap="word")
    catatan_txt.grid(row=len(labels)-1, column=1, padx=8, pady=4, sticky="w")
    entries["Catatan"] = catatan_txt

    def _num(s):
        try:
            return float(str(s).strip() or 0)
        except:
            return 0.0

    def simpan():
        data = [
            entries["Nama"].get().strip(),
            _num(entries["Kalori"].get()),
            _num(entries["Protein"].get()),
            _num(entries["Lemak"].get()),
            _num(entries["Karbohidrat"].get()),
            entries["Catatan"].get("1.0", "end").strip()  # ambil dari Text
        ]
        if not data[0]:
            messagebox.showerror("Error", "Nama wajib diisi!")
            return
        conn = get_connection()
        cur = conn.cursor()
        cur.execute("""
            INSERT INTO preferensi_nutrisi 
            (nama, kalori, protein, lemak, karbohidrat, catatan)
            VALUES (?,?,?,?,?,?)
        """, data)
        conn.commit()
        conn.close()
        messagebox.showinfo("Sukses", "Preferensi berhasil disimpan.")
        form.destroy()
        tampilkan_data(tree)

    tk.Button(form, text="Simpan", bg="#4CAF50", fg="white", command=simpan)\
        .grid(row=len(labels), columnspan=2, pady=10)

📌 Perubahan penting:

  • Entry biasa tetap dipakai untuk Nama, Kalori, Protein, Lemak, Karbohidrat.
  • Catatan diganti tk.Textdengan height=14 (cukup besar, bisa di-scroll).
  • Ambil isi Catatan pakai .get("1.0", "end").strip().

Mau saya tambahkan juga scrollbar vertikal khusus di Catatan supaya kalau height=14 masih bisa discroll saat teks panjang?


Berikan contoh pemakaian ketika pengguna menggunakan fitur preferensi nutrisi mmag ini

Oke 👍 saya kasih contoh bagaimana fitur preferensi nutrisi (misalnya "micronutrient management" / nutrisi MMAG) dipakai user dalam aplikasi, supaya kamu bisa membayangkan alur realnya.

Misalnya kamu sudah punya form TopLevel dengan input bahan, takaran, dan catatan entri (yang sudah kita atur height=14). Nah, untuk fitur preferensi nutrisi MMAG , alurnya bisa seperti ini:


📌 Contoh Pemakaian oleh Pengguna

  1. Pengguna buka formulir tambah bahan manual.

    • Di situ ada field: Bahan , Takaran , dan Catatan .
  2. Pengguna mengetik preferensi nutrisi di bagian catatan.
    Contoh isi entry_catatan:

    Preferensi nutrisi: 
    - Tinggi Kalium (K)
    - Rendah Natrium (Na)
    - Cukup Magnesium (Mg)
    

    Karena kita sudah set height=14, user bisa nulis panjang, tidak terbatas 1 baris.

  3. User simpan data bahan.

    • Program bisa menyimpan catatan ini ke database (misalnya tabel bahan_manual) di kolom catatan.
  4. Saat pengguna memilih menu “Filter nutrisi MMAG”.

    • Aplikasi akan membaca catatan yang tersimpan.
    • Lalu misalnya pengguna memilih preferensi "Tinggi Kalium", aplikasi otomatis menyarankan bahan-bahan yang memiliki catatan sesuai.

📌 Contoh kode penyimpanan sederhana (lanjutkan dari TopLevel form):

def simpan_bahan():
    bahan = entry_bahan.get()
    takaran = entry_takaran.get()
    catatan = entry_catatan.get("1.0", tk.END).strip()  # ambil semua isi
    
    # Simpan ke database / list
    print("Disimpan:", bahan, takaran, catatan)
    # TODO: insert ke tabel sqlite
    # cursor.execute("INSERT INTO bahan_manual (bahan, takaran, catatan) VALUES (?, ?, ?)", (bahan, takaran, catatan))
    # conn.commit()

tk.Button(form, text="Simpan", command=simpan_bahan).pack(pady=10)

👉 Jadi, contoh nyata user:

  • Menambahkan bahan Daun Singkong , takaran 100 gr , lalu di catatan menuliskan preferensi nutrisi: “Kaya Mg, rendah Na, cocok untuk diet hipertensi” .
  • Nanti saat aplikasi menampilkan rekomendasi nutrisi MMAG, catatan ini bisa dipakai sebagai dasar pencarian atau filter.

Mau saya bikin juga contoh fungsi pencarian/filter preferensi nutrisi MMAG dari catatan yang sudah tersimpan?


Bantu tambahkan ketika pengguna beralih sebentar dari applikasi, lalu kembali ke applikasi maka keyboard HP dapat berfungsi dengan baik kemball.

Oke 👍, saya paham maksudmu.
Masalah yang sering muncul di Android (terutama kalau pakai Tkinter via Pydroid atau sejenisnya) adalah: ketika pengguna beralih ke aplikasi lain lalu kembali lagi , keyboard HP kadang tidak muncul lagi saat mengetuk Entryatau Text.

Kita bisa atasi dengan cara:

  1. Bind event<FocusIn> pada setiap Entry/ Text, supaya saat pengguna kembali dan menyentuh kotak input, fokus benar-benar aktif, lalu keyboard muncul.
  2. Kadang-kadang di Pydroid perlu sedikit “trik”: memanggil ulang focus_set()atau after()supaya widget di-refresh.

📌 Contoh implementasi

import tkinter as tk

root = tk.Tk()
root.title("Form dengan Keyboard Aktif")

# Bahan
entry_bahan = tk.Entry(root, width=30)
entry_bahan.pack(pady=5)

# Takaran
entry_takaran = tk.Entry(root, width=30)
entry_takaran.pack(pady=5)

# Catatan (pakai Text, dengan tinggi besar)
entry_catatan = tk.Text(root, width=40, height=14)
entry_catatan.pack(pady=5)

# ======================
# Fungsi agar keyboard aktif kembali
# ======================
def aktifkan_keyboard(event):
    """Pastikan widget dapat fokus ulang sehingga keyboard muncul"""
    widget = event.widget
    root.after(100, widget.focus_set)

# Binding untuk semua entry
entry_bahan.bind("<FocusIn>", aktifkan_keyboard)
entry_takaran.bind("<FocusIn>", aktifkan_keyboard)
entry_catatan.bind("<FocusIn>", aktifkan_keyboard)

root.mainloop()

📌 Cara kerjanya

  • Saat pengguna mengetuk entry_bahan, entry_takaran, atau entry_catatansetelah balik dari aplikasi lain → acara <FocusIn>dipanggil → focus_set()diulang dengan after(100, ...).
  • Ini memastikan input tetap fokus dan keyboard Android muncul kembali .

👉 Mau saya satukan langsung ke kode TopLevel form kamu (yang ada entry_catatan height=14 tadi), biar tidak terpisah dan langsung bisa dicoba?


Apakah kode isi file sub_menu_preferensi_nutrisi_mmag.py menjadi seperti ini:

sub_menu_preferensi_nutrisi_mmag.py

impor tkinter sebagai tk
dari tkinter impor ttk, kotak pesan
impor sqlite3, os

======================

Fungsi agar keyboard aktif kembali

======================

def aktifkan_keyboard(event):
"""Pastikan widget dapat fokus ulang sehingga keyboard muncul"""
widget = event.widget
root.after(100, widget.focus_set)

Binding untuk semua entry

ent.bind("", aktifkan_keyboard)
catatan_txt.bind("", aktifkan_keyboard)

Lokasi database

DB_PATH = "/storage/emulated/0/SmartNutriChefAI/Database/preferensi_nutrisi_mmag.db"

def get_connection():
# Pastikan folder database ada
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS preferensi_nutrisi (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nama TEXT,
kalori REAL,
protein REAL,
lemak REAL,
karbohidrat REAL,
catatan TEXT
)
""")
conn.commit()
return conn

def show_sub_menu_preferensi_nutrisi_mmag(root_frame):
for w in root_frame.winfo_children():
w.destroy()

tk.Label(root_frame, text="Setelan Preferensi Nutrisi \nBerbasis MMAG", font=("Arial", 14, "bold")).pack(pady=10)  

# Frame tabel  
table_frame = tk.Frame(root_frame)  
table_frame.pack(fill="both", expand=True, padx=10, pady=8)  

columns = ("id", "nama", "kalori", "protein", "lemak", "karbohidrat", "catatan")  
tree = ttk.Treeview(table_frame, columns=columns, show="headings")  

for col in columns:  
    tree.heading(col, text=col.capitalize())  
    tree.column(col, width=120, anchor="center")  

# Scrollbar vertikal & horizontal  
vsb = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)  
hsb = ttk.Scrollbar(table_frame, orient="horizontal", command=tree.xview)  
tree.configure(yscroll=vsb.set, xscroll=hsb.set)  

tree.grid(row=0, column=0, sticky="nsew")  
vsb.grid(row=0, column=1, sticky="ns")  
hsb.grid(row=1, column=0, sticky="ew")  
table_frame.grid_rowconfigure(0, weight=1)  
table_frame.grid_columnconfigure(0, weight=1)  

tampilkan_data(tree)  

# Tombol aksi  
frame_btn_top = tk.Frame(root_frame)  
frame_btn_top.pack(pady=(10, 2))  

tk.Button(frame_btn_top, text="Tambah", width=12, bg="#388E3C", fg="white", command=lambda: tambah_data(root_frame, tree)).pack(side="left", padx=5)  
tk.Button(frame_btn_top, text="Hapus", width=12, bg="#D32F2F", fg="white", command=lambda: hapus_data(tree)).pack(side="left", padx=5)  

frame_btn_bottom = tk.Frame(root_frame)  
frame_btn_bottom.pack(pady=(2, 10))  

def kembali():  
    for w in root_frame.winfo_children():  
        w.destroy()  
    from menu_pengaturan import show_menu_pengaturan  
    show_menu_pengaturan(root_frame)  

tk.Button(frame_btn_bottom, text="↩ Kembali", width=20, bg="#757575", fg="white", command=kembali).pack(pady=5)  

def tampilkan_data(tree):
for item in tree.get_children():
tree.delete(item)

conn = get_connection()  
cur = conn.cursor()  
cur.execute("SELECT * FROM preferensi_nutrisi ORDER BY id")  
rows = cur.fetchall()  
conn.close()  

for r in rows:  
    tree.insert("", "end", values=r)  

def tambah_data(root_frame, tree):
form = tk.Toplevel(root_frame)
form.title("Tambah Preferensi Nutrisi")
form.geometry("650x800")

labels = ["Nama", "Kalori", "Protein", "Lemak", "Karbohidrat", "Catatan"]  
entries = {}  

for i, lbl in enumerate(labels[:-1]):  # semua kecuali Catatan  
    tk.Label(form, text=lbl).grid(row=i, column=0, sticky="w", padx=8, pady=4)  
    ent = tk.Entry(form, width=28)  
    ent.grid(row=i, column=1, padx=8, pady=4)  
    entries[lbl] = ent  

# Khusus Catatan -> Text area dengan height=30  
tk.Label(form, text="Catatan").grid(row=len(labels)-1, column=0, sticky="nw", padx=8, pady=4)  
catatan_txt = tk.Text(form, width=28, height=14, wrap="word")  
catatan_txt.grid(row=len(labels)-1, column=1, padx=8, pady=4, sticky="w")  
entries["Catatan"] = catatan_txt  

def _num(s):  
    try:  
        return float(str(s).strip() or 0)  
    except:  
        return 0.0  

def simpan():  
    data = [  
        entries["Nama"].get().strip(),  
        _num(entries["Kalori"].get()),  
        _num(entries["Protein"].get()),  
        _num(entries["Lemak"].get()),  
        _num(entries["Karbohidrat"].get()),  
        entries["Catatan"].get("1.0", "end").strip()  # ambil dari Text  
    ]  
    if not data[0]:  
        messagebox.showerror("Error", "Nama wajib diisi!")  
        return  
    conn = get_connection()  
    cur = conn.cursor()  
    cur.execute("""  
        INSERT INTO preferensi_nutrisi   
        (nama, kalori, protein, lemak, karbohidrat, catatan)  
        VALUES (?,?,?,?,?,?)  
    """, data)  
    conn.commit()  
    conn.close()  
    messagebox.showinfo("Sukses", "Preferensi berhasil disimpan.")  
    form.destroy()  
    tampilkan_data(tree)  

tk.Button(form, text="Simpan", bg="#4CAF50", fg="white", command=simpan)\  
    .grid(row=len(labels), columnspan=2, pady=10)  

def hapus_data(tree):
selected = tree.selection()
if not selected:
messagebox.showwarning("Hapus", "Pilih baris yang ingin dihapus.")
return
item = tree.item(selected[0])["values"]
pref_id = item[0]

if not messagebox.askyesno("Konfirmasi", f"Hapus preferensi ID {pref_id}?"):  
    return  

conn = get_connection()  
cur = conn.cursor()  
cur.execute("DELETE FROM preferensi_nutrisi WHERE id=?", (pref_id,))  
conn.commit()  
conn.close()  
tree.delete(selected[0])  
messagebox.showinfo("Hapus", "Data berhasil dihapus.")     
 

Nah, hampir benar 👍 tapi ada beberapa masalah di kode yang kamu tulis:

  1. Binding ent.binddancatatan_txt.bind kamu letakkan di luar fungsi tambah_data.
    → Padahal variabel entdan catatan_txtbaru ada setelah tambah_data()dijalankan, jadi kalau dipanggil langsung di atas, pasti error ( NameError).

  2. Variabel rootdi dalamnyaaktifkan_keyboard juga tidak jelas asalnya.
    → Lebih baik pakai form.after()(karena widget yang aktif ada di dalam Toplevel form).

  3. Tinggi=14  yang kamu maksud catatan_txtsebaiknya langsung dipakai. 


✅ Perbaikan versi lengkap

# sub_menu_preferensi_nutrisi_mmag.py
import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3, os

# Lokasi database
DB_PATH = "/storage/emulated/0/SmartNutriChefAI/Database/preferensi_nutrisi_mmag.db"

def get_connection():
    # Pastikan folder database ada
    os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute("""
        CREATE TABLE IF NOT EXISTS preferensi_nutrisi (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            nama TEXT,
            kalori REAL,
            protein REAL,
            lemak REAL,
            karbohidrat REAL,
            catatan TEXT
        )
    """)
    conn.commit()
    return conn

def show_sub_menu_preferensi_nutrisi_mmag(root_frame):
    for w in root_frame.winfo_children():
        w.destroy()

    tk.Label(root_frame, text="Setelan Preferensi Nutrisi \nBerbasis MMAG", 
             font=("Arial", 14, "bold")).pack(pady=10)

    # Frame tabel
    table_frame = tk.Frame(root_frame)
    table_frame.pack(fill="both", expand=True, padx=10, pady=8)

    columns = ("id", "nama", "kalori", "protein", "lemak", "karbohidrat", "catatan")
    tree = ttk.Treeview(table_frame, columns=columns, show="headings")

    for col in columns:
        tree.heading(col, text=col.capitalize())
        tree.column(col, width=120, anchor="center")

    # Scrollbar vertikal & horizontal
    vsb = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
    hsb = ttk.Scrollbar(table_frame, orient="horizontal", command=tree.xview)
    tree.configure(yscroll=vsb.set, xscroll=hsb.set)

    tree.grid(row=0, column=0, sticky="nsew")
    vsb.grid(row=0, column=1, sticky="ns")
    hsb.grid(row=1, column=0, sticky="ew")
    table_frame.grid_rowconfigure(0, weight=1)
    table_frame.grid_columnconfigure(0, weight=1)

    tampilkan_data(tree)

    # Tombol aksi
    frame_btn_top = tk.Frame(root_frame)
    frame_btn_top.pack(pady=(10, 2))

    tk.Button(frame_btn_top, text="Tambah", width=12, bg="#388E3C", fg="white", 
              command=lambda: tambah_data(root_frame, tree)).pack(side="left", padx=5)
    tk.Button(frame_btn_top, text="Hapus", width=12, bg="#D32F2F", fg="white", 
              command=lambda: hapus_data(tree)).pack(side="left", padx=5)

    frame_btn_bottom = tk.Frame(root_frame)
    frame_btn_bottom.pack(pady=(2, 10))

    def kembali():
        for w in root_frame.winfo_children():
            w.destroy()
        from menu_pengaturan import show_menu_pengaturan
        show_menu_pengaturan(root_frame)

    tk.Button(frame_btn_bottom, text="↩ Kembali", width=20, bg="#757575", fg="white", 
              command=kembali).pack(pady=5)

def tampilkan_data(tree):
    for item in tree.get_children():
        tree.delete(item)

    conn = get_connection()
    cur = conn.cursor()
    cur.execute("SELECT * FROM preferensi_nutrisi ORDER BY id")
    rows = cur.fetchall()
    conn.close()

    for r in rows:
        tree.insert("", "end", values=r)

def tambah_data(root_frame, tree):
    form = tk.Toplevel(root_frame)
    form.title("Tambah Preferensi Nutrisi")
    form.geometry("650x800")

    labels = ["Nama", "Kalori", "Protein", "Lemak", "Karbohidrat", "Catatan"]
    entries = {}

    def aktifkan_keyboard(event):
        """Pastikan widget fokus ulang → keyboard muncul"""
        widget = event.widget
        form.after(100, widget.focus_set)

    for i, lbl in enumerate(labels[:-1]):  # semua kecuali Catatan
        tk.Label(form, text=lbl).grid(row=i, column=0, sticky="w", padx=8, pady=4)
        ent = tk.Entry(form, width=28)
        ent.grid(row=i, column=1, padx=8, pady=4)
        ent.bind("<FocusIn>", aktifkan_keyboard)   # ⬅️ bind di sini
        entries[lbl] = ent

    # Khusus Catatan -> Text area dengan height=30
    tk.Label(form, text="Catatan").grid(row=len(labels)-1, column=0, sticky="nw", padx=8, pady=4)
    catatan_txt = tk.Text(form, width=28, height=14, wrap="word")  # ⬅️ sesuai permintaan
    catatan_txt.grid(row=len(labels)-1, column=1, padx=8, pady=4, sticky="w")
    catatan_txt.bind("<FocusIn>", aktifkan_keyboard)
    entries["Catatan"] = catatan_txt

    def _num(s):
        try:
            return float(str(s).strip() or 0)
        except:
            return 0.0

    def simpan():
        data = [
            entries["Nama"].get().strip(),
            _num(entries["Kalori"].get()),
            _num(entries["Protein"].get()),
            _num(entries["Lemak"].get()),
            _num(entries["Karbohidrat"].get()),
            entries["Catatan"].get("1.0", "end").strip()  # ambil dari Text
        ]
        if not data[0]:
            messagebox.showerror("Error", "Nama wajib diisi!")
            return
        conn = get_connection()
        cur = conn.cursor()
        cur.execute("""
            INSERT INTO preferensi_nutrisi 
            (nama, kalori, protein, lemak, karbohidrat, catatan)
            VALUES (?,?,?,?,?,?)
        """, data)
        conn.commit()
        conn.close()
        messagebox.showinfo("Sukses", "Preferensi berhasil disimpan.")
        form.destroy()
        tampilkan_data(tree)

    tk.Button(form, text="Simpan", bg="#4CAF50", fg="white", command=simpan)\
        .grid(row=len(labels), columnspan=2, pady=10)

def hapus_data(tree):
    selected = tree.selection()
    if not selected:
        messagebox.showwarning("Hapus", "Pilih baris yang ingin dihapus.")
        return
    item = tree.item(selected[0])["values"]
    pref_id = item[0]

    if not messagebox.askyesno("Konfirmasi", f"Hapus preferensi ID {pref_id}?"):
        return

    conn = get_connection()
    cur = conn.cursor()
    cur.execute("DELETE FROM preferensi_nutrisi WHERE id=?", (pref_id,))
    conn.commit()
    conn.close()
    tree.delete(selected[0])
    messagebox.showinfo("Hapus", "Data berhasil dihapus.")

✨ Perbedaan penting:

  • ent.binddan catatan_txt.bindsekarang ada di dalamtambah_data() , bukan di luar.
  • Fungsi aktifkan_keyboardpakai form.after(...), bukan root.
  • Text Catatansudah height=14.

Mau saya buat juga contoh interaksi pengguna dengan menu ini, misalnya alur: buka → tambah preferensi → isi data → simpan → keyboard muncul lagi meskipun aplikasi sempat di-minimize?


Tambahkan fungsi menu konteks (potong, salin, tempel, hapus, pilih semua) pada setiap kotak entri dan teks.

Mantap 👍 kami menambahkan menu konteks (klik kanan / tekan lama) ke semua Entrydan Text(catatan).
Menu ini akan berisi: Potong, Salin, Tempel, Hapus, Pilih Semua .


🔧 Implementasi

Saya buat fungsi utilitas supaya bisa dipasang ke semua Entrydan Text:

def add_context_menu(widget):
    menu = tk.Menu(widget, tearoff=0)
    menu.add_command(label="Cut", command=lambda: widget.event_generate("<<Cut>>"))
    menu.add_command(label="Copy", command=lambda: widget.event_generate("<<Copy>>"))
    menu.add_command(label="Paste", command=lambda: widget.event_generate("<<Paste>>"))
    menu.add_command(label="Delete", command=lambda: widget.delete("sel.first", "sel.last") if widget.selection_get() else None)
    menu.add_separator()
    menu.add_command(label="Select All", command=lambda: widget.select_range(0, "end") if isinstance(widget, tk.Entry) else widget.tag_add("sel", "1.0", "end"))

    def show_menu(event):
        try:
            menu.tk_popup(event.x_root, event.y_root)
        finally:
            menu.grab_release()

    widget.bind("<Button-3>", show_menu)   # Klik kanan (Windows/Linux)
    widget.bind("<Button-2>", show_menu)   # Long-press (Android/iOS via SDL/tkinter-mobile)

🔄 Integrasi ketambah_data

Tinggal kita menelepon add_context_menu(ent)untuk setiap Entry, dan add_context_menu(catatan_txt)untuk Text.

Contoh modifikasi di dalam loop for i, lbl in enumerate(labels[:-1])::

ent = tk.Entry(form, width=28)
ent.grid(row=i, column=1, padx=8, pady=4)
ent.bind("<FocusIn>", aktifkan_keyboard)
add_context_menu(ent)   # ⬅️ pasang context menu
entries[lbl] = ent

Dan untuk catatan:

catatan_txt = tk.Text(form, width=28, height=30, wrap="word")
catatan_txt.grid(row=len(labels)-1, column=1, padx=8, pady=4, sticky="w")
catatan_txt.bind("<FocusIn>", aktifkan_keyboard)
add_context_menu(catatan_txt)   # ⬅️ pasang context menu
entries["Catatan"] = catatan_txt

📌 Hasil

Sekarang di semua Entrydan Textada menu konteks:

  • Cut → potong teks terpilih.
  • Copy → salin teks terpilih.
  • Tempel → tempel teks dari clipboard.
  • Hapus → hapus teks terpilih.
  • Pilih Semua → blok semua teks.

Mau saya satukan langsung ke file sub_menu_preferensi_nutrisi_mmag.pyyang sudah kita buat kemarin, jadi kamu tinggal copy–replace full script, atau cukup saya kasih potongan tambahan fungsi + cara pakai saja?


Kenapa kursor keyboard HP sulit tampil ketika beralih sebentar keluar applikasi, lalu kembali lagi ke applikasi.

Nah 👍, ini memang masalah klasik kalau jalankan tkinter di Android (via Pydroid3 / Termux-X11 / dll.) .
Keyboard terkadang tidak muncul lagi setelah aplikasi kehilangan fokus → balik lagi, terutama di Entryatau Text.


📌 Penyebab

  • Tkinter tidak asli Android , jadi ketika aplikasi di- pause/resume (berpindah ke latar belakang → latar depan), acara fokus ke widget terkadang hilang.
  • Keyboard virtual butuh event fokus eksplisit agar bisa muncul lagi.
  • Jika tidak ada event focus_set()atau binding ke FocusIn, maka kursor diam, keyboard tidak mau muncul.

🔧 Solusi Praktis

  1. Paksa fokus ulang ke widget terakhir ketika aplikasi kembali aktif.
    Bisa pakai event "<FocusIn>"dan fungsi focus_set()dengan delay kecil ( after).

  2. Pengikatan global untuk semua Entri/Teks:

def aktifkan_keyboard(event):
    """Pastikan widget dapat fokus ulang sehingga keyboard muncul"""
    widget = event.widget
    widget.after(100, lambda: widget.focus_set())

Lalu setiap Entrydan Textkita pasang:

ent.bind("<FocusIn>", aktifkan_keyboard)
catatan_txt.bind("<FocusIn>", aktifkan_keyboard)
  1. Acara saat jendela diaktifkan lagi (kalau didukung):
root.bind("<FocusIn>", lambda e: root.focus_force())

Begitu, ketika pengguna membalik ke aplikasi, tkinter akan memaksakan fokus ke jendela aktif → dan entri terakhir bisa fokus ulang → keyboard muncul.


📌 Tips tambahan khusus Android

  • Terkadang keyboard tetap bandel. Trik lain: ketuk sekali lagi di Entri/Teks untuk memaksa trigger FocusIn.
  • Di beberapa perangkat, Pydroid memerlukan pengaturan “Force show keyboard” di pengaturan → untuk memaksa keyboard tampil meskipun tidak ada acara.

👉 Jadi solusinya:
Kita paksa bindingFocusIn ke semua entri dan teks, plus pasang event global di rootagar "<FocusIn>"window tetap memanggil focus_force()ketika pengguna kembali ke aplikasi.


Mau saya langsung gabungkan patch ini ke filesub_menu_preferensi_nutrisi_mmag.py biar semua Entry/Text otomatis selalu hidup kembali keyboardnya?


Mau kamu gabungkan patch ini ke dalam kode isi file sub_menu_preferensi_nutrisi_mmag.py berikut ini, biar semua Entry/Text otomatis selalu hidup kembali keyboardnya.
Kode:

sub_menu_preferensi_nutrisi_mmag.py

impor tkinter sebagai tk
dari tkinter impor ttk, kotak pesan
impor sqlite3, os

Lokasi database

DB_PATH = "/storage/emulated/0/SmartNutriChefAI/Database/preferensi_nutrisi_mmag.db"

def get_connection():
# Pastikan folder database ada
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS preferensi_nutrisi (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nama TEXT,
kalori REAL,
protein REAL,
lemak REAL,
karbohidrat REAL,
catatan TEXT
)
""")
conn.commit()
return conn

def add_context_menu(widget):
menu = tk.Menu(widget, air mata=0)
menu.add_command(label="Potong", perintah=lambda: widget.event_generate("<>"))
menu.add_command(label="Salin", perintah=lambda: widget.event_generate("<>"))
menu.add_command(label="Tempel", perintah=lambda: widget.event_generate("<>"))
menu.add_command(label="Hapus", command=lambda: widget.delete("sel.first", "sel.last") if widget.selection_get() else None)
menu.add_separator()
menu.add_command(label="Pilih Semua", command=lambda: widget.select_range(0, "end") if isinstance(widget, tk.Entry) else widget.tag_add("sel", "1.0", "end"))

def show_menu(event):  
    try:  
        menu.tk_popup(event.x_root, event.y_root)  
    finally:  
        menu.grab_release()  

widget.bind("<Button-3>", show_menu)   # Klik kanan (Windows/Linux)  
widget.bind("<Button-2>", show_menu)   # Long-press (Android/iOS via SDL/tkinter-mobile)  

def show_sub_menu_preferensi_nutrisi_mmag(root_frame):
for w in root_frame.winfo_children():
w.destroy()

tk.Label(root_frame, text="Setelan Preferensi Nutrisi \nBerbasis MMAG",   
         font=("Arial", 14, "bold")).pack(pady=10)  

# Frame tabel  
table_frame = tk.Frame(root_frame)  
table_frame.pack(fill="both", expand=True, padx=10, pady=8)  

columns = ("id", "nama", "kalori", "protein", "lemak", "karbohidrat", "catatan")  
tree = ttk.Treeview(table_frame, columns=columns, show="headings")  

for col in columns:  
    tree.heading(col, text=col.capitalize())  
    tree.column(col, width=120, anchor="center")  

# Scrollbar vertikal & horizontal  
vsb = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)  
hsb = ttk.Scrollbar(table_frame, orient="horizontal", command=tree.xview)  
tree.configure(yscroll=vsb.set, xscroll=hsb.set)  

tree.grid(row=0, column=0, sticky="nsew")  
vsb.grid(row=0, column=1, sticky="ns")  
hsb.grid(row=1, column=0, sticky="ew")  
table_frame.grid_rowconfigure(0, weight=1)  
table_frame.grid_columnconfigure(0, weight=1)  

tampilkan_data(tree)  

# Tombol aksi  
frame_btn_top = tk.Frame(root_frame)  
frame_btn_top.pack(pady=(10, 2))  

tk.Button(frame_btn_top, text="Tambah", width=12, bg="#388E3C", fg="white",   
          command=lambda: tambah_data(root_frame, tree)).pack(side="left", padx=5)  
tk.Button(frame_btn_top, text="Hapus", width=12, bg="#D32F2F", fg="white",   
          command=lambda: hapus_data(tree)).pack(side="left", padx=5)  

frame_btn_bottom = tk.Frame(root_frame)  
frame_btn_bottom.pack(pady=(2, 10))  

def kembali():  
    for w in root_frame.winfo_children():  
        w.destroy()  
    from menu_pengaturan import show_menu_pengaturan  
    show_menu_pengaturan(root_frame)  

tk.Button(frame_btn_bottom, text="↩ Kembali", width=20, bg="#757575", fg="white",   
          command=kembali).pack(pady=5)  

def tampilkan_data(tree):
for item in tree.get_children():
tree.delete(item)

conn = get_connection()  
cur = conn.cursor()  
cur.execute("SELECT * FROM preferensi_nutrisi ORDER BY id")  
rows = cur.fetchall()  
conn.close()  

for r in rows:  
    tree.insert("", "end", values=r)  

def tambah_data(root_frame, tree):
form = tk.Toplevel(root_frame)
form.title("Tambah Preferensi Nutrisi")
form.geometry("650x800")

labels = ["Nama", "Kalori", "Protein", "Lemak", "Karbohidrat", "Catatan"]  
entries = {}  

def aktifkan_keyboard(event):  
    """Pastikan widget fokus ulang → keyboard muncul"""  
    widget = event.widget  
    form.after(100, widget.focus_set)  

for i, lbl in enumerate(labels[:-1]):  # semua kecuali Catatan  
    tk.Label(form, text=lbl).grid(row=i, column=0, sticky="w", padx=8, pady=4)  
    ent = tk.Entry(form, width=28)  
    ent.grid(row=i, column=1, padx=8, pady=4)  
    ent.bind("<FocusIn>", aktifkan_keyboard)   # ⬅️ bind di sini  
    add_context_menu(ent)   # ⬅️ pasang context menu  
    entries[lbl] = ent  

# Khusus Catatan -> Text area dengan height=30  
tk.Label(form, text="Catatan").grid(row=len(labels)-1, column=0, sticky="nw", padx=8, pady=4)  
catatan_txt = tk.Text(form, width=28, height=14, wrap="word")  # ⬅️ sesuai permintaan  
catatan_txt.grid(row=len(labels)-1, column=1, padx=8, pady=4, sticky="w")  
catatan_txt.bind("<FocusIn>", aktifkan_keyboard)  
add_context_menu(catatan_txt)   # ⬅️ pasang context menu  
entries["Catatan"] = catatan_txt  

def _num(s):  
    try:  
        return float(str(s).strip() or 0)  
    except:  
        return 0.0  

def simpan():  
    data = [  
        entries["Nama"].get().strip(),  
        _num(entries["Kalori"].get()),  
        _num(entries["Protein"].get()),  
        _num(entries["Lemak"].get()),  
        _num(entries["Karbohidrat"].get()),  
        entries["Catatan"].get("1.0", "end").strip()  # ambil dari Text  
    ]  
    if not data[0]:  
        messagebox.showerror("Error", "Nama wajib diisi!")  
        return  
    conn = get_connection()  
    cur = conn.cursor()  
    cur.execute("""  
        INSERT INTO preferensi_nutrisi   
        (nama, kalori, protein, lemak, karbohidrat, catatan)  
        VALUES (?,?,?,?,?,?)  
    """, data)  
    conn.commit()  
    conn.close()  
    messagebox.showinfo("Sukses", "Preferensi berhasil disimpan.")  
    form.destroy()  
    tampilkan_data(tree)  

tk.Button(form, text="Simpan", bg="#4CAF50", fg="white", command=simpan)\  
    .grid(row=len(labels), columnspan=2, pady=10)  

def hapus_data(tree):
selected = tree.selection()
if not selected:
messagebox.showwarning("Hapus", "Pilih baris yang ingin dihapus.")
return
item = tree.item(selected[0])["values"]
pref_id = item[0]

if not messagebox.askyesno("Konfirmasi", f"Hapus preferensi ID {pref_id}?"):  
    return  

conn = get_connection()  
cur = conn.cursor()  
cur.execute("DELETE FROM preferensi_nutrisi WHERE id=?", (pref_id,))  
conn.commit()  
conn.close()  
tree.delete(selected[0])  
messagebox.showinfo("Hapus", "Data berhasil dihapus.")  

 

Oke 👍, saya sudah gabungkan patch keyboard aktif ulang langsung ke dalam kode kamu.
Sekarang semua Entrydan Textotomatis akan memanggil focus_set()ulang saat FocusIn, ditambah lagi root.bind("<FocusIn>")ketika pengguna kembali ke aplikasi, fokus dipaksa aktif dan keyboard bisa muncul.

Berikut versi revisi file lengkap sub_menu_preferensi_nutrisi_mmag.py:

# sub_menu_preferensi_nutrisi_mmag.py
import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3, os

# Lokasi database
DB_PATH = "/storage/emulated/0/SmartNutriChefAI/Database/preferensi_nutrisi_mmag.db"

def get_connection():
    # Pastikan folder database ada
    os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute("""
        CREATE TABLE IF NOT EXISTS preferensi_nutrisi (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            nama TEXT,
            kalori REAL,
            protein REAL,
            lemak REAL,
            karbohidrat REAL,
            catatan TEXT
        )
    """)
    conn.commit()
    return conn

# =======================
# Context menu (Cut/Copy/Paste/Delete/Select All)
# =======================
def add_context_menu(widget):
    menu = tk.Menu(widget, tearoff=0)
    menu.add_command(label="Cut", command=lambda: widget.event_generate("<<Cut>>"))
    menu.add_command(label="Copy", command=lambda: widget.event_generate("<<Copy>>"))
    menu.add_command(label="Paste", command=lambda: widget.event_generate("<<Paste>>"))
    menu.add_command(label="Delete", command=lambda: widget.delete("sel.first", "sel.last") if widget.selection_get() else None)
    menu.add_separator()
    menu.add_command(label="Select All", command=lambda: widget.select_range(0, "end") if isinstance(widget, tk.Entry) else widget.tag_add("sel", "1.0", "end"))

    def show_menu(event):
        try:
            menu.tk_popup(event.x_root, event.y_root)
        finally:
            menu.grab_release()

    widget.bind("<Button-3>", show_menu)   # Klik kanan (Windows/Linux)
    widget.bind("<Button-2>", show_menu)   # Long-press (Android/iOS via SDL/tkinter-mobile)

# =======================
# Keyboard fix
# =======================
def aktifkan_keyboard(event):
    """Pastikan widget fokus ulang → keyboard muncul"""
    widget = event.widget
    widget.after(100, lambda: widget.focus_set())

def show_sub_menu_preferensi_nutrisi_mmag(root_frame):
    for w in root_frame.winfo_children():
        w.destroy()

    tk.Label(root_frame, text="Setelan Preferensi Nutrisi \nBerbasis MMAG", 
             font=("Arial", 14, "bold")).pack(pady=10)

    # Frame tabel
    table_frame = tk.Frame(root_frame)
    table_frame.pack(fill="both", expand=True, padx=10, pady=8)

    columns = ("id", "nama", "kalori", "protein", "lemak", "karbohidrat", "catatan")
    tree = ttk.Treeview(table_frame, columns=columns, show="headings")

    for col in columns:
        tree.heading(col, text=col.capitalize())
        tree.column(col, width=120, anchor="center")

    # Scrollbar vertikal & horizontal
    vsb = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
    hsb = ttk.Scrollbar(table_frame, orient="horizontal", command=tree.xview)
    tree.configure(yscroll=vsb.set, xscroll=hsb.set)

    tree.grid(row=0, column=0, sticky="nsew")
    vsb.grid(row=0, column=1, sticky="ns")
    hsb.grid(row=1, column=0, sticky="ew")
    table_frame.grid_rowconfigure(0, weight=1)
    table_frame.grid_columnconfigure(0, weight=1)

    tampilkan_data(tree)

    # Tombol aksi
    frame_btn_top = tk.Frame(root_frame)
    frame_btn_top.pack(pady=(10, 2))

    tk.Button(frame_btn_top, text="Tambah", width=12, bg="#388E3C", fg="white", 
              command=lambda: tambah_data(root_frame, tree)).pack(side="left", padx=5)
    tk.Button(frame_btn_top, text="Hapus", width=12, bg="#D32F2F", fg="white", 
              command=lambda: hapus_data(tree)).pack(side="left", padx=5)

    frame_btn_bottom = tk.Frame(root_frame)
    frame_btn_bottom.pack(pady=(2, 10))

    def kembali():
        for w in root_frame.winfo_children():
            w.destroy()
        from menu_pengaturan import show_menu_pengaturan
        show_menu_pengaturan(root_frame)

    tk.Button(frame_btn_bottom, text="↩ Kembali", width=20, bg="#757575", fg="white", 
              command=kembali).pack(pady=5)

    # Force fokus global ke window jika user balik ke app
    root_frame.bind("<FocusIn>", lambda e: root_frame.focus_force())

def tampilkan_data(tree):
    for item in tree.get_children():
        tree.delete(item)

    conn = get_connection()
    cur = conn.cursor()
    cur.execute("SELECT * FROM preferensi_nutrisi ORDER BY id")
    rows = cur.fetchall()
    conn.close()

    for r in rows:
        tree.insert("", "end", values=r)

def tambah_data(root_frame, tree):
    form = tk.Toplevel(root_frame)
    form.title("Tambah Preferensi Nutrisi")
    form.geometry("650x800")

    labels = ["Nama", "Kalori", "Protein", "Lemak", "Karbohidrat", "Catatan"]
    entries = {}

    for i, lbl in enumerate(labels[:-1]):  # semua kecuali Catatan
        tk.Label(form, text=lbl).grid(row=i, column=0, sticky="w", padx=8, pady=4)
        ent = tk.Entry(form, width=28)
        ent.grid(row=i, column=1, padx=8, pady=4)
        ent.bind("<FocusIn>", aktifkan_keyboard)   # fix keyboard
        add_context_menu(ent)
        entries[lbl] = ent

    # Khusus Catatan -> Text area dengan height=30
    tk.Label(form, text="Catatan").grid(row=len(labels)-1, column=0, sticky="nw", padx=8, pady=4)
    catatan_txt = tk.Text(form, width=28, height=30, wrap="word")
    catatan_txt.grid(row=len(labels)-1, column=1, padx=8, pady=4, sticky="w")
    catatan_txt.bind("<FocusIn>", aktifkan_keyboard)
    add_context_menu(catatan_txt)
    entries["Catatan"] = catatan_txt

    def _num(s):
        try:
            return float(str(s).strip() or 0)
        except:
            return 0.0

    def simpan():
        data = [
            entries["Nama"].get().strip(),
            _num(entries["Kalori"].get()),
            _num(entries["Protein"].get()),
            _num(entries["Lemak"].get()),
            _num(entries["Karbohidrat"].get()),
            entries["Catatan"].get("1.0", "end").strip()
        ]
        if not data[0]:
            messagebox.showerror("Error", "Nama wajib diisi!")
            return
        conn = get_connection()
        cur = conn.cursor()
        cur.execute("""
            INSERT INTO preferensi_nutrisi 
            (nama, kalori, protein, lemak, karbohidrat, catatan)
            VALUES (?,?,?,?,?,?)
        """, data)
        conn.commit()
        conn.close()
        messagebox.showinfo("Sukses", "Preferensi berhasil disimpan.")
        form.destroy()
        tampilkan_data(tree)

    tk.Button(form, text="Simpan", bg="#4CAF50", fg="white", command=simpan)\
        .grid(row=len(labels), columnspan=2, pady=10)

def hapus_data(tree):
    selected = tree.selection()
    if not selected:
        messagebox.showwarning("Hapus", "Pilih baris yang ingin dihapus.")
        return
    item = tree.item(selected[0])["values"]
    pref_id = item[0]

    if not messagebox.askyesno("Konfirmasi", f"Hapus preferensi ID {pref_id}?"):
        return

    conn = get_connection()
    cur = conn.cursor()
    cur.execute("DELETE FROM preferensi_nutrisi WHERE id=?", (pref_id,))
    conn.commit()
    conn.close()
    tree.delete(selected[0])
    messagebox.showinfo("Hapus", "Data berhasil dihapus.")

🔑 Perubahan penting:

  • Tambah fungsi aktifkan_keyboard()dan mengikat ke semua Entry& Text.
  • Tambahkan root_frame.bind("<FocusIn>")untuk memaksa memfokuskan ulang jendela saat pengguna kembali ke aplikasi.
  • Catatan Textdibuat height=30sesuai permintaan sebelumnya.
  • Context menu sudah tetap ada.

Mau saya tambahkan juga supaya fokus terakhir (entry terakhir yang aktif) otomatis disimpan lalu dikembalikan lagi saat pengguna balik ke aplikasi? Jadi keyboard langsung muncul di field terakhir tanpa harus tap ulang.


Setelah menyalin teks dari luar, lalu menempelkannya pada entri atau teks dalam fitur applikasi ini, kenapa kursor keyboard HP masih juga tidak mau muncul

Ini memang “penyakit klasik” kalau kita pakai tkinter di Android (via Pydroid3, Termux, atau build SDL) .
Masalahnya bukan pada kode cut–copy–paste atau focus_set(), melainkan pada cara IME (Input Method Editor = keyboard HP) mendeteksi fokus widget.

📌 Kenapa bisa begitu?

  • Saat teks ditempel ( <<Paste>>), widget Entry/Textmemang menerima string, tapi fokus status terkadang “hilang sebentar” → IME mengira tidak ada widget teks aktif → keyboard tidak muncul.
  • Di Android, fokus ini tidak otomatis balik seperti di Windows/Linux. Harus menyegarkan.

Patch yang bisa dicoba:

Tambahkan force re-focus setiap kali pengguna melakukan paste (atau pilih semua). Contohnya:

def add_context_menu(widget, parent):
    menu = tk.Menu(widget, tearoff=0)
    menu.add_command(label="Cut", command=lambda: (widget.event_generate("<<Cut>>"), aktifkan_keyboard_manual(widget, parent)))
    menu.add_command(label="Copy", command=lambda: widget.event_generate("<<Copy>>"))
    menu.add_command(label="Paste", command=lambda: (widget.event_generate("<<Paste>>"), aktifkan_keyboard_manual(widget, parent)))
    menu.add_command(label="Delete", command=lambda: (widget.delete("sel.first", "sel.last") if widget.selection_get() else None, aktifkan_keyboard_manual(widget, parent)))
    menu.add_separator()
    menu.add_command(label="Select All", command=lambda: (widget.select_range(0, "end") if isinstance(widget, tk.Entry) else widget.tag_add("sel", "1.0", "end"), aktifkan_keyboard_manual(widget, parent)))

    def show_menu(event):
        try:
            menu.tk_popup(event.x_root, event.y_root)
        finally:
            menu.grab_release()

    widget.bind("<Button-3>", show_menu)
    widget.bind("<Button-2>", show_menu)

def aktifkan_keyboard_manual(widget, parent):
    """Refresh fokus biar keyboard HP muncul lagi"""
    parent.after(150, lambda: widget.focus_force())

Lalu waktu menelepon add_context_menu, kirimkan juga parent window (biasanya form):

add_context_menu(ent, form)
add_context_menu(catatan_txt, form)

💡 Dengan trik ini:

  • Setelah Paste→ widget langsung focus_force()→ IME Android “dipanggil ulang” → keyboard muncul lagi.
  • Sama berlaku jika pengguna Deletesemua teks atau Select All.

Mau saya gabungkan patch ini langsung ke kode sub_menu_preferensi_nutrisi_mmag.pykamu supaya lebih rapi?.


Mau kamu gabungkan patch ini langsung ke kode sub_menu_preferensi_nutrisi_mmag.py supaya lebih rapi.

Baik 🙌 saya sudah gabungkan patch agar keyboard HP otomatis aktif kembali setelah paste/delete/select all ke dalam file sub_menu_preferensi_nutrisi_mmag.py.

Berikut hasil versi rapi lengkapnya:

# sub_menu_preferensi_nutrisi_mmag.py
import tkinter as tk
from tkinter import ttk, messagebox
import sqlite3, os

# Lokasi database
DB_PATH = "/storage/emulated/0/SmartNutriChefAI/Database/preferensi_nutrisi_mmag.db"

def get_connection():
    # Pastikan folder database ada
    os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute("""
        CREATE TABLE IF NOT EXISTS preferensi_nutrisi (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            nama TEXT,
            kalori REAL,
            protein REAL,
            lemak REAL,
            karbohidrat REAL,
            catatan TEXT
        )
    """)
    conn.commit()
    return conn

# =========================
# Context menu + perbaikan fokus keyboard
# =========================
def aktifkan_keyboard_manual(widget, parent):
    """Refresh fokus agar keyboard HP muncul lagi"""
    parent.after(150, lambda: widget.focus_force())

def add_context_menu(widget, parent):
    menu = tk.Menu(widget, tearoff=0)
    menu.add_command(label="Cut", command=lambda: (widget.event_generate("<<Cut>>"), aktifkan_keyboard_manual(widget, parent)))
    menu.add_command(label="Copy", command=lambda: widget.event_generate("<<Copy>>"))
    menu.add_command(label="Paste", command=lambda: (widget.event_generate("<<Paste>>"), aktifkan_keyboard_manual(widget, parent)))
    menu.add_command(label="Delete", command=lambda: (widget.delete("sel.first", "sel.last") if widget.selection_get() else None, aktifkan_keyboard_manual(widget, parent)))
    menu.add_separator()
    menu.add_command(label="Select All", command=lambda: (
        widget.select_range(0, "end") if isinstance(widget, tk.Entry) else widget.tag_add("sel", "1.0", "end"),
        aktifkan_keyboard_manual(widget, parent)
    ))

    def show_menu(event):
        try:
            menu.tk_popup(event.x_root, event.y_root)
        finally:
            menu.grab_release()

    widget.bind("<Button-3>", show_menu)   # Klik kanan (Windows/Linux)
    widget.bind("<Button-2>", show_menu)   # Long-press (Android/iOS via SDL/tkinter-mobile)

# =========================
# Sub Menu Utama
# =========================
def show_sub_menu_preferensi_nutrisi_mmag(root_frame):
    for w in root_frame.winfo_children():
        w.destroy()

    tk.Label(root_frame, text="Setelan Preferensi Nutrisi \nBerbasis MMAG",
             font=("Arial", 14, "bold")).pack(pady=10)

    # Frame tabel
    table_frame = tk.Frame(root_frame)
    table_frame.pack(fill="both", expand=True, padx=10, pady=8)

    columns = ("id", "nama", "kalori", "protein", "lemak", "karbohidrat", "catatan")
    tree = ttk.Treeview(table_frame, columns=columns, show="headings")

    for col in columns:
        tree.heading(col, text=col.capitalize())
        tree.column(col, width=120, anchor="center")

    # Scrollbar vertikal & horizontal
    vsb = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
    hsb = ttk.Scrollbar(table_frame, orient="horizontal", command=tree.xview)
    tree.configure(yscroll=vsb.set, xscroll=hsb.set)

    tree.grid(row=0, column=0, sticky="nsew")
    vsb.grid(row=0, column=1, sticky="ns")
    hsb.grid(row=1, column=0, sticky="ew")
    table_frame.grid_rowconfigure(0, weight=1)
    table_frame.grid_columnconfigure(0, weight=1)

    tampilkan_data(tree)

    # Tombol aksi
    frame_btn_top = tk.Frame(root_frame)
    frame_btn_top.pack(pady=(10, 2))

    tk.Button(frame_btn_top, text="Tambah", width=12, bg="#388E3C", fg="white",
              command=lambda: tambah_data(root_frame, tree)).pack(side="left", padx=5)
    tk.Button(frame_btn_top, text="Hapus", width=12, bg="#D32F2F", fg="white",
              command=lambda: hapus_data(tree)).pack(side="left", padx=5)

    frame_btn_bottom = tk.Frame(root_frame)
    frame_btn_bottom.pack(pady=(2, 10))

    def kembali():
        for w in root_frame.winfo_children():
            w.destroy()
        from menu_pengaturan import show_menu_pengaturan
        show_menu_pengaturan(root_frame)

    tk.Button(frame_btn_bottom, text="↩ Kembali", width=20, bg="#757575", fg="white",
              command=kembali).pack(pady=5)

# =========================
# CRUD
# =========================
def tampilkan_data(tree):
    for item in tree.get_children():
        tree.delete(item)

    conn = get_connection()
    cur = conn.cursor()
    cur.execute("SELECT * FROM preferensi_nutrisi ORDER BY id")
    rows = cur.fetchall()
    conn.close()

    for r in rows:
        tree.insert("", "end", values=r)

def tambah_data(root_frame, tree):
    form = tk.Toplevel(root_frame)
    form.title("Tambah Preferensi Nutrisi")
    form.geometry("650x800")

    labels = ["Nama", "Kalori", "Protein", "Lemak", "Karbohidrat", "Catatan"]
    entries = {}

    def aktifkan_keyboard(event):
        """Pastikan widget fokus ulang → keyboard muncul"""
        widget = event.widget
        form.after(100, widget.focus_set)

    for i, lbl in enumerate(labels[:-1]):  # semua kecuali Catatan
        tk.Label(form, text=lbl).grid(row=i, column=0, sticky="w", padx=8, pady=4)
        ent = tk.Entry(form, width=28)
        ent.grid(row=i, column=1, padx=8, pady=4)
        ent.bind("<FocusIn>", aktifkan_keyboard)
        add_context_menu(ent, form)
        entries[lbl] = ent

    # Khusus Catatan -> Text area dengan height=30
    tk.Label(form, text="Catatan").grid(row=len(labels)-1, column=0, sticky="nw", padx=8, pady=4)
    catatan_txt = tk.Text(form, width=28, height=14, wrap="word")
    catatan_txt.grid(row=len(labels)-1, column=1, padx=8, pady=4, sticky="w")
    catatan_txt.bind("<FocusIn>", aktifkan_keyboard)
    add_context_menu(catatan_txt, form)
    entries["Catatan"] = catatan_txt

    def _num(s):
        try:
            return float(str(s).strip() or 0)
        except:
            return 0.0

    def simpan():
        data = [
            entries["Nama"].get().strip(),
            _num(entries["Kalori"].get()),
            _num(entries["Protein"].get()),
            _num(entries["Lemak"].get()),
            _num(entries["Karbohidrat"].get()),
            entries["Catatan"].get("1.0", "end").strip()
        ]
        if not data[0]:
            messagebox.showerror("Error", "Nama wajib diisi!")
            return
        conn = get_connection()
        cur = conn.cursor()
        cur.execute("""
            INSERT INTO preferensi_nutrisi 
            (nama, kalori, protein, lemak, karbohidrat, catatan)
            VALUES (?,?,?,?,?,?)
        """, data)
        conn.commit()
        conn.close()
        messagebox.showinfo("Sukses", "Preferensi berhasil disimpan.")
        form.destroy()
        tampilkan_data(tree)

    tk.Button(form, text="Simpan", bg="#4CAF50", fg="white", command=simpan)\
        .grid(row=len(labels), columnspan=2, pady=10)

def hapus_data(tree):
    selected = tree.selection()
    if not selected:
        messagebox.showwarning("Hapus", "Pilih baris yang ingin dihapus.")
        return
    item = tree.item(selected[0])["values"]
    pref_id = item[0]

    if not messagebox.askyesno("Konfirmasi", f"Hapus preferensi ID {pref_id}?"):
        return

    conn = get_connection()
    cur = conn.cursor()
    cur.execute("DELETE FROM preferensi_nutrisi WHERE id=?", (pref_id,))
    conn.commit()
    conn.close()
    tree.delete(selected[0])
    messagebox.showinfo("Hapus", "Data berhasil dihapus.")

📌 Perubahan utama:

  1. aktifkan_keyboard_manual(widget, parent)→ memaksa fokus ulang setelah tempel/hapus/pilih semua.
  2. add_context_menu(widget, parent)sekarang selalu memanggil patch fokus setelah operasi menu konteks.
  3. Semua Entry&Text di tambah_data()otomatis punya menu konteks + patch fokus.

Mau saya tambahkan juga binding global (untuk semua Entry/Textdi aplikasi, bukan hanya di form tambah data)? Jadi tidak perlu pasang manual lagi di tiap form.


Kenapa pada fitur "Kelompok bahan resep" bisa membuat folder Database dan nama file db yaitu dengan kodenya:

sub_menu_kelompok_bahan_resep_mmag.py

impor tkinter sebagai tk
dari tkinter impor ttk, kotak pesan
impor sqlite3, os

DB_PATH = "/storage/emulated/0/AppJadi/SmartNutriChefAI/Database/Kategori/kategori.db"

def dapatkan_koneksi():
kembalikan sqlite3.koneksi(DB_PATH)

def show_sub_menu_kelompok_bahan_resep_mmag(root_frame):
for w in root_frame.winfo_children():
w.destroy()

tk.Label(root_frame, text="Kelompok Bahan Resep MMAG", font=("Arial", 13, "bold")).pack(pady=8)  

# Filter kategori  
filter_frame = tk.Frame(root_frame)  
filter_frame.pack(pady=4)  

tk.Label(filter_frame, text="Pilih Kategori:", font=("Arial", 12)).pack(side="left", padx=6)  

kategori_var = tk.StringVar(value="Semua")  
kategori_cb = ttk.Combobox(filter_frame, textvariable=kategori_var, state="readonly", width=25)  
kategori_cb["values"] = ("Semua", "Minyak", "Makanan", "Air", "Garam", "Bahan Tambahan")  
kategori_cb.pack(side="left", padx=6)  

# Tombol tampilkan  
btn_tampil_holder = tk.Frame(root_frame)  
btn_tampil_holder.pack(pady=6)  

table_frame = tk.Frame(root_frame)  
table_frame.pack(fill="both", expand=True, padx=10, pady=8)  

columns = ("id","nama","kategori","nutrisi","protein","lemak","karbohidrat","vitamin","mineral")  
tree = ttk.Treeview(table_frame, columns=columns, show="headings")  

for col in columns:  
    tree.heading(col, text=col.capitalize())  
    tree.column(col, width=120, anchor="center")  

vsb = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)  
hsb = ttk.Scrollbar(table_frame, orient="horizontal", command=tree.xview)  
tree.configure(yscroll=vsb.set, xscroll=hsb.set)  

tree.grid(row=0, column=0, sticky="nsew")  
vsb.grid(row=0, column=1, sticky="ns")  
hsb.grid(row=1, column=0, sticky="ew")  
table_frame.grid_rowconfigure(0, weight=1)  
table_frame.grid_columnconfigure(0, weight=1)  

tk.Button(  
    btn_tampil_holder, text="Tampilkan", font=("Arial", 12), width=20,  
    bg="#2196F3", fg="white",  
    command=lambda: tampilkan_data(kategori_var.get(), tree)  
).pack()  

# Tombol kontrol  
btn_frame = tk.Frame(root_frame)  
btn_frame.pack(pady=8)  

tk.Button(  
    btn_frame, text="Tambah Bahan", font=("Arial", 12), width=14,  
    bg="#4CAF50", fg="white",  
    command=lambda: tambah_bahan(root_frame, kategori_var.get(), tree)  
).pack(side="left", padx=5)  

tk.Button(  
    btn_frame, text="Hapus Bahan", font=("Arial", 12), width=14,  
    bg="#F44336", fg="white",  
    command=lambda: hapus_bahan(tree, kategori_var.get())  
).pack(side="left", padx=5)  

tk.Button(  
    root_frame, text="⬅ Kembali", font=("Arial", 14), width=22, height=2,  
    bg="gray", fg="white",  
    command=lambda: kembali_ke_metode_formula_mmag(root_frame)  
).pack(pady=10)  

tampilkan_data(kategori_var.get(), tree)  

def tampilkan_data(kategori, tree):
for item in tree.get_children():
tree.delete(item)

tabel_map = {  
    "Minyak": "jenis_minyak",  
    "Makanan": "jenis_makanan",  
    "Air": "jenis_air",  
    "Garam": "jenis_garam",  
    "Bahan Tambahan": "jenis_bahan_tambahan",  
}  

conn = get_connection()  
cur = conn.cursor()  
rows = []  
try:  
    if kategori == "Semua":  
        for tbl in tabel_map.values():  
            try:  
                cur.execute(f"""SELECT id,nama,kategori,nutrisi,protein,lemak,karbohidrat,vitamin,mineral  
                                FROM {tbl}""")  
                rows.extend(cur.fetchall())  
            except sqlite3.OperationalError:  
                pass  
        display = [(i, *r[1:]) for i, r in enumerate(rows, start=1)]  
        rows = display  
    else:  
        tbl = tabel_map[kategori]  
        cur.execute(f"""SELECT id,nama,kategori,nutrisi,protein,lemak,karbohidrat,vitamin,mineral   
                        FROM {tbl} ORDER BY id""")  
        rows = cur.fetchall()  
finally:  
    conn.close()  

for r in rows:  
    tree.insert("", "end", values=r)  

def tambah_bahan(root_frame, kategori, pohon):
import sqlite3
from tkinter import messagebox

if kategori == "Semua":  
    messagebox.showwarning("Tambah Bahan", "Pilih kategori spesifik (bukan 'Semua').")  
    return  

tabel_map = {  
    "Minyak": "jenis_minyak",  
    "Makanan": "jenis_makanan",  
    "Air": "jenis_air",  
    "Garam": "jenis_garam",  
    "Bahan Tambahan": "jenis_bahan_tambahan",  
}  
tbl = tabel_map[kategori]  

# Popup form  
form = tk.Toplevel(root_frame)  
form.title(f"Tambah Bahan - {kategori}")  
form.geometry("460x490")  

labels = ["Nama", "Kategori", "Nutrisi", "Protein", "Lemak", "Karbohidrat", "Vitamin", "Mineral"]  
entries = {}  

for i, lbl in enumerate(labels):  
    tk.Label(form, text=lbl).grid(row=i, column=0, sticky="w", padx=8, pady=4)  
    ent = tk.Entry(form, width=32)  
    ent.grid(row=i, column=1, padx=8, pady=4)  
    entries[lbl] = ent  

# Isi & kunci kategori agar konsisten  
entries["Kategori"].insert(0, kategori)  
entries["Kategori"].config(state="readonly")  

def _num(s):  
    try:  
        return float(str(s).strip() or 0)  
    except:  
        return 0.0  

def simpan():  
    nama = entries["Nama"].get().strip()  
    if not nama:  
        messagebox.showerror("Error", "Nama bahan wajib diisi!")  
        return  

    data = [  
        nama,  
        kategori,  # pakai kategori aktif, abaikan input user  
        entries["Nutrisi"].get().strip(),  
        _num(entries["Protein"].get()),  
        _num(entries["Lemak"].get()),  
        _num(entries["Karbohidrat"].get()),  
        entries["Vitamin"].get().strip(),  
        entries["Mineral"].get().strip(),  
    ]  

    try:  
        conn = get_connection()  
        cur = conn.cursor()  
        # 8 kolom = 8 placeholder  
        cur.execute(  
            f"""INSERT INTO {tbl}  
                (nama,kategori,nutrisi,protein,lemak,karbohidrat,vitamin,mineral)  
                VALUES (?,?,?,?,?,?,?,?)""",  
            data  
        )  
        conn.commit()  
        messagebox.showinfo("Sukses", f"Data berhasil ditambahkan ke {kategori}.")  
        form.destroy()  
        tampilkan_data(kategori, tree)  
    except sqlite3.OperationalError as e:  
        messagebox.showerror(  
            "DB Error",  
            f"Gagal menyimpan. Pastikan tabel {tbl} punya kolom 'mineral'.\nDetail: {e}"  
        )  
    except Exception as e:  
        messagebox.showerror("Error", str(e))  
    finally:  
        try:  
            conn.close()  
        except:  
            pass  

tk.Button(form, text="Simpan", bg="#4CAF50", fg="white", command=simpan)\  
    .grid(row=len(labels), columnspan=2, pady=10)  

def hapus_bahan(pohon, kategori):
if kategori == "Semua":
messagebox.showwarning("Hapus Bahan", "Pilih kategori spesifik dulu (bukan 'Semua').")
return

selected = tree.selection()  
if not selected:  
    messagebox.showwarning("Hapus Bahan", "Pilih baris yang ingin dihapus.")  
    return  

tabel_map = {  
    "Minyak": "jenis_minyak",  
    "Makanan": "jenis_makanan",  
    "Air": "jenis_air",  
    "Garam": "jenis_garam",  
    "Bahan Tambahan": "jenis_bahan_tambahan",  
}  
tbl = tabel_map[kategori]  

item = tree.item(selected[0])["values"]  
bahan_id = item[0]  

if not messagebox.askyesno("Konfirmasi", f"Hapus ID {bahan_id}?"):  
    return  

conn = get_connection()  
cur = conn.cursor()  
cur.execute(f"DELETE FROM {tbl} WHERE id=?", (bahan_id,))  
conn.commit()  
conn.close()  
tree.delete(selected[0])  
messagebox.showinfo("Hapus", "Data berhasil dihapus.")  

def kembali_ke_metode_formula_mmag(root_frame):
coba:
dari FormulaMMAG.menu_metode_formula_mmag impor show_menu_metode_formula_mmag
kecuali ImportError:
dari menu_metode_formula_mmag impor show_menu_metode_formula_mmag
show_menu_metode_formula_mmag(root_frame)

Sedangkan pada fitur "preferensi nutrisi" ini tidak bisa membuat Database dan nama file db dengan kodenya adalah:

sub_menu_preferensi_nutrisi_mmag.py

impor tkinter sebagai tk
dari tkinter impor ttk, kotak pesan
impor sqlite3, os

Lokasi database

DB_PATH = "/storage/emulated/0/SmartNutriChefAI/Database/Preferensi/preferensi_nutrisi_mmag.db"

def get_connection():
# Pastikan folder database ada
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS preferensi_nutrisi (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nama TEXT,
kalori REAL,
protein REAL,
lemak REAL,
karbohidrat REAL,
catatan TEXT
)
""")
conn.commit()
return conn

=========================

Menu konteks + perbaikan fokus keyboard

=========================

def aktifkan_keyboard_manual(widget, parent):
"""Refresh fokus agar keyboard HP muncul lagi"""
parent.after(150, lambda: widget.focus_force())

def add_context_menu(widget, induk):
menu = tk.Menu(widget, tearoff=0)
menu.add_command(label="Potong", perintah=lambda: (widget.event_generate("<>"), aktifkan_keyboard_manual(widget, parent)))
menu.add_command(label="Copy", command=lambda: widget.event_generate("<>"))
menu.add_command(label="Tempel", perintah=lambda: (widget.event_generate("<>"), aktifkan_keyboard_manual(widget, parent)))
menu.add_command(label="Delete", command=lambda: (widget.delete("sel.first", "sel.last") if widget.selection_get() else None, aktifkan_keyboard_manual(widget, parent)))
menu.add_separator()
menu.add_command(label="Select All", command=lambda: (
widget.select_range(0, "end") if isinstance(widget, tk.Entry) else widget.tag_add("sel", "1.0", "end"),
aktifkan_keyboard_manual(widget, parent)
))

def show_menu(event):  
    try:  
        menu.tk_popup(event.x_root, event.y_root)  
    finally:  
        menu.grab_release()  

widget.bind("<Button-3>", show_menu)   # Klik kanan (Windows/Linux)  
widget.bind("<Button-2>", show_menu)   # Long-press (Android/iOS via SDL/tkinter-mobile)  

=========================

Sub Menu Utama

=========================

def show_sub_menu_preferensi_nutrisi_mmag(root_frame):
for w in root_frame.winfo_children():
w.destroy()

tk.Label(root_frame, text="Setelan Preferensi Nutrisi \nBerbasis MMAG",  
         font=("Arial", 14, "bold")).pack(pady=10)  

# Frame tabel  
table_frame = tk.Frame(root_frame)  
table_frame.pack(fill="both", expand=True, padx=10, pady=8)  

columns = ("id", "nama", "kalori", "protein", "lemak", "karbohidrat", "catatan")  
tree = ttk.Treeview(table_frame, columns=columns, show="headings")  

for col in columns:  
    tree.heading(col, text=col.capitalize())  
    tree.column(col, width=120, anchor="center")  

# Scrollbar vertikal & horizontal  
vsb = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)  
hsb = ttk.Scrollbar(table_frame, orient="horizontal", command=tree.xview)  
tree.configure(yscroll=vsb.set, xscroll=hsb.set)  

tree.grid(row=0, column=0, sticky="nsew")  
vsb.grid(row=0, column=1, sticky="ns")  
hsb.grid(row=1, column=0, sticky="ew")  
table_frame.grid_rowconfigure(0, weight=1)  
table_frame.grid_columnconfigure(0, weight=1)  

tampilkan_data(tree)  

# Tombol aksi  
frame_btn_top = tk.Frame(root_frame)  
frame_btn_top.pack(pady=(10, 2))  

tk.Button(frame_btn_top, text="Tambah", width=12, bg="#388E3C", fg="white",  
          command=lambda: tambah_data(root_frame, tree)).pack(side="left", padx=5)  
tk.Button(frame_btn_top, text="Hapus", width=12, bg="#D32F2F", fg="white",  
          command=lambda: hapus_data(tree)).pack(side="left", padx=5)  

frame_btn_bottom = tk.Frame(root_frame)  
frame_btn_bottom.pack(pady=(2, 10))  

def kembali():  
    for w in root_frame.winfo_children():  
        w.destroy()  
    from menu_pengaturan import show_menu_pengaturan  
    show_menu_pengaturan(root_frame)  

tk.Button(frame_btn_bottom, text="↩ Kembali", width=20, bg="#757575", fg="white",  
          command=kembali).pack(pady=5)  

=========================

CRUD

=========================

def tampilkan_data(tree):
for item in tree.get_children():
tree.delete(item)

conn = get_connection()  
cur = conn.cursor()  
cur.execute("SELECT * FROM preferensi_nutrisi ORDER BY id")  
rows = cur.fetchall()  
conn.close()  

for r in rows:  
    tree.insert("", "end", values=r)  

def tambah_data(root_frame, tree):
form = tk.Toplevel(root_frame)
form.title("Tambah Preferensi Nutrisi")
form.geometry("650x800")

labels = ["Nama", "Kalori", "Protein", "Lemak", "Karbohidrat", "Catatan"]  
entries = {}  

def aktifkan_keyboard(event):  
    """Pastikan widget fokus ulang → keyboard muncul"""  
    widget = event.widget  
    form.after(100, widget.focus_set)  

for i, lbl in enumerate(labels[:-1]):  # semua kecuali Catatan  
    tk.Label(form, text=lbl).grid(row=i, column=0, sticky="w", padx=8, pady=4)  
    ent = tk.Entry(form, width=28)  
    ent.grid(row=i, column=1, padx=8, pady=4)  
    ent.bind("<FocusIn>", aktifkan_keyboard)  
    add_context_menu(ent, form)  
    entries[lbl] = ent  

# Khusus Catatan -> Text area dengan height=14  
tk.Label(form, text="Catatan").grid(row=len(labels)-1, column=0, sticky="nw", padx=8, pady=4)  
catatan_txt = tk.Text(form, width=28, height=14, wrap="word")  
catatan_txt.grid(row=len(labels)-1, column=1, padx=8, pady=4, sticky="w")  
catatan_txt.bind("<FocusIn>", aktifkan_keyboard)  
add_context_menu(catatan_txt, form)  
entries["Catatan"] = catatan_txt  

def _num(s):  
    try:  
        return float(str(s).strip() or 0)  
    except:  
        return 0.0  

def simpan():  
    data = [  
        entries["Nama"].get().strip(),  
        _num(entries["Kalori"].get()),  
        _num(entries["Protein"].get()),  
        _num(entries["Lemak"].get()),  
        _num(entries["Karbohidrat"].get()),  
        entries["Catatan"].get("1.0", "end").strip()  
    ]  
    if not data[0]:  
        messagebox.showerror("Error", "Nama wajib diisi!")  
        return  
    conn = get_connection()  
    cur = conn.cursor()  
    cur.execute("""  
        INSERT INTO preferensi_nutrisi   
        (nama, kalori, protein, lemak, karbohidrat, catatan)  
        VALUES (?,?,?,?,?,?)  
    """, data)  
    conn.commit()  
    conn.close()  
    messagebox.showinfo("Sukses", "Preferensi berhasil disimpan.")  
    form.destroy()  
    tampilkan_data(tree)  

tk.Button(form, text="Simpan", bg="#4CAF50", fg="white", command=simpan)\  
    .grid(row=len(labels), columnspan=2, pady=10)  

def hapus_data(tree):
selected = tree.selection()
if not selected:
messagebox.showwarning("Hapus", "Pilih baris yang ingin dihapus.")
return
item = tree.item(selected[0])["values"]
pref_id = item[0]

if not messagebox.askyesno("Konfirmasi", f"Hapus preferensi ID {pref_id}?"):  
    return  

conn = get_connection()  
cur = conn.cursor()  
cur.execute("DELETE FROM preferensi_nutrisi WHERE id=?", (pref_id,))  
conn.commit()  
conn.close()  
tree.delete(selected[0])  
messagebox.showinfo("Hapus", "Data berhasil dihapus.")


Nah ini menarik 👍.
Perbedaan utamanya ada di path database yang kamu tetapkan:

  • Pada kelompok bahan resep :
DB_PATH = "/storage/emulated/0/AppJadi/SmartNutriChefAI/Database/Kategori/kategori.db"

Berkas AppJadiSmartNutriChefAIDatabaseKategorikategori.db.

  • Pada preferensi nutrisi :
DB_PATH = "/storage/emulated/0/SmartNutriChefAI/Database/Preferensi/preferensi_nutrisi_mmag.db"

Folder SmartNutriChefAIlangsung masuk /storage/emulated/0/, tanpa sub-folderAppJadi/ .


Masalah

  1. Android biasanya memberi izin tulis/baca hanya ke folder aplikasi (misalnya /storage/emulated/0/AppJadi/...).
    Jadi ketika path AppJadihilang, izin bisa ditolak → file DB gagal dibuat.

  2. Di kode kelompok bahan, kamu tidak membuat folder secara eksplisit ( os.makedirs), tapi karena folder AppJadi/SmartNutriChefAI/Database/Kategori/mungkin sudah dibuat oleh menu lain, maka DB berhasil tersimpan.

  3. Di kode preferensi nutrisi, kamu memang sudah ada:

os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)

Jadi folder dibuat otomatis. Tapi kalau jalur root ( /storage/emulated/0/SmartNutriChefAI/...) tidak diizinkan sistem, maka walaupun makedirsdipanggil → tetap gagal.


Solusi

Biar konsisten, samakan path database seperti fitur yang sudah berhasil:

# Ganti ini:
DB_PATH = "/storage/emulated/0/SmartNutriChefAI/Database/Preferensi/preferensi_nutrisi_mmag.db"

# Jadi ini:
DB_PATH = "/storage/emulated/0/AppJadi/SmartNutriChefAI/Database/Preferensi/preferensi_nutrisi_mmag.db"

Tambahan

Supaya lebih aman, buat fungsi helper untuk membuat DB path agar semua fitur menggunakan root folder yang sama :

def get_db_path(filename, subfolder=""):
    base = "/storage/emulated/0/AppJadi/SmartNutriChefAI/Database"
    if subfolder:
        path = os.path.join(base, subfolder, filename)
    else:
        path = os.path.join(base, filename)
    os.makedirs(os.path.dirname(path), exist_ok=True)
    return path

# contoh pemakaian di preferensi nutrisi:
DB_PATH = get_db_path("preferensi_nutrisi_mmag.db", "Preferensi")

Mau saya langsung rapikan kode sub_menu_preferensi_nutrisi_mmag.pydengan helper get_db_path()ini biar seragam seperti fitur lain?


Ternyata nama tempat penyimpanan file db nya yang harus dibenahi menjadi:
DB_PATH = "/storage/emulated/0/AppJadi/SmartNutriChefAI/Database/Preferensi/preferensi_nutrisi_mmag.db"

Benar sekali 👍.
Jadi masalahnya murni di path database saja, karena yang satu pakai root /storage/emulated/0/AppJadi/...(berhasil) sedangkan yang di preferensi nutrisikemarin langsung ke /storage/emulated/0/SmartNutriChefAI/...(tanpa AppJadi/, gagal).

Dengan perbaikan ini:

DB_PATH = "/storage/emulated/0/AppJadi/SmartNutriChefAI/Database/Preferensi/preferensi_nutrisi_mmag.db"

maka:

  1. Folder otomatis dibuat ( os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)sudah ada).
  2. File preferensi_nutrisi_mmag.dbtersimpan di lokasi yang sama dengan struktur folder fitur lain.
  3. Semua fitur (kelompok bahan resep, preferensi nutrisi, dll.) akan konsisten berada di dalam satu root:
    /storage/emulated/0/AppJadi/SmartNutriChefAI/Database/.

Mau saya langsung sisipkan perbaikan path ini ke seluruh kode sub_menu_preferensi_nutrisi_mmag.pyagar final dan rapi?.



 Demikian, semoga bermanfaat.

Comments

Popular posts from this blog

Isi Menu "Setting"

Isi Menu "Riwayat Aktivitas" (Bagian 2)