@@ -12,7 +12,7 @@ import hashlib
from functools import partial
from PyQt5 . QtWidgets import ( QApplication , QMainWindow , QWidget , QVBoxLayout , QHBoxLayout , QPushButton , QLabel , QTabWidget , QTabBar ,
QTextEdit , QFileDialog , QMessageBox , QLineEdit , QCheckBox , QStackedWidget , QScrollArea , QFormLayout , QGroupBox , QRadioButton , QComboBox ,
QListWidget , QListWidgetItem , QGridLayout , QFrame , QDialog , QTextBrowser , QInputDialog )
QListWidget , QListWidgetItem , QGridLayout , QFrame , QDialog , QTextBrowser , QInputDialog , QDialogButtonBox )
from PyQt5 . QtCore import Qt , QProcess , QSize , QTimer , QProcessEnvironment , QPropertyAnimation , QEasingCurve
from PyQt5 . QtGui import QIcon , QFont , QTextCursor , QPixmap , QPainter , QDesktopServices
from PyQt5 . QtNetwork import QLocalServer , QLocalSocket
@@ -474,10 +474,9 @@ class WinetricksManagerDialog(QDialog):
self . log_output . setText ( self . INFO_TEXT )
main_layout . addWidget ( self . log_output )
# Кнопки управления
# Кнопки управления, выровненные по правому краю
button_layout = QHBoxLayout ( )
self . status_label = QLabel ( " Загрузка компонентов... " )
button_layout . addWidget ( self . status_label , 1 )
button_layout . addStretch ( 1 )
self . apply_button = QPushButton ( " Применить " )
self . apply_button . setEnabled ( False )
@@ -548,7 +547,6 @@ class WinetricksManagerDialog(QDialog):
def load_all_categories ( self ) :
""" Запускает загрузку всех категорий. """
self . loading_count = len ( self . categories )
self . category_statuses = { name : " загрузка... " for name in self . categories . keys ( ) }
for internal_name in self . categories . values ( ) :
self . _start_load_process ( internal_name )
@@ -602,13 +600,6 @@ class WinetricksManagerDialog(QDialog):
process . finished . connect ( partial ( self . _on_load_finished , category ) )
process . start ( self . winetricks_path , [ category , " list " ] )
def _update_status_label ( self ) :
""" Обновляет текстовую метку состояния загрузки. """
status_parts = [ ]
for name , status in self . category_statuses . items ( ) :
status_parts . append ( f " { name } : { status } " )
self . status_label . setText ( " | " . join ( status_parts ) )
def _parse_winetricks_log ( self ) :
""" Читает winetricks.log и возвращает множество установленных компонентов. """
installed_verbs = set ( )
@@ -681,22 +672,15 @@ class WinetricksManagerDialog(QDialog):
if exit_code != 0 or exit_status != QProcess . NormalExit :
error_string = process . errorString ( ) if process else " N/A "
self . _log ( f " --- Ошибка загрузки категории ' { category } ' (код: { exit_code } ) --- " , " red " )
self . category_statuses [ category_display_name ] = " ошибка "
self . _update_status_label ( ) # Показываем ошибку в статусе
self . _log ( f " --- Ошибка загрузки категории ' { category_display_name } ' (код: { exit_code } ) --- " , " red " )
if exit_status == QProcess . CrashExit :
self . _log ( " --- Процесс winetricks завершился аварийно. --- " , " red " )
# По умолчанию используется "Неизвестная ошибка", которая не очень полезна.
if error_string != " Неизвестная ошибка " :
self . _log ( f " --- Системная ошибка: { error_string } --- " , " red " )
self . _log ( output if output . strip ( ) else " Winetricks не вернул вывод. Проверьте, что он работает корректно. " )
self . _log ( " -------------------------------------------------- " , " red " )
else :
self . category_statuses [ category_display_name ] = " готово "
installed_verbs = self . _parse_winetricks_log ( )
# Обновляем статус только если это была сетевая загрузка
if from_cache is None :
self . _update_status_label ( )
found_items = self . _parse_winetricks_list_output ( output , installed_verbs , list_widget )
if from_cache is None : # Только если мы не читали из кэша
@@ -721,7 +705,6 @@ class WinetricksManagerDialog(QDialog):
self . loading_count - = 1
if self . loading_count == 0 :
self . status_label . setText ( " Готово. " )
self . _update_ui_state ( )
def _on_item_changed ( self , item ) :
@@ -862,11 +845,6 @@ class WinetricksManagerDialog(QDialog):
# 3. Обрабатываем успех
self . _log ( " \n === В с е операции успешно завершены === " )
self . _show_message_box ( " Успех " ,
" Операции с компонентами были успешно выполнены. " ,
QMessageBox . Information ,
{ " buttons " : { " Да " : QMessageBox . AcceptRole } } )
self . apply_button . setEnabled ( True )
self . reinstall_button . setEnabled ( False ) # Сбрасываем в неактивное состояние
self . close_button . setEnabled ( True )
@@ -876,7 +854,6 @@ class WinetricksManagerDialog(QDialog):
search_edit . clear ( )
# Перезагружаем данные, чтобы обновить состояние
self . status_label . setText ( " Обновление данных... " )
self . initial_states . clear ( )
self . load_all_categories ( )
self . installation_finished = True
@@ -1355,6 +1332,75 @@ class CreatePrefixDialog(QDialog):
self . accept ( )
class FileAssociationsDialog ( QDialog ) :
""" Диалог для управления ассоциациями файлов (WH_XDG_OPEN). """
def __init__ ( self , current_associations , parent = None ) :
super ( ) . __init__ ( parent )
self . setWindowTitle ( " Настройка ассоциаций файлов " )
self . setMinimumWidth ( 450 )
self . setModal ( True )
self . new_associations = current_associations
layout = QVBoxLayout ( self )
layout . setSpacing ( 10 ) # Добавляем вертикальный отступ между виджетами
info_label = QLabel (
" Укажите расширения файлов, которые должны открываться нативными<br> "
" приложениями Linux. Чтобы удалить все ассоциации, очистите поле.<br><br> "
" <b>Пример:</b> <code>pdf docx txt</code> "
)
info_label . setWordWrap ( True )
info_label . setTextFormat ( Qt . RichText )
layout . addWidget ( info_label )
self . associations_edit = QLineEdit ( )
# Если ассоциации не заданы (значение "0"), поле будет пустым, чтобы показать подсказку
if current_associations != " 0 " :
self . associations_edit . setText ( current_associations )
self . associations_edit . setPlaceholderText ( " Введите расширения через пробел... " )
layout . addWidget ( self . associations_edit )
# Запрещенные расширения
forbidden_label = QLabel (
" <small><b>Запрещено использовать:</b> cpl, dll, exe, lnk, msi</small> "
)
forbidden_label . setTextFormat ( Qt . RichText ) # Включаем обработку HTML
layout . addWidget ( forbidden_label )
button_box = QDialogButtonBox ( QDialogButtonBox . Ok | QDialogButtonBox . Cancel )
button_box . accepted . connect ( self . validate_and_accept )
button_box . rejected . connect ( self . reject )
layout . addWidget ( button_box )
def validate_and_accept ( self ) :
""" Проверяет введенные данные перед закрытием. """
forbidden_extensions = { " cpl " , " dll " , " exe " , " lnk " , " msi " }
# Получаем введенные расширения, очищаем от лишних пробелов
input_text = self . associations_edit . text ( ) . lower ( ) . strip ( )
entered_extensions = { ext . strip ( ) for ext in input_text . split ( ) if ext . strip ( ) }
found_forbidden = entered_extensions . intersection ( forbidden_extensions )
if found_forbidden :
msg_box = QMessageBox ( self )
msg_box . setIcon ( QMessageBox . Warning )
msg_box . setWindowTitle ( " Недопустимые расширения " )
msg_box . setTextFormat ( Qt . RichText )
msg_box . setText (
" Следующие расширения запрещены и не могут быть использованы:<br><br> "
f " <b> { ' , ' . join ( sorted ( list ( found_forbidden ) ) ) } </b> "
)
msg_box . exec_ ( )
return
# Сохраняем результат в виде отсортированной строки
self . new_associations = " " . join ( sorted ( list ( entered_extensions ) ) )
self . accept ( )
class ComponentVersionSelectionDialog ( QDialog ) :
""" Диалог для выбора версии компонента (DXVK, VKD3D). """
@@ -2142,6 +2188,13 @@ class WineHelperGUI(QMainWindow):
self . vkd3d_manage_button . setToolTip ( " Установка или удаление определенной версии vkd3d-proton в префиксе. " )
management_layout . addWidget ( self . vkd3d_manage_button , 5 , 1 )
self . file_associations_button = QPushButton ( " Ассоциации файлов " )
self . file_associations_button . setMinimumHeight ( 32 )
self . file_associations_button . clicked . connect ( self . open_file_associations_manager )
self . file_associations_button . setToolTip (
" Настройка открытия определенных типов файлов с помощью нативных приложений Linux. " )
management_layout . addWidget ( self . file_associations_button , 6 , 0 , 1 , 2 )
# --- Правая сторона: Информационный блок и кнопки установки ---
right_column_widget = QWidget ( )
right_column_layout = QVBoxLayout ( right_column_widget )
@@ -2174,7 +2227,7 @@ class WineHelperGUI(QMainWindow):
right_column_layout . setStretch ( 0 , 1 ) # Информационное окно растягивается
right_column_layout . setStretch ( 1 , 0 ) # Группа кнопок не растягивается
management_layout . addWidget ( right_column_widget , 0 , 2 , 6 , 1 )
management_layout . addWidget ( right_column_widget , 0 , 2 , 7 , 1 )
management_layout . setColumnStretch ( 0 , 1 )
management_layout . setColumnStretch ( 1 , 1 )
@@ -2381,8 +2434,9 @@ class WineHelperGUI(QMainWindow):
" VKD3D_VER " : ( " Версия VKD3D " , lambda v : v if v else " Н е установлено" ) ,
" WINEESYNC " : ( " ESync " , lambda v : " Включен " if v == " 1 " else " Выключен " ) ,
" WINEFSYNC " : ( " FSync " , lambda v : " Включен " if v == " 1 " else " Выключен " ) ,
" WH_XDG_OPEN " : ( " Ассоциации файлов " , lambda v : v if v and v != " 0 " else " Н е заданы" ) ,
}
display_order = [ " WINEPREFIX " , " WINEARCH " , " WH_WINE_USE " , " BASE_PFX " , " DXVK_VER " , " VKD3D_VER " , " WINEESYNC " , " WINEFSYNC " ]
display_order = [ " WINEPREFIX " , " WINEARCH " , " WH_WINE_USE " , " BASE_PFX " , " DXVK_VER " , " VKD3D_VER " , " WINEESYNC " , " WINEFSYNC " , " WH_XDG_OPEN " ]
html_content = f ' <p style= " line-height: 1.3; font-size: 9pt; " > '
html_content + = f " <b>Имя:</b> { html . escape ( prefix_name ) } <br> "
@@ -2682,6 +2736,64 @@ class WineHelperGUI(QMainWindow):
if exit_code == 0 :
self . update_prefix_info_display ( prefix_name )
def open_file_associations_manager ( self ) :
""" Открывает диалог для управления ассоциациями файлов. """
prefix_name = self . current_managed_prefix_name
if not prefix_name :
QMessageBox . warning ( self , " Ошибка " , " Сначала выберите префикс. " )
return
current_associations = self . _get_prefix_component_version ( prefix_name , " WH_XDG_OPEN " ) or " "
dialog = FileAssociationsDialog ( current_associations , self )
if dialog . exec_ ( ) == QDialog . Accepted :
new_associations = dialog . new_associations
# Запускаем обновление, только если значение изменилось
if new_associations != current_associations :
self . run_update_associations_command ( prefix_name , new_associations )
def run_update_associations_command ( self , prefix_name , new_associations ) :
""" Выполняет команду обновления ассоциаций файлов. """
prefix_path = os . path . join ( Var . USER_WORK_PATH , " prefixes " , prefix_name )
self . command_dialog = QDialog ( self )
self . command_dialog . setWindowTitle ( " Обновление ассоциаций файлов " )
self . command_dialog . setMinimumSize ( 750 , 400 )
self . command_dialog . setModal ( True )
self . command_dialog . setWindowFlags ( self . command_dialog . windowFlags ( ) & ~ Qt . WindowCloseButtonHint )
layout = QVBoxLayout ( )
self . command_log_output = QTextEdit ( )
self . command_log_output . setReadOnly ( True )
self . command_log_output . setFont ( QFont ( ' DejaVu Sans Mono ' , 10 ) )
layout . addWidget ( self . command_log_output )
self . command_close_button = QPushButton ( " Закрыть " )
self . command_close_button . setEnabled ( False )
self . command_close_button . clicked . connect ( self . command_dialog . close )
layout . addWidget ( self . command_close_button )
self . command_dialog . setLayout ( layout )
self . command_process = QProcess ( self . command_dialog )
self . command_process . setProcessChannelMode ( QProcess . MergedChannels )
self . command_process . readyReadStandardOutput . connect ( self . _handle_command_output )
self . command_process . finished . connect (
lambda exit_code , exit_status : self . _handle_component_install_finished (
prefix_name , exit_code , exit_status
)
)
env = QProcessEnvironment . systemEnvironment ( )
env . insert ( " WINEPREFIX " , prefix_path )
# Устанавливаем новую переменную окружения для скрипта
env . insert ( " WH_XDG_OPEN " , new_associations )
self . command_process . setProcessEnvironment ( env )
args = [ " init-prefix " ]
self . command_log_output . append ( f " Выполнение: { shlex . quote ( self . winehelper_path ) } { ' ' . join ( shlex . quote ( a ) for a in args ) } " )
self . command_process . start ( self . winehelper_path , args )
self . command_dialog . exec_ ( )
def create_launcher_for_prefix ( self ) :
"""
Открывает диалог для создания ярлыка для приложения внутри выбранного префикса.
@@ -3514,7 +3626,7 @@ class WineHelperGUI(QMainWindow):
QMessageBox . critical ( self , " Ошибка " , f " Н е удалось модифицировать команду для отладки: { e } " )
return
process = QProcess ( self )
process = QProcess ( )
env = QProcessEnvironment . systemEnvironment ( )
cmd_start_index = 0
@@ -3532,7 +3644,10 @@ class WineHelperGUI(QMainWindow):
arguments = clean_command [ cmd_start_index + 1 : ]
process . setProcessEnvironment ( env )
process . finished . connect ( lambda : self . _on_app_process_finished ( desktop_path ) )
# Используем functools.partial для надежной передачи аргументов
# и избегания проблем с замыканием в lambda.
process . finished . connect ( partial ( self . _on_app_process_finished , desktop_path ) )
try :
process . start ( program , arguments )
@@ -3551,6 +3666,36 @@ class WineHelperGUI(QMainWindow):
QMessageBox . critical ( self , " Ошибка " ,
f " Н е удалось обработать команду запуска:\n { command_str } \n \n Ошибка: { str ( e ) } " )
def closeEvent ( self , event ) :
""" Обрабатывает событие закрытия главного окна. """
if self . running_apps :
msg_box = QMessageBox ( self )
msg_box . setWindowTitle ( ' Подтверждение выхода ' )
msg_box . setTextFormat ( Qt . RichText )
msg_box . setText ( ' <font color= " red " >В с е запущенные приложения будут закрыты вместе с WineHelper.</font><br><br> '
" Вы уверены, что хотите выйти? " )
msg_box . setIcon ( QMessageBox . Question )
yes_button = msg_box . addButton ( " Да " , QMessageBox . YesRole )
no_button = msg_box . addButton ( " Нет " , QMessageBox . NoRole )
msg_box . setDefaultButton ( no_button )
msg_box . exec_ ( )
if msg_box . clickedButton ( ) == yes_button :
# Корректно завершаем все дочерние процессы
for desktop_path , process in list ( self . running_apps . items ( ) ) :
if process . state ( ) == QProcess . Running :
print ( f " Завершение процесса для { desktop_path } ... " )
process . terminate ( )
if not process . waitForFinished ( 2000 ) : # Ждем 2 сек
process . kill ( ) # Если не закрылся, убиваем
event . accept ( )
else :
event . ignore ( )
else :
super ( ) . closeEvent ( event )
def uninstall_app ( self ) :
""" Удаляет выбранное установленное приложение и е г о префикс """
if not self . current_selected_app or ' desktop_path ' not in self . current_selected_app :