MoleditPy 1.16.0a1__tar.gz → 1.16.0a2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/PKG-INFO +1 -1
  2. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/pyproject.toml +1 -1
  3. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/MoleditPy.egg-info/PKG-INFO +1 -1
  4. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/MoleditPy.egg-info/SOURCES.txt +2 -1
  5. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/main.py +1 -4
  6. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/constants.py +1 -1
  7. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window.py +10 -7
  8. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window_app_state.py +44 -6
  9. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window_main_init.py +86 -5
  10. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window_project_io.py +15 -2
  11. moleditpy-1.16.0a2/tests/test_project_io_wrappers.py +0 -0
  12. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/README.md +0 -0
  13. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/setup.cfg +0 -0
  14. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  15. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  16. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/MoleditPy.egg-info/requires.txt +0 -0
  17. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/MoleditPy.egg-info/top_level.txt +0 -0
  18. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/__init__.py +0 -0
  19. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/__main__.py +0 -0
  20. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/__init__.py +0 -0
  21. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/about_dialog.py +0 -0
  22. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/align_plane_dialog.py +0 -0
  23. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/alignment_dialog.py +0 -0
  24. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/analysis_window.py +0 -0
  25. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/angle_dialog.py +0 -0
  26. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/assets/icon.icns +0 -0
  27. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/assets/icon.ico +0 -0
  28. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/assets/icon.png +0 -0
  29. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/atom_item.py +0 -0
  30. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/bond_item.py +0 -0
  31. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/bond_length_dialog.py +0 -0
  32. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/calculation_worker.py +0 -0
  33. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/color_settings_dialog.py +0 -0
  34. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/constrained_optimization_dialog.py +0 -0
  35. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/custom_interactor_style.py +0 -0
  36. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/custom_qt_interactor.py +0 -0
  37. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/dialog3_d_picking_mixin.py +0 -0
  38. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/dihedral_dialog.py +0 -0
  39. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window_compute.py +0 -0
  40. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window_dialog_manager.py +0 -0
  41. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window_edit_3d.py +0 -0
  42. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window_edit_actions.py +0 -0
  43. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window_export.py +0 -0
  44. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window_molecular_parsers.py +0 -0
  45. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window_string_importers.py +0 -0
  46. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window_ui_manager.py +0 -0
  47. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window_view_3d.py +0 -0
  48. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/main_window_view_loaders.py +0 -0
  49. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/mirror_dialog.py +0 -0
  50. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/molecular_data.py +0 -0
  51. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/molecule_scene.py +0 -0
  52. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/move_group_dialog.py +0 -0
  53. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/periodic_table_dialog.py +0 -0
  54. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/planarize_dialog.py +0 -0
  55. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/settings_dialog.py +0 -0
  56. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/template_preview_item.py +0 -0
  57. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/template_preview_view.py +0 -0
  58. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/translation_dialog.py +0 -0
  59. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/user_template_dialog.py +0 -0
  60. {moleditpy-1.16.0a1 → moleditpy-1.16.0a2}/src/moleditpy/modules/zoomable_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 1.16.0a1
3
+ Version: 1.16.0a2
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/HiroYokoyama/python_molecular_editor
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy"
7
7
 
8
- version = "1.16.0a1"
8
+ version = "1.16.0a2"
9
9
 
10
10
  authors = [
11
11
  { name="HiroYokoyama", email="titech.yoko.hiro@gmail.com" },
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 1.16.0a1
3
+ Version: 1.16.0a2
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/HiroYokoyama/python_molecular_editor
@@ -60,4 +60,5 @@ src/moleditpy/modules/user_template_dialog.py
60
60
  src/moleditpy/modules/zoomable_view.py
61
61
  src/moleditpy/modules/assets/icon.icns
62
62
  src/moleditpy/modules/assets/icon.ico
63
- src/moleditpy/modules/assets/icon.png
63
+ src/moleditpy/modules/assets/icon.png
64
+ tests/test_project_io_wrappers.py
@@ -22,10 +22,7 @@ try:
22
22
  except Exception:
23
23
  # When executed as a standalone script (python main.py) the package-relative
24
24
  # import won't work; fall back to absolute import that works with sys.path
25
- try:
26
- from .modules.main_window import MainWindow
27
- except Exception:
28
- from modules.main_window import MainWindow
25
+ from modules.main_window import MainWindow
29
26
 
30
27
  def main():
31
28
  # --- Windows タスクバーアイコンのための追加処理 ---
@@ -4,7 +4,7 @@ from PyQt6.QtGui import QFont, QColor
4
4
  from rdkit import Chem
5
5
 
6
6
  #Version
7
- VERSION = '1.16.0a1'
7
+ VERSION = '1.16.0a2'
8
8
 
9
9
  ATOM_RADIUS = 18
10
10
  BOND_OFFSET = 3.5
@@ -109,6 +109,9 @@ class MainWindow(QMainWindow):
109
109
  # create a small proxy (BoundFeature) that will forward call to
110
110
  # the helper class with the MainWindow instance as the first
111
111
  # argument.
112
+ # Undo/Redo操作中に状態復元中であることを示すフラグ
113
+ # 他のモジュールが呼び出される前に初期化する
114
+ self._is_restoring_state = False
112
115
 
113
116
  class BoundFeature:
114
117
  """Bind a feature-class method calls to the MainWindow.
@@ -395,7 +398,7 @@ class MainWindow(QMainWindow):
395
398
 
396
399
  def load_mol_file(self, file_path=None):
397
400
  # --- MOVED TO main_window_molecular_parsers.py ---
398
- return self.main_window_molecular_parsers.load_mol_file(file_path=None)
401
+ return self.main_window_molecular_parsers.load_mol_file(file_path)
399
402
 
400
403
  def load_mol_for_3d_viewing(self):
401
404
  # --- MOVED TO main_window_view_loaders.py ---
@@ -403,9 +406,9 @@ class MainWindow(QMainWindow):
403
406
 
404
407
  def load_xyz_for_3d_viewing(self, file_path=None):
405
408
  # --- MOVED TO main_window_view_loaders.py ---
406
- return self.main_window_view_loaders.load_xyz_for_3d_viewing(file_path=None)
409
+ return self.main_window_view_loaders.load_xyz_for_3d_viewing(file_path)
407
410
 
408
- def load_xyz_file(self, file_path):
411
+ def load_xyz_file(self, file_path=None):
409
412
  # --- MOVED TO main_window_molecular_parsers.py ---
410
413
  return self.main_window_molecular_parsers.load_xyz_file(file_path)
411
414
 
@@ -427,7 +430,7 @@ class MainWindow(QMainWindow):
427
430
 
428
431
  def load_raw_data(self, file_path=None):
429
432
  # --- MOVED TO main_window_project_io.py ---
430
- return self.main_window_project_io.load_raw_data(file_path=None)
433
+ return self.main_window_project_io.load_raw_data(file_path)
431
434
 
432
435
  def save_as_json(self):
433
436
  # --- MOVED TO main_window_project_io.py ---
@@ -439,11 +442,11 @@ class MainWindow(QMainWindow):
439
442
 
440
443
  def load_json_data(self, file_path=None):
441
444
  # --- MOVED TO main_window_project_io.py ---
442
- return self.main_window_project_io.load_json_data(file_path=None)
445
+ return self.main_window_project_io.load_json_data(file_path)
443
446
 
444
447
  def open_project_file(self, file_path=None):
445
448
  # --- MOVED TO main_window_project_io.py ---
446
- return self.main_window_project_io.open_project_file(file_path=None)
449
+ return self.main_window_project_io.open_project_file(file_path)
447
450
 
448
451
  def load_from_json_data(self, json_data):
449
452
  # --- MOVED TO main_window_app_state.py ---
@@ -611,7 +614,7 @@ class MainWindow(QMainWindow):
611
614
 
612
615
  def load_mol_file_for_3d_viewing(self, file_path=None):
613
616
  # --- MOVED TO main_window_view_loaders.py ---
614
- return self.main_window_view_loaders.load_mol_file_for_3d_viewing(file_path=None)
617
+ return self.main_window_view_loaders.load_mol_file_for_3d_viewing(file_path)
615
618
 
616
619
  def load_command_line_file(self, file_path):
617
620
  # --- MOVED TO main_window_main_init.py ---
@@ -80,9 +80,13 @@ except Exception:
80
80
  class MainWindowAppState(object):
81
81
  """ main_window.py から分離された機能クラス """
82
82
 
83
- def __init__(self, main_window):
84
- """ クラスの初期化 """
85
- self.mw = main_window
83
+ def __init__(self):
84
+ """
85
+ クラスの初期化
86
+ BoundFeature経由で呼ばれるため、'self' には MainWindow インスタンスが渡されます。
87
+ """
88
+ self.DEBUG_UNDO = False
89
+
86
90
 
87
91
 
88
92
  def get_current_state(self):
@@ -234,6 +238,9 @@ class MainWindowAppState(object):
234
238
 
235
239
 
236
240
  def push_undo_state(self):
241
+ if self._is_restoring_state:
242
+ return
243
+
237
244
  current_state_for_comparison = {
238
245
  'atoms': {k: (v['symbol'], v['item'].pos().x(), v['item'].pos().y(), v.get('charge', 0), v.get('radical', 0)) for k, v in self.data.atoms.items()},
239
246
  'bonds': {k: (v['order'], v.get('stereo', 0)) for k, v in self.data.bonds.items()},
@@ -254,8 +261,15 @@ class MainWindowAppState(object):
254
261
  }
255
262
 
256
263
  if not last_state_for_comparison or current_state_for_comparison != last_state_for_comparison:
257
- state = self.get_current_state()
264
+ # Deepcopy state to ensure saved states are immutable and not affected
265
+ # by later modifications to objects referenced from the state.
266
+ state = copy.deepcopy(self.get_current_state())
258
267
  self.undo_stack.append(state)
268
+ if getattr(self, 'DEBUG_UNDO', False):
269
+ try:
270
+ print(f"DEBUG_UNDO: push_undo_state -> new stack size: {len(self.undo_stack)}")
271
+ except Exception:
272
+ pass
259
273
  self.redo_stack.clear()
260
274
  # 初期化完了後のみ変更があったことを記録
261
275
  if self.initialization_complete:
@@ -320,6 +334,11 @@ class MainWindowAppState(object):
320
334
  self.undo_stack.clear()
321
335
  self.redo_stack.clear()
322
336
  self.push_undo_state()
337
+ if getattr(self, 'DEBUG_UNDO', False):
338
+ try:
339
+ print(f"DEBUG_UNDO: reset_undo_stack -> undo={len(self.undo_stack)} redo={len(self.redo_stack)}")
340
+ except Exception:
341
+ pass
323
342
 
324
343
 
325
344
 
@@ -327,7 +346,12 @@ class MainWindowAppState(object):
327
346
  if len(self.undo_stack) > 1:
328
347
  self.redo_stack.append(self.undo_stack.pop())
329
348
  state = self.undo_stack[-1]
330
- self.set_state_from_data(state)
349
+ self._is_restoring_state = True
350
+ try:
351
+ self.set_state_from_data(state)
352
+ finally:
353
+ self._is_restoring_state = False
354
+
331
355
 
332
356
  # Undo後に3D構造の状態に基づいてメニューを再評価
333
357
  if self.current_mol and self.current_mol.GetNumAtoms() > 0:
@@ -337,6 +361,11 @@ class MainWindowAppState(object):
337
361
  # 3D構造がない場合は3D編集機能を無効化
338
362
  self._enable_3d_edit_actions(False)
339
363
 
364
+ if getattr(self, 'DEBUG_UNDO', False):
365
+ try:
366
+ print(f"DEBUG_UNDO: undo -> undo_stack size: {len(self.undo_stack)}, redo_stack size: {len(self.redo_stack)}")
367
+ except Exception:
368
+ pass
340
369
  self.update_undo_redo_actions()
341
370
  self.update_realtime_info()
342
371
  self.view_2d.setFocus()
@@ -347,7 +376,11 @@ class MainWindowAppState(object):
347
376
  if self.redo_stack:
348
377
  state = self.redo_stack.pop()
349
378
  self.undo_stack.append(state)
350
- self.set_state_from_data(state)
379
+ self._is_restoring_state = True
380
+ try:
381
+ self.set_state_from_data(state)
382
+ finally:
383
+ self._is_restoring_state = False
351
384
 
352
385
  # Redo後に3D構造の状態に基づいてメニューを再評価
353
386
  if self.current_mol and self.current_mol.GetNumAtoms() > 0:
@@ -357,6 +390,11 @@ class MainWindowAppState(object):
357
390
  # 3D構造がない場合は3D編集機能を無効化
358
391
  self._enable_3d_edit_actions(False)
359
392
 
393
+ if getattr(self, 'DEBUG_UNDO', False):
394
+ try:
395
+ print(f"DEBUG_UNDO: redo -> undo_stack size: {len(self.undo_stack)}, redo_stack size: {len(self.redo_stack)}")
396
+ except Exception:
397
+ pass
360
398
  self.update_undo_redo_actions()
361
399
  self.update_realtime_info()
362
400
  self.view_2d.setFocus()
@@ -36,6 +36,65 @@ from PyQt6.QtGui import (
36
36
  from PyQt6.QtCore import (
37
37
  Qt, QPointF, QRectF, QLineF, QUrl, QTimer
38
38
  )
39
+ import platform
40
+ import subprocess
41
+ try:
42
+ import winreg
43
+ except Exception:
44
+ winreg = None
45
+
46
+
47
+ def detect_system_dark_mode():
48
+ """Return True if the OS prefers dark app theme, False if light, or None if unknown.
49
+
50
+ This is a best-effort, cross-platform check supporting Windows (registry),
51
+ macOS (defaults read), and GNOME/GTK-based Linux (gsettings). Return
52
+ None if no reliable information is available.
53
+ """
54
+ try:
55
+ # Windows: read registry AppsUseLightTheme (0 = dark, 1 = light)
56
+ if platform.system() == 'Windows' and winreg is not None:
57
+ try:
58
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
59
+ r'Software\Microsoft\Windows\CurrentVersion\Themes\Personalize') as k:
60
+ val, _ = winreg.QueryValueEx(k, 'AppsUseLightTheme')
61
+ return False if int(val) == 0 else True
62
+ except Exception:
63
+ pass
64
+
65
+ # macOS: 'defaults read -g AppleInterfaceStyle' returns 'Dark' in dark mode
66
+ if platform.system() == 'Darwin':
67
+ try:
68
+ p = subprocess.run(['defaults', 'read', '-g', 'AppleInterfaceStyle'], capture_output=True, text=True)
69
+ if p.returncode == 0 and p.stdout.strip().lower() == 'dark':
70
+ return True
71
+ # Key absence implies light mode
72
+ return False
73
+ except Exception:
74
+ pass
75
+
76
+ # Linux / GNOME: try color-scheme gsetting; fallback to gtk-theme detection
77
+ if platform.system() == 'Linux':
78
+ try:
79
+ p = subprocess.run(['gsettings', 'get', 'org.gnome.desktop.interface', 'color-scheme'], capture_output=True, text=True)
80
+ if p.returncode == 0:
81
+ out = p.stdout.strip().strip("'\n ")
82
+ if 'dark' in out.lower():
83
+ return True
84
+ if 'light' in out.lower():
85
+ return False
86
+ except Exception:
87
+ pass
88
+
89
+ try:
90
+ p = subprocess.run(['gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-theme'], capture_output=True, text=True)
91
+ if p.returncode == 0 and '-dark' in p.stdout.lower():
92
+ return True
93
+ except Exception:
94
+ pass
95
+ except Exception:
96
+ pass
97
+ return None
39
98
 
40
99
 
41
100
  # Use centralized Open Babel availability from package-level __init__
@@ -406,10 +465,19 @@ class MainWindowMainInit(object):
406
465
  toolbar.addSeparator()
407
466
 
408
467
  # --- アイコン前景色を決めるヘルパー(ダーク/ライトモード対応) ---
409
- def _icon_foreground_color():
410
- """Return a QColor for icon foreground (black on light backgrounds, white on dark backgrounds).
468
+ # Use module-level detector `detect_system_dark_mode()` so tests and other
469
+ # modules can reuse the logic.
470
+
411
471
 
412
- Priority: explicit setting 'icon_foreground' in settings -> infer from configured background color -> infer from application palette.
472
+ def _icon_foreground_color():
473
+ """Return a QColor for icon foreground.
474
+
475
+ NOTE: this application inverts the usual foreground mapping so icons
476
+ will be the *opposite* color to the background (i.e., black on dark
477
+ backgrounds, white on light backgrounds). This intentionally reverses
478
+ the previous behavior so button icons don't blend into the 3D-view
479
+ background. Priority: explicit setting in 'icon_foreground' -> OS
480
+ theme preference -> configured 3D background -> application palette.
413
481
  """
414
482
  try:
415
483
  fg_hex = self.settings.get('icon_foreground')
@@ -420,13 +488,25 @@ class MainWindowMainInit(object):
420
488
  except Exception:
421
489
  pass
422
490
 
491
+ # 1) Prefer the system/OS dark-mode preference if available.
492
+ try:
493
+ os_pref = detect_system_dark_mode()
494
+ # Invert the color so in dark-pref OS we return black, in light we return white
495
+ if os_pref is not None:
496
+ return QColor('#000000') if os_pref else QColor('#FFFFFF')
497
+ except Exception:
498
+ pass
499
+
423
500
  try:
501
+ # Keep background_color as a fallback: if system preference isn't
502
+ # available we'll use the configured 3D view background from settings.
424
503
  bg_hex = self.settings.get('background_color')
425
504
  if bg_hex:
426
505
  bg = QColor(bg_hex)
427
506
  if bg.isValid():
428
507
  lum = 0.2126 * bg.redF() + 0.7152 * bg.greenF() + 0.0722 * bg.blueF()
429
- return QColor('#FFFFFF') if lum < 0.5 else QColor('#000000')
508
+ # Inverted: return black on dark (lum<0.5), white on light
509
+ return QColor('#000000') if lum < 0.5 else QColor('#FFFFFF')
430
510
  except Exception:
431
511
  pass
432
512
 
@@ -435,7 +515,8 @@ class MainWindowMainInit(object):
435
515
  # palette.window() returns a QBrush; call color()
436
516
  window_bg = pal.window().color()
437
517
  lum = 0.2126 * window_bg.redF() + 0.7152 * window_bg.greenF() + 0.0722 * window_bg.blueF()
438
- return QColor('#FFFFFF') if lum < 0.5 else QColor('#000000')
518
+ # Inverted mapping for palette fallback
519
+ return QColor('#000000') if lum < 0.5 else QColor('#FFFFFF')
439
520
  except Exception:
440
521
  return QColor('#000000')
441
522
 
@@ -164,6 +164,11 @@ class MainWindowProjectIo(object):
164
164
  # Replace current file with the newly saved file so subsequent saves go to this path
165
165
  self.current_file_path = file_path
166
166
  self.update_window_title()
167
+ # Mark this state as the last saved state for undo tracking
168
+ try:
169
+ self._saved_state = copy.deepcopy(self.get_current_state())
170
+ except Exception:
171
+ pass
167
172
 
168
173
  self.statusBar().showMessage(f"Project saved to {file_path}")
169
174
 
@@ -217,6 +222,10 @@ class MainWindowProjectIo(object):
217
222
  # Update current file to the newly saved raw file
218
223
  self.current_file_path = file_path
219
224
  self.update_window_title()
225
+ try:
226
+ self._saved_state = copy.deepcopy(self.get_current_state())
227
+ except Exception:
228
+ pass
220
229
 
221
230
  self.statusBar().showMessage(f"Project saved to {file_path}")
222
231
 
@@ -249,8 +258,12 @@ class MainWindowProjectIo(object):
249
258
  self.has_unsaved_changes = False
250
259
  self.current_file_path = file_path
251
260
  self.update_window_title()
252
-
253
- self.statusBar().showMessage(f"Project loaded from {file_path}")
261
+ try:
262
+ self._saved_state= copy.deepcopy(self.et_current_state())
263
+ except Exception:
264
+ pass
265
+
266
+ self.statusBar.showMessage(f"Project loaded from {file_path}")
254
267
 
255
268
  QTimer.singleShot(0, self.fit_to_view)
256
269
 
File without changes
File without changes
File without changes