utils.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. """
  2. A.L.I.G. Project - Utils
  3. ------------------------------
  4. """
  5. import os
  6. import sys
  7. from PIL import Image
  8. import datetime
  9. from PyQt6.QtWidgets import (
  10. QDialog, QLabel, QPushButton, QFrame, QHBoxLayout, QVBoxLayout
  11. )
  12. from PyQt6.QtCore import Qt
  13. from PyQt6.QtGui import QFont
  14. from core.translations import TRANSLATIONS
  15. def get_app_paths():
  16. """Détermine le chemin de base et le chemin d'exécution."""
  17. if getattr(sys, 'frozen', False):
  18. # Mode compilé (.exe)
  19. base_path = sys._MEIPASS
  20. app_path = os.path.dirname(sys.executable)
  21. else:
  22. # Mode script (.py)
  23. base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
  24. app_path = base_path
  25. return base_path, app_path
  26. def save_dashboard_data(config_manager, matrix, gcode_content, estimated_time=0):
  27. """
  28. Gère la miniature et les stats (incluant le temps de simulation)
  29. """
  30. try:
  31. base_path, app_path = get_app_paths()
  32. thumb_dir = os.path.join(app_path, "assets", "thumbnails")
  33. os.makedirs(thumb_dir, exist_ok=True)
  34. # 1. Normalisation et Inversion
  35. mat = matrix.astype('float32')
  36. min_val, max_val = mat.min(), mat.max()
  37. if max_val > min_val:
  38. mat = (mat - min_val) / (max_val - min_val) * 255
  39. inverted_matrix = (255 - mat).astype('uint8')
  40. img_gray = Image.fromarray(inverted_matrix).convert("L")
  41. # 2. Redimensionnement proportionnel
  42. target_size = 150
  43. img_gray.thumbnail((target_size, target_size), Image.Resampling.LANCZOS)
  44. # 3. CRÉATION DU CANEVAS TRANSPARENT
  45. square_img = Image.new('RGBA', (target_size, target_size), (0, 0, 0, 0))
  46. img_rgba = img_gray.convert("RGBA")
  47. # 4. Centrage
  48. offset = ((target_size - img_rgba.size[0]) // 2, (target_size - img_rgba.size[1]) // 2)
  49. square_img.paste(img_rgba, offset)
  50. # 5. Sauvegarde en PNG
  51. timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  52. thumb_path = os.path.join(thumb_dir, f"thumb_{timestamp}.png")
  53. square_img.save(thumb_path, "PNG")
  54. # --- 6. MISE À JOUR DES STATISTIQUES ---
  55. current_lines = int(config_manager.get_item("stats", "total_lines", 0))
  56. current_gcodes = int(config_manager.get_item("stats", "total_gcodes", 0))
  57. current_time = float(config_manager.get_item("stats", "total_time_seconds", 0.0))
  58. # Calcul des nouvelles valeurs
  59. new_lines = len(gcode_content.splitlines())
  60. # Conversion forcée en types Python natifs
  61. total_lines_updated = int(current_lines + new_lines)
  62. total_gcodes_updated = int(current_gcodes + 1)
  63. total_time_updated = float(current_time + float(estimated_time))
  64. # Enregistrement des données cumulées
  65. config_manager.set_item("stats", "total_lines", total_lines_updated)
  66. config_manager.set_item("stats", "total_gcodes", total_gcodes_updated)
  67. config_manager.set_item("stats", "total_time_seconds", total_time_updated)
  68. # Enregistrement du dernier projet
  69. config_manager.set_item("stats", "last_project_time", float(estimated_time))
  70. config_manager.save()
  71. return thumb_path
  72. except Exception as e:
  73. print(f"Error saving dashboard data: {e}")
  74. import traceback
  75. traceback.print_exc()
  76. return None
  77. def ask_confirmation(parent, message, action_callback, danger_color="#8b0000"):
  78. """
  79. Crée une fenêtre de confirmation modale réutilisable.
  80. """
  81. # 1. RÉCUPÉRATION DE LA LANGUE
  82. config = getattr(parent, "config_manager", None)
  83. if not config and hasattr(parent, "app"): # Si le parent est une View
  84. config = parent.app.config_manager
  85. lang = config.get_item("machine_settings", "language", "English") if config else "English"
  86. texts = TRANSLATIONS.get(lang, TRANSLATIONS["English"]).get("common", {
  87. "confirm_title": "Confirmation",
  88. "confirm_subtitle": "This action is irreversible.",
  89. "btn_cancel": "Cancel",
  90. "btn_confirm": "Confirm"
  91. })
  92. # 2. CRÉATION DE LA DIALOG PYQT6
  93. dialog = QDialog(parent)
  94. dialog.setWindowTitle(texts.get("confirm_title", "Confirmation"))
  95. dialog.setFixedSize(300, 160)
  96. dialog.setWindowModality(Qt.WindowModality.ApplicationModal)
  97. # Centrage par rapport au parent
  98. parent_geo = parent.geometry()
  99. x = parent_geo.x() + (parent_geo.width() // 2) - 150
  100. y = parent_geo.y() + (parent_geo.height() // 2) - 80
  101. dialog.move(x, y)
  102. # 3. LAYOUT PRINCIPAL
  103. layout = QVBoxLayout(dialog)
  104. layout.setContentsMargins(20, 20, 20, 15)
  105. layout.setSpacing(5)
  106. # Label message principal
  107. lbl_title = QLabel(message)
  108. font_title = QFont("Arial", 12)
  109. font_title.setBold(True)
  110. lbl_title.setFont(font_title)
  111. lbl_title.setAlignment(Qt.AlignmentFlag.AlignCenter)
  112. lbl_title.setWordWrap(True)
  113. layout.addWidget(lbl_title)
  114. # Label sous-titre
  115. lbl_subtitle = QLabel(texts.get("confirm_subtitle", "This action is irreversible."))
  116. lbl_subtitle.setFont(QFont("Arial", 10))
  117. lbl_subtitle.setStyleSheet("color: gray;")
  118. lbl_subtitle.setAlignment(Qt.AlignmentFlag.AlignCenter)
  119. layout.addWidget(lbl_subtitle)
  120. layout.addSpacing(10)
  121. # 4. BOUTONS
  122. btn_frame = QFrame()
  123. btn_layout = QHBoxLayout(btn_frame)
  124. btn_layout.setContentsMargins(0, 0, 0, 0)
  125. btn_layout.setSpacing(10)
  126. btn_cancel = QPushButton(texts.get("btn_cancel", "Cancel"))
  127. btn_cancel.setFixedWidth(100)
  128. btn_cancel.setStyleSheet("background-color: gray; color: white; border-radius: 4px; padding: 5px;")
  129. btn_cancel.clicked.connect(dialog.reject)
  130. btn_confirm = QPushButton(texts.get("btn_confirm", "Confirm"))
  131. btn_confirm.setFixedWidth(100)
  132. btn_confirm.setStyleSheet(
  133. f"background-color: {danger_color}; color: white; border-radius: 4px; padding: 5px;"
  134. )
  135. btn_confirm.clicked.connect(lambda: [action_callback(), dialog.accept()])
  136. btn_layout.addWidget(btn_cancel)
  137. btn_layout.addWidget(btn_confirm)
  138. layout.addWidget(btn_frame, alignment=Qt.AlignmentFlag.AlignCenter)
  139. dialog.exec()
  140. def truncate_path(path, max_length=60):
  141. if not path or len(path) <= max_length:
  142. return path
  143. path = path.replace("\\", "/")
  144. # Gestion des chemins réseau (commençant par //)
  145. prefix = ""
  146. if path.startswith("//"):
  147. prefix = "//"
  148. path = path[2:]
  149. parts = path.split("/")
  150. filename = parts[-1]
  151. # On définit le début (Drive ou premier dossier)
  152. start_part = prefix + parts[0]
  153. # Si le nom de fichier lui-même est plus long que la limite
  154. if len(filename) >= max_length - 5:
  155. name_part, ext_part = (filename.rsplit('.', 1) + [""])[:2]
  156. if ext_part:
  157. ext_part = "." + ext_part
  158. remaining_space = max_length - len(start_part) - len(ext_part) - 5
  159. return f"{start_part}/...{name_part[-remaining_space:]}{ext_part}"
  160. # Tentative d'inclure le dossier parent si possible
  161. if len(parts) > 2:
  162. parent = parts[-2]
  163. if len(start_part) + len(parent) + len(filename) + 5 <= max_length:
  164. return f"{start_part}/.../{parent}/{filename}"
  165. # Sinon, simple Debut + ... + Fichier
  166. return f"{start_part}/.../{filename}"