1367 lines
50 KiB
Python
1367 lines
50 KiB
Python
import csv
|
|
import json
|
|
import os
|
|
|
|
import config
|
|
from PyQt5.QtWidgets import (
|
|
QAbstractItemView,
|
|
QDialog,
|
|
QFrame,
|
|
QHeaderView,
|
|
QHBoxLayout,
|
|
QInputDialog,
|
|
QLabel,
|
|
QLineEdit,
|
|
QMessageBox,
|
|
QPushButton,
|
|
QTableWidget,
|
|
QTableWidgetItem,
|
|
QStyle,
|
|
QStyleOptionViewItem,
|
|
QVBoxLayout,
|
|
)
|
|
from PyQt5.QtCore import Qt, QEvent
|
|
from PyQt5.QtGui import QFont
|
|
|
|
class CustomNotificationDialog(QDialog):
|
|
"""Custom styled notification dialog for success, error, warning, and question messages"""
|
|
|
|
def __init__(self, parent=None, dialog_type="success", title="Success!", message="Operation completed successfully",
|
|
button_text="OK", show_cancel=False, cancel_text="Cancel", compact=False):
|
|
super().__init__(parent)
|
|
self.dialog_type = dialog_type # "success", "error", "warning", "question"
|
|
self.result_value = False
|
|
self.compact = compact
|
|
|
|
# Remove window frame and set flags
|
|
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
|
|
self.setAttribute(Qt.WA_TranslucentBackground)
|
|
|
|
self.initUI(title, message, button_text, show_cancel, cancel_text)
|
|
|
|
def initUI(self, title, message, button_text, show_cancel, cancel_text):
|
|
# Main layout with margins for shadow effect
|
|
main_layout = QVBoxLayout()
|
|
outer_margin = 14 if self.compact else 20
|
|
main_layout.setContentsMargins(outer_margin, outer_margin, outer_margin, outer_margin)
|
|
|
|
# Content widget (the card)
|
|
content_widget = QDialog()
|
|
content_widget.setFixedWidth(340 if self.compact else 450)
|
|
|
|
# Set colors based on dialog type
|
|
if self.dialog_type == "success":
|
|
border_color = "#48bb78" # Green
|
|
icon_bg = "#48bb78"
|
|
icon = "✓"
|
|
button_color = "#48bb78"
|
|
button_hover = "#38a169"
|
|
elif self.dialog_type == "error":
|
|
border_color = "#f56565" # Red
|
|
icon_bg = "#f56565"
|
|
icon = "✕"
|
|
button_color = "#f56565"
|
|
button_hover = "#e53e3e"
|
|
elif self.dialog_type == "warning":
|
|
border_color = "#ed8936" # Orange
|
|
icon_bg = "#ed8936"
|
|
icon = "!"
|
|
button_color = "#ed8936"
|
|
button_hover = "#dd6b20"
|
|
else: # question
|
|
border_color = "#4299e1" # Blue
|
|
icon_bg = "#4299e1"
|
|
icon = "?"
|
|
button_color = "#4299e1"
|
|
button_hover = "#3182ce"
|
|
|
|
content_widget.setStyleSheet(f"""
|
|
QDialog {{
|
|
background-color: white;
|
|
border-top: 4px solid {border_color};
|
|
border-radius: 8px;
|
|
}}
|
|
""")
|
|
|
|
content_layout = QVBoxLayout()
|
|
if self.compact:
|
|
content_layout.setContentsMargins(24, 28, 24, 24)
|
|
content_layout.setSpacing(12)
|
|
else:
|
|
content_layout.setContentsMargins(40, 50, 40, 40)
|
|
content_layout.setSpacing(20)
|
|
|
|
# Icon at the top
|
|
icon_label = QLabel(icon)
|
|
icon_label.setAlignment(Qt.AlignCenter)
|
|
icon_size = 54 if self.compact else 80
|
|
icon_label.setFixedSize(icon_size, icon_size)
|
|
icon_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {icon_bg};
|
|
color: white;
|
|
font-size: {34 if self.compact else 48}px;
|
|
font-weight: bold;
|
|
border-radius: {icon_size // 2}px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}}
|
|
""")
|
|
|
|
# Create a container to center the icon
|
|
icon_container = QHBoxLayout()
|
|
icon_container.addStretch()
|
|
icon_container.addWidget(icon_label)
|
|
icon_container.addStretch()
|
|
content_layout.addLayout(icon_container)
|
|
|
|
# Title
|
|
title_label = QLabel(title)
|
|
title_label.setAlignment(Qt.AlignCenter)
|
|
title_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
font-size: {22 if self.compact else 32}px;
|
|
font-weight: bold;
|
|
color: {border_color if self.dialog_type != 'question' else '#2d3748'};
|
|
margin-top: {4 if self.compact else 10}px;
|
|
margin-bottom: {4 if self.compact else 10}px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}}
|
|
""")
|
|
content_layout.addWidget(title_label)
|
|
|
|
# Separator line
|
|
separator = QLabel()
|
|
separator.setFixedHeight(3)
|
|
separator.setStyleSheet(f"background-color: {border_color}; border-radius: 1px;")
|
|
separator.setFixedWidth(100)
|
|
separator_container = QHBoxLayout()
|
|
separator_container.addStretch()
|
|
separator_container.addWidget(separator)
|
|
separator_container.addStretch()
|
|
content_layout.addLayout(separator_container)
|
|
|
|
# Message
|
|
message_label = QLabel(message)
|
|
message_label.setAlignment(Qt.AlignCenter)
|
|
message_label.setWordWrap(True)
|
|
message_font_size = 17 if self.compact else 19
|
|
message_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
font-size: {message_font_size}px;
|
|
color: #718096;
|
|
line-height: 1.5;
|
|
margin-top: 8px;
|
|
margin-bottom: 12px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}}
|
|
""")
|
|
content_layout.addWidget(message_label)
|
|
|
|
# Buttons
|
|
button_layout = QHBoxLayout()
|
|
button_layout.setSpacing(10 if self.compact else 15)
|
|
|
|
if show_cancel:
|
|
# Cancel button (outlined)
|
|
cancel_btn = QPushButton(cancel_text)
|
|
cancel_btn.setFixedHeight(36 if self.compact else 45)
|
|
cancel_btn.setFixedWidth(120 if self.compact else 150)
|
|
cancel_btn.setCursor(Qt.PointingHandCursor)
|
|
cancel_btn.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background-color: white;
|
|
color: #718096;
|
|
border: 2px solid #e2e8f0;
|
|
border-radius: {18 if self.compact else 25}px;
|
|
font-size: {13 if self.compact else 16}px;
|
|
font-weight: 600;
|
|
padding: {6 if self.compact else 8}px {16 if self.compact else 24}px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}}
|
|
QPushButton:hover {{
|
|
background-color: #f7fafc;
|
|
border-color: #cbd5e0;
|
|
color: #4a5568;
|
|
}}
|
|
QPushButton:pressed {{
|
|
background-color: #edf2f7;
|
|
}}
|
|
""")
|
|
cancel_btn.clicked.connect(self.on_cancel)
|
|
button_layout.addWidget(cancel_btn)
|
|
|
|
# Main button (filled)
|
|
ok_btn = QPushButton(button_text)
|
|
ok_btn.setFixedHeight(36 if self.compact else 45)
|
|
ok_btn.setFixedWidth(120 if self.compact else 150)
|
|
ok_btn.setCursor(Qt.PointingHandCursor)
|
|
ok_btn.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background-color: {button_color};
|
|
color: white;
|
|
border: none;
|
|
border-radius: {18 if self.compact else 25}px;
|
|
font-size: {13 if self.compact else 16}px;
|
|
font-weight: 600;
|
|
padding: {6 if self.compact else 8}px {16 if self.compact else 24}px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}}
|
|
QPushButton:hover {{
|
|
background-color: {button_hover};
|
|
}}
|
|
QPushButton:pressed {{
|
|
background-color: {button_hover};
|
|
padding-top: 10px;
|
|
padding-bottom: 6px;
|
|
}}
|
|
""")
|
|
ok_btn.clicked.connect(self.on_ok)
|
|
button_layout.addWidget(ok_btn)
|
|
|
|
content_layout.addLayout(button_layout)
|
|
content_widget.setLayout(content_layout)
|
|
|
|
main_layout.addWidget(content_widget)
|
|
self.setLayout(main_layout)
|
|
|
|
# Apply shadow effect
|
|
from PyQt5.QtWidgets import QGraphicsDropShadowEffect
|
|
from PyQt5.QtGui import QColor
|
|
shadow = QGraphicsDropShadowEffect()
|
|
shadow.setBlurRadius(30)
|
|
shadow.setXOffset(0)
|
|
shadow.setYOffset(10)
|
|
shadow.setColor(QColor(0, 0, 0, 60))
|
|
content_widget.setGraphicsEffect(shadow)
|
|
|
|
def on_ok(self):
|
|
self.result_value = True
|
|
self.accept()
|
|
|
|
def on_cancel(self):
|
|
self.result_value = False
|
|
self.reject()
|
|
|
|
@staticmethod
|
|
def show_success(parent=None, title="Success!", message="Operation completed successfully", button_text="OK", compact=False):
|
|
"""Show a success notification"""
|
|
dialog = CustomNotificationDialog(parent, "success", title, message, button_text, compact=compact)
|
|
dialog.exec_()
|
|
|
|
@staticmethod
|
|
def show_error(parent=None, title="Error!", message="An error occurred", button_text="Try Again", compact=False):
|
|
"""Show an error notification"""
|
|
dialog = CustomNotificationDialog(parent, "error", title, message, button_text, compact=compact)
|
|
dialog.exec_()
|
|
|
|
@staticmethod
|
|
def show_warning(parent=None, title="Warning!", message="Please check your input", button_text="OK", compact=False):
|
|
"""Show a warning notification"""
|
|
dialog = CustomNotificationDialog(parent, "warning", title, message, button_text, compact=compact)
|
|
dialog.exec_()
|
|
|
|
@staticmethod
|
|
def show_question(parent=None, title="Confirm", message="Are you sure?", button_text="Yes", cancel_text="No", compact=False):
|
|
"""Show a question dialog and return True if user clicks Yes"""
|
|
dialog = CustomNotificationDialog(parent, "question", title, message, button_text, True, cancel_text, compact=compact)
|
|
dialog.exec_()
|
|
return dialog.result_value
|
|
|
|
|
|
class CustomPredictionDialog(QDialog):
|
|
"""Custom styled dialog for showing prediction results with save option"""
|
|
|
|
def __init__(self, parent=None, nama="", prediction="", paket_info=None):
|
|
super().__init__(parent)
|
|
self.result_save = False
|
|
|
|
# Remove window frame and set flags
|
|
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
|
|
self.setAttribute(Qt.WA_TranslucentBackground)
|
|
|
|
self.initUI(nama, prediction, paket_info)
|
|
|
|
def initUI(self, nama, prediction, paket_info):
|
|
# Main layout with margins for shadow effect
|
|
main_layout = QVBoxLayout()
|
|
main_layout.setContentsMargins(20, 20, 20, 20)
|
|
|
|
# Content widget (the card)
|
|
content_widget = QDialog()
|
|
content_widget.setFixedWidth(500)
|
|
|
|
border_color = "#4299e1" # Blue
|
|
|
|
content_widget.setStyleSheet(f"""
|
|
QDialog {{
|
|
background-color: white;
|
|
border-top: 4px solid {border_color};
|
|
border-radius: 8px;
|
|
}}
|
|
""")
|
|
|
|
content_layout = QVBoxLayout()
|
|
content_layout.setContentsMargins(40, 50, 40, 40)
|
|
content_layout.setSpacing(15)
|
|
|
|
# Icon at the top
|
|
icon_label = QLabel("🎓")
|
|
icon_label.setAlignment(Qt.AlignCenter)
|
|
icon_label.setFixedSize(80, 80)
|
|
icon_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
background-color: {border_color};
|
|
color: white;
|
|
font-size: 48px;
|
|
font-weight: bold;
|
|
border-radius: 40px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}}
|
|
""")
|
|
|
|
# Create a container to center the icon
|
|
icon_container = QHBoxLayout()
|
|
icon_container.addStretch()
|
|
icon_container.addWidget(icon_label)
|
|
icon_container.addStretch()
|
|
content_layout.addLayout(icon_container)
|
|
|
|
# Title
|
|
title_label = QLabel("Hasil Prediksi")
|
|
title_label.setAlignment(Qt.AlignCenter)
|
|
title_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
font-size: 32px;
|
|
font-weight: bold;
|
|
color: #2d3748;
|
|
margin-top: 10px;
|
|
margin-bottom: 10px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}}
|
|
""")
|
|
content_layout.addWidget(title_label)
|
|
|
|
# Separator line
|
|
separator = QLabel()
|
|
separator.setFixedHeight(3)
|
|
separator.setStyleSheet(f"background-color: {border_color}; border-radius: 1px;")
|
|
separator.setFixedWidth(100)
|
|
separator_container = QHBoxLayout()
|
|
separator_container.addStretch()
|
|
separator_container.addWidget(separator)
|
|
separator_container.addStretch()
|
|
content_layout.addLayout(separator_container)
|
|
|
|
# Student name
|
|
nama_label = QLabel(f"Nama Siswa: {nama}")
|
|
nama_label.setAlignment(Qt.AlignCenter)
|
|
nama_label.setStyleSheet("""
|
|
QLabel {
|
|
font-size: 18px;
|
|
color: #2d3748;
|
|
font-weight: bold;
|
|
margin-top: 10px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}
|
|
""")
|
|
content_layout.addWidget(nama_label)
|
|
|
|
# Prediction result
|
|
pred_label = QLabel(f"Paket Terpilih: {prediction}")
|
|
pred_label.setAlignment(Qt.AlignCenter)
|
|
pred_label.setStyleSheet("""
|
|
QLabel {
|
|
font-size: 20px;
|
|
color: #4299e1;
|
|
font-weight: bold;
|
|
margin-top: 5px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}
|
|
""")
|
|
content_layout.addWidget(pred_label)
|
|
|
|
# Paket detail
|
|
if paket_info:
|
|
kelas_info = f" ({paket_info.get('kelas_tersedia', 1)} kelas tersedia)"
|
|
detail_text = f"{paket_info.get('nama', 'Unknown')}{kelas_info}"
|
|
detail_label = QLabel(detail_text)
|
|
detail_label.setAlignment(Qt.AlignCenter)
|
|
detail_label.setWordWrap(True)
|
|
detail_label.setStyleSheet("""
|
|
QLabel {
|
|
font-size: 16px;
|
|
color: #718096;
|
|
margin-top: 5px;
|
|
margin-bottom: 10px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}
|
|
""")
|
|
content_layout.addWidget(detail_label)
|
|
|
|
# Question
|
|
question_label = QLabel("Apakah Anda ingin menyimpan hasil prediksi ini?")
|
|
question_label.setAlignment(Qt.AlignCenter)
|
|
question_label.setWordWrap(True)
|
|
question_label.setStyleSheet("""
|
|
QLabel {
|
|
font-size: 16px;
|
|
color: #4a5568;
|
|
margin-top: 10px;
|
|
margin-bottom: 20px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}
|
|
""")
|
|
content_layout.addWidget(question_label)
|
|
|
|
# Buttons
|
|
button_layout = QHBoxLayout()
|
|
button_layout.setSpacing(15)
|
|
|
|
# Cancel button (outlined)
|
|
cancel_btn = QPushButton("Batal")
|
|
cancel_btn.setFixedHeight(45)
|
|
cancel_btn.setFixedWidth(150)
|
|
cancel_btn.setCursor(Qt.PointingHandCursor)
|
|
cancel_btn.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background-color: white;
|
|
color: #718096;
|
|
border: 2px solid #e2e8f0;
|
|
border-radius: 25px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
padding: 8px 24px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}}
|
|
QPushButton:hover {{
|
|
background-color: #f7fafc;
|
|
border-color: #cbd5e0;
|
|
color: #4a5568;
|
|
}}
|
|
QPushButton:pressed {{
|
|
background-color: #edf2f7;
|
|
}}
|
|
""")
|
|
cancel_btn.clicked.connect(self.on_cancel)
|
|
button_layout.addWidget(cancel_btn)
|
|
|
|
# Save button (filled)
|
|
save_btn = QPushButton("💾 Simpan")
|
|
save_btn.setFixedHeight(45)
|
|
save_btn.setFixedWidth(150)
|
|
save_btn.setCursor(Qt.PointingHandCursor)
|
|
save_btn.setStyleSheet(f"""
|
|
QPushButton {{
|
|
background-color: {border_color};
|
|
color: white;
|
|
border: none;
|
|
border-radius: 25px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
padding: 8px 24px;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}}
|
|
QPushButton:hover {{
|
|
background-color: #3182ce;
|
|
}}
|
|
QPushButton:pressed {{
|
|
background-color: #3182ce;
|
|
padding-top: 10px;
|
|
padding-bottom: 6px;
|
|
}}
|
|
""")
|
|
save_btn.clicked.connect(self.on_save)
|
|
button_layout.addWidget(save_btn)
|
|
|
|
content_layout.addLayout(button_layout)
|
|
content_widget.setLayout(content_layout)
|
|
|
|
main_layout.addWidget(content_widget)
|
|
self.setLayout(main_layout)
|
|
|
|
# Apply shadow effect
|
|
from PyQt5.QtWidgets import QGraphicsDropShadowEffect
|
|
from PyQt5.QtGui import QColor
|
|
shadow = QGraphicsDropShadowEffect()
|
|
shadow.setBlurRadius(30)
|
|
shadow.setXOffset(0)
|
|
shadow.setYOffset(10)
|
|
shadow.setColor(QColor(0, 0, 0, 60))
|
|
content_widget.setGraphicsEffect(shadow)
|
|
|
|
def on_save(self):
|
|
self.result_save = True
|
|
self.accept()
|
|
|
|
def on_cancel(self):
|
|
self.result_save = False
|
|
self.reject()
|
|
|
|
@staticmethod
|
|
def show_prediction(parent=None, nama="", prediction="", paket_info=None):
|
|
"""Show a prediction result dialog and return True if user wants to save"""
|
|
dialog = CustomPredictionDialog(parent, nama, prediction, paket_info)
|
|
dialog.exec_()
|
|
return dialog.result_save
|
|
|
|
|
|
class StudentNamePickerDialog(QDialog):
|
|
"""Dialog untuk memilih nama siswa dari dataset final."""
|
|
|
|
def __init__(self, parent=None, initial_name=""):
|
|
super().__init__(parent)
|
|
self.selected_name = ""
|
|
self.initial_name = initial_name.strip()
|
|
self.base_names = self._load_base_names()
|
|
self.catalog_state = self._load_catalog_state()
|
|
self.all_names = self._build_name_list()
|
|
self.filtered_names = list(self.all_names)
|
|
self.current_page = 1
|
|
self.items_per_page = 10
|
|
self.visible_table_rows = 6
|
|
|
|
self.setWindowTitle("Pilih Nama Siswa")
|
|
self.setWindowFlag(Qt.WindowContextHelpButtonHint, False)
|
|
self.setModal(True)
|
|
self.setMinimumSize(760, 620)
|
|
self.resize(780, 640)
|
|
self.initUI()
|
|
self.apply_filter()
|
|
self._select_initial_name()
|
|
# Set window position to center of parent
|
|
if self.parent():
|
|
parent_rect = self.parent().frameGeometry()
|
|
self.move(parent_rect.center() - self.rect().center())
|
|
|
|
def showEvent(self, event):
|
|
super().showEvent(event)
|
|
# Prevent QLineEdit from receiving focus automatically when dialog opens.
|
|
# Clear focus from search/add inputs and set focus to the dialog itself.
|
|
try:
|
|
if hasattr(self, 'search_input'):
|
|
self.search_input.clearFocus()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
if hasattr(self, 'add_input'):
|
|
self.add_input.clearFocus()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
self.setFocus()
|
|
except Exception:
|
|
pass
|
|
|
|
def _dataset_path(self):
|
|
return config.get_resource_path(os.path.join("data", "dataset_smakom_final.csv"))
|
|
|
|
def _catalog_path(self):
|
|
return config.get_writable_path(os.path.join("data", "student_name_catalog.json"))
|
|
|
|
def _normalize_name(self, name):
|
|
return " ".join(str(name).strip().split()).casefold()
|
|
|
|
def _display_name(self, raw_name):
|
|
return " ".join(str(raw_name).replace("_", " ").split())
|
|
|
|
def _load_base_names(self):
|
|
names = []
|
|
seen = set()
|
|
dataset_path = self._dataset_path()
|
|
|
|
if not os.path.exists(dataset_path):
|
|
return names
|
|
|
|
try:
|
|
with open(dataset_path, "r", encoding="utf-8-sig", newline="") as csv_file:
|
|
reader = csv.DictReader(csv_file)
|
|
for row in reader:
|
|
raw_name = row.get("nama_siswa", "")
|
|
display_name = self._display_name(raw_name)
|
|
if not display_name:
|
|
continue
|
|
normalized = self._normalize_name(display_name)
|
|
if normalized in seen:
|
|
continue
|
|
seen.add(normalized)
|
|
names.append(display_name)
|
|
except Exception:
|
|
return names
|
|
|
|
return names
|
|
|
|
def _load_catalog_state(self):
|
|
default_state = {"added": [], "deleted": []}
|
|
catalog_path = self._catalog_path()
|
|
|
|
if not os.path.exists(catalog_path):
|
|
return default_state
|
|
|
|
try:
|
|
with open(catalog_path, "r", encoding="utf-8") as json_file:
|
|
data = json.load(json_file)
|
|
except Exception:
|
|
return default_state
|
|
|
|
if isinstance(data, list):
|
|
return {"added": [str(item).strip() for item in data if str(item).strip()], "deleted": []}
|
|
|
|
if isinstance(data, dict):
|
|
added = data.get("added") or data.get("names") or []
|
|
deleted = data.get("deleted") or []
|
|
return {
|
|
"added": [str(item).strip() for item in added if str(item).strip()],
|
|
"deleted": [str(item).strip() for item in deleted if str(item).strip()],
|
|
}
|
|
|
|
return default_state
|
|
|
|
def _save_catalog_state(self):
|
|
catalog_path = self._catalog_path()
|
|
catalog_dir = os.path.dirname(catalog_path)
|
|
if catalog_dir:
|
|
os.makedirs(catalog_dir, exist_ok=True)
|
|
|
|
payload = {
|
|
"added": self.catalog_state.get("added", []),
|
|
"deleted": self.catalog_state.get("deleted", []),
|
|
}
|
|
|
|
with open(catalog_path, "w", encoding="utf-8") as json_file:
|
|
json.dump(payload, json_file, ensure_ascii=False, indent=2)
|
|
|
|
def _build_name_list(self):
|
|
deleted = {self._normalize_name(name) for name in self.catalog_state.get("deleted", [])}
|
|
result = []
|
|
seen = set()
|
|
|
|
for name in self.catalog_state.get("added", []):
|
|
normalized = self._normalize_name(name)
|
|
if not normalized or normalized in deleted or normalized in seen:
|
|
continue
|
|
seen.add(normalized)
|
|
result.append(name)
|
|
|
|
for name in self.base_names:
|
|
normalized = self._normalize_name(name)
|
|
if normalized in deleted or normalized in seen:
|
|
continue
|
|
seen.add(normalized)
|
|
result.append(name)
|
|
|
|
return result
|
|
|
|
def initUI(self):
|
|
main_layout = QVBoxLayout(self)
|
|
main_layout.setContentsMargins(12, 12, 12, 12)
|
|
|
|
card = QFrame()
|
|
card.setStyleSheet("""
|
|
QFrame {
|
|
background: white;
|
|
border-radius: 16px;
|
|
}
|
|
""")
|
|
card_layout = QVBoxLayout(card)
|
|
card_layout.setContentsMargins(20, 18, 20, 16)
|
|
card_layout.setSpacing(10)
|
|
|
|
header_layout = QVBoxLayout()
|
|
header_layout.setSpacing(0)
|
|
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
title = QLabel("Pilih Nama Siswa")
|
|
title.setContentsMargins(0, 0, 0, 0)
|
|
title.setStyleSheet("font-size: 28px; font-weight: 700; color: #1f3b66; font-family: 'Segoe UI', Arial, sans-serif; margin: 0; padding: 0; line-height: 1;")
|
|
header_layout.addWidget(title)
|
|
|
|
subtitle = QLabel("Cari, tambah, hapus, lalu pilih nama yang akan dimasukkan ke form.")
|
|
subtitle.setWordWrap(True)
|
|
subtitle.setContentsMargins(0, 0, 0, 0)
|
|
subtitle.setStyleSheet("font-size: 17px; color: #56708f; font-family: 'Segoe UI', Arial, sans-serif; margin: 0; padding: 0; line-height: 1;")
|
|
header_layout.addWidget(subtitle)
|
|
|
|
card_layout.addLayout(header_layout)
|
|
|
|
search_row = QHBoxLayout()
|
|
search_row.setSpacing(10)
|
|
|
|
self.search_input = QLineEdit()
|
|
self.search_input.setPlaceholderText("Cari nama siswa...")
|
|
self.search_input.setMinimumHeight(42)
|
|
self.search_input.setStyleSheet("""
|
|
QLineEdit {
|
|
border: 1px solid #d6deea;
|
|
border-radius: 10px;
|
|
padding: 9px 14px;
|
|
font-size: 17px;
|
|
background: #f8fbff;
|
|
color: #1f2937;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}
|
|
QLineEdit:focus {
|
|
border-color: #2c7be5;
|
|
background: white;
|
|
}
|
|
""")
|
|
self.search_input.textChanged.connect(self.apply_filter)
|
|
self.search_input.returnPressed.connect(self.apply_filter)
|
|
search_row.addWidget(self.search_input, 1)
|
|
|
|
self.add_input = QLineEdit()
|
|
self.add_input.setPlaceholderText("Tambah nama baru...")
|
|
self.add_input.setMinimumHeight(42)
|
|
self.add_input.setStyleSheet(self.search_input.styleSheet())
|
|
self.add_input.returnPressed.connect(self.add_name)
|
|
search_row.addWidget(self.add_input, 1)
|
|
|
|
card_layout.addLayout(search_row)
|
|
|
|
action_row = QHBoxLayout()
|
|
action_row.setContentsMargins(0, 2, 0, 0)
|
|
action_row.setSpacing(12)
|
|
|
|
add_btn = QPushButton("Tambah")
|
|
add_btn.setFixedHeight(40)
|
|
add_btn.setFixedWidth(110)
|
|
add_btn.setCursor(Qt.PointingHandCursor)
|
|
add_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background: #118a7e;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 10px;
|
|
padding: 0 16px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}
|
|
QPushButton:hover {
|
|
background: #0e7369;
|
|
}
|
|
""")
|
|
add_btn.clicked.connect(self.add_name)
|
|
action_row.addWidget(add_btn)
|
|
|
|
self.delete_btn = QPushButton("Hapus Terpilih")
|
|
self.delete_btn.setFixedHeight(40)
|
|
self.delete_btn.setFixedWidth(160)
|
|
self.delete_btn.setCursor(Qt.PointingHandCursor)
|
|
self.delete_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background: #ef5a5a;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 10px;
|
|
padding: 0 14px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}
|
|
QPushButton:hover {
|
|
background: #d94b4b;
|
|
}
|
|
QPushButton:disabled {
|
|
background: #c8d3e2;
|
|
color: #7b8aa0;
|
|
}
|
|
""")
|
|
self.delete_btn.clicked.connect(self.delete_selected_names)
|
|
action_row.addWidget(self.delete_btn)
|
|
|
|
self.pilih_btn = QPushButton("Pilih")
|
|
self.pilih_btn.setFixedHeight(40)
|
|
self.pilih_btn.setFixedWidth(96)
|
|
self.pilih_btn.setCursor(Qt.PointingHandCursor)
|
|
self.pilih_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background: #1f3b66;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 10px;
|
|
padding: 0 14px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}
|
|
QPushButton:hover {
|
|
background: #182f53;
|
|
}
|
|
QPushButton:disabled {
|
|
background: #c8d3e2;
|
|
color: #7b8aa0;
|
|
}
|
|
""")
|
|
self.pilih_btn.clicked.connect(self.pick_selected_name)
|
|
action_row.addWidget(self.pilih_btn)
|
|
|
|
self.edit_btn = QPushButton("Edit")
|
|
self.edit_btn.setFixedHeight(40)
|
|
self.edit_btn.setFixedWidth(96)
|
|
self.edit_btn.setCursor(Qt.PointingHandCursor)
|
|
self.edit_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background: #16a34a;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 10px;
|
|
padding: 0 14px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}
|
|
QPushButton:hover {
|
|
background: #15803d;
|
|
}
|
|
QPushButton:disabled {
|
|
background: #c8d3e2;
|
|
color: #7b8aa0;
|
|
}
|
|
""")
|
|
self.edit_btn.clicked.connect(self.edit_selected_name)
|
|
action_row.addWidget(self.edit_btn)
|
|
|
|
tutup_btn = QPushButton("Tutup")
|
|
tutup_btn.setFixedHeight(40)
|
|
tutup_btn.setFixedWidth(96)
|
|
tutup_btn.setCursor(Qt.PointingHandCursor)
|
|
tutup_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background: #6c7a92;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 10px;
|
|
padding: 0 14px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}
|
|
QPushButton:hover {
|
|
background: #5d6a7f;
|
|
}
|
|
""")
|
|
tutup_btn.clicked.connect(self.reject)
|
|
action_row.addWidget(tutup_btn)
|
|
|
|
action_row.addStretch()
|
|
|
|
card_layout.addLayout(action_row)
|
|
|
|
self.table = QTableWidget(0, 3)
|
|
self.table.setHorizontalHeaderLabels(["No", "Nama Siswa", "Hapus"])
|
|
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
self.table.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
self.table.setAlternatingRowColors(True)
|
|
self.table.verticalHeader().setVisible(False)
|
|
self.table.setShowGrid(False)
|
|
self.table.setStyleSheet("""
|
|
QTableWidget {
|
|
border: 1px solid #dbe4f0;
|
|
border-radius: 12px;
|
|
background: white;
|
|
alternate-background-color: #f7faff;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
font-size: 17px;
|
|
color: #1f2937;
|
|
}
|
|
QTableWidget::item {
|
|
padding: 8px 10px;
|
|
}
|
|
QHeaderView::section {
|
|
background: #eef4fb;
|
|
color: #173458;
|
|
font-weight: 700;
|
|
border: none;
|
|
border-bottom: 1px solid #dbe4f0;
|
|
padding: 8px 3px;
|
|
font-size: 17px;
|
|
}
|
|
""")
|
|
self.table.verticalHeader().setDefaultSectionSize(42)
|
|
header_height = self.table.horizontalHeader().sizeHint().height()
|
|
table_frame = self.table.frameWidth() * 2
|
|
visible_height = header_height + (self.visible_table_rows * self.table.verticalHeader().defaultSectionSize()) + table_frame + 2
|
|
self.table.setFixedHeight(visible_height)
|
|
header = self.table.horizontalHeader()
|
|
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
|
header.setSectionResizeMode(1, QHeaderView.Stretch)
|
|
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
|
header_item = self.table.horizontalHeaderItem(0)
|
|
if header_item:
|
|
header_item.setTextAlignment(Qt.AlignCenter | Qt.AlignVCenter)
|
|
header_item = self.table.horizontalHeaderItem(1)
|
|
if header_item:
|
|
header_item.setTextAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
header_item = self.table.horizontalHeaderItem(2)
|
|
if header_item:
|
|
header_item.setTextAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
|
self.table.cellDoubleClicked.connect(self._handle_cell_double_clicked)
|
|
self.table.cellClicked.connect(self._handle_cell_clicked)
|
|
self.table.itemSelectionChanged.connect(self._sync_action_state)
|
|
self.table.itemChanged.connect(self._sync_action_state)
|
|
self.table.viewport().installEventFilter(self)
|
|
card_layout.addWidget(self.table)
|
|
|
|
pagination_row = QHBoxLayout()
|
|
pagination_row.setSpacing(16)
|
|
pagination_row.setContentsMargins(2, 8, 2, 0)
|
|
pagination_row.addStretch()
|
|
|
|
self.prev_btn = QPushButton("◀ Previous")
|
|
self.prev_btn.setFixedHeight(40)
|
|
self.prev_btn.setFixedWidth(132)
|
|
self.prev_btn.setCursor(Qt.PointingHandCursor)
|
|
self.prev_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background: #e8eef6;
|
|
color: #38506d;
|
|
border: none;
|
|
border-radius: 10px;
|
|
padding: 0 14px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}
|
|
QPushButton:hover {
|
|
background: #dce6f1;
|
|
}
|
|
QPushButton:disabled {
|
|
background: #edf2f7;
|
|
color: #a0aec0;
|
|
}
|
|
""")
|
|
self.prev_btn.clicked.connect(self.previous_page)
|
|
pagination_row.addWidget(self.prev_btn)
|
|
|
|
self.page_label = QLabel()
|
|
self.page_label.setAlignment(Qt.AlignCenter)
|
|
self.page_label.setStyleSheet("font-size: 16px; color: #3d5574; font-weight: 600; font-family: 'Segoe UI', Arial, sans-serif;")
|
|
pagination_row.addWidget(self.page_label)
|
|
|
|
self.next_btn = QPushButton("Next ▶")
|
|
self.next_btn.setFixedHeight(40)
|
|
self.next_btn.setFixedWidth(132)
|
|
self.next_btn.setCursor(Qt.PointingHandCursor)
|
|
self.next_btn.setStyleSheet(self.prev_btn.styleSheet())
|
|
self.next_btn.clicked.connect(self.next_page)
|
|
pagination_row.addWidget(self.next_btn)
|
|
pagination_row.addStretch()
|
|
|
|
card_layout.addLayout(pagination_row)
|
|
main_layout.addWidget(card)
|
|
|
|
def _sync_action_state(self):
|
|
checked = self._has_checked_rows()
|
|
selected = bool(self.get_selected_name())
|
|
|
|
if checked and selected:
|
|
self.table.blockSignals(True)
|
|
self.table.clearSelection()
|
|
self.table.setCurrentCell(-1, -1)
|
|
self.table.blockSignals(False)
|
|
selected = False
|
|
|
|
if hasattr(self, "pilih_btn"):
|
|
self.pilih_btn.setEnabled(selected and not checked)
|
|
if hasattr(self, "edit_btn"):
|
|
self.edit_btn.setEnabled(selected and not checked)
|
|
if hasattr(self, "delete_btn"):
|
|
self.delete_btn.setEnabled(checked)
|
|
|
|
def _clear_checked_rows(self):
|
|
for row in range(self.table.rowCount()):
|
|
item = self.table.item(row, 2)
|
|
if item and item.checkState() == Qt.Checked:
|
|
item.setCheckState(Qt.Unchecked)
|
|
|
|
def _handle_cell_clicked(self, row, column):
|
|
del row
|
|
if column == 2:
|
|
self.table.blockSignals(True)
|
|
self.table.clearSelection()
|
|
self.table.setCurrentCell(-1, -1)
|
|
self.table.blockSignals(False)
|
|
self._sync_action_state()
|
|
return
|
|
|
|
def _handle_cell_double_clicked(self, row, column):
|
|
# Ignore double-clicks on the checkbox column so they don't trigger name picking
|
|
if column == 2:
|
|
return
|
|
self.pick_selected_name()
|
|
|
|
if column in (0, 1) and self._has_checked_rows():
|
|
self.table.blockSignals(True)
|
|
self._clear_checked_rows()
|
|
self.table.blockSignals(False)
|
|
|
|
self._sync_action_state()
|
|
|
|
def _has_checked_rows(self):
|
|
for row in range(self.table.rowCount()):
|
|
item = self.table.item(row, 2)
|
|
if item and item.checkState() == Qt.Checked:
|
|
return True
|
|
return False
|
|
|
|
def _select_initial_name(self):
|
|
if not self.initial_name:
|
|
return
|
|
normalized_initial = self._normalize_name(self.initial_name)
|
|
for row, name in enumerate(self.filtered_names):
|
|
if self._normalize_name(name) == normalized_initial:
|
|
self.current_page = row // self.items_per_page + 1
|
|
self.render_table()
|
|
self.table.selectRow(row % self.items_per_page)
|
|
break
|
|
|
|
def clear_search(self):
|
|
self.search_input.clear()
|
|
self.apply_filter()
|
|
|
|
def apply_filter(self):
|
|
search_text = self.search_input.text().strip().casefold()
|
|
if search_text:
|
|
self.filtered_names = [name for name in self.all_names if search_text in name.casefold()]
|
|
else:
|
|
self.filtered_names = list(self.all_names)
|
|
self.current_page = 1
|
|
self.render_table()
|
|
|
|
def render_table(self):
|
|
self.table.blockSignals(True)
|
|
self.table.clearContents()
|
|
self.table.clearSpans()
|
|
|
|
total_items = len(self.filtered_names)
|
|
total_pages = max(1, (total_items + self.items_per_page - 1) // self.items_per_page)
|
|
self.current_page = min(max(1, self.current_page), total_pages)
|
|
start_index = (self.current_page - 1) * self.items_per_page
|
|
end_index = min(start_index + self.items_per_page, total_items)
|
|
page_items = self.filtered_names[start_index:end_index]
|
|
|
|
self.table.setRowCount(len(page_items) if page_items else 1)
|
|
self.table.setSpan(0, 0, 1, 1)
|
|
|
|
if not page_items:
|
|
empty_item = QTableWidgetItem("Tidak ada nama ditemukan")
|
|
empty_item.setTextAlignment(Qt.AlignCenter)
|
|
empty_item.setFlags(Qt.ItemIsEnabled)
|
|
self.table.setItem(0, 0, empty_item)
|
|
self.table.setSpan(0, 0, 1, 3)
|
|
else:
|
|
for row, name in enumerate(page_items):
|
|
no_item = QTableWidgetItem(str(start_index + row + 1))
|
|
no_item.setTextAlignment(Qt.AlignCenter)
|
|
no_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
self.table.setItem(row, 0, no_item)
|
|
|
|
name_item = QTableWidgetItem(name)
|
|
name_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
self.table.setItem(row, 1, name_item)
|
|
|
|
delete_item = QTableWidgetItem()
|
|
delete_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
|
|
delete_item.setCheckState(Qt.Unchecked)
|
|
delete_item.setTextAlignment(Qt.AlignCenter)
|
|
self.table.setItem(row, 2, delete_item)
|
|
|
|
self.table.blockSignals(False)
|
|
self.table.clearSelection()
|
|
self.table.setCurrentCell(-1, -1)
|
|
self.update_pagination_controls()
|
|
self._sync_action_state()
|
|
|
|
def eventFilter(self, obj, event):
|
|
if obj is getattr(self, "table", None).viewport():
|
|
if event.type() in (QEvent.MouseButtonPress, QEvent.MouseButtonRelease):
|
|
index = self.table.indexAt(event.pos())
|
|
if index.isValid() and index.column() == 2:
|
|
# Toggle checkbox state on mouse release and consume the event so
|
|
# the table's default selection behavior doesn't interfere.
|
|
if event.type() == QEvent.MouseButtonRelease:
|
|
item = self.table.item(index.row(), 2)
|
|
if item:
|
|
new_state = Qt.Unchecked if item.checkState() == Qt.Checked else Qt.Checked
|
|
item.setCheckState(new_state)
|
|
self._sync_action_state()
|
|
return True
|
|
return super().eventFilter(obj, event)
|
|
|
|
def update_pagination_controls(self):
|
|
total_items = len(self.filtered_names)
|
|
total_pages = max(1, (total_items + self.items_per_page - 1) // self.items_per_page)
|
|
self.page_label.setText(f"Halaman {self.current_page} dari {total_pages} | {total_items} data")
|
|
self.prev_btn.setEnabled(self.current_page > 1)
|
|
self.next_btn.setEnabled(self.current_page < total_pages)
|
|
|
|
def previous_page(self):
|
|
if self.current_page > 1:
|
|
self.current_page -= 1
|
|
self.render_table()
|
|
|
|
def next_page(self):
|
|
total_items = len(self.filtered_names)
|
|
total_pages = max(1, (total_items + self.items_per_page - 1) // self.items_per_page)
|
|
if self.current_page < total_pages:
|
|
self.current_page += 1
|
|
self.render_table()
|
|
|
|
def get_selected_name(self):
|
|
current_row = self.table.currentRow()
|
|
if current_row < 0:
|
|
return ""
|
|
name_item = self.table.item(current_row, 1)
|
|
if not name_item:
|
|
return ""
|
|
return name_item.text().strip()
|
|
|
|
def add_name(self):
|
|
new_name = " ".join(self.add_input.text().strip().split())
|
|
if not new_name:
|
|
QMessageBox.warning(self, "Validasi", "Nama baru masih kosong.")
|
|
return
|
|
|
|
normalized_new = self._normalize_name(new_name)
|
|
if any(self._normalize_name(name) == normalized_new for name in self.all_names):
|
|
QMessageBox.warning(self, "Validasi", "Nama sudah ada di daftar.")
|
|
return
|
|
|
|
deleted = self.catalog_state.setdefault("deleted", [])
|
|
deleted[:] = [name for name in deleted if self._normalize_name(name) != normalized_new]
|
|
|
|
added = self.catalog_state.setdefault("added", [])
|
|
# Insert new names at the beginning so the newest appears at the top
|
|
added.insert(0, new_name)
|
|
self.add_input.clear()
|
|
self._save_catalog_state()
|
|
self.all_names = self._build_name_list()
|
|
self.apply_filter()
|
|
|
|
def delete_selected_names(self):
|
|
checked_names = []
|
|
for row in range(self.table.rowCount()):
|
|
name_item = self.table.item(row, 1)
|
|
delete_item = self.table.item(row, 2)
|
|
if not name_item or not delete_item:
|
|
continue
|
|
if delete_item.checkState() == Qt.Checked:
|
|
checked_names.append(name_item.text().strip())
|
|
|
|
if not checked_names:
|
|
QMessageBox.information(self, "Info", "Centang minimal satu nama untuk dihapus.")
|
|
return
|
|
|
|
deleted = self.catalog_state.setdefault("deleted", [])
|
|
added = self.catalog_state.setdefault("added", [])
|
|
checked_normalized = {self._normalize_name(name) for name in checked_names}
|
|
|
|
for name in checked_names:
|
|
normalized = self._normalize_name(name)
|
|
if any(self._normalize_name(item) == normalized for item in added):
|
|
added[:] = [item for item in added if self._normalize_name(item) != normalized]
|
|
elif not any(self._normalize_name(item) == normalized for item in deleted):
|
|
deleted.append(name)
|
|
|
|
if self.selected_name and self._normalize_name(self.selected_name) in checked_normalized:
|
|
self.selected_name = ""
|
|
|
|
self._save_catalog_state()
|
|
self.all_names = self._build_name_list()
|
|
self.apply_filter()
|
|
|
|
class EditNameDialog(QDialog):
|
|
"""Custom dialog to edit a student name with improved styling."""
|
|
def __init__(self, parent=None, initial_name=""):
|
|
super().__init__(parent)
|
|
self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint)
|
|
self.setAttribute(Qt.WA_TranslucentBackground)
|
|
self.initial_name = initial_name
|
|
self.result_name = None
|
|
self.initUI()
|
|
|
|
def initUI(self):
|
|
main_layout = QVBoxLayout()
|
|
main_layout.setContentsMargins(16, 16, 16, 16)
|
|
|
|
card = QDialog()
|
|
card.setFixedWidth(420)
|
|
card.setStyleSheet("""
|
|
QDialog {
|
|
background: white;
|
|
border-radius: 10px;
|
|
}
|
|
""")
|
|
|
|
card_layout = QVBoxLayout(card)
|
|
card_layout.setContentsMargins(20, 18, 20, 18)
|
|
card_layout.setSpacing(10)
|
|
|
|
title = QLabel("Edit Nama")
|
|
title.setStyleSheet("font-size: 24px; font-weight: 700; color: #1f3b66; font-family: 'Segoe UI', Arial, sans-serif;")
|
|
title.setAlignment(Qt.AlignLeft)
|
|
card_layout.addWidget(title)
|
|
|
|
label = QLabel("Nama baru:")
|
|
label.setStyleSheet("font-size: 15px; color: #334155; font-weight: 600;")
|
|
card_layout.addWidget(label)
|
|
|
|
self.input = QLineEdit()
|
|
self.input.setText(self.initial_name)
|
|
self.input.setMinimumHeight(42)
|
|
self.input.setStyleSheet("""
|
|
QLineEdit {
|
|
border: 1px solid #d6deea;
|
|
border-radius: 8px;
|
|
padding: 7px 10px;
|
|
font-size: 16px;
|
|
color: #0f172a;
|
|
background: #fbfdff;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
}
|
|
QLineEdit:focus {
|
|
border-color: #2c7be5;
|
|
background: white;
|
|
}
|
|
""")
|
|
self.input.selectAll()
|
|
card_layout.addWidget(self.input)
|
|
|
|
# Buttons: Close on the left, Edit on the right (Close placed to left of Edit)
|
|
btn_row = QHBoxLayout()
|
|
btn_row.addStretch()
|
|
|
|
cancel_btn = QPushButton("Tutup")
|
|
cancel_btn.setFixedHeight(38)
|
|
cancel_btn.setFixedWidth(96)
|
|
cancel_btn.setCursor(Qt.PointingHandCursor)
|
|
cancel_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: white;
|
|
color: #718096;
|
|
border: 2px solid #e2e8f0;
|
|
border-radius: 20px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #f7fafc;
|
|
}
|
|
""")
|
|
cancel_btn.clicked.connect(self.on_cancel)
|
|
btn_row.addWidget(cancel_btn)
|
|
|
|
btn_row.addSpacing(10)
|
|
|
|
edit_btn = QPushButton("Edit")
|
|
edit_btn.setFixedHeight(38)
|
|
edit_btn.setFixedWidth(96)
|
|
edit_btn.setCursor(Qt.PointingHandCursor)
|
|
edit_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #16a34a;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 20px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #15803d;
|
|
}
|
|
""")
|
|
edit_btn.clicked.connect(self.on_edit)
|
|
btn_row.addWidget(edit_btn)
|
|
|
|
card_layout.addLayout(btn_row)
|
|
|
|
from PyQt5.QtWidgets import QGraphicsDropShadowEffect
|
|
from PyQt5.QtGui import QColor
|
|
shadow = QGraphicsDropShadowEffect()
|
|
shadow.setBlurRadius(24)
|
|
shadow.setXOffset(0)
|
|
shadow.setYOffset(8)
|
|
shadow.setColor(QColor(0, 0, 0, 60))
|
|
card.setGraphicsEffect(shadow)
|
|
|
|
main_layout.addWidget(card)
|
|
self.setLayout(main_layout)
|
|
|
|
def on_edit(self):
|
|
text = " ".join(self.input.text().strip().split())
|
|
if not text:
|
|
QMessageBox.warning(self, "Validasi", "Nama baru tidak boleh kosong.")
|
|
return
|
|
self.result_name = text
|
|
self.accept()
|
|
|
|
def on_cancel(self):
|
|
self.result_name = None
|
|
self.reject()
|
|
|
|
def edit_selected_name(self):
|
|
old_name = self.get_selected_name()
|
|
if not old_name:
|
|
QMessageBox.information(self, "Info", "Pilih satu nama terlebih dahulu.")
|
|
return
|
|
|
|
dialog = StudentNamePickerDialog.EditNameDialog(self, initial_name=old_name)
|
|
result = dialog.exec_()
|
|
if result != QDialog.Accepted or not dialog.result_name:
|
|
return
|
|
|
|
new_name = " ".join(dialog.result_name.strip().split())
|
|
if not new_name:
|
|
QMessageBox.warning(self, "Validasi", "Nama baru tidak boleh kosong.")
|
|
return
|
|
|
|
normalized_new = self._normalize_name(new_name)
|
|
normalized_old = self._normalize_name(old_name)
|
|
|
|
if normalized_new == normalized_old:
|
|
return
|
|
|
|
if any(self._normalize_name(name) == normalized_new for name in self.all_names):
|
|
QMessageBox.warning(self, "Validasi", "Nama baru sudah ada di daftar.")
|
|
return
|
|
|
|
added = self.catalog_state.setdefault("added", [])
|
|
deleted = self.catalog_state.setdefault("deleted", [])
|
|
|
|
if any(self._normalize_name(item) == normalized_old for item in added):
|
|
added[:] = [item for item in added if self._normalize_name(item) != normalized_old]
|
|
else:
|
|
if not any(self._normalize_name(item) == normalized_old for item in deleted):
|
|
deleted.append(old_name)
|
|
|
|
deleted[:] = [item for item in deleted if self._normalize_name(item) != normalized_new]
|
|
added[:] = [item for item in added if self._normalize_name(item) != normalized_new]
|
|
added.insert(0, new_name)
|
|
|
|
self.selected_name = new_name
|
|
self._save_catalog_state()
|
|
self.all_names = self._build_name_list()
|
|
self.apply_filter()
|
|
|
|
def pick_selected_name(self, *_):
|
|
name = self.get_selected_name()
|
|
if not name:
|
|
QMessageBox.information(self, "Info", "Pilih nama dari tabel terlebih dahulu.")
|
|
return
|
|
|
|
self.selected_name = name
|
|
self.accept()
|
|
|
|
def accept(self):
|
|
if not self.selected_name:
|
|
self.selected_name = self.get_selected_name()
|
|
super().accept()
|
|
|
|
@staticmethod
|
|
def show_picker(parent=None, initial_name=""):
|
|
dialog = StudentNamePickerDialog(parent, initial_name=initial_name)
|
|
result = dialog.exec_()
|
|
if result == QDialog.Accepted and dialog.selected_name:
|
|
return dialog.selected_name
|
|
return None
|