99from game_manager import GameManager
1010from config_manager import ConfigManager
1111
12+ class CancelledError (Exception ):
13+ pass
14+
1215class InstallThread (QThread ):
1316 update_progress = Signal (str , int , int )
1417 finished = Signal (bool , str )
18+
1519 def __init__ (self , manager , version , proton_type , custom_path = None , custom_type = None ):
1620 super ().__init__ ()
1721 self .manager = manager
1822 self .version = version
1923 self .proton_type = proton_type
2024 self .custom_path = custom_path
2125 self .custom_type = custom_type
26+ self ._is_canceled = False
27+
28+ def cancel (self ):
29+ self ._is_canceled = True
30+
2231 def run (self ):
2332 def progress_callback (stage , value , total ):
2433 self .update_progress .emit (stage , value , total )
34+ if self ._is_canceled :
35+ raise CancelledError ("Installation cancelled" )
36+
2537 try :
2638 if self .proton_type in ['GE' , 'Official' , 'Experimental' ]:
2739 success , message = self .manager .install_proton (self .version , self .proton_type if self .proton_type != 'Experimental' else 'Official' , progress_callback )
@@ -31,14 +43,18 @@ def progress_callback(stage, value, total):
3143 else :
3244 success , message = self .manager .install_custom_folder (self .custom_path , self .version )
3345 self .finished .emit (success , message )
46+ except CancelledError as e :
47+ self .finished .emit (False , str (e ))
3448 except Exception as e :
3549 self .finished .emit (False , str (e ))
3650
3751class LoadProtonsThread (QThread ):
3852 protons_loaded = Signal (list )
53+
3954 def __init__ (self , manager ):
4055 super ().__init__ ()
4156 self .manager = manager
57+
4258 def run (self ):
4359 try :
4460 protons = self .manager .get_installed_protons ()
@@ -346,6 +362,13 @@ def add_game(self):
346362 app_id_widget .setVisible (runner_combo .currentText () == 'Steam' )
347363 dlg_layout .addWidget (app_id_widget , row , 0 , 1 , 3 )
348364 row += 1
365+ fps_label = QLabel ('FPS Limit:' )
366+ fps_edit = QLineEdit ()
367+ fps_edit .setPlaceholderText ("Enter FPS limit (e.g., 60)" )
368+ dlg_layout .addWidget (fps_label , row , 0 )
369+ dlg_layout .addWidget (fps_edit , row , 1 , 1 , 2 )
370+ row += 1
371+
349372 def update_visibility (text ):
350373 proton_widget .setVisible (text == 'Proton' )
351374 prefix_widget .setVisible (text in ['Wine' , 'Proton' ])
@@ -355,6 +378,7 @@ def update_visibility(text):
355378 esync_check .setVisible (text in ['Wine' , 'Proton' ])
356379 fsync_check .setVisible (text in ['Wine' , 'Proton' ])
357380 dxvk_async_check .setVisible (text in ['Wine' , 'Proton' ])
381+
358382 runner_combo .currentTextChanged .connect (update_visibility )
359383 button_layout = QHBoxLayout ()
360384 ok_btn = QPushButton (QIcon .fromTheme ("dialog-ok" ), 'Add Game' )
@@ -378,6 +402,7 @@ def update_visibility(text):
378402 enable_dxvk_async = dxvk_async_check .isChecked ()
379403 app_id = app_id_edit .text () if runner == 'Steam' else ''
380404 prefix = prefix_edit .text () if runner in ['Wine' , 'Proton' ] else ''
405+ fps_limit = fps_edit .text ()
381406 if runner == 'Proton' :
382407 runner = proton_combo .currentText ()
383408 if not name or (runner != 'Steam' and not exe ) or (runner == 'Steam' and not app_id ):
@@ -399,7 +424,8 @@ def update_visibility(text):
399424 'enable_esync' : enable_esync ,
400425 'enable_fsync' : enable_fsync ,
401426 'enable_dxvk_async' : enable_dxvk_async ,
402- 'app_id' : app_id
427+ 'app_id' : app_id ,
428+ 'fps_limit' : fps_limit
403429 }
404430 self .game_manager .add_game (game )
405431 self .load_games ()
@@ -414,6 +440,12 @@ def launch_game(self):
414440 game = next ((g for g in self .games if g ['name' ] == name ), None )
415441 if game :
416442 try :
443+ # Dodaj fps limit do launch_options jeśli gamescope jest używany
444+ launch_options = game .get ('launch_options' , '' ).split ()
445+ fps_limit = game .get ('fps_limit' , '' )
446+ if fps_limit and '--gamescope' in launch_options :
447+ launch_options .append (f'--framerate-limit={ fps_limit } ' )
448+ game ['launch_options' ] = ' ' .join (launch_options )
417449 self .game_manager .launch_game (game , '--gamescope' in game .get ('launch_options' , '' ))
418450 except Exception as e :
419451 logging .error (f"Error launching { name } : { e } " )
@@ -436,23 +468,107 @@ def configure_game(self):
436468 return
437469 name = self .games_list .item (selected , 0 ).text ()
438470 game = next ((g for g in self .games if g ['name' ] == name ), None )
439- if not game or game ['runner' ] in ['Native' , 'Flatpak' , 'Steam' ]:
440- QMessageBox .information (self , 'Info' , 'No configuration needed for this runner' )
471+ if not game :
441472 return
442- if not shutil .which ('winetricks' ):
443- QMessageBox .warning (self , 'Error' , 'Winetricks not installed. Please install it.' )
444- return
445- tricks_dialog = QDialog (self )
446- tricks_dialog .setWindowTitle ("Install Winetricks Libraries" )
447- tricks_layout = QVBoxLayout ()
473+ config_dialog = QDialog (self )
474+ config_dialog .setWindowTitle ("Configure Game" )
475+ dlg_layout = QGridLayout ()
476+ row = 0
477+ # Podstawowe pola
478+ name_label = QLabel ('Game Name:' )
479+ name_edit = QLineEdit (game ['name' ])
480+ dlg_layout .addWidget (name_label , row , 0 )
481+ dlg_layout .addWidget (name_edit , row , 1 , 1 , 2 )
482+ row += 1
483+ exe_label = QLabel ('Executable / App ID:' )
484+ exe_edit = QLineEdit (game ['exe' ])
485+ browse_btn = QPushButton (QIcon .fromTheme ("folder" ), 'Browse' )
486+ browse_btn .clicked .connect (lambda : exe_edit .setText (QFileDialog .getOpenFileName (self , 'Select Executable' , '/' , 'Executables (*.exe *.bat);;All Files (*)' )[0 ]))
487+ dlg_layout .addWidget (exe_label , row , 0 )
488+ dlg_layout .addWidget (exe_edit , row , 1 )
489+ dlg_layout .addWidget (browse_btn , row , 2 )
490+ row += 1
491+ runner_label = QLabel ('Runner:' )
492+ runner_combo = QComboBox ()
493+ runner_combo .addItems (['Native' , 'Wine' , 'Proton' , 'Flatpak' , 'Steam' ])
494+ runner_combo .setCurrentText ('Proton' if 'Proton' in game ['runner' ] else game ['runner' ])
495+ dlg_layout .addWidget (runner_label , row , 0 )
496+ dlg_layout .addWidget (runner_combo , row , 1 , 1 , 2 )
497+ row += 1
498+ proton_label = QLabel ('Proton Version:' )
499+ proton_combo = QComboBox ()
500+ proton_combo .addItems ([p ['version' ] for p in self .proton_manager .get_installed_protons ()])
501+ proton_combo .setCurrentText (game ['runner' ] if 'Proton' in game ['runner' ] else '' )
502+ proton_widget = QWidget ()
503+ proton_layout = QHBoxLayout ()
504+ proton_layout .addWidget (proton_label )
505+ proton_layout .addWidget (proton_combo )
506+ proton_widget .setLayout (proton_layout )
507+ proton_widget .setVisible (runner_combo .currentText () == 'Proton' )
508+ dlg_layout .addWidget (proton_widget , row , 0 , 1 , 3 )
509+ row += 1
510+ prefix_label = QLabel ('Wine/Proton Prefix:' )
511+ prefix_edit = QLineEdit (game ['prefix' ])
512+ prefix_browse_btn = QPushButton (QIcon .fromTheme ("folder" ), 'Browse' )
513+ prefix_browse_btn .clicked .connect (lambda : prefix_edit .setText (QFileDialog .getExistingDirectory (self , 'Select Prefix Directory' )))
514+ prefix_widget = QWidget ()
515+ prefix_layout = QHBoxLayout ()
516+ prefix_layout .addWidget (prefix_label )
517+ prefix_layout .addWidget (prefix_edit )
518+ prefix_layout .addWidget (prefix_browse_btn )
519+ prefix_widget .setLayout (prefix_layout )
520+ prefix_widget .setVisible (runner_combo .currentText () in ['Wine' , 'Proton' ])
521+ dlg_layout .addWidget (prefix_widget , row , 0 , 1 , 3 )
522+ row += 1
523+ launch_label = QLabel ('Launch Options:' )
524+ launch_edit = QLineEdit (game .get ('launch_options' , '' ))
525+ dlg_layout .addWidget (launch_label , row , 0 )
526+ dlg_layout .addWidget (launch_edit , row , 1 , 1 , 2 )
527+ row += 1
528+ dxvk_check = QCheckBox ("Enable DXVK/VKD3D" )
529+ dxvk_check .setChecked (game .get ('enable_dxvk' , False ))
530+ dxvk_check .setVisible (runner_combo .currentText () in ['Wine' , 'Proton' ])
531+ dlg_layout .addWidget (dxvk_check , row , 0 )
532+ row += 1
533+ esync_check = QCheckBox ("Enable Esync (Override)" )
534+ esync_check .setChecked (game .get ('enable_esync' , False ))
535+ esync_check .setVisible (runner_combo .currentText () in ['Wine' , 'Proton' ])
536+ dlg_layout .addWidget (esync_check , row , 0 )
537+ row += 1
538+ fsync_check = QCheckBox ("Enable Fsync (Override)" )
539+ fsync_check .setChecked (game .get ('enable_fsync' , False ))
540+ fsync_check .setVisible (runner_combo .currentText () in ['Wine' , 'Proton' ])
541+ dlg_layout .addWidget (fsync_check , row , 0 )
542+ row += 1
543+ dxvk_async_check = QCheckBox ("Enable DXVK Async (Override)" )
544+ dxvk_async_check .setChecked (game .get ('enable_dxvk_async' , False ))
545+ dxvk_async_check .setVisible (runner_combo .currentText () in ['Wine' , 'Proton' ])
546+ dlg_layout .addWidget (dxvk_async_check , row , 0 )
547+ row += 1
548+ app_id_widget = QWidget ()
549+ app_id_layout = QHBoxLayout ()
550+ app_id_label = QLabel ('Steam App ID:' )
551+ app_id_edit = QLineEdit (game .get ('app_id' , '' ))
552+ app_id_layout .addWidget (app_id_label )
553+ app_id_layout .addWidget (app_id_edit )
554+ app_id_widget .setLayout (app_id_layout )
555+ app_id_widget .setVisible (runner_combo .currentText () == 'Steam' )
556+ dlg_layout .addWidget (app_id_widget , row , 0 , 1 , 3 )
557+ row += 1
558+ fps_label = QLabel ('FPS Limit:' )
559+ fps_edit = QLineEdit (game .get ('fps_limit' , '' ))
560+ dlg_layout .addWidget (fps_label , row , 0 )
561+ dlg_layout .addWidget (fps_edit , row , 1 , 1 , 2 )
562+ row += 1
563+ # Sekcja winetricks
448564 tricks_label = QLabel ("Select libraries to install:" )
449565 tricks_list = QComboBox ()
450566 popular_tricks = ['dotnet48' , 'vcrun2019' , 'dxvk' , 'vkd3d' , 'corefonts' , 'd3dcompiler_47' , 'physx' , 'msls31' ]
451567 tricks_list .addItems (popular_tricks )
452568 install_btn = QPushButton ('Install Selected' )
453569 def install_trick ():
454570 trick = tricks_list .currentText ()
455- prefix = game [ 'prefix' ]
571+ prefix = prefix_edit . text ()
456572 env = os .environ .copy ()
457573 env ['WINEPREFIX' ] = prefix
458574 try :
@@ -461,14 +577,80 @@ def install_trick():
461577 except Exception as e :
462578 QMessageBox .warning (self , 'Error' , str (e ))
463579 install_btn .clicked .connect (install_trick )
464- tricks_layout .addWidget (tricks_label )
465- tricks_layout .addWidget (tricks_list )
466- tricks_layout .addWidget (install_btn )
467- tricks_dialog .setLayout (tricks_layout )
468- tricks_dialog .exec ()
469- env = os .environ .copy ()
470- env ['WINEPREFIX' ] = game ['prefix' ]
471- subprocess .Popen (['winetricks' ], env = env )
580+ dlg_layout .addWidget (tricks_label , row , 0 )
581+ dlg_layout .addWidget (tricks_list , row , 1 )
582+ dlg_layout .addWidget (install_btn , row , 2 )
583+ row += 1
584+ winetricks_btn = QPushButton ('Open Winetricks' )
585+ def open_winetricks ():
586+ prefix = prefix_edit .text ()
587+ env = os .environ .copy ()
588+ env ['WINEPREFIX' ] = prefix
589+ subprocess .Popen (['winetricks' ], env = env )
590+ winetricks_btn .clicked .connect (open_winetricks )
591+ dlg_layout .addWidget (winetricks_btn , row , 0 , 1 , 3 )
592+ row += 1
593+
594+ def update_visibility (text ):
595+ proton_widget .setVisible (text == 'Proton' )
596+ prefix_widget .setVisible (text in ['Wine' , 'Proton' ])
597+ app_id_widget .setVisible (text == 'Steam' )
598+ browse_btn .setVisible (text != 'Steam' )
599+ dxvk_check .setVisible (text in ['Wine' , 'Proton' ])
600+ esync_check .setVisible (text in ['Wine' , 'Proton' ])
601+ fsync_check .setVisible (text in ['Wine' , 'Proton' ])
602+ dxvk_async_check .setVisible (text in ['Wine' , 'Proton' ])
603+
604+ runner_combo .currentTextChanged .connect (update_visibility )
605+ button_layout = QHBoxLayout ()
606+ ok_btn = QPushButton (QIcon .fromTheme ("dialog-ok" ), 'Save Changes' )
607+ cancel_btn = QPushButton (QIcon .fromTheme ("dialog-cancel" ), 'Cancel' )
608+ ok_btn .clicked .connect (config_dialog .accept )
609+ cancel_btn .clicked .connect (config_dialog .reject )
610+ button_layout .addStretch ()
611+ button_layout .addWidget (ok_btn )
612+ button_layout .addWidget (cancel_btn )
613+ dlg_layout .addLayout (button_layout , row , 0 , 1 , 3 )
614+ config_dialog .setLayout (dlg_layout )
615+ config_dialog .resize (600 , 600 )
616+ if config_dialog .exec () == QDialog .Accepted :
617+ new_name = name_edit .text ()
618+ exe = exe_edit .text ()
619+ runner = runner_combo .currentText ()
620+ launch_options = launch_edit .text ()
621+ enable_dxvk = dxvk_check .isChecked ()
622+ enable_esync = esync_check .isChecked ()
623+ enable_fsync = fsync_check .isChecked ()
624+ enable_dxvk_async = dxvk_async_check .isChecked ()
625+ app_id = app_id_edit .text () if runner == 'Steam' else ''
626+ prefix = prefix_edit .text () if runner in ['Wine' , 'Proton' ] else ''
627+ fps_limit = fps_edit .text ()
628+ if runner == 'Proton' :
629+ runner = proton_combo .currentText ()
630+ if not new_name or (runner != 'Steam' and not exe ) or (runner == 'Steam' and not app_id ):
631+ QMessageBox .warning (self , 'Error' , 'Name and Executable/App ID required' )
632+ return
633+ if runner in ['Wine' , 'Proton' ] and not prefix :
634+ prefix = os .path .join (self .config_manager .prefixes_dir , new_name .replace (' ' , '_' ))
635+ if prefix :
636+ os .makedirs (prefix , exist_ok = True )
637+ if os .name == 'posix' and ':' in exe :
638+ exe = exe .replace ('\\ ' , '/' ).replace ('C:' , '/drive_c' )
639+ # Aktualizuj game
640+ game ['name' ] = new_name
641+ game ['exe' ] = exe
642+ game ['runner' ] = runner
643+ game ['prefix' ] = prefix
644+ game ['launch_options' ] = launch_options
645+ game ['enable_dxvk' ] = enable_dxvk
646+ game ['enable_esync' ] = enable_esync
647+ game ['enable_fsync' ] = enable_fsync
648+ game ['enable_dxvk_async' ] = enable_dxvk_async
649+ game ['app_id' ] = app_id
650+ game ['fps_limit' ] = fps_limit
651+ self .config_manager .save_games (self .games )
652+ self .load_games ()
653+ QMessageBox .information (self , 'Success' , 'Game configured successfully!' )
472654
473655 def install_proton (self ):
474656 install_dialog = QDialog (self )
@@ -510,6 +692,7 @@ def install_proton(self):
510692 custom_widget .setLayout (custom_layout )
511693 custom_widget .setVisible (False )
512694 layout .addWidget (custom_widget )
695+
513696 def update_ui (text ):
514697 if text == 'Custom' :
515698 version_widget .setVisible (False )
@@ -525,8 +708,10 @@ def update_ui(text):
525708 available = self .proton_manager .get_available_official (stable = False )
526709 version_combo .clear ()
527710 version_combo .addItems (available or ["No versions available" ])
711+
528712 type_combo .currentTextChanged .connect (update_ui )
529713 update_ui (type_combo .currentText ())
714+
530715 def browse_custom ():
531716 if custom_type_combo .currentText () == 'Tar.gz File' :
532717 path = QFileDialog .getOpenFileName (self , 'Select Tar.gz' , '' , 'Tar.gz (*.tar.gz)' )[0 ]
@@ -536,6 +721,7 @@ def browse_custom():
536721 path_edit .setText (path )
537722 if not name_edit .text ():
538723 name_edit .setText (os .path .basename (path ).replace ('.tar.gz' , '' ))
724+
539725 browse_btn .clicked .connect (browse_custom )
540726 button_layout = QHBoxLayout ()
541727 ok_btn = QPushButton (QIcon .fromTheme ("dialog-ok" ), 'Install' )
@@ -552,7 +738,7 @@ def browse_custom():
552738 proton_type = type_combo .currentText ()
553739 progress = QProgressDialog (f"Installing { proton_type } Proton..." , "Cancel" , 0 , 100 , self )
554740 progress .setWindowModality (Qt .WindowModal )
555- progress .setAutoClose (True )
741+ progress .setAutoClose (False ) # Zmiana na False, aby ręcznie zamykać
556742 version = version_combo .currentText () if proton_type != 'Custom' else name_edit .text ()
557743 custom_path = path_edit .text () if proton_type == 'Custom' else None
558744 custom_type = custom_type_combo .currentText () if proton_type == 'Custom' else None
@@ -563,14 +749,15 @@ def browse_custom():
563749 thread .update_progress .connect (lambda stage , value , total : progress .setLabelText (stage ) or progress .setValue (int (value * 100 / total ) if total else 0 ))
564750 thread .finished .connect (lambda success , message : self .install_finished (success , message , version , progress ))
565751 thread .start ()
566- progress .canceled .connect (thread .terminate )
752+ progress .canceled .connect (thread .cancel )
567753
568754 def install_finished (self , success , message , version , progress ):
569755 progress .setValue (100 )
570756 if success :
571757 QMessageBox .information (self , 'Success' , f'Proton { version } installed' )
572758 else :
573759 QMessageBox .warning (self , 'Error' , f'Failed to install Proton { version } : { message } ' )
760+ progress .close ()
574761 self .start_proton_loading ()
575762
576763 def update_proton (self ):
@@ -590,12 +777,12 @@ def update_proton(self):
590777 return
591778 progress = QProgressDialog (f"Updating { proton_type } Proton to { new_version } ..." , "Cancel" , 0 , 100 , self )
592779 progress .setWindowModality (Qt .WindowModal )
593- progress .setAutoClose (True )
780+ progress .setAutoClose (False )
594781 thread = InstallThread (self .proton_manager , new_version , new_type )
595782 thread .update_progress .connect (lambda stage , value , total : progress .setLabelText (stage ) or progress .setValue (int (value * 100 / total ) if total else 0 ))
596783 thread .finished .connect (lambda success , message : self .update_finished (success , message , new_version , version , progress ))
597784 thread .start ()
598- progress .canceled .connect (thread .terminate )
785+ progress .canceled .connect (thread .cancel )
599786
600787 def update_finished (self , success , message , new_version , old_version , progress ):
601788 progress .setValue (100 )
@@ -604,6 +791,7 @@ def update_finished(self, success, message, new_version, old_version, progress):
604791 QMessageBox .information (self , 'Success' , f'Updated to { new_version } ' )
605792 else :
606793 QMessageBox .warning (self , 'Error' , f'Failed to update to { new_version } : { message } ' )
794+ progress .close ()
607795 self .start_proton_loading ()
608796
609797 def remove_proton (self ):
0 commit comments