@@ -13,7 +13,7 @@ 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 , QDialogButtonBox , QSystemTrayIcon , QMenu )
from PyQt5 . QtCore import Qt , QProcess , QSize , QTimer , QProcessEnvironment , QPropertyAnimation , QEasingCurve
from PyQt5 . QtCore import Qt , QProcess , QSize , QTimer , QProcessEnvironment , QPropertyAnimation , QEasingCurve , pyqtSignal
from PyQt5 . QtGui import QIcon , QFont , QTextCursor , QPixmap , QPainter , QCursor
from PyQt5 . QtNetwork import QLocalServer , QLocalSocket
@@ -428,6 +428,8 @@ class WinetricksManagerDialog(QDialog):
" Для переустановки компонента: Выделите е г о в списке и нажмите кнопку «Переустановить». "
)
installation_complete = pyqtSignal ( )
def __init__ ( self , prefix_path , winetricks_path , parent = None , wine_executable = None ) :
super ( ) . __init__ ( parent )
self . prefix_path = prefix_path
@@ -617,12 +619,33 @@ class WinetricksManagerDialog(QDialog):
self . _log ( f " --- Предупреждение: не удалось прочитать { log_path } : { e } --- " )
return installed_verbs
def _parse_winetricks_list_output ( self , output , installed_verbs , list_widget ) :
def _parse_winetricks_list_output ( self , output , installed_verbs , list_widget , category ):
""" Парсит вывод ' winetricks list ' и заполняет QListWidget. """
# Regex, который обрабатывает строки как с префиксом статуса '[ ]', так и без него.
# 1. `(?:\[(.)]\s+)?` - опциональная группа для статуса (напр. '[x]').
# 2. `([^\s]+)` - имя компонента (без пробелов).
# 3. `(.*)` - оставшаяся часть строки (описание).
# Определяем шаблоны для фильтрации на основе категории
dlls_blacklist_pattern = None
fonts_blacklist_pattern = None
settings_blacklist_pattern = None
if category == ' dlls ' :
# Исключаем d3d*, directx9, dont_use, dxvk*, vkd3d*, galliumnine, faudio*, Foundation
dlls_blacklist_pattern = re . compile (
r ' ^(d3d|directx9|dont_use|dxvk|vkd3d|galliumnine|faudio|foundation) ' , re . IGNORECASE
)
elif category == ' fonts ' :
fonts_blacklist_pattern = re . compile (
r ' ^(dont_use) ' , re . IGNORECASE
)
elif category == ' settings ' :
# Исключаем vista*, alldlls, autostart_*, bad*, good*, win*, videomemory*, vd=*, isolate_home
settings_blacklist_pattern = re . compile (
r ' ^(vista|alldlls|autostart_|bad|good|win|videomemory|vd=|isolate_home) ' , re . IGNORECASE
)
line_re = re . compile ( r " ^ \ s*(?: \ [(.)] \ s+)?([^ \ s]+) \ s*(.*) " )
found_items = False
@@ -643,6 +666,14 @@ class WinetricksManagerDialog(QDialog):
if ' / ' in name or ' \\ ' in name or name . lower ( ) in ( ' executing ' , ' using ' , ' warning: ' ) or name . endswith ( ' : ' ) :
continue
# Применяем фильтры для черных списков
if dlls_blacklist_pattern and dlls_blacklist_pattern . search ( name ) :
continue
if fonts_blacklist_pattern and fonts_blacklist_pattern . search ( name ) :
continue
if settings_blacklist_pattern and settings_blacklist_pattern . search ( name ) :
continue
is_checked = name in installed_verbs
item_text = f " { name . ljust ( 27 ) } { description . strip ( ) } "
item = QListWidgetItem ( item_text )
@@ -681,7 +712,7 @@ class WinetricksManagerDialog(QDialog):
self . _log ( " -------------------------------------------------- " , " red " )
else :
installed_verbs = self . _parse_winetricks_log ( )
found_items = self . _parse_winetricks_list_output ( output , installed_verbs , list_widget )
found_items = self . _parse_winetricks_list_output ( output , installed_verbs , list_widget , category )
if from_cache is None : # Только если мы не читали из кэша
# Сохраняем успешный результат в кэш
@@ -856,6 +887,7 @@ class WinetricksManagerDialog(QDialog):
# Перезагружаем данные, чтобы обновить состояние
self . initial_states . clear ( )
self . load_all_categories ( )
self . installation_complete . emit ( )
self . installation_finished = True
def closeEvent ( self , event ) :
@@ -1762,6 +1794,7 @@ class WineHelperGUI(QMainWindow):
if show_global :
self . backup_button . setVisible ( False )
self . create_log_button . setVisible ( False )
self . open_log_dir_button . setVisible ( False )
self . uninstall_button . setVisible ( False )
self . current_selected_app = None
@@ -1877,6 +1910,12 @@ class WineHelperGUI(QMainWindow):
self . backup_button . clicked . connect ( self . backup_prefix_for_app )
installed_global_layout . addWidget ( self . backup_button )
self . open_log_dir_button = QPushButton ( " Открыть папку с логом/резервной копией префикса " )
self . open_log_dir_button . setIcon ( QIcon . fromTheme ( " folder-open " ) )
self . open_log_dir_button . clicked . connect ( self . open_log_directory )
self . open_log_dir_button . setVisible ( False ) # Скрыта по умолчанию
installed_global_layout . addWidget ( self . open_log_dir_button )
self . uninstall_button = QPushButton ( " Удалить префикс " )
self . uninstall_button . setIcon ( QIcon . fromTheme ( " user-trash " ) )
self . uninstall_button . clicked . connect ( self . uninstall_app )
@@ -2172,6 +2211,13 @@ class WineHelperGUI(QMainWindow):
self . created_prefix_selector . currentIndexChanged . connect ( self . on_created_prefix_selected )
selector_layout . addWidget ( self . created_prefix_selector , 1 )
self . open_prefix_folder_button = QPushButton ( )
self . open_prefix_folder_button . setIcon ( QIcon . fromTheme ( " folder-open " ) )
self . open_prefix_folder_button . setToolTip ( " Открыть папку префикса в файловом менеджере " )
self . open_prefix_folder_button . setEnabled ( False )
self . open_prefix_folder_button . clicked . connect ( self . open_selected_prefix_folder )
selector_layout . addWidget ( self . open_prefix_folder_button )
self . create_base_pfx_button = QPushButton ( )
self . create_base_pfx_button . setIcon ( QIcon . fromTheme ( " document-export " ) )
self . create_base_pfx_button . setToolTip ( " Создать шаблон из выбранного префикса (для опытных пользователей) " )
@@ -2316,6 +2362,24 @@ class WineHelperGUI(QMainWindow):
management_layout . setColumnStretch ( 2 , 2 )
container_layout . addWidget ( self . prefix_management_groupbox )
# --- Кнопка полного удаления ---
# Добавляем разделитель и кнопку в основной контейнер управления
container_layout . addSpacing ( 15 )
self . remove_all_button = QPushButton ( " Удалить все данные WineHelper " )
self . remove_all_button . setStyleSheet ( """
QPushButton:!disabled {
background-color: #d32f2f;
color: white;
padding: 5px;
}
""" )
self . remove_all_button . setMinimumHeight ( 32 )
self . remove_all_button . setFont ( QFont ( ' Arial ' , 10 , QFont . Bold ) )
self . remove_all_button . setToolTip ( " ВНИМАНИЕ: Удаляет В С Е данные WineHelper, включая все префиксы, настройки и ярлыки. " )
self . remove_all_button . clicked . connect ( self . remove_all_data )
container_layout . addWidget ( self . remove_all_button )
layout . addWidget ( self . management_container_groupbox )
layout . addStretch ( )
self . add_tab ( self . prefix_tab , " Менеджер префиксов " )
@@ -2357,6 +2421,7 @@ class WineHelperGUI(QMainWindow):
prefix_names = [ ]
self . created_prefix_selector . blockSignals ( True )
self . remove_all_button . setEnabled ( bool ( prefix_names ) )
self . created_prefix_selector . clear ( )
if prefix_names :
self . created_prefix_selector . addItems ( prefix_names )
@@ -2372,7 +2437,9 @@ class WineHelperGUI(QMainWindow):
self . current_managed_prefix_name = None
self . _setup_prefix_management_panel ( None )
self . delete_prefix_button . setEnabled ( False )
self . remove_all_button . setEnabled ( False )
self . create_base_pfx_button . setEnabled ( False )
self . open_prefix_folder_button . setEnabled ( False )
else :
# Прокручиваем к выбранному элементу, чтобы он был виден в списке
self . created_prefix_selector . view ( ) . scrollTo (
@@ -2382,7 +2449,9 @@ class WineHelperGUI(QMainWindow):
self . current_managed_prefix_name = prefix_name
self . _setup_prefix_management_panel ( prefix_name )
self . delete_prefix_button . setEnabled ( True )
self . remove_all_button . setEnabled ( True )
self . create_base_pfx_button . setEnabled ( True )
self . open_prefix_folder_button . setEnabled ( True )
def delete_selected_prefix ( self ) :
""" Удаляет префикс, выбранный в выпадающем списке на вкладке ' Менеджер префиксов ' . """
@@ -2491,6 +2560,21 @@ class WineHelperGUI(QMainWindow):
self . _run_simple_command ( " create-base-pfx " , [ prefix_name ] )
self . command_dialog . exec_ ( )
def open_selected_prefix_folder ( self ) :
""" Открывает папку выбранного префикса в системном файловом менеджере. """
prefix_name = self . current_managed_prefix_name
if not prefix_name :
return
prefix_path = os . path . join ( Var . USER_WORK_PATH , " prefixes " , prefix_name )
if os . path . isdir ( prefix_path ) :
try :
subprocess . Popen ( [ ' xdg-open ' , prefix_path ] )
except Exception as e :
QMessageBox . warning ( self , " Ошибка " , f " Н е удалось открыть директорию:\n { prefix_path } \n \n Ошибка: { e } " )
else :
QMessageBox . warning ( self , " Ошибка " , f " Директория префикса не найдена: \n { prefix_path } " )
def _setup_prefix_management_panel ( self , prefix_name ) :
""" Настраивает панель управления префиксом на основе текущего состояния. """
is_prefix_selected = bool ( prefix_name )
@@ -2549,19 +2633,39 @@ class WineHelperGUI(QMainWindow):
self . esync_button . blockSignals ( False )
self . fsync_button . blockSignals ( False )
# --- Чтение и отображение установленных компонентов Winetricks ---
winetricks_log_path = os . path . join ( Var . USER_WORK_PATH , " prefixes " , prefix_name , " winetricks.log " )
installed_verbs = [ ]
if os . path . exists ( winetricks_log_path ) :
try :
with open ( winetricks_log_path , ' r ' , encoding = ' utf-8 ' ) as f :
for line in f :
verb = line . split ( ' # ' , 1 ) [ 0 ] . strip ( )
if verb :
installed_verbs . append ( verb )
except IOError as e :
print ( f " Ошибка чтения winetricks.log: { e } " )
# Фильтруем служебные компоненты, чтобы не засорять вывод
verbs_to_ignore = {
' isolate_home ' , ' winxp ' , ' win7 ' , ' win10 ' , ' win11 ' ,
' vista ' , ' win2k ' , ' win2k3 ' , ' win2k8 ' , ' win8 ' , ' win81 ' ,
' workaround ' , ' internal '
}
display_verbs = sorted ( [ v for v in installed_verbs if v not in verbs_to_ignore ] )
# Карта для красивого отображения известных переменных
display_map = {
" WINEPREFIX " : ( " Путь " , lambda v : v ) ,
" WINEARCH " : ( " Архитектура " , lambda v : " 64-bit " if v == " win64 " else " 32-bit " ) ,
" WH_WINE_USE " : ( " Версия Wine " , lambda v : " Системная " if v == " system " else v ) ,
" BASE_PFX " : ( " Тип " , lambda v : ' Чистый ' if v == " none " else ' С рекомендуемыми библиотеками' ) ,
" DXVK_VER " : ( " Версия DXVK " , lambda v : v if v else " Н е установлено" ) ,
" 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 " , " WH_XDG_OPEN " ]
display_order = [ " WINEPREFIX " , " WINEARCH " , " WH_WINE_USE " , " 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> "
@@ -2583,6 +2687,15 @@ class WineHelperGUI(QMainWindow):
html_content + = " <br><b>Дополнительные параметры:</b><br> "
html_content + = other_vars_html
html_content + = " <br><b>Компоненты (Winetricks):</b> "
if display_verbs :
# Используем span вместо div, чтобы избежать лишних отступов
html_content + = ' <span style= " max-height: 120px; overflow-y: auto; " > '
html_content + = " , " . join ( html . escape ( v ) for v in display_verbs )
html_content + = ' </span> '
else :
html_content + = " Н е установлены"
html_content + = " </p> "
self . prefix_info_display . setHtml ( html_content )
@@ -3139,9 +3252,6 @@ class WineHelperGUI(QMainWindow):
""" Открывает диалог создания нового префикса. """
dialog = CreatePrefixDialog ( self )
if dialog . exec_ ( ) == QDialog . Accepted :
if not self . _show_license_agreement_dialog ( ) :
return
self . start_prefix_creation (
prefix_name = dialog . prefix_name ,
wine_arch = dialog . wine_arch ,
@@ -3378,6 +3488,7 @@ class WineHelperGUI(QMainWindow):
self . installed_global_action_widget . setVisible ( True )
self . backup_button . setVisible ( True )
self . create_log_button . setVisible ( True )
self . update_open_log_dir_button_visibility ( )
self . uninstall_button . setVisible ( True )
self . manual_install_path_widget . setVisible ( False )
@@ -3386,6 +3497,27 @@ class WineHelperGUI(QMainWindow):
self . current_selected_app = None
self . info_panel . setVisible ( False )
def update_open_log_dir_button_visibility ( self ) :
""" Проверяет наличие лог-файла или бэкапов и обновляет видимость кнопки. """
log_dir_path = os . path . join ( os . path . expanduser ( " ~ " ) , " winehelper_backup_log " )
is_visible = False
if os . path . isdir ( log_dir_path ) :
# Кнопка должна быть видна, если директория не пуста.
if os . listdir ( log_dir_path ) :
is_visible = True
self . open_log_dir_button . setVisible ( is_visible )
def open_log_directory ( self ) :
""" Открывает директорию с лог-файлами. """
log_dir_path = os . path . join ( os . path . expanduser ( " ~ " ) , " winehelper_backup_log " )
if os . path . isdir ( log_dir_path ) :
try :
subprocess . Popen ( [ ' xdg-open ' , log_dir_path ] )
except Exception as e :
QMessageBox . warning ( self , " Ошибка " , f " Н е удалось открыть директорию:\n { log_dir_path } \n \n Ошибка: { e } " )
else :
QMessageBox . information ( self , " Информация " , f " Директория с логами не найдена: \n { log_dir_path } " )
def _get_prefix_name_for_selected_app ( self ) :
""" Извлекает имя префикса для выбранного приложения. """
if not self . current_selected_app or ' desktop_path ' not in self . current_selected_app :
@@ -3426,8 +3558,8 @@ class WineHelperGUI(QMainWindow):
msg_box = QMessageBox ( self )
msg_box . setWindowTitle ( " Создание резервной копии " )
msg_box . setText (
f " Будет создана резервная копия префикса ' { prefix_name } ' . \n "
f " Файл будет сохранен на вашем Рабочем столе в формате .whpack. \n \n Продолжить? "
f " Будет создана резервная копия префикса ' { prefix_name } ' . \n \n "
f " Файл будет сохранен в домашней директории в папке winehelper_backup_log/ в формате .whpack. \n \n Продолжить? "
)
msg_box . addButton ( yes_button , QMessageBox . YesRole )
msg_box . addButton ( no_button , QMessageBox . NoRole )
@@ -3460,6 +3592,7 @@ class WineHelperGUI(QMainWindow):
self . command_process . setProcessChannelMode ( QProcess . MergedChannels )
self . command_process . readyReadStandardOutput . connect ( self . _handle_command_output )
self . command_process . finished . connect ( self . _handle_command_finished )
self . command_process . finished . connect ( self . update_open_log_dir_button_visibility )
winehelper_path = self . winehelper_path
args = [ " backup-prefix " , prefix_name ]
@@ -3525,9 +3658,9 @@ class WineHelperGUI(QMainWindow):
msg_box = QMessageBox ( self )
msg_box . setWindowTitle ( " Создание лога " )
msg_box . setText (
" Приложение будет запущено в режиме отладки. \n "
" После закрытия приложения лог будет сохранен в вашем домашнем каталоге "
" под именем ' winehelper.log ' . "
" Приложение будет запущено в режиме отладки. \n \n "
" После закрытия приложения лог будет сохранен в папке ' winehelper_backup_log ' "
" в вашем домашнем каталоге под именем ' winehelper.log ' . "
)
msg_box . addButton ( yes_button , QMessageBox . YesRole )
msg_box . addButton ( no_button , QMessageBox . NoRole )
@@ -3569,6 +3702,7 @@ class WineHelperGUI(QMainWindow):
wine_executable = self . _get_wine_executable_for_prefix ( prefix_name )
dialog = WinetricksManagerDialog ( prefix_path , winetricks_path , self , wine_executable = wine_executable )
dialog . installation_complete . connect ( lambda : self . update_prefix_info_display ( prefix_name ) )
dialog . exec_ ( )
def _get_wine_executable_for_prefix ( self , prefix_name ) :
@@ -3793,6 +3927,7 @@ class WineHelperGUI(QMainWindow):
# и избегания проблем с замыканием в lambda.
process . finished . connect ( partial ( self . _on_app_process_finished , desktop_path ) )
process . finished . connect ( self . update_open_log_dir_button_visibility )
try :
process . start ( program , arguments )
@@ -3811,6 +3946,55 @@ class WineHelperGUI(QMainWindow):
QMessageBox . critical ( self , " Ошибка " ,
f " Н е удалось обработать команду запуска:\n { command_str } \n \n Ошибка: { str ( e ) } " )
def remove_all_data ( self ) :
""" Запускает процесс полного удаления всех данных WineHelper. """
# Первое подтверждение
msg_box1 = QMessageBox ( self )
msg_box1 . setIcon ( QMessageBox . Critical )
msg_box1 . setWindowTitle ( ' Подтверждение полного удаления ' )
msg_box1 . setText (
" <h2>ВНИМАНИЕ!</h2> "
" <p>Это действие полностью и безвозвратно удалит <b>В С Е </b> данные, связанные с WineHelper, включая:</p> "
" <ul> "
" <li>В с е созданные префиксы и установленные в них программы.</li> "
" <li>В с е ярлыки в меню и на рабочем столе.</li> "
" <li>В с е настройки, кэш и резервные копии.</li> "
" </ul> "
" <p>Продолжить?</p> "
)
msg_box1 . setTextFormat ( Qt . RichText )
yes_button1 = msg_box1 . addButton ( " Да, я понимаю " , QMessageBox . YesRole )
no_button1 = msg_box1 . addButton ( " Отмена " , QMessageBox . NoRole )
msg_box1 . setDefaultButton ( no_button1 )
msg_box1 . exec_ ( )
if msg_box1 . clickedButton ( ) != yes_button1 :
return
# Второе, финальное подтверждение
msg_box2 = QMessageBox ( self )
msg_box2 . setIcon ( QMessageBox . Critical )
msg_box2 . setWindowTitle ( ' Последнее предупреждение ' )
msg_box2 . setText ( " <h3>Вы уверены, что хотите удалить ВСЁ?</h3><p>Это действие необратимо.</p> " )
msg_box2 . setTextFormat ( Qt . RichText )
yes_button2 = msg_box2 . addButton ( " Да, удалить всё " , QMessageBox . DestructiveRole )
no_button2 = msg_box2 . addButton ( " Нет, я передумал " , QMessageBox . RejectRole )
msg_box2 . setDefaultButton ( no_button2 )
msg_box2 . exec_ ( )
if msg_box2 . clickedButton ( ) != yes_button2 :
return
# Запускаем команду и выходим из приложения
try :
# Запускаем команду в фоне и не ждем е е завершения
subprocess . Popen ( [ self . winehelper_path , " remove-all " , " --force " ] )
# Сообщаем пользователю и закрываем GUI
QMessageBox . information ( self , " Удаление " , " Запущена процедура удаления WineHelper. Приложение будет закрыто. " )
self . quit_application ( )
except Exception as e :
QMessageBox . critical ( self , " Ошибка " , f " Н е удалось запустить команду удаления: { e } " )
def quit_application ( self ) :
""" Инициирует процесс выхода из приложения. """
self . is_quitting = True
@@ -4511,6 +4695,7 @@ class WineHelperGUI(QMainWindow):
self . command_process . deleteLater ( )
self . command_process = None
self . command_close_button . setEnabled ( True )
self . command_log_output . ensureCursorVisible ( )
def _handle_launcher_creation_finished ( self , exit_code , exit_status ) :
""" Обрабатывает завершение создания ярлыка. """