33import sys
44
55import psutil
6- from PySide6 import QtCore , QtWidgets
6+ from PySide6 import QtCore , QtGui , QtWidgets
77
88from .ui import applist
99from ..applications import LOCAL_PATH , AppList
1010from ..constants import TRACKING_IGNORE , TRACKING_DISABLE
1111
1212
13+ PROCESS_LOAD_TEXT = 'Loading processes list...'
14+
15+
16+ class ProcessWorker (QtCore .QThread ):
17+ """Load a list of running processes in a thread."""
18+
19+ process_found = QtCore .Signal (str )
20+ loading_finished = QtCore .Signal ()
21+
22+ def __init__ (self , parent : QtWidgets .QWidget | None = None ) -> None :
23+ super ().__init__ (parent )
24+ self ._running = True
25+
26+ def run (self ) -> None :
27+ seen : set [str ] = set ()
28+ for proc in sorted (psutil .process_iter (attrs = ['exe' , 'create_time' ]),
29+ key = lambda proc : proc .info ['create_time' ] or 0.0 , reverse = True ):
30+ if not self ._running :
31+ return
32+
33+ path = proc .info ['exe' ]
34+ if path is None :
35+ continue
36+
37+ exe = os .path .basename (path )
38+ if exe in seen :
39+ continue
40+ seen .add (exe )
41+
42+ self .process_found .emit (exe )
43+
44+ self .loading_finished .emit ()
45+
46+ def cancel (self ) -> None :
47+ """Called by the main thread to tell this thread to stop."""
48+ self ._running = False
49+
50+
1351class AppListWindow (QtWidgets .QDialog ):
1452 """Interface to the AppList.txt file."""
1553
@@ -22,9 +60,12 @@ def __init__(self, parent: QtWidgets.QWidget) -> None:
2260 self .ui .setupUi (self )
2361 self .ui .advanced .setChecked (False )
2462
63+ self .worker = ProcessWorker (self )
64+
2565 self .ui .save .clicked .connect (self .save )
2666 self .ui .executable .currentTextChanged .connect (self .update_profile_suggestion )
2767 self .ui .executable .currentTextChanged .connect (self .update_matching_apps )
68+ self .ui .executable .currentTextChanged .connect (self .update_button_state )
2869 self .ui .executable .currentIndexChanged .connect (self .executable_changed )
2970 self .ui .window_title .textChanged .connect (self .update_profile_suggestion )
3071 self .ui .window_title_enabled .toggled .connect (self .update_profile_suggestion )
@@ -36,21 +77,30 @@ def __init__(self, parent: QtWidgets.QWidget) -> None:
3677
3778 self ._populate_process_list ()
3879
80+ def closeEvent (self , event : QtGui .QCloseEvent ) -> None :
81+ """Safely shut down background threads before closing."""
82+ if self .worker .isRunning ():
83+ self .worker .cancel ()
84+ self .worker .wait ()
85+
86+ super ().closeEvent (event )
87+
3988 def _populate_process_list (self ) -> None :
4089 """Load in the list of all processes."""
4190 self .ui .executable .clear ()
91+ self .ui .executable .addItem (PROCESS_LOAD_TEXT )
4292
43- seen = set ( )
44- for proc in sorted ( psutil . process_iter ( attrs = [ 'exe' , 'create_time' ]),
45- key = lambda proc : proc . info [ 'create_time' ], reverse = True ):
46- path = proc . info [ 'exe' ]
47- if path is None :
48- continue
49- exe = os . path . basename ( path )
50- if exe in seen :
51- continue
52- seen . add ( exe )
53- self .ui .executable .addItem ( exe )
93+ self . worker . process_found . connect ( self . process_found )
94+ self . worker . loading_finished . connect ( self . processes_loaded )
95+ self . worker . start ()
96+
97+ @ QtCore . Slot ( str )
98+ def process_found ( self , exe : str ) -> None :
99+ self . ui . executable . addItem ( exe )
100+
101+ @ QtCore . Slot ()
102+ def processes_loaded ( self ) -> None :
103+ self .ui .executable .removeItem ( 0 )
54104
55105 @QtCore .Slot (int )
56106 def executable_changed (self , index : int ) -> None :
@@ -65,6 +115,8 @@ def update_profile_suggestion(self) -> None:
65115 """Update the default profile name."""
66116 if self .ui .window_title_enabled .isChecked () and self .ui .window_title .text ():
67117 self .ui .profile_name .setPlaceholderText (self .ui .window_title .text ())
118+ elif self .ui .executable .currentText () == PROCESS_LOAD_TEXT :
119+ self .ui .profile_name .setPlaceholderText ('' )
68120 else :
69121 self .ui .profile_name .setPlaceholderText (os .path .splitext (self .ui .executable .currentText ())[0 ])
70122
@@ -95,6 +147,13 @@ def update_matching_apps(self, force: bool = False) -> None:
95147 self .ui .rules .addItem (item )
96148 self .ui .rules .sortItems ()
97149
150+ @QtCore .Slot ()
151+ def update_button_state (self ) -> None :
152+ """Update the state of the create and remove buttons while loading."""
153+ loading = self .ui .executable .currentText () == PROCESS_LOAD_TEXT
154+ self .ui .create .setEnabled (not loading )
155+ self .ui .remove .setEnabled (not loading )
156+
98157 @QtCore .Slot ()
99158 def create_new_rule (self ) -> None :
100159 """Create or update a rule with the current data."""
0 commit comments