Compare commits

...

12 Commits

Author SHA1 Message Date
Mikhail Tergoev
d5f337e6b4 Merge branch 'minergenon-devel' 2025-09-29 23:54:59 +03:00
Sergey Palcheh
904c9c9895 revision of the sub-tab Authors 2025-09-29 21:24:37 +06:00
Sergey Palcheh
1d4ee1fd70 the prefix control display is enabled by default 2025-09-29 20:40:07 +06:00
Sergey Palcheh
02a2256c8c fixed the character input in the name of the prefix being created 2025-09-29 20:27:33 +06:00
Mikhail Tergoev
cbcdba204e TODO: system menu directory 2025-09-29 16:05:26 +03:00
Mikhail Tergoev
66c56f6ecf removed broken README.MD 2025-09-29 15:52:21 +03:00
Mikhail Tergoev
221b59eda7 added README.MD 2025-09-29 15:50:46 +03:00
Mikhail Tergoev
adf5f78360 kill_wine worked only with WH 2025-09-29 14:40:57 +03:00
Mikhail Tergoev
01f19cd94d first print_license_agreement before run_autoinstall 2025-09-29 14:23:31 +03:00
Mikhail Tergoev
117e497f94 Merge branch 'minergenon-devel' 2025-09-29 14:06:36 +03:00
Sergey Palcheh
3527846c6c added to the tray show/hide 2025-09-29 11:33:23 +06:00
Sergey Palcheh
553d427d66 added a gui tray 2025-09-28 21:26:39 +06:00
2 changed files with 131 additions and 29 deletions

View File

@@ -126,6 +126,12 @@ WH_TESTINSTALL_DIR="$DATA_PATH/testinstall"
WH_WINETRICKS="$DATA_PATH/winetricks_$WINETRICKS_VERSION" WH_WINETRICKS="$DATA_PATH/winetricks_$WINETRICKS_VERSION"
WH_MENU_DIR="$HOME/.local/share/applications/WineHelper" WH_MENU_DIR="$HOME/.local/share/applications/WineHelper"
# TODO: system menu directory
# /usr/share/desktop-directories/WineHelper.directory
# /etc/xdg/menus/applications-merged/WineHelper.menu
# user menu directory
WH_MENU_CATEGORY="$HOME/.local/share/desktop-directories/WineHelper.directory" WH_MENU_CATEGORY="$HOME/.local/share/desktop-directories/WineHelper.directory"
WH_MENU_CONFIG="$HOME/.config/menus/applications-merged/WineHelper.menu" WH_MENU_CONFIG="$HOME/.config/menus/applications-merged/WineHelper.menu"
@@ -1328,7 +1334,8 @@ use_winetricks () {
} }
kill_wine () { kill_wine () {
wine_pids=$(ls -l /proc/*/exe 2>/dev/null | grep -E 'wine(64)?-preloader|wineserver' | awk -F/ '{print $3}') wine_pids=$(ls -l /proc/*/exe 2>/dev/null | grep -E 'wine(64)?-preloader|wineserver' \
| grep "$USER_WORK_PATH" | awk -F/ '{print $3}')
for pw_kill_pids in ${wine_pids}; do for pw_kill_pids in ${wine_pids}; do
if ps cax | grep "${pw_kill_pids}" ; then if ps cax | grep "${pw_kill_pids}" ; then
@@ -1416,6 +1423,12 @@ wine_run_install () {
} }
run_autoinstall () { run_autoinstall () {
if [[ $WH_USE_GUI == "1" ]] \
&& [[ $(ps -o command= -p "$PPID" | awk '{print $2}') =~ "$DATA_PATH/winehelper_gui.py" ]]
then print_ok "Соглашения приняты из графического интерфейса."
else print_license_agreement
fi
if [[ $1 == "--clear-pfx" ]] ; then if [[ $1 == "--clear-pfx" ]] ; then
export CLEAR_PREFIX="1" export CLEAR_PREFIX="1"
shift shift

View File

@@ -10,11 +10,11 @@ import time
import json import json
import hashlib import hashlib
from functools import partial from functools import partial
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,QPushButton, QLabel, QTabWidget, QTabBar, from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QTabWidget, QTabBar,
QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, QFormLayout, QGroupBox, QRadioButton, QComboBox, QTextEdit, QFileDialog, QMessageBox, QLineEdit, QCheckBox, QStackedWidget, QScrollArea, QFormLayout, QGroupBox, QRadioButton, QComboBox,
QListWidget, QListWidgetItem, QGridLayout, QFrame, QDialog, QTextBrowser, QInputDialog, QDialogButtonBox) 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
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QDesktopServices from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPixmap, QPainter, QCursor
from PyQt5.QtNetwork import QLocalServer, QLocalSocket from PyQt5.QtNetwork import QLocalServer, QLocalSocket
@@ -1210,9 +1210,9 @@ class CreatePrefixDialog(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.parent_gui = parent # Store reference to main window self.parent_gui = parent # Сохранить ссылку на главное окно
self.setWindowTitle("Создание нового префикса") self.setWindowTitle("Создание нового префикса")
self.setMinimumSize(500, 250) self.setMinimumSize(680, 250)
self.setModal(True) self.setModal(True)
# Attributes to store results # Attributes to store results
@@ -1226,9 +1226,22 @@ class CreatePrefixDialog(QDialog):
form_layout = QFormLayout() form_layout = QFormLayout()
form_layout.setSpacing(10) form_layout.setSpacing(10)
# Создаем виджет для поля ввода и предупреждения
name_input_widget = QWidget()
name_input_layout = QVBoxLayout(name_input_widget)
name_input_layout.setContentsMargins(0, 0, 0, 0)
name_input_layout.setSpacing(2)
self.prefix_name_edit = QLineEdit() self.prefix_name_edit = QLineEdit()
self.prefix_name_edit.setPlaceholderText("Например: my_prefix") self.prefix_name_edit.setPlaceholderText("Например: my_prefix")
form_layout.addRow("<b>Имя нового префикса:</b>", self.prefix_name_edit) name_input_layout.addWidget(self.prefix_name_edit)
self.name_warning_label = QLabel("Имя может содержать только латинские буквы, цифры, тире и знаки подчеркивания.")
self.name_warning_label.setStyleSheet("color: red;")
self.name_warning_label.setVisible(False)
name_input_layout.addWidget(self.name_warning_label)
form_layout.addRow("<b>Имя нового префикса:</b>", name_input_widget)
arch_widget = QWidget() arch_widget = QWidget()
arch_layout = QHBoxLayout(arch_widget) arch_layout = QHBoxLayout(arch_widget)
@@ -1285,7 +1298,7 @@ class CreatePrefixDialog(QDialog):
# Connect signals # Connect signals
self.arch_win32_radio.toggled.connect(self.clear_wine_version_selection) self.arch_win32_radio.toggled.connect(self.clear_wine_version_selection)
self.prefix_name_edit.textChanged.connect(self.update_create_button_state) self.prefix_name_edit.textChanged.connect(self.validate_prefix_name)
self.wine_version_edit.textChanged.connect(self.update_create_button_state) self.wine_version_edit.textChanged.connect(self.update_create_button_state)
def open_wine_version_dialog(self): def open_wine_version_dialog(self):
@@ -1301,11 +1314,28 @@ class CreatePrefixDialog(QDialog):
self.wine_version_edit.clear() self.wine_version_edit.clear()
self.selected_wine_version_value = None self.selected_wine_version_value = None
def validate_prefix_name(self, text):
"""Проверяет имя префикса в реальном времени и показывает/скрывает предупреждение."""
valid_pattern = r'^[a-zA-Z0-9_-]*$'
if re.match(valid_pattern, text):
self.name_warning_label.setVisible(False)
else:
# Удаляем недопустимые символы
cleaned_text = re.sub(r'[^a-zA-Z0-9_-]', '', text)
# Блокируем сигналы, чтобы избежать рекурсии при изменении текста
self.prefix_name_edit.blockSignals(True)
self.prefix_name_edit.setText(cleaned_text)
self.prefix_name_edit.blockSignals(False)
self.name_warning_label.setVisible(True)
self.update_create_button_state()
def update_create_button_state(self): def update_create_button_state(self):
"""Включает или выключает кнопку 'Создать'.""" """Включает или выключает кнопку 'Создать'."""
name_ok = bool(self.prefix_name_edit.text().strip()) name_ok = bool(self.prefix_name_edit.text().strip())
version_ok = bool(self.wine_version_edit.text().strip()) version_ok = bool(self.wine_version_edit.text().strip())
self.create_button.setEnabled(name_ok and version_ok) # Кнопка активна, только если имя валидно и версия выбрана
self.create_button.setEnabled(name_ok and version_ok and not self.name_warning_label.isVisible())
def accept_creation(self): def accept_creation(self):
"""Валидирует данные, сохраняет их и закрывает диалог с успехом.""" """Валидирует данные, сохраняет их и закрывает диалог с успехом."""
@@ -1315,8 +1345,8 @@ class CreatePrefixDialog(QDialog):
QMessageBox.warning(self, "Ошибка", "Имя префикса не может быть пустым.") QMessageBox.warning(self, "Ошибка", "Имя префикса не может быть пустым.")
return return
if not re.match(r'^[a-zA-Z0-9_.-]+$', prefix_name): if not re.match(r'^[a-zA-Z0-9_-]+$', prefix_name):
QMessageBox.warning(self, "Ошибка", "Имя префикса может содержать только латинские буквы, цифры, точки, дефисы и подчеркивания.") QMessageBox.warning(self, "Ошибка", "Имя префикса может содержать только латинские буквы, цифры, дефисы и знаки подчеркивания.")
return return
prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name) prefix_path = os.path.join(Var.USER_WORK_PATH, "prefixes", prefix_name)
@@ -1581,6 +1611,7 @@ class WineHelperGUI(QMainWindow):
self.current_managed_prefix_name = None # Имя префикса, выбранного в выпадающем списке self.current_managed_prefix_name = None # Имя префикса, выбранного в выпадающем списке
self.prefixes_before_install = set() self.prefixes_before_install = set()
self.is_quitting = False # Флаг для корректного выхода из приложения
self.command_output_buffer = "" self.command_output_buffer = ""
self.command_last_line_was_progress = False self.command_last_line_was_progress = False
# Создаем главный виджет и layout # Создаем главный виджет и layout
@@ -1647,6 +1678,50 @@ class WineHelperGUI(QMainWindow):
self.raise_() self.raise_()
self.activateWindow() self.activateWindow()
def create_tray_icon(self):
"""Создает и настраивает иконку в системном трее."""
if not QSystemTrayIcon.isSystemTrayAvailable():
print("Системный трей не доступен.")
return
self.tray_icon = QSystemTrayIcon(self)
icon_path = Var.WH_ICON_PATH
if icon_path and os.path.exists(icon_path):
pixmap = QPixmap(icon_path)
if not pixmap.isNull():
self.tray_icon.setIcon(QIcon(pixmap))
# Создаем и сохраняем меню как атрибут класса, чтобы оно не удалялось
self.tray_menu = QMenu(self)
toggle_visibility_action = self.tray_menu.addAction("Показать/Скрыть")
toggle_visibility_action.triggered.connect(self.toggle_visibility)
self.tray_menu.addSeparator()
quit_action = self.tray_menu.addAction("Выход")
quit_action.triggered.connect(self.quit_application)
self.tray_icon.activated.connect(self.on_tray_icon_activated)
self.tray_icon.show()
def on_tray_icon_activated(self, reason):
"""Обрабатывает клики по иконке в трее."""
# Показываем меню при левом клике
if reason == QSystemTrayIcon.Trigger:
# Получаем позицию курсора и показываем меню
self.tray_menu.popup(QCursor.pos())
def toggle_visibility(self):
"""Переключает видимость главного окна."""
if self.isVisible() and self.isActiveWindow():
self.hide()
else:
# Сначала скрываем, чтобы "сбросить" состояние, затем активируем.
# Это помогает обойти проблемы с фокусом и переключением рабочих столов.
self.hide()
self.activate()
def add_tab(self, widget, title): def add_tab(self, widget, title):
"""Добавляет вкладку в кастомный TabBar и страницу в StackedWidget.""" """Добавляет вкладку в кастомный TabBar и страницу в StackedWidget."""
self.tab_bar.addTab(title) self.tab_bar.addTab(title)
@@ -2088,7 +2163,7 @@ class WineHelperGUI(QMainWindow):
# --- Контейнер для выбора и управления созданными префиксами --- # --- Контейнер для выбора и управления созданными префиксами ---
self.management_container_groupbox = QGroupBox() self.management_container_groupbox = QGroupBox()
self.management_container_groupbox.setVisible(False) # Скрыт, пока нет префиксов self.management_container_groupbox.setVisible(True) # Всегда виден
container_layout = QVBoxLayout(self.management_container_groupbox) container_layout = QVBoxLayout(self.management_container_groupbox)
selector_layout = QHBoxLayout() selector_layout = QHBoxLayout()
@@ -2268,8 +2343,8 @@ class WineHelperGUI(QMainWindow):
def _load_created_prefixes(self): def _load_created_prefixes(self):
"""Загружает и обновляет список созданных префиксов в выпадающем списке.""" """Загружает и обновляет список созданных префиксов в выпадающем списке."""
prefixes_root_path = os.path.join(Var.USER_WORK_PATH, "prefixes") prefixes_root_path = os.path.join(Var.USER_WORK_PATH, "prefixes")
if not os.path.isdir(prefixes_root_path): has_prefixes_dir = os.path.isdir(prefixes_root_path)
self.management_container_groupbox.setVisible(False) if not has_prefixes_dir:
return return
try: try:
@@ -2288,12 +2363,9 @@ class WineHelperGUI(QMainWindow):
self.created_prefix_selector.blockSignals(False) self.created_prefix_selector.blockSignals(False)
if not prefix_names: if not prefix_names:
self.management_container_groupbox.setVisible(False)
self.on_created_prefix_selected(-1) # Убедимся, что панель управления сброшена self.on_created_prefix_selected(-1) # Убедимся, что панель управления сброшена
return return
self.management_container_groupbox.setVisible(True)
def on_created_prefix_selected(self, index): def on_created_prefix_selected(self, index):
"""Обрабатывает выбор префикса из выпадающего списка.""" """Обрабатывает выбор префикса из выпадающего списка."""
if index == -1: if index == -1:
@@ -2969,14 +3041,15 @@ class WineHelperGUI(QMainWindow):
authors_text = QTextEdit() authors_text = QTextEdit()
authors_text.setReadOnly(True) authors_text.setReadOnly(True)
authors_text.setHtml(""" authors_text.setHtml("""
<div style="text-align: center;"> <div style="text-align: center; font-size: 10pt;">
<h2>Разработчики</h2> <p><span style="font-size: 11pt;"><b>Разработчики</b></span><br>
Михаил Тергоев (fidel)<br> Михаил Тергоев (fidel)<br>
Сергей Пальчех (minergenon)</p> Сергей Пальчех (minergenon)</p>
<p><b>Помощники</b><br> <p><span style="font-size: 11pt;"><b>Помощники</b></span><br>
Иван Мажукин (vanomj)</p> Иван Мажукин (vanomj)</p>
<p><b>Идея и поддержка:</b><br> <p><span style="font-size: 11pt;"><b>Идея и поддержка</b></span><br>
сообщество ALT Linux</p> ООО "Базальт СПО"<br>
ALT Linux Team</p>
<br> <br>
<p>Отдельная благодарность всем, кто вносит свой вклад в развитие проекта,<br> <p>Отдельная благодарность всем, кто вносит свой вклад в развитие проекта,<br>
тестирует и сообщает об ошибках!</p> тестирует и сообщает об ошибках!</p>
@@ -3154,9 +3227,6 @@ class WineHelperGUI(QMainWindow):
self.created_prefix_selector.setCurrentText(prefix_name) self.created_prefix_selector.setCurrentText(prefix_name)
if not self.management_container_groupbox.isVisible():
self.management_container_groupbox.setVisible(True)
def update_installed_apps(self): def update_installed_apps(self):
"""Обновляет список установленных приложений в виде кнопок""" """Обновляет список установленных приложений в виде кнопок"""
# Если активная кнопка находится в списке удаляемых, сбрасываем ее # Если активная кнопка находится в списке удаляемых, сбрасываем ее
@@ -3741,8 +3811,14 @@ class WineHelperGUI(QMainWindow):
QMessageBox.critical(self, "Ошибка", QMessageBox.critical(self, "Ошибка",
f"Не удалось обработать команду запуска:\n{command_str}\n\nОшибка: {str(e)}") f"Не удалось обработать команду запуска:\n{command_str}\n\nОшибка: {str(e)}")
def quit_application(self):
"""Инициирует процесс выхода из приложения."""
self.is_quitting = True
self.close() # Инициируем событие закрытия, которое будет обработано в closeEvent
def closeEvent(self, event): def closeEvent(self, event):
"""Обрабатывает событие закрытия главного окна.""" """Обрабатывает событие закрытия главного окна."""
# Теперь любое закрытие окна (крестик или выход из меню) инициирует выход
if self.running_apps: if self.running_apps:
msg_box = QMessageBox(self) msg_box = QMessageBox(self)
msg_box.setWindowTitle('Подтверждение выхода') msg_box.setWindowTitle('Подтверждение выхода')
@@ -3758,16 +3834,27 @@ class WineHelperGUI(QMainWindow):
msg_box.exec_() msg_box.exec_()
if msg_box.clickedButton() == yes_button: if msg_box.clickedButton() == yes_button:
# Отключаем обработчики сигналов от всех запущенных процессов,
# так как мы собираемся их принудительно завершить и выйти.
# Это предотвращает ошибку RuntimeError при закрытии.
for process in self.running_apps.values():
process.finished.disconnect()
# Используем встроенную команду killall для надежного завершения всех процессов wine # Используем встроенную команду killall для надежного завершения всех процессов wine
print("Завершение всех запущенных приложений через 'winehelper killall'...") print("Завершение всех запущенных приложений через 'winehelper killall'...")
kill_proc = QProcess() # Используем subprocess.run, который дождется завершения команды
kill_proc.start(self.winehelper_path, ["killall"]) subprocess.run([self.winehelper_path, "killall"], check=False, capture_output=True)
kill_proc.waitForFinished(5000) # Даем до 5 секунд на завершение
# Принудительно дожидаемся завершения всех дочерних процессов
for process in self.running_apps.values():
process.waitForFinished(5000) # Ждем до 5 секунд
QApplication.instance().quit()
event.accept() event.accept()
else: else:
event.ignore() event.ignore()
else: else:
super().closeEvent(event) QApplication.instance().quit() # Если нет запущенных приложений, просто выходим
def uninstall_app(self): def uninstall_app(self):
"""Удаляет выбранное установленное приложение и его префикс""" """Удаляет выбранное установленное приложение и его префикс"""
@@ -4514,6 +4601,8 @@ def main():
# Сохраняем ссылку на сервер, чтобы он не был удален сборщиком мусора # Сохраняем ссылку на сервер, чтобы он не был удален сборщиком мусора
window.server = server window.server = server
window.show() window.show()
# Создаем иконку в системном трее после создания окна
window.create_tray_icon()
return app.exec_() return app.exec_()
return 1 return 1