1- from PyQt5 .QtCore import Qt
1+ from PyQt5 .QtCore import QObject , Qt , QThread , pyqtSignal , pyqtSlot
22from PyQt5 .QtWidgets import (
33 QMenu ,
4+ QMessageBox ,
5+ QProgressDialog ,
46 QPushButton ,
57 QSplitter ,
68 QTreeWidget ,
911 QWidget ,
1012)
1113
12- from .feature_details_panel import (
13- FaultFeatureDetailsPanel ,
14- FoldedFeatureDetailsPanel ,
15- FoliationFeatureDetailsPanel ,
16- StructuralFrameFeatureDetailsPanel ,
17- )
1814from LoopStructural .modelling .features import FeatureType
1915
2016# Import the AddFaultDialog
2117from .add_fault_dialog import AddFaultDialog
2218from .add_foliation_dialog import AddFoliationDialog
2319from .add_unconformity_dialog import AddUnconformityDialog
20+ from .feature_details_panel import (
21+ FaultFeatureDetailsPanel ,
22+ FoldedFeatureDetailsPanel ,
23+ FoliationFeatureDetailsPanel ,
24+ StructuralFrameFeatureDetailsPanel ,
25+ )
2426
2527
2628class GeologicalModelTab (QWidget ):
2729 def __init__ (self , parent = None , * , model_manager = None , data_manager = None ):
2830 super ().__init__ (parent )
2931 self .model_manager = model_manager
3032 self .data_manager = data_manager
31- self .model_manager .observers .append (self .update_feature_list )
33+ # Register update observer using Observable API if available
34+ if self .model_manager is not None :
35+ try :
36+ # listen for model-level updates
37+ self ._disp_model = self .model_manager .attach (
38+ self .update_feature_list , 'model_updated'
39+ )
40+ # show progress when model updates start/finish (covers indirect calls)
41+ self ._disp_update_start = self .model_manager .attach (
42+ lambda _obs , _ev , * a , ** k : self ._on_model_update_started (),
43+ 'model_update_started' ,
44+ )
45+ self ._disp_update_finish = self .model_manager .attach (
46+ lambda _obs , _ev , * a , ** k : self ._on_model_update_finished (),
47+ 'model_update_finished' ,
48+ )
49+ except Exception :
50+ # fallback to legacy list
51+ try :
52+ self .model_manager .observers .append (self .update_feature_list )
53+ except Exception :
54+ pass
3255 # Main layout
3356 mainLayout = QVBoxLayout (self )
3457
@@ -75,6 +98,10 @@ def __init__(self, parent=None, *, model_manager=None, data_manager=None):
7598 # Connect feature selection to update details panel
7699 self .featureList .itemClicked .connect (self .on_feature_selected )
77100
101+ # thread handle to keep worker alive while running
102+ self ._model_update_thread = None
103+ self ._model_update_worker = None
104+
78105 def show_add_feature_menu (self , * args ):
79106 menu = QMenu (self )
80107 add_fault = menu .addAction ("Add Fault" )
@@ -89,6 +116,7 @@ def show_add_feature_menu(self, *args):
89116 self .open_add_foliation_dialog ()
90117 elif action == add_unconformity :
91118 self .open_add_unconformity_dialog ()
119+
92120 def open_add_fault_dialog (self ):
93121 dialog = AddFaultDialog (self )
94122 if dialog .exec_ () == dialog .Accepted :
@@ -102,16 +130,95 @@ def open_add_foliation_dialog(self):
102130 )
103131 if dialog .exec_ () == dialog .Accepted :
104132 pass
133+
105134 def open_add_unconformity_dialog (self ):
106135 dialog = AddUnconformityDialog (
107136 self , data_manager = self .data_manager , model_manager = self .model_manager
108137 )
109138 if dialog .exec_ () == dialog .Accepted :
110139 pass
140+
111141 def initialize_model (self ):
112- self .model_manager .update_model ()
142+ # Run update_model in a background thread to avoid blocking the UI.
143+ if not self .model_manager :
144+ return
145+
146+ # create progress dialog (indeterminate)
147+ progress = QProgressDialog ("Updating geological model..." , "Cancel" , 0 , 0 , self )
148+ progress .setWindowModality (Qt .ApplicationModal )
149+ progress .setWindowTitle ("Updating Model" )
150+ progress .setCancelButton (None )
151+ progress .setMinimumDuration (0 )
152+ progress .show ()
153+
154+ # worker and thread
155+ thread = QThread (self )
156+ worker = _ModelUpdateWorker (self .model_manager )
157+ worker .moveToThread (thread )
158+
159+ # When thread starts run worker.run
160+ thread .started .connect (worker .run )
161+
162+ # on worker finished, notify observers on main thread and cleanup
163+ def _on_finished ():
164+ try :
165+ # notify observers now on main thread
166+ try :
167+ self .model_manager .notify ('model_updated' )
168+ except Exception :
169+ for obs in getattr (self .model_manager , 'observers' , []):
170+ try :
171+ obs ()
172+ except Exception :
173+ pass
174+ finally :
175+ try :
176+ progress .close ()
177+ except Exception :
178+ pass
179+ # cleanup worker/thread
180+ try :
181+ worker .deleteLater ()
182+ except Exception :
183+ pass
184+ try :
185+ thread .quit ()
186+ thread .wait (2000 )
187+ except Exception :
188+ pass
189+
190+ def _on_error (tb ):
191+ try :
192+ progress .close ()
193+ except Exception :
194+ pass
195+ try :
196+ QMessageBox .critical (
197+ self ,
198+ "Model update failed" ,
199+ f"An error occurred while updating the model:\n { tb } " ,
200+ )
201+ except Exception :
202+ pass
203+ # ensure thread cleanup
204+ try :
205+ worker .deleteLater ()
206+ except Exception :
207+ pass
208+ try :
209+ thread .quit ()
210+ thread .wait (2000 )
211+ except Exception :
212+ pass
213+
214+ worker .finished .connect (_on_finished )
215+ worker .error .connect (_on_error )
216+ thread .finished .connect (thread .deleteLater )
217+ self ._model_update_thread = thread
218+ self ._model_update_worker = worker
219+ thread .start ()
113220
114- def update_feature_list (self ):
221+ def update_feature_list (self , * args , ** kwargs ):
115222 self .featureList .clear () # Clear the feature list before populating it
116223 for feature in self .model_manager .features ():
117224 if feature .name .startswith ("__" ):
@@ -153,6 +260,43 @@ def on_feature_selected(self, item):
153260 splitter .widget (1 ).deleteLater () # Remove the existing widget
154261 splitter .addWidget (self .featureDetailsPanel ) # Add the new widget
155262
263+ def _on_model_update_started (self ):
264+ """Show a non-blocking indeterminate progress dialog for model updates.
265+
266+ This method is invoked via the Observable notifications and ensures the
267+ user sees that a background or foreground update is in progress.
268+ """
269+ print ("Model update started - showing progress dialog" )
270+ try :
271+ if getattr (self , '_progress_dialog' , None ) is None :
272+ self ._progress_dialog = QProgressDialog (
273+ "Updating geological model..." , None , 0 , 0 , self
274+ )
275+ self ._progress_dialog .setWindowTitle ("Updating Model" )
276+ self ._progress_dialog .setWindowModality (Qt .ApplicationModal )
277+ self ._progress_dialog .setCancelButton (None )
278+ self ._progress_dialog .setMinimumDuration (0 )
279+ self ._progress_dialog .show ()
280+ except Exception :
281+ pass
282+
283+ def _on_model_update_finished (self ):
284+ """Close the progress dialog shown for model updates."""
285+ print ("Model update finished - closing progress dialog" )
286+ try :
287+ if getattr (self , '_progress_dialog' , None ) is not None :
288+ try :
289+ self ._progress_dialog .close ()
290+ except Exception :
291+ pass
292+ try :
293+ self ._progress_dialog .deleteLater ()
294+ except Exception :
295+ pass
296+ self ._progress_dialog = None
297+ except Exception :
298+ pass
299+
156300 def show_feature_context_menu (self , pos ):
157301 # Show context menu only for items
158302 item = self .featureList .itemAt (pos )
@@ -197,10 +341,50 @@ def delete_feature(self, item):
197341
198342 # Notify observers to refresh UI
199343 try :
200- for obs in getattr (self .model_manager , 'observers' , []):
201- try :
202- obs ()
203- except Exception :
204- pass
344+ # Prefer notify API
345+ try :
346+ self .model_manager .notify ('model_updated' )
347+ except Exception :
348+ # fallback to legacy observers list
349+ for obs in getattr (self .model_manager , 'observers' , []):
350+ try :
351+ obs ()
352+ except Exception :
353+ pass
205354 except Exception :
206355 pass
356+
357+
358+ class _ModelUpdateWorker (QObject ):
359+ """Worker that runs model_manager.update_model in a background thread.
360+
361+ Emits finished when done and error with a string if an exception occurs.
362+ """
363+
364+ finished = pyqtSignal ()
365+ error = pyqtSignal (str )
366+
367+ def __init__ (self , model_manager ):
368+ super ().__init__ ()
369+ self .model_manager = model_manager
370+
371+ @pyqtSlot ()
372+ def run (self ):
373+ try :
374+ # perform the expensive update
375+ # run update without notifying observers from the background thread
376+ try :
377+ self .model_manager .update_model (notify_observers = False )
378+ except TypeError :
379+ # fallback if update_model signature not available
380+ self .model_manager .update_model ()
381+ except Exception as e :
382+ try :
383+ import traceback
384+
385+ tb = traceback .format_exc ()
386+ except Exception :
387+ tb = str (e )
388+ self .error .emit (tb )
389+ finally :
390+ self .finished .emit ()
0 commit comments